Skip to content

Commit 85443d7

Browse files
Merge pull request #83 from foundersandcoders/feat/about-settings-history-app-shell
Give the About, History, and Settings Screens the App-Shell Treatment
2 parents b2e6d89 + 02c2df2 commit 85443d7

8 files changed

Lines changed: 806 additions & 464 deletions

File tree

docs/roadmaps/v5a-2606/enhanced.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ because the shell rollout and signature features build on them.
1515
|-------|--------------------------------------------------------------|--------|
1616
| **A** | Foundations (theme, layout primitives, keymap) | Complete |
1717
| **S** | Security & dependency maintenance (Dependabot) | Complete |
18-
| **B** | App-shell rollout across screens | In Progress (B1-B4 done) |
18+
| **B** | App-shell rollout across screens | Complete |
1919
| **C** | Signature UX features (help, toasts, progress, transitions) | Ready |
20-
| **D** | Polish (palette, command palette, dark mode, schema display) | Blocked (needs B) |
20+
| **D** | Polish (palette, command palette, dark mode, schema display) | Ready (B done) |
2121
| **E** | Tutorial & demo resources (Charm VHS recordings) | Blocked (needs B/C) |
2222

2323
> [!NOTE]
@@ -228,10 +228,40 @@ registry, and a header breadcrumb.
228228
commit `6ae7750`). Filter-tab selection aligned with the default
229229
`currentFilter` (`377ed0a`). Merged via PRs #79 (success), #80
230230
(check-results), #81 (validation-explorer).
231-
- [ ] **TR.B5** `refactor/mapping-screens-app-shell` — mapping-builder /
232-
-editor / -save; fixes the weak two-panel focus model. — **depends on TR.S2**
233-
- [ ] **TR.B6** `refactor/config-screens-app-shell` — settings + history + about.
234-
**depends on TR.S2**
231+
- [x] **TR.B5** `refactor/mapping-screens-app-shell` — mapping-builder /
232+
-editor / -save; fixes the weak two-panel focus model. — **depends on TR.S2**.
233+
**Done:** migrated mapping-builder, mapping-editor, and mapping-save onto
234+
`appShell()` + `panel()` + `Keymap`; unified the mapping-editor focus
235+
authority on the app-shell, replacing the previous weak two-panel focus
236+
model. Merged via PR #82.
237+
- [x] **TR.B6** `refactor/config-screens-app-shell` — settings + history + about.
238+
**depends on TR.S2**. **Done:** about, history, and settings all
239+
migrated onto `appShell()` + `panel()` + `Keymap`. History gets a
240+
two-pane layout (Submissions list + Detail) mirroring the check-results
241+
precedent, with the Validate/Cross-check/Delete keybar entries driven by
242+
`Keymap` `when` guards on the current selection. Along the way, found
243+
and fixed a real bug the migration surfaced: `SelectRenderable.selectedIndex`
244+
is write-only on the real `@opentui/core` API (setter only, no getter) —
245+
reading it always returns `undefined`; the read path is the
246+
`getSelectedIndex()` method instead. Fixed in history.ts and added
247+
`getSelectedIndex()` to the shared test double
248+
(`tests/fixtures/tui/opentui.ts`) so this class of bug fails tests going
249+
forward. **Not yet fixed:** `mapping-builder.ts:340,350` and
250+
`mapping-editor.ts:557,584` (merged in TR.B5) still read
251+
`.selectedIndex` directly and have the same latent bug — flagged for a
252+
follow-up fix, out of scope here. Settings is the most involved of the
253+
three migrations: inline text/dropdown edit renderables, a two-press
254+
reset-confirm, section-header skip-navigation, and directory fields that
255+
round-trip through the file-picker all carried over unchanged. The
256+
single status line that used to juggle five different messages
257+
(nav bar, edit prompt, dropdown prompt, reset-confirm, "Saved!") now
258+
routes through `shell.setFooter()` / `refreshFooter()`, mirroring
259+
history's pattern; "Saved!" self-clears after 2s via `setTimeout`
260+
instead of persisting until the next navigation. Save/Reset/Back
261+
bindings carry `when: () => !this.editing` guards so they neither fire
262+
nor show in the keybar mid-edit, replacing the old manual
263+
`if (this.editing) return` gate. All TUI screens are now on the
264+
app-shell framework — Phase B complete.
235265

236266
## Phase C — Signature UX features
237267

src/tui/screens/about.ts

Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
/** ====== About Iris Screen ======
22
* Read-only screen showing software metadata, version, and runtime info.
33
*/
4-
import {
5-
BoxRenderable,
6-
TextRenderable,
7-
type KeyEvent,
8-
t,
9-
fg,
10-
link,
11-
underline,
12-
} from '@opentui/core';
4+
import { TextRenderable, t, fg, link, underline } from '@opentui/core';
135
import type { RenderContext, Renderer } from '../types';
146
import { theme, PALETTE } from '../../../assets/brand/theme';
157
import type { Screen, ScreenResult, ScreenData } from '../utils/router';
16-
import { DEFAULT_CONFIG } from '../../lib/types/configTypes';
8+
import { appShell, panel, type AppShell, type Panel } from '../components';
9+
import { Keymap } from '../utils/keymap';
1710

1811
const CONTAINER_ID = 'about-root';
1912

@@ -23,59 +16,54 @@ import packageJson from '../../../package.json';
2316
export class AboutScreen implements Screen {
2417
readonly name = 'about';
2518
private renderer: Renderer;
26-
private keyHandler?: (key: KeyEvent) => void;
19+
private shell?: AppShell;
20+
private infoPanel?: Panel;
21+
private keymap?: Keymap;
2722

2823
constructor(ctx: RenderContext) {
2924
this.renderer = ctx.renderer;
3025
}
3126

3227
async render(_data?: ScreenData): Promise<ScreenResult> {
33-
this.buildUI();
34-
3528
return new Promise((resolve) => {
36-
this.keyHandler = (key: KeyEvent) => {
37-
if (key.name === 'escape' || key.name === 'q') {
38-
this.renderer.keyInput.off('keypress', this.keyHandler!);
39-
resolve({ action: 'pop' });
40-
}
41-
};
42-
this.renderer.keyInput.on('keypress', this.keyHandler);
29+
const keymap = this.buildUI(resolve);
30+
keymap.attach(this.renderer);
4331
});
4432
}
4533

4634
cleanup(): void {
47-
if (this.keyHandler) {
48-
this.renderer.keyInput.off('keypress', this.keyHandler);
49-
}
35+
this.keymap?.detach(this.renderer);
5036
this.renderer.root.remove(CONTAINER_ID);
5137
}
5238

53-
private buildUI(): void {
54-
const container = new BoxRenderable(this.renderer, {
39+
private buildUI(resolve: (result: ScreenResult) => void): Keymap {
40+
const finish = () => resolve({ action: 'pop' });
41+
this.keymap = new Keymap({
42+
bindings: [],
43+
onBack: finish,
44+
onQuit: finish,
45+
});
46+
const keymap = this.keymap;
47+
48+
this.shell = appShell(this.renderer, {
5549
id: CONTAINER_ID,
56-
flexDirection: 'column',
57-
width: '100%',
58-
height: '100%',
59-
backgroundColor: theme.background,
50+
breadcrumb: 'About',
51+
footer: keymap.toKeybar(),
6052
});
6153

62-
// Title
63-
container.add(new TextRenderable(this.renderer, {
64-
content: 'About Iris',
65-
fg: theme.primary,
66-
}));
54+
this.infoPanel = panel(this.renderer, { title: 'About Iris', flexGrow: 1 });
6755

68-
// Description (before fields)
69-
container.add(new TextRenderable(this.renderer, {
70-
content: ' ILR toolkit for converting learner data from CSV',
56+
// Description
57+
this.infoPanel.add(new TextRenderable(this.renderer, {
58+
content: 'ILR toolkit for converting learner data from CSV',
7159
fg: theme.textMuted,
7260
}));
73-
container.add(new TextRenderable(this.renderer, {
74-
content: ' exports into ILR-compliant XML for ESFA submission.',
61+
this.infoPanel.add(new TextRenderable(this.renderer, {
62+
content: 'exports into ILR-compliant XML for ESFA submission.',
7563
fg: theme.textMuted,
7664
}));
7765

78-
container.add(new TextRenderable(this.renderer, { content: '' }));
66+
this.infoPanel.add(new TextRenderable(this.renderer, { content: '' }));
7967

8068
// Software info
8169
const labelColour = theme.text;
@@ -92,26 +80,20 @@ export class AboutScreen implements Screen {
9280
for (const field of fields) {
9381
const padding = ' '.repeat(Math.max(1, 22 - field.label.length));
9482
if (field.url) {
95-
container.add(new TextRenderable(this.renderer, {
96-
content: t` ${fg(labelColour)(`${field.label}${padding}`)}${link(field.url)(underline(fg(linkColour)(field.value)))}`,
83+
this.infoPanel.add(new TextRenderable(this.renderer, {
84+
content: t`${fg(labelColour)(`${field.label}${padding}`)}${link(field.url)(underline(fg(linkColour)(field.value)))}`,
9785
}));
9886
} else {
99-
container.add(new TextRenderable(this.renderer, {
100-
content: ` ${field.label}${padding}${field.value}`,
87+
this.infoPanel.add(new TextRenderable(this.renderer, {
88+
content: `${field.label}${padding}${field.value}`,
10189
fg: labelColour,
10290
}));
10391
}
10492
}
10593

106-
// Fill remaining space (explicit background so it paints the theme colour, not the terminal default)
107-
container.add(new BoxRenderable(this.renderer, { flexGrow: 1, backgroundColor: theme.background }));
108-
109-
// Status bar
110-
container.add(new TextRenderable(this.renderer, {
111-
content: '[ESC] Back',
112-
fg: theme.textMuted,
113-
}));
94+
this.shell.content.add(this.infoPanel.box);
95+
this.renderer.root.add(this.shell.root);
11496

115-
this.renderer.root.add(container);
97+
return keymap;
11698
}
11799
}

0 commit comments

Comments
 (0)