tuiboard 0.9.0 → 0.9.1

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.1] - 2026-09-08
11
+
12
+ ### Fixed
13
+ - **Modals were unreadable in single-pane.** With one zone filling the screen a
14
+ dialog floated over it as an absolute overlay — and had nothing to paint over,
15
+ because the theme leaves panel backgrounds transparent so the terminal shows
16
+ through. The keyboard reference interleaved with the pane character by
17
+ character; the new-task and edit dialogs were effectively invisible. A modal
18
+ now takes the pane's place, exactly as the four-zone layout drops it into the
19
+ Agenda's slot: with one zone on screen, that zone *is* the slot.
20
+ - **A dialog standing in for a pane now fills it**, in both axes, like the zone
21
+ it replaces — instead of keeping a slot-sized box while the rest of the strip
22
+ sits empty, which read as a window that had failed to open. In the four-zone
23
+ layout it still matches the Agenda's slot to the cell, or the whole dashboard
24
+ would shift when a modal opens; verified by the frame borders landing on
25
+ identical columns with and without a modal.
26
+
10
27
  ## [0.9.0] - 2026-09-08
11
28
 
12
29
  ### Added
@@ -273,6 +290,7 @@ First public release on npm. This entry captures the full feature set at launch.
273
290
 
274
291
  Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
275
292
 
293
+ [0.9.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.9.1
276
294
  [0.9.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.9.0
277
295
  [0.8.5]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.5
278
296
  [0.8.4]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuiboard",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live Claude Code agent view. Use only the panels you want.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/ui/Modal.tsx CHANGED
@@ -11,7 +11,7 @@
11
11
  * checks `ui.modal` first and bails if set (only Escape passes through).
12
12
  */
13
13
 
14
- import { For, Show, createMemo, createSignal } from "solid-js";
14
+ import { For, Show, createContext, createMemo, createSignal, useContext } from "solid-js";
15
15
 
16
16
  import { isTask } from "~/parser/markdown";
17
17
  import {
@@ -29,14 +29,23 @@ import type { PriorityLevel, TimeBlock } from "~/types";
29
29
  * slot (the Dashboard renders it there while a modal is open) with no reflow. */
30
30
  const MODAL_WIDTH = AGENDA_WIDTH;
31
31
 
32
+ /**
33
+ * Whether the dialog is standing in for a whole pane (single-pane) or sitting
34
+ * in the Agenda's slot (four-zone). It decides how wide the dialog may be, and
35
+ * only ModalLayer is in a position to know.
36
+ */
37
+ const SinglePaneContext = createContext<() => boolean>(() => false);
38
+
32
39
  export function ModalLayer(props: { store: TuiStore }) {
33
40
  const modal = createMemo(() => props.store.state.ui.modal);
34
41
  return (
42
+ <SinglePaneContext.Provider value={() => props.store.singlePane()}>
35
43
  <Show when={modal()}>
36
44
  {/* Each modal's DialogShell IS the panel box (border + title + slot
37
45
  dimensions), so it drops into the Agenda's slot directly. */}
38
46
  <ModalRouter store={props.store} modal={modal()!} />
39
47
  </Show>
48
+ </SinglePaneContext.Provider>
40
49
  );
41
50
  }
42
51
 
@@ -194,7 +203,30 @@ function BoardNewModal(props: { store: TuiStore }) {
194
203
  );
195
204
  }
196
205
 
206
+ /**
207
+ * How wide the dialog may be.
208
+ *
209
+ * In the four-zone layout it takes the Agenda's slot and must match it to the
210
+ * cell, or the whole dashboard shifts when a modal opens — that invariant is
211
+ * why the requested width used to be ignored outright. In single-pane the
212
+ * dialog takes a whole pane's place instead, so there it can use what the
213
+ * terminal actually offers: the keyboard reference asks for 92 columns and is
214
+ * unreadable squeezed into 50, while a 60-column strip needs it to shrink
215
+ * rather than overflow.
216
+ */
217
+ function dialogWidth(singlePane: boolean): number {
218
+ if (!singlePane) return MODAL_WIDTH;
219
+ // Standing in for a pane means behaving like one: take the strip. A dialog
220
+ // that keeps its slot-sized box while the rest of the screen sits empty
221
+ // reads as a window that failed to open, not as a panel.
222
+ const terminal = process.stdout.columns ?? 80;
223
+ return Math.max(20, terminal - 4);
224
+ }
225
+
197
226
  function DialogShell(props: DialogShellProps) {
227
+ const singlePane = useContext(SinglePaneContext);
228
+ const width = () => dialogWidth(singlePane());
229
+ // `width` survives as the four-zone hint it always was; single-pane fills.
198
230
  void props.width;
199
231
  return (
200
232
  <box
@@ -204,9 +236,12 @@ function DialogShell(props: DialogShellProps) {
204
236
  // slot at the exact same size and the dashboard doesn't shift when a
205
237
  // modal opens. The title rides in the top border like the columns/zones.
206
238
  flexDirection: "column",
207
- width: MODAL_WIDTH,
208
- minWidth: MODAL_WIDTH,
209
- flexGrow: 0,
239
+ width: width(),
240
+ minWidth: Math.min(MODAL_WIDTH, width()),
241
+ // In the Agenda's slot the box must not grow, or the dashboard shifts.
242
+ // Filling a pane, it must — in both axes, like the zone it replaces.
243
+ flexGrow: singlePane() ? 1 : 0,
244
+ height: singlePane() ? "100%" : undefined,
210
245
  marginLeft: 1,
211
246
  backgroundColor: T.panelBgActive,
212
247
  border: true,
@@ -55,17 +55,19 @@ export function Dashboard(props: { store: TuiStore }) {
55
55
  * zone of the normal layout and BoardOnly already respects ui.zoomed
56
56
  * to render only the active panel between them.
57
57
  *
58
- * Modals: the normal layout drops them into the Agenda's slot, which doesn't
59
- * exist while zoomed so here the modal floats as a centered absolute overlay
60
- * on top of the zoomed view. The user stays zoomed; close the modal and the
61
- * zoomed view is exactly as they left it (no exit-zoom / re-zoom flip).
58
+ * Modals take the pane's place, exactly as the four-zone layout drops them
59
+ * into the Agenda's slot: with one zone filling the screen, that zone IS the
60
+ * slot. They used to float as an absolute overlay, which had nothing to paint
61
+ * over the theme leaves panel backgrounds transparent so the terminal shows
62
+ * through — so the dialog's text interleaved with the pane underneath and both
63
+ * became unreadable. Closing returns to the pane exactly as it was.
62
64
  */
63
65
  function ZoomedLayout(props: { store: TuiStore }) {
64
66
  const ui = () => props.store.state.ui;
65
67
  const zone = () => ui().activeZone;
66
68
 
67
69
  return (
68
- <>
70
+ <Show when={ui().modal} fallback={
69
71
  <Show when={zone() === "timeline"} fallback={
70
72
  <Show when={zone() === "agents"} fallback={<BoardOnly store={props.store} />}>
71
73
  <AgentsOnly store={props.store} />
@@ -73,27 +75,9 @@ function ZoomedLayout(props: { store: TuiStore }) {
73
75
  }>
74
76
  <TimelineOnly store={props.store} />
75
77
  </Show>
76
- {/* Modal overlay — only while zoomed AND a modal is open. Absolute + high
77
- zIndex so it paints over the zoomed view; centered; transparent
78
- backdrop so the board stays visible behind the (opaque) modal panel. */}
79
- <Show when={ui().modal}>
80
- <box
81
- style={{
82
- position: "absolute",
83
- top: 0,
84
- left: 0,
85
- right: 0,
86
- bottom: 0,
87
- zIndex: 100,
88
- flexDirection: "column",
89
- alignItems: "center",
90
- justifyContent: "center",
91
- }}
92
- >
93
- <ModalLayer store={props.store} />
94
- </box>
95
- </Show>
96
- </>
78
+ }>
79
+ <ModalLayer store={props.store} />
80
+ </Show>
97
81
  );
98
82
  }
99
83