@teaui/core 1.7.8 → 1.9.8

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 (118) hide show
  1. package/.dist/Buffer.d.ts +5 -0
  2. package/.dist/Buffer.js +23 -0
  3. package/.dist/Buffer.js.map +1 -1
  4. package/.dist/Color.js.map +1 -1
  5. package/.dist/Screen.d.ts +2 -1
  6. package/.dist/Screen.js +24 -10
  7. package/.dist/Screen.js.map +1 -1
  8. package/.dist/Theme.d.ts +7 -3
  9. package/.dist/Theme.js +42 -25
  10. package/.dist/Theme.js.map +1 -1
  11. package/.dist/UI.js +3 -1
  12. package/.dist/UI.js.map +1 -1
  13. package/.dist/View.d.ts +29 -0
  14. package/.dist/View.js +40 -2
  15. package/.dist/View.js.map +1 -1
  16. package/.dist/Viewport.d.ts +17 -3
  17. package/.dist/Viewport.js +21 -11
  18. package/.dist/Viewport.js.map +1 -1
  19. package/.dist/components/Alert.d.ts +76 -0
  20. package/.dist/components/Alert.js +133 -0
  21. package/.dist/components/Alert.js.map +1 -0
  22. package/.dist/components/Box.d.ts +6 -2
  23. package/.dist/components/Box.js +51 -14
  24. package/.dist/components/Box.js.map +1 -1
  25. package/.dist/components/Button.d.ts +5 -2
  26. package/.dist/components/Button.js +44 -13
  27. package/.dist/components/Button.js.map +1 -1
  28. package/.dist/components/Calendar.d.ts +4 -1
  29. package/.dist/components/Calendar.js +371 -137
  30. package/.dist/components/Calendar.js.map +1 -1
  31. package/.dist/components/Callout.d.ts +12 -0
  32. package/.dist/components/Callout.js +82 -0
  33. package/.dist/components/Callout.js.map +1 -0
  34. package/.dist/components/Checkbox.js +1 -1
  35. package/.dist/components/Checkbox.js.map +1 -1
  36. package/.dist/components/Drawer.d.ts +3 -0
  37. package/.dist/components/Drawer.js +71 -2
  38. package/.dist/components/Drawer.js.map +1 -1
  39. package/.dist/components/Dropdown.js +20 -12
  40. package/.dist/components/Dropdown.js.map +1 -1
  41. package/.dist/components/Header.d.ts +1 -1
  42. package/.dist/components/Header.js +12 -10
  43. package/.dist/components/Header.js.map +1 -1
  44. package/.dist/components/Input.d.ts +1 -0
  45. package/.dist/components/Input.js +39 -15
  46. package/.dist/components/Input.js.map +1 -1
  47. package/.dist/components/Legend.d.ts +20 -0
  48. package/.dist/components/Legend.js +180 -0
  49. package/.dist/components/Legend.js.map +1 -0
  50. package/.dist/components/Log.js +3 -4
  51. package/.dist/components/Log.js.map +1 -1
  52. package/.dist/components/Modal.js +1 -1
  53. package/.dist/components/Modal.js.map +1 -1
  54. package/.dist/components/Notification.d.ts +35 -0
  55. package/.dist/components/Notification.js +120 -0
  56. package/.dist/components/Notification.js.map +1 -0
  57. package/.dist/components/Page.d.ts +55 -0
  58. package/.dist/components/Page.js +374 -0
  59. package/.dist/components/Page.js.map +1 -0
  60. package/.dist/components/Progress.d.ts +15 -0
  61. package/.dist/components/Progress.js +62 -16
  62. package/.dist/components/Progress.js.map +1 -1
  63. package/.dist/components/Scrollable.d.ts +60 -15
  64. package/.dist/components/Scrollable.js +209 -58
  65. package/.dist/components/Scrollable.js.map +1 -1
  66. package/.dist/components/ScrollableList.d.ts +16 -9
  67. package/.dist/components/ScrollableList.js +50 -19
  68. package/.dist/components/ScrollableList.js.map +1 -1
  69. package/.dist/components/Slider.d.ts +2 -1
  70. package/.dist/components/Slider.js +151 -46
  71. package/.dist/components/Slider.js.map +1 -1
  72. package/.dist/components/Space.d.ts +0 -3
  73. package/.dist/components/Space.js +0 -1
  74. package/.dist/components/Space.js.map +1 -1
  75. package/.dist/components/Spinner.js +1 -1
  76. package/.dist/components/Spinner.js.map +1 -1
  77. package/.dist/components/Stack.js +33 -2
  78. package/.dist/components/Stack.js.map +1 -1
  79. package/.dist/components/Table.js +43 -60
  80. package/.dist/components/Table.js.map +1 -1
  81. package/.dist/components/Tabs.d.ts +8 -0
  82. package/.dist/components/Tabs.js +22 -5
  83. package/.dist/components/Tabs.js.map +1 -1
  84. package/.dist/components/ToggleGroup.js +1 -1
  85. package/.dist/components/ToggleGroup.js.map +1 -1
  86. package/.dist/components/ZStack.d.ts +9 -0
  87. package/.dist/components/ZStack.js +68 -5
  88. package/.dist/components/ZStack.js.map +1 -1
  89. package/.dist/components/index.d.ts +5 -2
  90. package/.dist/components/index.js +4 -1
  91. package/.dist/components/index.js.map +1 -1
  92. package/.dist/events/index.d.ts +6 -2
  93. package/.dist/events/key.d.ts +18 -4
  94. package/.dist/events/key.js +42 -6
  95. package/.dist/events/key.js.map +1 -1
  96. package/.dist/events/mouse.d.ts +1 -0
  97. package/.dist/events/mouse.js.map +1 -1
  98. package/.dist/events/translate.js +17 -4
  99. package/.dist/events/translate.js.map +1 -1
  100. package/.dist/index.d.ts +1 -1
  101. package/.dist/index.js.map +1 -1
  102. package/.dist/inspect.js +3 -0
  103. package/.dist/inspect.js.map +1 -1
  104. package/.dist/managers/FocusManager.d.ts +2 -1
  105. package/.dist/managers/FocusManager.js +39 -4
  106. package/.dist/managers/FocusManager.js.map +1 -1
  107. package/.dist/managers/MouseManager.js +2 -0
  108. package/.dist/managers/MouseManager.js.map +1 -1
  109. package/.dist/renderToAnsi.js +1 -1
  110. package/.dist/renderToAnsi.js.map +1 -1
  111. package/.dist/terminal.d.ts +1 -0
  112. package/.dist/testing.d.ts +29 -2
  113. package/.dist/testing.js +34 -27
  114. package/.dist/testing.js.map +1 -1
  115. package/package.json +5 -4
  116. package/.dist/components/Pin.d.ts +0 -25
  117. package/.dist/components/Pin.js +0 -80
  118. package/.dist/components/Pin.js.map +0 -1
@@ -2,6 +2,7 @@ import { View } from '../View.js';
2
2
  import { Point, Rect, Size } from '../geometry.js';
3
3
  import { isMouseClicked, } from '../events/index.js';
4
4
  import { Style } from '../Style.js';
5
+ import { lineWidth } from '@teaui/term';
5
6
  const MONTH_NAMES = [
6
7
  'January',
7
8
  'February',
@@ -32,15 +33,21 @@ const MONTH_SHORT = [
32
33
  ];
33
34
  const DAY_HEADERS_SUN = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
34
35
  const DAY_HEADERS_MON = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
36
+ const CLOSE = '×';
37
+ const ARROW_UP = ' [ ↑ ] ';
38
+ const ARROW_DOWN = ' [ ↓ ] ';
35
39
  // Width: " Su Mo Tu We Th Fr Sa " = 1 + 7*3 = 22
36
40
  const CALENDAR_WIDTH = 22;
37
- const NAV_BUTTON_WIDTH = 2;
41
+ const NAV_BUTTON_WIDTH = 3;
38
42
  const CLOSE_BUTTON_WIDTH = 3;
39
43
  const MONTH_LABEL_X = 3;
40
44
  // Height: header + weekday row + 6 week rows = 8
41
45
  const CALENDAR_HEIGHT = 8;
42
46
  export class Calendar extends View {
47
+ // configurable so that tests can test 'today' rendering
48
+ #today;
43
49
  #date;
50
+ #cursorDate;
44
51
  #visibleDate;
45
52
  #onChangeVisible;
46
53
  #onChange;
@@ -51,30 +58,39 @@ export class Calendar extends View {
51
58
  // Range selection state
52
59
  #rangeStart;
53
60
  #rangeEnd;
61
+ #rangeNextSelection = 'start';
62
+ #shiftSelecting = false;
54
63
  // Year picker state
55
64
  #yearScrollOffset = 0;
56
65
  #yearSearch = '';
57
66
  // Mouse drag tracking
58
67
  #dragStartDate;
59
68
  // Mouse hover tracking
60
- #hoverPrevMonth = false;
61
- #hoverNextMonth = false;
62
- #hoverMonthLabel = false;
63
- #hoverYearLabel = false;
69
+ #hoverClose = false;
70
+ #hoverPrevButton = false;
71
+ #hoverNextButton = false;
72
+ #hoverMonthLabel;
73
+ #hoverYearLabel;
74
+ #hoverDate;
64
75
  constructor(props = {}) {
65
76
  super(props);
66
- const now = new Date();
67
- this.#date = props.date ?? now;
77
+ const now = props.now ?? new Date();
78
+ this.#today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
79
+ const date = props.date ?? this.#today;
80
+ this.#date = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
81
+ this.#cursorDate = this.#date;
68
82
  this.#visibleDate =
69
- props.visibleDate ?? new Date(now.getFullYear(), now.getMonth(), 1);
83
+ props.visibleDate ??
84
+ new Date(this.#today.getFullYear(), this.#today.getMonth(), 1);
70
85
  this.#onChangeVisible = props.onChangeVisible;
71
86
  this.#onChange = props.onChange;
72
87
  this.#selection = props.selection ?? 'single';
73
88
  this.#firstDayOfWeek = props.firstDayOfWeek ?? 0;
74
89
  }
75
90
  update(props) {
76
- if (props.date !== undefined)
77
- this.#date = props.date;
91
+ if (props.date !== undefined) {
92
+ this.#date = new Date(props.date.getFullYear(), props.date.getMonth(), props.date.getDate(), 0, 0, 0, 0);
93
+ }
78
94
  if (props.visibleDate !== undefined)
79
95
  this.#visibleDate = props.visibleDate;
80
96
  if (props.onChangeVisible !== undefined)
@@ -94,6 +110,13 @@ export class Calendar extends View {
94
110
  this.#date = value;
95
111
  this.invalidateRender();
96
112
  }
113
+ get cursorDate() {
114
+ return this.#cursorDate;
115
+ }
116
+ set cursorDate(value) {
117
+ this.#cursorDate = value;
118
+ this.invalidateRender();
119
+ }
97
120
  get visibleDate() {
98
121
  return this.#visibleDate;
99
122
  }
@@ -107,16 +130,65 @@ export class Calendar extends View {
107
130
  naturalSize() {
108
131
  return new Size(CALENDAR_WIDTH, CALENDAR_HEIGHT);
109
132
  }
133
+ get #hoverStyle() {
134
+ return new Style({
135
+ foreground: this.theme.text().foreground,
136
+ background: this.theme.darkenColor,
137
+ });
138
+ }
139
+ get #buttonHoverStyle() {
140
+ return this.theme.ui({ isHover: true });
141
+ }
142
+ get #rangeGapStyle() {
143
+ return new Style({ background: this.theme.darkenColor });
144
+ }
145
+ get #weekdayStyle() {
146
+ return new Style({
147
+ bold: true,
148
+ foreground: this.theme.highlightColor,
149
+ background: this.theme.text().background,
150
+ });
151
+ }
152
+ get #rangeEndpointStyle() {
153
+ return new Style({
154
+ bold: true,
155
+ foreground: this.theme.textColor,
156
+ background: this.theme.darkenColor,
157
+ });
158
+ }
159
+ get #inRangeStyle() {
160
+ return new Style({
161
+ foreground: this.theme.highlightColor,
162
+ background: this.theme.darkenColor,
163
+ });
164
+ }
165
+ get #todayStyle() {
166
+ return new Style({
167
+ bold: true,
168
+ foreground: this.theme.contrastTextColor,
169
+ background: this.theme.text().background,
170
+ });
171
+ }
172
+ get #cursorStyle() {
173
+ return new Style({
174
+ bold: true,
175
+ foreground: this.theme.text().foreground,
176
+ background: this.theme.darkenColor,
177
+ });
178
+ }
179
+ get #selectedStyle() {
180
+ return new Style({
181
+ bold: true,
182
+ foreground: this.theme.textColor,
183
+ background: this.theme.highlightColor,
184
+ });
185
+ }
110
186
  #widgetRect(viewport) {
111
187
  return new Rect(Point.zero, [
112
188
  Math.min(CALENDAR_WIDTH, viewport.contentSize.width),
113
189
  Math.min(CALENDAR_HEIGHT, viewport.contentSize.height),
114
190
  ]);
115
191
  }
116
- #today() {
117
- const now = new Date();
118
- return new Date(now.getFullYear(), now.getMonth(), now.getDate());
119
- }
120
192
  #isSameDay(a, b) {
121
193
  return (a.getFullYear() === b.getFullYear() &&
122
194
  a.getMonth() === b.getMonth() &&
@@ -152,8 +224,10 @@ export class Calendar extends View {
152
224
  }
153
225
  #startRangeSelection(date, syncVisibleDate = true) {
154
226
  this.#rangeStart = date;
155
- this.#rangeEnd = undefined;
227
+ this.#rangeEnd = date;
228
+ this.#rangeNextSelection = 'end';
156
229
  this.#date = date;
230
+ this.#cursorDate = date;
157
231
  if (syncVisibleDate) {
158
232
  this.#syncVisibleDate(date);
159
233
  }
@@ -165,7 +239,9 @@ export class Calendar extends View {
165
239
  return;
166
240
  }
167
241
  this.#rangeEnd = date;
242
+ this.#rangeNextSelection = 'start';
168
243
  this.#date = date;
244
+ this.#cursorDate = date;
169
245
  if (syncVisibleDate) {
170
246
  this.#syncVisibleDate(date);
171
247
  }
@@ -176,7 +252,7 @@ export class Calendar extends View {
176
252
  }
177
253
  #selectDate(date) {
178
254
  if (this.#selection === 'range') {
179
- if (!this.#rangeStart || this.#rangeEnd) {
255
+ if (this.#rangeNextSelection === 'start') {
180
256
  this.#startRangeSelection(date);
181
257
  }
182
258
  else {
@@ -185,24 +261,59 @@ export class Calendar extends View {
185
261
  return;
186
262
  }
187
263
  this.#date = date;
264
+ this.#cursorDate = date;
188
265
  this.#onChange?.(date, date);
189
266
  this.#syncVisibleDate(date);
190
267
  this.invalidateRender();
191
268
  }
269
+ #selectCursorDate(date) {
270
+ this.#cursorDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
271
+ this.#syncVisibleDate(date);
272
+ this.invalidateRender();
273
+ }
274
+ #moveCursorDateBy(days) {
275
+ this.#shiftSelecting = false;
276
+ const nextDate = new Date(this.#cursorDate);
277
+ nextDate.setDate(nextDate.getDate() + days);
278
+ this.#cursorDate = nextDate;
279
+ // Navigate months if needed
280
+ if (nextDate.getMonth() !== this.#visibleDate.getMonth() ||
281
+ nextDate.getFullYear() !== this.#visibleDate.getFullYear()) {
282
+ const vis = new Date(nextDate.getFullYear(), nextDate.getMonth(), 1);
283
+ this.#visibleDate = vis;
284
+ this.#onChangeVisible?.(vis);
285
+ }
286
+ this.invalidateRender();
287
+ }
288
+ #shiftSelectBy(days) {
289
+ if (!this.#shiftSelecting) {
290
+ this.#rangeStart = new Date(this.#cursorDate);
291
+ }
292
+ this.#moveCursorDateBy(days);
293
+ this.#shiftSelecting = true;
294
+ this.#rangeEnd = new Date(this.#cursorDate);
295
+ this.#rangeNextSelection = 'start';
296
+ }
192
297
  #selectMonth(month) {
193
298
  const d = new Date(this.#visibleDate.getFullYear(), month, 1);
194
299
  this.#visibleDate = d;
195
- this.#displayMode = 'days';
300
+ this.#selectDisplayMode('days');
196
301
  this.#onChangeVisible?.(d);
197
302
  this.invalidateRender();
198
303
  }
199
304
  #selectYear(year) {
200
305
  const d = new Date(year, this.#visibleDate.getMonth(), 1);
201
306
  this.#visibleDate = d;
202
- this.#displayMode = 'days';
307
+ this.#selectDisplayMode('days');
203
308
  this.#onChangeVisible?.(d);
204
309
  this.invalidateRender();
205
310
  }
311
+ #selectDisplayMode(mode) {
312
+ this.#displayMode = mode;
313
+ this.#hoverClose = false;
314
+ this.#hoverMonthLabel = undefined;
315
+ this.#hoverYearLabel = undefined;
316
+ }
206
317
  #getDateAtPosition(x, y) {
207
318
  if (y < 2 || y > 7)
208
319
  return undefined;
@@ -239,6 +350,9 @@ export class Calendar extends View {
239
350
  }
240
351
  receiveMouse(event, system) {
241
352
  super.receiveMouse(event, system);
353
+ if (event.name === 'mouse.button.down') {
354
+ system.requestFocus();
355
+ }
242
356
  if (this.#displayMode === 'days') {
243
357
  this.#receiveMouseDays(event);
244
358
  }
@@ -249,40 +363,55 @@ export class Calendar extends View {
249
363
  this.#receiveMouseYears(event);
250
364
  }
251
365
  }
366
+ #formatMonthTitle(date) {
367
+ const totalWidth = 10;
368
+ const textWidth = lineWidth(MONTH_NAMES[date.getMonth()]);
369
+ const pad = Math.max(0, totalWidth - textWidth);
370
+ return (' '.repeat(Math.floor(pad / 2)) +
371
+ MONTH_NAMES[date.getMonth()] +
372
+ ' '.repeat(Math.ceil(pad / 2)));
373
+ }
374
+ #formatYearTitle(date) {
375
+ return ` ${String(date.getFullYear())} `;
376
+ }
377
+ #yearLabelX(yearStr) {
378
+ return CALENDAR_WIDTH - NAV_BUTTON_WIDTH - lineWidth(yearStr);
379
+ }
252
380
  #receiveMouseDays(event) {
253
381
  const x = event.position.x;
254
382
  const y = event.position.y;
255
383
  // Reset hover states
256
- this.#hoverPrevMonth = false;
257
- this.#hoverNextMonth = false;
258
- this.#hoverMonthLabel = false;
259
- this.#hoverYearLabel = false;
384
+ this.#hoverPrevButton = false;
385
+ this.#hoverNextButton = false;
386
+ this.#hoverMonthLabel = undefined;
387
+ this.#hoverYearLabel = undefined;
260
388
  if (y === 0) {
261
- const monthName = MONTH_NAMES[this.#visibleDate.getMonth()];
262
- const yearStr = String(this.#visibleDate.getFullYear());
263
- const yearStart = CALENDAR_WIDTH - NAV_BUTTON_WIDTH - 1 - yearStr.length;
389
+ const monthName = this.#formatMonthTitle(this.#visibleDate);
390
+ const yearStr = this.#formatYearTitle(this.#visibleDate);
391
+ const yearStart = this.#yearLabelX(yearStr);
264
392
  if (x < NAV_BUTTON_WIDTH) {
265
- this.#hoverPrevMonth = true;
393
+ this.#hoverPrevButton = true;
266
394
  if (isMouseClicked(event)) {
267
395
  this.#navigateMonth(-1);
268
396
  }
269
397
  }
270
398
  else if (x >= CALENDAR_WIDTH - NAV_BUTTON_WIDTH) {
271
- this.#hoverNextMonth = true;
399
+ this.#hoverNextButton = true;
272
400
  if (isMouseClicked(event)) {
273
401
  this.#navigateMonth(1);
274
402
  }
275
403
  }
276
- else if (x >= MONTH_LABEL_X && x < MONTH_LABEL_X + monthName.length) {
277
- this.#hoverMonthLabel = true;
404
+ else if (x >= MONTH_LABEL_X &&
405
+ x < MONTH_LABEL_X + lineWidth(monthName)) {
406
+ this.#hoverMonthLabel = 'true';
278
407
  if (isMouseClicked(event)) {
279
- this.#displayMode = 'months';
408
+ this.#selectDisplayMode('months');
280
409
  }
281
410
  }
282
- else if (x >= yearStart && x < yearStart + yearStr.length) {
283
- this.#hoverYearLabel = true;
411
+ else if (x >= yearStart && x < yearStart + lineWidth(yearStr)) {
412
+ this.#hoverYearLabel = 'true';
284
413
  if (isMouseClicked(event)) {
285
- this.#displayMode = 'years';
414
+ this.#selectDisplayMode('years');
286
415
  this.#yearScrollOffset = 0;
287
416
  this.#yearSearch = '';
288
417
  }
@@ -290,6 +419,7 @@ export class Calendar extends View {
290
419
  return;
291
420
  }
292
421
  const date = this.#getDateAtPosition(x, y);
422
+ this.#hoverDate = date;
293
423
  if (!date) {
294
424
  if (event.name === 'mouse.button.cancel' ||
295
425
  event.name === 'mouse.button.up') {
@@ -300,9 +430,7 @@ export class Calendar extends View {
300
430
  if (this.#selection === 'range') {
301
431
  switch (event.name) {
302
432
  case 'mouse.button.down':
303
- if (this.#rangeStart &&
304
- !this.#rangeEnd &&
305
- !this.#isSameDay(this.#rangeStart, date)) {
433
+ if (this.#rangeNextSelection === 'end' && this.#rangeStart) {
306
434
  this.#dragStartDate = undefined;
307
435
  this.#finishRangeSelection(date);
308
436
  }
@@ -317,6 +445,7 @@ export class Calendar extends View {
317
445
  this.#rangeStart = this.#dragStartDate;
318
446
  this.#rangeEnd = date;
319
447
  this.#date = date;
448
+ this.#cursorDate = date;
320
449
  this.invalidateRender();
321
450
  }
322
451
  break;
@@ -342,50 +471,72 @@ export class Calendar extends View {
342
471
  #receiveMouseMonths(event) {
343
472
  const x = event.position.x;
344
473
  const y = event.position.y;
345
- if (isMouseClicked(event)) {
346
- if (y === 0 && x >= CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH) {
347
- // Close button
348
- this.#displayMode = 'days';
474
+ // Reset hover states
475
+ this.#hoverClose = false;
476
+ this.#hoverMonthLabel = undefined;
477
+ if (y === 0 && x >= CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH) {
478
+ this.#hoverClose = true;
479
+ if (isMouseClicked(event)) {
480
+ this.#selectDisplayMode('days');
349
481
  this.invalidateRender();
350
482
  return;
351
483
  }
352
- // Month grid: 4 rows × 3 columns, starting at y=2
353
- if (y >= 2 && y <= 5) {
354
- const col = Math.floor(x / 7);
355
- const row = y - 2;
356
- if (col >= 0 && col < 3 && row >= 0 && row < 4) {
357
- const month = row * 3 + col;
484
+ }
485
+ // Month grid: 4 rows × 3 columns, starting at y=2
486
+ if (y >= 2 && y <= 5) {
487
+ const col = Math.floor(x / 7);
488
+ const row = y - 2;
489
+ if (col >= 0 && col < 3 && row >= 0 && row < 4) {
490
+ const month = row * 3 + col;
491
+ this.#hoverMonthLabel = MONTH_SHORT[month];
492
+ if (isMouseClicked(event)) {
358
493
  this.#selectMonth(month);
359
494
  }
360
495
  }
361
496
  }
497
+ this.invalidateRender();
362
498
  }
363
499
  #receiveMouseYears(event) {
364
500
  const x = event.position.x;
365
501
  const y = event.position.y;
366
- if (isMouseClicked(event)) {
367
- if (y === 0 && x >= CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH) {
368
- // Close button
369
- this.#displayMode = 'days';
502
+ // Reset hover states
503
+ this.#hoverClose = false;
504
+ this.#hoverPrevButton = false;
505
+ this.#hoverNextButton = false;
506
+ this.#hoverYearLabel = undefined;
507
+ if (y === 0 && x >= CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH) {
508
+ this.#hoverClose = true;
509
+ if (isMouseClicked(event)) {
510
+ this.#selectDisplayMode('days');
370
511
  this.invalidateRender();
371
512
  return;
372
513
  }
373
- // Year rows at y=2..6 (5 visible years)
374
- if (y >= 2 && y <= 6) {
375
- const yearIndex = y - 2;
376
- const baseYear = this.#visibleDate.getFullYear() - 2 + this.#yearScrollOffset;
377
- this.#selectYear(baseYear + yearIndex);
378
- }
379
- // Scroll arrows
380
- if (y === 1) {
514
+ }
515
+ // Scroll arrows
516
+ if (y === 1) {
517
+ this.#hoverPrevButton = true;
518
+ if (isMouseClicked(event)) {
381
519
  this.#yearScrollOffset -= 5;
382
520
  this.invalidateRender();
383
521
  }
384
- else if (y === 7) {
522
+ }
523
+ else if (y === 7) {
524
+ this.#hoverNextButton = true;
525
+ if (isMouseClicked(event)) {
385
526
  this.#yearScrollOffset += 5;
386
527
  this.invalidateRender();
387
528
  }
388
529
  }
530
+ // Year rows at y=2..6 (5 visible years)
531
+ if (y >= 2 && y <= 6) {
532
+ const yearIndex = y - 2;
533
+ const baseYear = this.#visibleDate.getFullYear() - 2 + this.#yearScrollOffset;
534
+ const year = baseYear + yearIndex;
535
+ this.#hoverYearLabel = String(year);
536
+ if (isMouseClicked(event)) {
537
+ this.#selectYear(year);
538
+ }
539
+ }
389
540
  // Scroll wheel
390
541
  if (event.name === 'mouse.wheel.up') {
391
542
  this.#yearScrollOffset -= 1;
@@ -395,6 +546,7 @@ export class Calendar extends View {
395
546
  this.#yearScrollOffset += 1;
396
547
  this.invalidateRender();
397
548
  }
549
+ this.invalidateRender();
398
550
  }
399
551
  receiveKey(event) {
400
552
  if (this.#displayMode === 'days') {
@@ -409,20 +561,43 @@ export class Calendar extends View {
409
561
  }
410
562
  #receiveKeyDays(event) {
411
563
  switch (event.name) {
564
+ case 't':
565
+ this.#selectCursorDate(new Date());
566
+ break;
412
567
  case 'left':
413
- this.#moveDateBy(-1);
568
+ if (event.shift) {
569
+ this.#shiftSelectBy(-1);
570
+ }
571
+ else {
572
+ this.#moveCursorDateBy(-1);
573
+ }
414
574
  break;
415
575
  case 'right':
416
- this.#moveDateBy(1);
576
+ if (event.shift) {
577
+ this.#shiftSelectBy(1);
578
+ }
579
+ else {
580
+ this.#moveCursorDateBy(1);
581
+ }
417
582
  break;
418
583
  case 'up':
419
- this.#moveDateBy(-7);
584
+ if (event.shift) {
585
+ this.#shiftSelectBy(-7);
586
+ }
587
+ else {
588
+ this.#moveCursorDateBy(-7);
589
+ }
420
590
  break;
421
591
  case 'down':
422
- this.#moveDateBy(7);
592
+ if (event.shift) {
593
+ this.#shiftSelectBy(7);
594
+ }
595
+ else {
596
+ this.#moveCursorDateBy(7);
597
+ }
423
598
  break;
424
599
  case 'return':
425
- this.#selectDate(this.#date);
600
+ this.#selectDate(this.#cursorDate);
426
601
  break;
427
602
  case 'escape':
428
603
  // Do nothing in day view
@@ -434,14 +609,14 @@ export class Calendar extends View {
434
609
  this.#navigateMonth(1);
435
610
  break;
436
611
  case 'home': {
437
- const d = new Date(this.#visibleDate.getFullYear(), this.#visibleDate.getMonth(), 1);
438
- this.#date = d;
612
+ const nextDate = new Date(this.#visibleDate.getFullYear(), this.#visibleDate.getMonth(), 1);
613
+ this.#cursorDate = nextDate;
439
614
  this.invalidateRender();
440
615
  break;
441
616
  }
442
617
  case 'end': {
443
- const d = new Date(this.#visibleDate.getFullYear(), this.#visibleDate.getMonth() + 1, 0);
444
- this.#date = d;
618
+ const nextDate = new Date(this.#visibleDate.getFullYear(), this.#visibleDate.getMonth() + 1, 0);
619
+ this.#cursorDate = nextDate;
445
620
  this.invalidateRender();
446
621
  break;
447
622
  }
@@ -450,7 +625,7 @@ export class Calendar extends View {
450
625
  #receiveKeyMonths(event) {
451
626
  switch (event.name) {
452
627
  case 'escape':
453
- this.#displayMode = 'days';
628
+ this.#selectDisplayMode('days');
454
629
  this.invalidateRender();
455
630
  break;
456
631
  case 'return':
@@ -493,7 +668,7 @@ export class Calendar extends View {
493
668
  #receiveKeyYears(event) {
494
669
  switch (event.name) {
495
670
  case 'escape':
496
- this.#displayMode = 'days';
671
+ this.#selectDisplayMode('days');
497
672
  this.#yearSearch = '';
498
673
  this.invalidateRender();
499
674
  break;
@@ -542,19 +717,6 @@ export class Calendar extends View {
542
717
  }
543
718
  this.invalidateRender();
544
719
  }
545
- #moveDateBy(days) {
546
- const d = new Date(this.#date);
547
- d.setDate(d.getDate() + days);
548
- this.#date = d;
549
- // Navigate months if needed
550
- if (d.getMonth() !== this.#visibleDate.getMonth() ||
551
- d.getFullYear() !== this.#visibleDate.getFullYear()) {
552
- const vis = new Date(d.getFullYear(), d.getMonth(), 1);
553
- this.#visibleDate = vis;
554
- this.#onChangeVisible?.(vis);
555
- }
556
- this.invalidateRender();
557
- }
558
720
  render(viewport) {
559
721
  const hasFocus = viewport.registerFocus();
560
722
  this.#hasFocus = hasFocus;
@@ -574,54 +736,37 @@ export class Calendar extends View {
574
736
  break;
575
737
  }
576
738
  }
577
- get #selectedStyle() {
578
- return new Style({
579
- bold: true,
580
- foreground: this.theme.textColor,
581
- background: this.theme.highlightColor,
582
- });
583
- }
584
739
  #renderDays(viewport) {
585
- const today = this.#today();
586
- const year = this.#visibleDate.getFullYear();
740
+ const today = this.#today;
587
741
  const month = this.#visibleDate.getMonth();
588
- const monthName = MONTH_NAMES[month];
589
- const yearStr = String(year);
742
+ const monthName = this.#formatMonthTitle(this.#visibleDate);
743
+ const yearStr = this.#formatYearTitle(this.#visibleDate);
590
744
  const textStyle = this.theme.text();
591
745
  const dimStyle = this.theme.text({ isPlaceholder: true });
746
+ const hoverStyle = this.#hoverStyle;
592
747
  const headerStyle = this.theme.ui({ isHover: false });
593
748
  const selectedStyle = this.#selectedStyle;
594
- const todayStyle = new Style({
595
- bold: true,
596
- foreground: this.theme.brightTextColor,
597
- background: textStyle.background,
598
- });
599
- const rangeStyle = new Style({
600
- foreground: this.theme.highlightColor,
601
- background: textStyle.background,
602
- });
749
+ const todayStyle = this.#todayStyle;
750
+ const inRangeStyle = this.#inRangeStyle;
751
+ const rangeEndpointStyle = this.#rangeEndpointStyle;
603
752
  viewport.paint(textStyle, this.#widgetRect(viewport));
604
753
  // Header: "◃ June 2026 ▹"
605
- const prevArrow = this.#hoverPrevMonth ? '◂ ' : '◃ ';
606
- const nextArrow = this.#hoverNextMonth ? ' ▸' : ' ▹';
754
+ const prevArrow = this.#hoverPrevButton ? ' ◂ ' : ' ◃ ';
755
+ const nextArrow = this.#hoverNextButton ? ' ▸ ' : ' ▹ ';
607
756
  const monthLabelStyle = this.#hoverMonthLabel
608
- ? this.theme.ui({ isHover: true })
757
+ ? this.#buttonHoverStyle
609
758
  : headerStyle;
610
759
  const yearLabelStyle = this.#hoverYearLabel
611
- ? this.theme.ui({ isHover: true })
760
+ ? this.#buttonHoverStyle
612
761
  : headerStyle;
613
- const yearX = CALENDAR_WIDTH - NAV_BUTTON_WIDTH - 1 - yearStr.length;
614
- viewport.write(prevArrow, new Point(0, 0), headerStyle);
762
+ const yearStart = this.#yearLabelX(yearStr);
763
+ viewport.write(prevArrow, new Point(0, 0), this.#hoverPrevButton ? this.#buttonHoverStyle : headerStyle);
615
764
  viewport.write(monthName, new Point(MONTH_LABEL_X, 0), monthLabelStyle);
616
- viewport.write(yearStr, new Point(yearX, 0), yearLabelStyle);
617
- viewport.write(nextArrow, new Point(CALENDAR_WIDTH - NAV_BUTTON_WIDTH, 0), headerStyle);
765
+ viewport.write(yearStr, new Point(yearStart, 0), yearLabelStyle);
766
+ viewport.write(nextArrow, new Point(CALENDAR_WIDTH - NAV_BUTTON_WIDTH, 0), this.#hoverNextButton ? this.#buttonHoverStyle : headerStyle);
618
767
  // Weekday headers
619
768
  const dayHeaders = this.#firstDayOfWeek === 1 ? DAY_HEADERS_MON : DAY_HEADERS_SUN;
620
- const weekdayStyle = new Style({
621
- bold: true,
622
- foreground: this.theme.highlightColor,
623
- background: textStyle.background,
624
- });
769
+ const weekdayStyle = this.#weekdayStyle;
625
770
  for (let i = 0; i < 7; i++) {
626
771
  viewport.write(dayHeaders[i], new Point(1 + i * 3, 1), weekdayStyle);
627
772
  }
@@ -630,17 +775,30 @@ export class Calendar extends View {
630
775
  for (let week = 0; week < 6; week++) {
631
776
  for (let day = 0; day < 7; day++) {
632
777
  const date = grid[week][day];
778
+ const isHover = this.#hoverDate && this.#isSameDay(date, this.#hoverDate);
633
779
  const isCurrentMonth = date.getMonth() === month;
634
780
  const isToday = this.#isSameDay(date, today);
635
- const isSelected = this.#isSameDay(date, this.#date);
781
+ const isSelected = this.#isSameDay(date, this.#date) && this.#selection === 'single';
782
+ const isCursor = !this.#isSameDay(this.#cursorDate, this.#date) &&
783
+ this.#isSameDay(date, this.#cursorDate);
636
784
  const isInRange = this.#isInRange(date);
637
785
  const dayNum = String(date.getDate()).padStart(2, ' ');
786
+ const isRangeEndpoint = this.#rangeStart &&
787
+ this.#rangeEnd &&
788
+ (this.#isSameDay(date, this.#rangeStart) ||
789
+ this.#isSameDay(date, this.#rangeEnd));
638
790
  let style;
639
- if (isSelected) {
791
+ if (isRangeEndpoint) {
792
+ style = rangeEndpointStyle;
793
+ }
794
+ else if (isSelected) {
640
795
  style = selectedStyle;
641
796
  }
797
+ else if (isCursor) {
798
+ style = this.#cursorStyle;
799
+ }
642
800
  else if (isInRange) {
643
- style = rangeStyle;
801
+ style = inRangeStyle;
644
802
  }
645
803
  else if (isToday) {
646
804
  style = todayStyle;
@@ -648,32 +806,96 @@ export class Calendar extends View {
648
806
  else if (!isCurrentMonth) {
649
807
  style = dimStyle;
650
808
  }
809
+ else if (isHover) {
810
+ style = hoverStyle;
811
+ }
651
812
  else {
652
813
  style = textStyle;
653
814
  }
654
815
  viewport.write(dayNum, new Point(1 + day * 3, 2 + week), style);
816
+ // Paint the gap between consecutive in-range days
817
+ if (day < 6) {
818
+ const nextDate = grid[week][day + 1];
819
+ const thisInRange = isSelected || isInRange;
820
+ const nextInRange = this.#isSameDay(nextDate, this.#date) || this.#isInRange(nextDate);
821
+ if (thisInRange && nextInRange) {
822
+ const gapStyle = this.#rangeGapStyle;
823
+ viewport.write(' ', new Point(1 + day * 3 + 2, 2 + week), gapStyle);
824
+ }
825
+ }
826
+ }
827
+ if (this.#selection === 'range' &&
828
+ this.#rangeStart &&
829
+ this.#rangeEnd &&
830
+ !this.#isSameDay(this.#rangeStart, this.#rangeEnd)) {
831
+ // Paint the leading gap before the first in-range day of each row
832
+ // (the space at position 0 on the row, before the first day column)
833
+ const firstDate = grid[week][0];
834
+ const firstInRange = this.#isSameDay(firstDate, this.#date) || this.#isInRange(firstDate);
835
+ if (firstInRange) {
836
+ const gapStyle = this.#rangeGapStyle;
837
+ viewport.write(' ', new Point(0, 2 + week), gapStyle);
838
+ }
839
+ // Paint the trailing gap after the last in-range day of each row
840
+ const lastDate = grid[week][6];
841
+ const lastInRange = this.#isSameDay(lastDate, this.#date) || this.#isInRange(lastDate);
842
+ if (lastInRange) {
843
+ const gapStyle = this.#rangeGapStyle;
844
+ viewport.write(' ', new Point(1 + 6 * 3 + 2, 2 + week), gapStyle);
845
+ }
846
+ }
847
+ }
848
+ // Focus border
849
+ if (this.#hasFocus) {
850
+ const borderStyle = weekdayStyle;
851
+ // Top row (weekday header row): ╭...╮ with ─ between headers
852
+ viewport.write('╭', new Point(0, 1), borderStyle);
853
+ viewport.write('╮', new Point(CALENDAR_WIDTH - 1, 1), borderStyle);
854
+ for (let i = 0; i < 6; i++) {
855
+ viewport.write('─', new Point(3 + i * 3, 1), borderStyle);
856
+ }
857
+ // Middle rows: │...│
858
+ for (let week = 0; week < 5; week++) {
859
+ viewport.write('│', new Point(0, 2 + week), borderStyle);
860
+ viewport.write('│', new Point(CALENDAR_WIDTH - 1, 2 + week), borderStyle);
861
+ }
862
+ // Bottom row (last week row): ╰...╯ with ─ between days
863
+ viewport.write('╰', new Point(0, 7), borderStyle);
864
+ viewport.write('╯', new Point(CALENDAR_WIDTH - 1, 7), borderStyle);
865
+ for (let i = 0; i < 6; i++) {
866
+ viewport.write('─', new Point(3 + i * 3, 7), borderStyle);
655
867
  }
656
868
  }
657
869
  }
658
870
  #renderMonths(viewport) {
659
871
  const textStyle = this.theme.text();
660
872
  const headerStyle = this.theme.ui({ isHover: false });
661
- const selectedStyle = this.#selectedStyle;
662
873
  const currentMonth = this.#visibleDate.getMonth();
663
874
  viewport.paint(textStyle, this.#widgetRect(viewport));
664
875
  // Header
665
- const monthName = MONTH_NAMES[currentMonth];
876
+ const monthName = this.#formatMonthTitle(this.#visibleDate);
666
877
  const headerText = monthName;
667
- const headerX = Math.max(0, Math.floor((CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH - headerText.length) / 2) + 1);
878
+ const headerX = Math.max(0, Math.floor((CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH - lineWidth(headerText)) / 2) + 1);
668
879
  viewport.write(headerText, new Point(headerX, 0), headerStyle);
669
- viewport.write(' × ', new Point(CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH, 0), headerStyle);
880
+ const closeStyle = this.#hoverClose ? this.#buttonHoverStyle : headerStyle;
881
+ viewport.write(` ${CLOSE} `, new Point(CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH, 0), closeStyle);
670
882
  // Month grid: 4 rows × 3 columns
671
883
  for (let row = 0; row < 4; row++) {
672
884
  for (let col = 0; col < 3; col++) {
673
885
  const m = row * 3 + col;
674
886
  const name = MONTH_SHORT[m];
675
887
  const isSelected = m === currentMonth;
676
- const style = isSelected ? selectedStyle : textStyle;
888
+ const isHover = this.#hoverMonthLabel === name;
889
+ let style;
890
+ if (isSelected) {
891
+ style = this.#selectedStyle;
892
+ }
893
+ else if (isHover) {
894
+ style = this.#hoverStyle;
895
+ }
896
+ else {
897
+ style = textStyle;
898
+ }
677
899
  // Each column is ~7 chars wide (fits in 22 width: 7*3=21 + 1 padding)
678
900
  const x = 1 + col * 7;
679
901
  const y = 2 + row;
@@ -685,36 +907,48 @@ export class Calendar extends View {
685
907
  #renderYears(viewport) {
686
908
  const textStyle = this.theme.text();
687
909
  const headerStyle = this.theme.ui({ isHover: false });
688
- const selectedStyle = this.#selectedStyle;
689
910
  const currentYear = this.#visibleDate.getFullYear();
690
911
  viewport.paint(textStyle, this.#widgetRect(viewport));
691
912
  // Header
692
913
  const headerText = 'Year';
693
- const headerX = Math.max(0, Math.floor((CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH - headerText.length) / 2) + 1);
914
+ const headerX = Math.max(0, Math.floor((CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH - lineWidth(headerText)) / 2) + 1);
694
915
  viewport.write(headerText, new Point(headerX, 0), headerStyle);
695
- viewport.write(' × ', new Point(CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH, 0), headerStyle);
916
+ const closeStyle = this.#hoverClose ? this.#buttonHoverStyle : headerStyle;
917
+ viewport.write(` ${CLOSE} `, new Point(CALENDAR_WIDTH - CLOSE_BUTTON_WIDTH, 0), closeStyle);
696
918
  // Scroll indicator
697
- viewport.write('↑', new Point(Math.floor(CALENDAR_WIDTH / 2), 1), headerStyle);
919
+ const prevStyle = this.#hoverPrevButton
920
+ ? this.#buttonHoverStyle
921
+ : headerStyle;
922
+ viewport.write(ARROW_UP, new Point(Math.floor((CALENDAR_WIDTH - lineWidth(ARROW_UP)) / 2), 1), prevStyle);
698
923
  // 5 visible years
699
924
  const baseYear = currentYear - 2 + this.#yearScrollOffset;
700
925
  for (let i = 0; i < 5; i++) {
701
926
  const year = baseYear + i;
702
- const isSelected = year === currentYear;
703
- const style = isSelected ? selectedStyle : textStyle;
704
927
  const yearStr = String(year);
705
- const x = Math.floor((CALENDAR_WIDTH - yearStr.length) / 2);
928
+ const isSelected = year === currentYear;
929
+ const isHover = this.#hoverYearLabel === yearStr;
930
+ let style;
931
+ if (isSelected) {
932
+ style = this.#selectedStyle;
933
+ }
934
+ else if (isHover) {
935
+ style = this.#hoverStyle;
936
+ }
937
+ else {
938
+ style = textStyle;
939
+ }
940
+ const x = Math.floor((CALENDAR_WIDTH - lineWidth(yearStr)) / 2);
706
941
  viewport.write(yearStr, new Point(x, 2 + i), style);
707
942
  }
708
943
  // Down arrow
709
- viewport.write('↓', new Point(Math.floor(CALENDAR_WIDTH / 2), 7), headerStyle);
944
+ const nextStyle = this.#hoverNextButton
945
+ ? this.#buttonHoverStyle
946
+ : headerStyle;
947
+ viewport.write(ARROW_DOWN, new Point(Math.floor((CALENDAR_WIDTH - lineWidth(ARROW_DOWN)) / 2), 7), nextStyle);
710
948
  // Search line (if searching)
711
949
  if (this.#yearSearch) {
712
950
  const searchLabel = `Search: ${this.#yearSearch}`;
713
- viewport.write(searchLabel, new Point(0, 7), new Style({
714
- dim: true,
715
- foreground: textStyle.foreground,
716
- background: textStyle.background,
717
- }));
951
+ viewport.write(searchLabel, new Point(0, 7), textStyle.merge({ dim: true }));
718
952
  }
719
953
  }
720
954
  }