Skip to content

Commit ec8a6c8

Browse files
Merge pull request #276 from RtlZeroMemory/codex/split-widgets-ui
refactor(core): split widget ui factories by family
2 parents 0e804dd + 093af04 commit ec8a6c8

File tree

10 files changed

+1053
-1580
lines changed

10 files changed

+1053
-1580
lines changed

packages/core/src/widgets/__tests__/composition.animationOrchestration.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ describe("composition animation hooks - orchestration", () => {
216216
const next = h.render((hooks) => useParallel(hooks, running));
217217
render = next;
218218
h.runPending(next.pendingEffects);
219-
return Math.abs((next.result[0]?.value ?? 0) - 10) <= 0.2;
219+
const entry = next.result[0];
220+
if (!entry) return false;
221+
return Math.abs(entry.value - 10) <= 0.2 && entry.isAnimating === false;
220222
});
221223

222224
assert.equal(render.result[0]?.isAnimating, false);
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type {
2+
CodeEditorProps,
3+
CommandPaletteProps,
4+
DialogProps,
5+
DiffViewerProps,
6+
DropdownProps,
7+
FilePickerProps,
8+
FileTreeExplorerProps,
9+
LayerProps,
10+
LogsConsoleProps,
11+
ModalProps,
12+
PanelGroupProps,
13+
ResizablePanelProps,
14+
SplitPaneProps,
15+
TableProps,
16+
ToastContainerProps,
17+
ToolApprovalDialogProps,
18+
TreeProps,
19+
VNode,
20+
} from "../types.js";
21+
import { text } from "./basic.js";
22+
import { type UiChild, filterChildren } from "./helpers.js";
23+
import { button } from "./interactive.js";
24+
25+
export function dialog(props: DialogProps): VNode {
26+
const { message, actions, onClose, ...modalProps } = props;
27+
const baseId = modalProps.id ?? "dialog";
28+
return {
29+
kind: "modal",
30+
props: {
31+
...modalProps,
32+
...(onClose !== undefined ? { onClose } : {}),
33+
content: typeof message === "string" ? text(message) : message,
34+
actions: actions.map((action, index) => {
35+
const intentProps = action.intent === undefined ? {} : { intent: action.intent };
36+
return button({
37+
id: action.id ?? `${baseId}-action-${String(index)}`,
38+
label: action.label,
39+
onPress: action.onPress,
40+
...intentProps,
41+
...(action.disabled === true ? { disabled: true } : {}),
42+
...(action.focusable === false ? { focusable: false } : {}),
43+
});
44+
}),
45+
},
46+
};
47+
}
48+
49+
export function modal(props: ModalProps): VNode {
50+
return { kind: "modal", props };
51+
}
52+
53+
export function dropdown(props: DropdownProps): VNode {
54+
return { kind: "dropdown", props };
55+
}
56+
57+
export function layer(props: LayerProps): VNode {
58+
return { kind: "layer", props };
59+
}
60+
61+
export function table<T>(props: TableProps<T>): VNode {
62+
return { kind: "table", props: props as TableProps<unknown> };
63+
}
64+
65+
export function tree<T>(props: TreeProps<T>): VNode {
66+
return { kind: "tree", props: props as TreeProps<unknown> };
67+
}
68+
69+
export function commandPalette(props: CommandPaletteProps): VNode {
70+
return { kind: "commandPalette", props };
71+
}
72+
73+
export function filePicker(props: FilePickerProps): VNode {
74+
return { kind: "filePicker", props };
75+
}
76+
77+
export function fileTreeExplorer(props: FileTreeExplorerProps): VNode {
78+
return { kind: "fileTreeExplorer", props };
79+
}
80+
81+
export function splitPane(props: SplitPaneProps, children: readonly UiChild[] = []): VNode {
82+
return { kind: "splitPane", props, children: filterChildren(children) };
83+
}
84+
85+
export function panelGroup(props: PanelGroupProps, children: readonly UiChild[] = []): VNode {
86+
return { kind: "panelGroup", props, children: filterChildren(children) };
87+
}
88+
89+
export function resizablePanel(
90+
props: ResizablePanelProps = {},
91+
children: readonly UiChild[] = [],
92+
): VNode {
93+
return { kind: "resizablePanel", props, children: filterChildren(children) };
94+
}
95+
96+
export function codeEditor(props: CodeEditorProps): VNode {
97+
return { kind: "codeEditor", props };
98+
}
99+
100+
export function diffViewer(props: DiffViewerProps): VNode {
101+
return { kind: "diffViewer", props };
102+
}
103+
104+
export function toolApprovalDialog(props: ToolApprovalDialogProps): VNode {
105+
return { kind: "toolApprovalDialog", props };
106+
}
107+
108+
export function logsConsole(props: LogsConsoleProps): VNode {
109+
return { kind: "logsConsole", props };
110+
}
111+
112+
export function toastContainer(props: ToastContainerProps): VNode {
113+
return { kind: "toastContainer", props };
114+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { TextStyle } from "../style.js";
2+
import type {
3+
BoxProps,
4+
ColumnProps,
5+
DividerProps,
6+
GridProps,
7+
RowProps,
8+
ScopedThemeOverride,
9+
SpacerProps,
10+
TextProps,
11+
VNode,
12+
} from "../types.js";
13+
import {
14+
type UiChild,
15+
filterChildren,
16+
isTextProps,
17+
maybeReverseChildren,
18+
resolveBoxPreset,
19+
} from "./helpers.js";
20+
21+
const EMPTY_TEXT_PROPS = Object.freeze({}) as TextProps;
22+
const DEFAULT_STACK_GAP = 1;
23+
24+
export function text(content: string): VNode;
25+
export function text(content: string, style: TextStyle): VNode;
26+
export function text(content: string, props: TextProps): VNode;
27+
export function text(content: string, styleOrProps?: TextStyle | TextProps): VNode {
28+
if (styleOrProps === undefined) return { kind: "text", text: content, props: EMPTY_TEXT_PROPS };
29+
if (isTextProps(styleOrProps)) return { kind: "text", text: content, props: styleOrProps };
30+
return { kind: "text", text: content, props: { style: styleOrProps } };
31+
}
32+
33+
export function box(props: BoxProps = {}, children: readonly UiChild[] = []): VNode {
34+
return { kind: "box", props: resolveBoxPreset(props), children: filterChildren(children) };
35+
}
36+
37+
export function row(props: RowProps = {}, children: readonly UiChild[] = []): VNode {
38+
const resolved = props.gap === undefined ? { gap: DEFAULT_STACK_GAP, ...props } : props;
39+
const filtered = filterChildren(children);
40+
return {
41+
kind: "row",
42+
props: resolved,
43+
children: maybeReverseChildren(filtered, resolved.reverse),
44+
};
45+
}
46+
47+
export function column(props: ColumnProps = {}, children: readonly UiChild[] = []): VNode {
48+
const resolved = props.gap === undefined ? { gap: DEFAULT_STACK_GAP, ...props } : props;
49+
const filtered = filterChildren(children);
50+
return {
51+
kind: "column",
52+
props: resolved,
53+
children: maybeReverseChildren(filtered, resolved.reverse),
54+
};
55+
}
56+
57+
export function themed(
58+
themeOverride: ScopedThemeOverride,
59+
children: readonly UiChild[] = [],
60+
): VNode {
61+
return { kind: "themed", props: { theme: themeOverride }, children: filterChildren(children) };
62+
}
63+
64+
export function grid(props: GridProps, ...children: UiChild[]): VNode {
65+
return { kind: "grid", props, children: filterChildren(children) };
66+
}
67+
68+
export function spacer(props: SpacerProps = {}): VNode {
69+
return { kind: "spacer", props };
70+
}
71+
72+
export function divider(props: DividerProps = {}): VNode {
73+
return { kind: "divider", props };
74+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type { RegisteredBinding } from "../../keybindings/index.js";
2+
import type {
3+
BadgeProps,
4+
CalloutProps,
5+
EmptyProps,
6+
ErrorBoundaryProps,
7+
ErrorDisplayProps,
8+
GaugeProps,
9+
IconProps,
10+
KbdProps,
11+
LinkProps,
12+
ProgressProps,
13+
RichTextProps,
14+
RichTextSpan,
15+
SkeletonProps,
16+
SpinnerProps,
17+
StatusProps,
18+
TagProps,
19+
VNode,
20+
} from "../types.js";
21+
import { column, divider, row, text } from "./basic.js";
22+
import type { KeybindingHelpOptions } from "./helpers.js";
23+
24+
export function icon(iconPath: string, props: Omit<IconProps, "icon"> = {}): VNode {
25+
return { kind: "icon", props: { icon: iconPath, ...props } };
26+
}
27+
28+
export function spinner(props: SpinnerProps = {}): VNode {
29+
return { kind: "spinner", props };
30+
}
31+
32+
export function progress(value: number, props: Omit<ProgressProps, "value"> = {}): VNode {
33+
return { kind: "progress", props: { value, ...props } };
34+
}
35+
36+
export function skeleton(width: number, props: Omit<SkeletonProps, "width"> = {}): VNode {
37+
return { kind: "skeleton", props: { width, ...props } };
38+
}
39+
40+
export function richText(
41+
spans: readonly RichTextSpan[],
42+
props: Omit<RichTextProps, "spans"> = {},
43+
): VNode {
44+
return { kind: "richText", props: { spans, ...props } };
45+
}
46+
47+
export function kbd(keys: string | readonly string[], props: Omit<KbdProps, "keys"> = {}): VNode {
48+
return { kind: "kbd", props: { keys, ...props } };
49+
}
50+
51+
function keybindingSequenceToKbdInput(sequence: string): string | readonly string[] {
52+
const normalized = sequence.trim();
53+
if (normalized.length === 0) return "";
54+
const parts = normalized.split(/\s+/);
55+
if (parts.length <= 1) return normalized;
56+
return Object.freeze(parts);
57+
}
58+
59+
function keybindingComparator(a: RegisteredBinding, b: RegisteredBinding): number {
60+
const byMode = a.mode.localeCompare(b.mode);
61+
if (byMode !== 0) return byMode;
62+
return a.sequence.localeCompare(b.sequence);
63+
}
64+
65+
export function keybindingHelp(
66+
bindings: readonly RegisteredBinding[],
67+
options: KeybindingHelpOptions = {},
68+
): VNode {
69+
const title = options.title ?? "Keyboard Shortcuts";
70+
const emptyText = options.emptyText ?? "No shortcuts registered.";
71+
const showMode =
72+
options.showMode ??
73+
(() => {
74+
const firstMode = bindings[0]?.mode;
75+
if (firstMode === undefined) return false;
76+
return bindings.some((binding) => binding.mode !== firstMode);
77+
})();
78+
79+
const rows = options.sort === false ? [...bindings] : [...bindings].sort(keybindingComparator);
80+
81+
return column(
82+
{
83+
gap: 1,
84+
...(options.key === undefined ? {} : { key: options.key }),
85+
},
86+
[
87+
text(title, { style: { bold: true } }),
88+
divider({ char: "·" }),
89+
rows.length > 0
90+
? column(
91+
{ gap: 0 },
92+
rows.map((binding, i) => {
93+
const keyTokens = keybindingSequenceToKbdInput(binding.sequence);
94+
const sequenceNode = Array.isArray(keyTokens)
95+
? kbd(keyTokens, { separator: " " })
96+
: kbd(keyTokens);
97+
const descriptionNode =
98+
binding.description === undefined
99+
? text("No description", { dim: true })
100+
: text(binding.description);
101+
return row(
102+
{
103+
key: `keybinding-help-${binding.mode}-${binding.sequence}-${String(i)}`,
104+
gap: 1,
105+
items: "center",
106+
wrap: true,
107+
},
108+
[
109+
sequenceNode,
110+
showMode ? text(`[${binding.mode}]`, { dim: true }) : null,
111+
descriptionNode,
112+
],
113+
);
114+
}),
115+
)
116+
: text(emptyText, { dim: true }),
117+
],
118+
);
119+
}
120+
121+
export function badge(textValue: string, props: Omit<BadgeProps, "text"> = {}): VNode {
122+
return { kind: "badge", props: { text: textValue, ...props } };
123+
}
124+
125+
export function status(
126+
statusValue: StatusProps["status"],
127+
props: Omit<StatusProps, "status"> = {},
128+
): VNode {
129+
return { kind: "status", props: { status: statusValue, ...props } };
130+
}
131+
132+
export function tag(textValue: string, props: Omit<TagProps, "text"> = {}): VNode {
133+
return { kind: "tag", props: { text: textValue, ...props } };
134+
}
135+
136+
export function gauge(value: number, props: Omit<GaugeProps, "value"> = {}): VNode {
137+
return { kind: "gauge", props: { value, ...props } };
138+
}
139+
140+
export function empty(title: string, props: Omit<EmptyProps, "title"> = {}): VNode {
141+
return { kind: "empty", props: { title, ...props } };
142+
}
143+
144+
export function errorDisplay(
145+
message: string,
146+
props: Omit<ErrorDisplayProps, "message"> = {},
147+
): VNode {
148+
return { kind: "errorDisplay", props: { message, ...props } };
149+
}
150+
151+
export function errorBoundary(props: ErrorBoundaryProps): VNode {
152+
return { kind: "errorBoundary", props };
153+
}
154+
155+
export function callout(message: string, props: Omit<CalloutProps, "message"> = {}): VNode {
156+
return { kind: "callout", props: { message, ...props } };
157+
}
158+
159+
export function link(props: LinkProps): VNode {
160+
return { kind: "link", props };
161+
}

0 commit comments

Comments
 (0)