@cueloop/client 0.1.0-alpha.20 → 0.1.0-alpha.21
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/package.json +5 -5
- package/src/App.plan-review.test.tsx +61 -0
- package/src/App.rendering.test.tsx +6 -2
- package/src/App.review-panel.test.tsx +97 -0
- package/src/App.tsx +95 -22
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.tsx +66 -13
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +116 -0
- package/src/components/ReviewRail.tsx +14 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +140 -0
- package/src/components/primitives/Card.tsx +2 -1
- package/src/components/primitives/Dialog.tsx +2 -1
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/stories-app.tsx +2 -1
- package/src/config.test.ts +51 -1
- package/src/config.ts +57 -22
- package/src/keymap.test.ts +44 -4
- package/src/keymap.ts +30 -5
- package/src/review-panel.test.ts +111 -0
- package/src/review-panel.ts +76 -0
- package/src/session-controller.ts +17 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import { REVIEW_MAX_WIDTH, REVIEW_MIN_WIDTH } from "../review-panel";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { ReviewPanel } from "./ReviewPanel";
|
|
6
|
+
import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
|
|
7
|
+
|
|
8
|
+
export const meta: StoryMeta = { title: "ReviewPanel" };
|
|
9
|
+
|
|
10
|
+
const rail = {
|
|
11
|
+
session: fixturePlanSession(),
|
|
12
|
+
selectedId: FIXTURE_ANNOTATIONS[0]!.id,
|
|
13
|
+
resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
|
|
14
|
+
railTab: "review" as const,
|
|
15
|
+
pendingCount: 2,
|
|
16
|
+
cardEdit: null,
|
|
17
|
+
submitConfirm: null,
|
|
18
|
+
onTabChange: () => {},
|
|
19
|
+
onSelectCard: () => {},
|
|
20
|
+
onActivateCard: () => {},
|
|
21
|
+
onSubmitRequest: () => {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
|
|
25
|
+
|
|
26
|
+
/** The plan area to the left of the divider, so the layout reads in context. */
|
|
27
|
+
function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
28
|
+
return (
|
|
29
|
+
<box style={{ flexDirection: "row", width: "100%", height: "100%", backgroundColor: DARK.bg }}>
|
|
30
|
+
<box style={{ flexGrow: 1, paddingLeft: 1 }}>
|
|
31
|
+
<text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
|
|
32
|
+
</box>
|
|
33
|
+
{children}
|
|
34
|
+
</box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ExpandedNarrow: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<PanelFrame>
|
|
41
|
+
<ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} height={24} dragging={false} rail={rail} {...panelCallbacks} />
|
|
42
|
+
</PanelFrame>
|
|
43
|
+
),
|
|
44
|
+
expectedColors: [DARK.panel, DARK.border],
|
|
45
|
+
size: { width: 80, height: 24 },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const ExpandedWide: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<PanelFrame>
|
|
51
|
+
<ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} height={24} dragging={false} rail={rail} {...panelCallbacks} />
|
|
52
|
+
</PanelFrame>
|
|
53
|
+
),
|
|
54
|
+
expectedColors: [DARK.panel, DARK.green],
|
|
55
|
+
size: { width: 90, height: 24 },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Dragging accents the divider glyph column. */
|
|
59
|
+
export const DraggingDivider: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<PanelFrame>
|
|
62
|
+
<ReviewPanel mode="expanded" width={34} height={24} dragging={true} rail={rail} {...panelCallbacks} />
|
|
63
|
+
</PanelFrame>
|
|
64
|
+
),
|
|
65
|
+
expectedColors: [DARK.accent],
|
|
66
|
+
size: { width: 80, height: 24 },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Compact: Story = {
|
|
70
|
+
render: () => (
|
|
71
|
+
<PanelFrame>
|
|
72
|
+
<ReviewPanel mode="compact" width={34} height={24} dragging={false} rail={rail} {...panelCallbacks} />
|
|
73
|
+
</PanelFrame>
|
|
74
|
+
),
|
|
75
|
+
expectedColors: [DARK.accent, DARK.green],
|
|
76
|
+
size: { width: 60, height: 24 },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Hidden: Story = {
|
|
80
|
+
render: () => (
|
|
81
|
+
<PanelFrame>
|
|
82
|
+
<ReviewPanel mode="hidden" width={34} height={24} dragging={false} rail={rail} {...panelCallbacks} />
|
|
83
|
+
</PanelFrame>
|
|
84
|
+
),
|
|
85
|
+
size: { width: 60, height: 24 },
|
|
86
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review surface's outer shell: the grabbable divider plus the rail in one
|
|
3
|
+
* of three modes - expanded (the full ReviewRail, resizable), compact (a narrow
|
|
4
|
+
* count + dots strip that hands width back to the plan), or hidden (nothing at
|
|
5
|
+
* all, no leftover tab). The keybinding cycles all three; the clickable chevron
|
|
6
|
+
* toggles expanded and compact only. App owns the width/mode state and the drag
|
|
7
|
+
* on the container; this file only reads them. The geometry (width clamp, the
|
|
8
|
+
* divider glyph column, the cycle) lives in review-panel.ts so this stays
|
|
9
|
+
* declarative.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from "react";
|
|
13
|
+
import type { Annotation } from "@cueloop/schema";
|
|
14
|
+
import type { Theme } from "../theme";
|
|
15
|
+
import { useComponentTheme } from "./theme-context";
|
|
16
|
+
import { ReviewRail, type ReviewRailHandle, type ReviewRailProps } from "./ReviewRail";
|
|
17
|
+
import { REVIEW_COMPACT_WIDTH, reviewRowsToDivider, type ReviewPanelMode } from "../review-panel";
|
|
18
|
+
|
|
19
|
+
export interface ReviewPanelProps {
|
|
20
|
+
mode: ReviewPanelMode;
|
|
21
|
+
/** Expanded-rail width in columns (already clamped by the app). */
|
|
22
|
+
width: number;
|
|
23
|
+
/** Rows for the divider glyph column - the height of the plan/rail row. */
|
|
24
|
+
height: number;
|
|
25
|
+
/** Accent the divider while a drag is live. */
|
|
26
|
+
dragging: boolean;
|
|
27
|
+
/** Arm a divider drag; the app tracks the drag itself on the container. */
|
|
28
|
+
onDividerGrab: () => void;
|
|
29
|
+
/** Chevron click: expanded <-> compact (never hidden). */
|
|
30
|
+
onToggle: () => void;
|
|
31
|
+
/** The expanded ReviewRail's props; width and onCollapse are supplied here. */
|
|
32
|
+
rail: Omit<ReviewRailProps, "width" | "onCollapse" | "theme">;
|
|
33
|
+
railRef?: React.Ref<ReviewRailHandle>;
|
|
34
|
+
theme?: Theme;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A single-column stack of `│`, accent while dragging. Grabbing arms a drag. */
|
|
38
|
+
function ReviewDivider({
|
|
39
|
+
dragging,
|
|
40
|
+
rows,
|
|
41
|
+
onGrab,
|
|
42
|
+
theme,
|
|
43
|
+
}: {
|
|
44
|
+
dragging: boolean;
|
|
45
|
+
rows: number;
|
|
46
|
+
onGrab: () => void;
|
|
47
|
+
theme?: Theme;
|
|
48
|
+
}): React.ReactNode {
|
|
49
|
+
const tokens = useComponentTheme(theme);
|
|
50
|
+
return (
|
|
51
|
+
<box style={{ width: 1 }} onMouseDown={onGrab}>
|
|
52
|
+
<text fg={dragging ? tokens.accent : tokens.border}>{reviewRowsToDivider(rows)}</text>
|
|
53
|
+
</box>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The compact strip: the card count, one kind-colored dot per card, and the
|
|
58
|
+
* `«` chevron left-bound to match the expanded `»` gap. */
|
|
59
|
+
function CompactRail({
|
|
60
|
+
annotations,
|
|
61
|
+
onExpand,
|
|
62
|
+
theme,
|
|
63
|
+
}: {
|
|
64
|
+
annotations: Annotation[];
|
|
65
|
+
onExpand: () => void;
|
|
66
|
+
theme?: Theme;
|
|
67
|
+
}): React.ReactNode {
|
|
68
|
+
const tokens = useComponentTheme(theme);
|
|
69
|
+
const dotColor = (annotation: Annotation): string =>
|
|
70
|
+
annotation.kind === "suggestion" ? tokens.green : tokens.accent;
|
|
71
|
+
return (
|
|
72
|
+
<box
|
|
73
|
+
style={{ width: REVIEW_COMPACT_WIDTH, backgroundColor: tokens.panel, flexDirection: "column", paddingTop: 1 }}
|
|
74
|
+
>
|
|
75
|
+
<box style={{ alignItems: "center" }}>
|
|
76
|
+
<text fg={tokens.accent}>{String(annotations.length)}</text>
|
|
77
|
+
</box>
|
|
78
|
+
<box style={{ height: 1 }} />
|
|
79
|
+
<box style={{ flexDirection: "column", alignItems: "center" }}>
|
|
80
|
+
{annotations.map((annotation) => (
|
|
81
|
+
<text key={annotation.id} fg={dotColor(annotation)}>
|
|
82
|
+
●
|
|
83
|
+
</text>
|
|
84
|
+
))}
|
|
85
|
+
</box>
|
|
86
|
+
<box style={{ flexGrow: 1 }} />
|
|
87
|
+
<box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
|
|
88
|
+
<text fg={tokens.textDim}>«</text>
|
|
89
|
+
</box>
|
|
90
|
+
</box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function ReviewPanel({
|
|
95
|
+
mode,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
dragging,
|
|
99
|
+
onDividerGrab,
|
|
100
|
+
onToggle,
|
|
101
|
+
rail,
|
|
102
|
+
railRef,
|
|
103
|
+
theme,
|
|
104
|
+
}: ReviewPanelProps): React.ReactNode {
|
|
105
|
+
if (mode === "hidden") return null;
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<ReviewDivider dragging={dragging} rows={height} onGrab={onDividerGrab} theme={theme} />
|
|
109
|
+
{mode === "expanded" ? (
|
|
110
|
+
<ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
|
|
111
|
+
) : (
|
|
112
|
+
<CompactRail annotations={rail.session.annotations} onExpand={onToggle} theme={theme} />
|
|
113
|
+
)}
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -40,8 +40,13 @@ export interface ReviewRailProps {
|
|
|
40
40
|
onSelectCard: (id: string) => void;
|
|
41
41
|
onActivateCard: (id: string) => void;
|
|
42
42
|
onSubmitRequest: () => void;
|
|
43
|
-
/** Rail column width; the app derives it from the
|
|
43
|
+
/** Rail column width; the app derives it from the persisted review layout. */
|
|
44
44
|
width?: number;
|
|
45
|
+
/**
|
|
46
|
+
* When set, renders the muted `»` chevron pinned at the rail's bottom-left,
|
|
47
|
+
* one column from the divider. Clicking it collapses the panel to compact.
|
|
48
|
+
*/
|
|
49
|
+
onCollapse?: () => void;
|
|
45
50
|
theme?: Theme;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -64,6 +69,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
64
69
|
onActivateCard,
|
|
65
70
|
onSubmitRequest,
|
|
66
71
|
width = 34,
|
|
72
|
+
onCollapse,
|
|
67
73
|
theme,
|
|
68
74
|
}: ReviewRailProps,
|
|
69
75
|
handleRef,
|
|
@@ -141,6 +147,13 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
141
147
|
)}
|
|
142
148
|
</>
|
|
143
149
|
)}
|
|
150
|
+
{/* the collapse chevron: muted, left-bound, one column from the divider.
|
|
151
|
+
» points right - it hands the width back toward the plan */}
|
|
152
|
+
{onCollapse ? (
|
|
153
|
+
<box style={{ flexDirection: "row" }} onMouseUp={onCollapse}>
|
|
154
|
+
<text fg={tokens.textDim}>»</text>
|
|
155
|
+
</box>
|
|
156
|
+
) : null}
|
|
144
157
|
</box>
|
|
145
158
|
);
|
|
146
159
|
});
|
|
@@ -1315,5 +1315,145 @@ exports[`stories catalog WalkWizard/WithAgentNote renders, snapshots, and carrie
|
|
|
1315
1315
|
|
|
1316
1316
|
|
|
1317
1317
|
|
|
1318
|
+
"
|
|
1319
|
+
`;
|
|
1320
|
+
|
|
1321
|
+
exports[`stories catalog ReviewPanel/Compact renders, snapshots, and carries its colors: ReviewPanel/Compact 1`] = `
|
|
1322
|
+
" The plan takes the width the panel hands back. │
|
|
1323
|
+
│ 2
|
|
1324
|
+
│
|
|
1325
|
+
│ ●
|
|
1326
|
+
│ ●
|
|
1327
|
+
│
|
|
1328
|
+
│
|
|
1329
|
+
│
|
|
1330
|
+
│
|
|
1331
|
+
│
|
|
1332
|
+
│
|
|
1333
|
+
│
|
|
1334
|
+
│
|
|
1335
|
+
│
|
|
1336
|
+
│
|
|
1337
|
+
│
|
|
1338
|
+
│
|
|
1339
|
+
│
|
|
1340
|
+
│
|
|
1341
|
+
│
|
|
1342
|
+
│
|
|
1343
|
+
│
|
|
1344
|
+
│
|
|
1345
|
+
│ «
|
|
1346
|
+
"
|
|
1347
|
+
`;
|
|
1348
|
+
|
|
1349
|
+
exports[`stories catalog ReviewPanel/DraggingDivider renders, snapshots, and carries its colors: ReviewPanel/DraggingDivider 1`] = `
|
|
1350
|
+
" The plan takes the width the panel hands │
|
|
1351
|
+
back. │ Review (2) Agent
|
|
1352
|
+
│
|
|
1353
|
+
│ ▸ COMMENT · pending
|
|
1354
|
+
│ "persists sessions"
|
|
1355
|
+
│ Which daemon owns this?
|
|
1356
|
+
│
|
|
1357
|
+
│ SUGGESTION · pending
|
|
1358
|
+
│ "move the store"
|
|
1359
|
+
│ move the store behind one i…
|
|
1360
|
+
│
|
|
1361
|
+
│
|
|
1362
|
+
│
|
|
1363
|
+
│
|
|
1364
|
+
│
|
|
1365
|
+
│
|
|
1366
|
+
│
|
|
1367
|
+
│
|
|
1368
|
+
│
|
|
1369
|
+
│
|
|
1370
|
+
│
|
|
1371
|
+
│
|
|
1372
|
+
│ Submit review (2) ⏎
|
|
1373
|
+
│ »
|
|
1374
|
+
"
|
|
1375
|
+
`;
|
|
1376
|
+
|
|
1377
|
+
exports[`stories catalog ReviewPanel/ExpandedNarrow renders, snapshots, and carries its colors: ReviewPanel/ExpandedNarrow 1`] = `
|
|
1378
|
+
" The plan takes the width the panel hands back. │
|
|
1379
|
+
│ Review (2) Agent
|
|
1380
|
+
│
|
|
1381
|
+
│ ▸ COMMENT · pending
|
|
1382
|
+
│ "persists sessions"
|
|
1383
|
+
│ Which daemon owns
|
|
1384
|
+
│ this?
|
|
1385
|
+
│
|
|
1386
|
+
│ SUGGESTION · pending
|
|
1387
|
+
│ "move the store"
|
|
1388
|
+
│ move the store
|
|
1389
|
+
│ behind one i…
|
|
1390
|
+
│
|
|
1391
|
+
│
|
|
1392
|
+
│
|
|
1393
|
+
│
|
|
1394
|
+
│
|
|
1395
|
+
│
|
|
1396
|
+
│
|
|
1397
|
+
│
|
|
1398
|
+
│
|
|
1399
|
+
│
|
|
1400
|
+
│ Submit review (2) ⏎
|
|
1401
|
+
│ »
|
|
1402
|
+
"
|
|
1403
|
+
`;
|
|
1404
|
+
|
|
1405
|
+
exports[`stories catalog ReviewPanel/ExpandedWide renders, snapshots, and carries its colors: ReviewPanel/ExpandedWide 1`] = `
|
|
1406
|
+
" The plan takes the width the panel │
|
|
1407
|
+
hands back. │ Review (2) Agent
|
|
1408
|
+
│
|
|
1409
|
+
│ ▸ COMMENT · pending
|
|
1410
|
+
│ "persists sessions"
|
|
1411
|
+
│ Which daemon owns this?
|
|
1412
|
+
│
|
|
1413
|
+
│ SUGGESTION · pending
|
|
1414
|
+
│ "move the store"
|
|
1415
|
+
│ move the store behind one i…
|
|
1416
|
+
│
|
|
1417
|
+
│
|
|
1418
|
+
│
|
|
1419
|
+
│
|
|
1420
|
+
│
|
|
1421
|
+
│
|
|
1422
|
+
│
|
|
1423
|
+
│
|
|
1424
|
+
│
|
|
1425
|
+
│
|
|
1426
|
+
│
|
|
1427
|
+
│
|
|
1428
|
+
│ Submit review (2) ⏎
|
|
1429
|
+
│ »
|
|
1430
|
+
"
|
|
1431
|
+
`;
|
|
1432
|
+
|
|
1433
|
+
exports[`stories catalog ReviewPanel/Hidden renders, snapshots, and carries its colors: ReviewPanel/Hidden 1`] = `
|
|
1434
|
+
" The plan takes the width the panel hands back.
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
|
|
1318
1458
|
"
|
|
1319
1459
|
`;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import React from "react";
|
|
9
9
|
import type { Theme } from "../../theme";
|
|
10
10
|
import { useComponentTheme } from "../theme-context";
|
|
11
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
11
12
|
|
|
12
13
|
export interface CardProps {
|
|
13
14
|
title?: string;
|
|
@@ -44,7 +45,7 @@ export function Card({
|
|
|
44
45
|
marginLeft,
|
|
45
46
|
marginRight,
|
|
46
47
|
border: true,
|
|
47
|
-
borderStyle:
|
|
48
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
48
49
|
borderColor: borderColor ?? tokens.border,
|
|
49
50
|
backgroundColor: backgroundColor ?? tokens.elevated,
|
|
50
51
|
flexDirection: "column",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import type { Theme } from "../../theme";
|
|
9
9
|
import { useComponentTheme } from "../theme-context";
|
|
10
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
10
11
|
|
|
11
12
|
export interface DialogProps {
|
|
12
13
|
isOpen: boolean;
|
|
@@ -38,7 +39,7 @@ export function Dialog({ isOpen, title, width, height, theme, children }: Dialog
|
|
|
38
39
|
width,
|
|
39
40
|
height,
|
|
40
41
|
border: true,
|
|
41
|
-
borderStyle:
|
|
42
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
42
43
|
borderColor: tokens.accent,
|
|
43
44
|
backgroundColor: tokens.panel,
|
|
44
45
|
flexDirection: "column",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locks the single frame border rule: every bordered frame reads this token,
|
|
3
|
+
* so a regression to square corners (or any other style) fails here first,
|
|
4
|
+
* before the slower VRT snapshots catch it. Decision: rounded (issue #116).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { expect, test } from "bun:test";
|
|
8
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
9
|
+
|
|
10
|
+
test("frames use the rounded border style", () => {
|
|
11
|
+
expect(FRAME_BORDER_STYLE).toBe("rounded");
|
|
12
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one frame border rule for the design system. Every bordered frame -
|
|
3
|
+
* cards, dialogs, and the stories gallery chrome - reads its corner style
|
|
4
|
+
* from FRAME_BORDER_STYLE, so the rounded-corner decision lives in a single
|
|
5
|
+
* place and can never drift between frames. Buttons stay text-first and
|
|
6
|
+
* borderless: the frame they sit in carries the border, not the button.
|
|
7
|
+
*
|
|
8
|
+
* Decision: rounded on the frames, not on the buttons (issue #116).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BorderStyle } from "@opentui/core";
|
|
12
|
+
|
|
13
|
+
/** Corner style shared by every bordered frame in the app. */
|
|
14
|
+
export const FRAME_BORDER_STYLE: BorderStyle = "rounded";
|
|
@@ -11,6 +11,7 @@ import { createRoot, useKeyboard } from "@opentui/react";
|
|
|
11
11
|
import { DARK } from "../theme";
|
|
12
12
|
import { ThemeProvider } from "./theme-context";
|
|
13
13
|
import { loadStories, type LoadedStory } from "./story";
|
|
14
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
14
15
|
|
|
15
16
|
function StoriesApp({ stories, onExit }: { stories: LoadedStory[]; onExit: () => void }): React.ReactNode {
|
|
16
17
|
const moduleTitles = useMemo(() => [...new Set(stories.map((story) => story.moduleTitle))], [stories]);
|
|
@@ -60,7 +61,7 @@ function StoriesApp({ stories, onExit }: { stories: LoadedStory[]; onExit: () =>
|
|
|
60
61
|
flexGrow: 1,
|
|
61
62
|
flexDirection: "column",
|
|
62
63
|
border: true,
|
|
63
|
-
borderStyle:
|
|
64
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
64
65
|
borderColor: DARK.border,
|
|
65
66
|
}}
|
|
66
67
|
title={` ${current.storyName} (${moduleStories.indexOf(current) + 1}/${moduleStories.length}) `}
|
package/src/config.test.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { DEFAULT_KEYS, actionFor, loadConfig } from "./config";
|
|
5
|
+
import { DEFAULT_KEYS, actionFor, loadConfig, persistReviewState, persistReviewWidth } from "./config";
|
|
6
|
+
import { REVIEW_DEFAULT_WIDTH, REVIEW_MAX_WIDTH } from "./review-panel";
|
|
6
7
|
import { DARK } from "./theme";
|
|
7
8
|
|
|
8
9
|
describe("loadConfig", () => {
|
|
@@ -82,6 +83,55 @@ describe("loadConfig", () => {
|
|
|
82
83
|
rmSync(dir, { recursive: true, force: true });
|
|
83
84
|
}
|
|
84
85
|
});
|
|
86
|
+
|
|
87
|
+
test("[ui] review panel defaults to an expanded rail at the default width", () => {
|
|
88
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
89
|
+
expect(config.ui.reviewState).toBe("expanded");
|
|
90
|
+
expect(config.ui.reviewWidth).toBe(REVIEW_DEFAULT_WIDTH);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("[ui] parses review_width (clamped) and review_state", () => {
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg6-"));
|
|
95
|
+
const path = join(dir, "config.toml");
|
|
96
|
+
writeFileSync(path, `[ui]\nreview_width = 999\nreview_state = "compact"\n`);
|
|
97
|
+
try {
|
|
98
|
+
const config = loadConfig({ userConfigPath: path });
|
|
99
|
+
expect(config.ui.reviewWidth).toBe(REVIEW_MAX_WIDTH); // out-of-range width clamps
|
|
100
|
+
expect(config.ui.reviewState).toBe("compact");
|
|
101
|
+
} finally {
|
|
102
|
+
rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("[ui] ignores an unknown review_state and a non-numeric width", () => {
|
|
107
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg7-"));
|
|
108
|
+
const path = join(dir, "config.toml");
|
|
109
|
+
writeFileSync(path, `[ui]\nreview_width = "wide"\nreview_state = "sideways"\n`);
|
|
110
|
+
try {
|
|
111
|
+
const config = loadConfig({ userConfigPath: path });
|
|
112
|
+
expect(config.ui.reviewWidth).toBe(REVIEW_DEFAULT_WIDTH);
|
|
113
|
+
expect(config.ui.reviewState).toBe("expanded");
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("persistReviewWidth and persistReviewState round-trip through the config file", () => {
|
|
120
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg8-"));
|
|
121
|
+
const path = join(dir, "config.toml");
|
|
122
|
+
try {
|
|
123
|
+
persistReviewWidth(42, path);
|
|
124
|
+
persistReviewState("hidden", path);
|
|
125
|
+
const config = loadConfig({ userConfigPath: path });
|
|
126
|
+
expect(config.ui.reviewWidth).toBe(42);
|
|
127
|
+
expect(config.ui.reviewState).toBe("hidden");
|
|
128
|
+
// a second write replaces the key in place rather than appending a duplicate
|
|
129
|
+
persistReviewWidth(30, path);
|
|
130
|
+
expect(loadConfig({ userConfigPath: path }).ui.reviewWidth).toBe(30);
|
|
131
|
+
} finally {
|
|
132
|
+
rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
85
135
|
});
|
|
86
136
|
|
|
87
137
|
describe("integrations.obsidian config", () => {
|