@vybestack/llxprt-ui 0.7.0-nightly.251214.7c8736a50 → 0.7.0-nightly.251215.66bf0bc39
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/bun.lock +74 -149
- package/logos/.gitkeep +1 -0
- package/logos/ansi-light.png +0 -0
- package/logos/default-light.png +0 -0
- package/logos/green-screen.png +0 -0
- package/package.json +4 -4
- package/scripts/image-harness.ts +618 -0
- package/scripts/visual-header-demo.ts +99 -0
- package/scripts/visual-regression.sh +187 -0
- package/src/ui/components/HeaderBar.tsx +15 -93
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { createCliRenderer } from '@vybestack/opentui-core';
|
|
5
|
+
import {
|
|
6
|
+
BoxRenderable,
|
|
7
|
+
ImageRenderable,
|
|
8
|
+
TextRenderable,
|
|
9
|
+
} from '@vybestack/opentui-core';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
type CliRenderer = Awaited<ReturnType<typeof createCliRenderer>>;
|
|
15
|
+
|
|
16
|
+
function formatError(err: unknown): string {
|
|
17
|
+
if (err instanceof Error) {
|
|
18
|
+
return err.stack ?? err.message;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return String(err);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main(): Promise<void> {
|
|
25
|
+
const logoPath = path.resolve(__dirname, '../llxprt.png');
|
|
26
|
+
let logo: Buffer;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
logo = readFileSync(logoPath);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new Error(`Failed to read logo: ${logoPath}\n${formatError(err)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bg = '#fafafa';
|
|
35
|
+
const border = '#2d7d46';
|
|
36
|
+
const fg = '#222222';
|
|
37
|
+
|
|
38
|
+
let renderer: CliRenderer | undefined;
|
|
39
|
+
try {
|
|
40
|
+
renderer = await createCliRenderer({
|
|
41
|
+
useAlternateScreen: false,
|
|
42
|
+
exitOnCtrlC: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const header = new BoxRenderable(renderer, {
|
|
46
|
+
id: 'header',
|
|
47
|
+
flexDirection: 'row',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'flex-start',
|
|
50
|
+
border: true,
|
|
51
|
+
height: 3,
|
|
52
|
+
minHeight: 3,
|
|
53
|
+
maxHeight: 3,
|
|
54
|
+
paddingTop: 0,
|
|
55
|
+
paddingBottom: 0,
|
|
56
|
+
paddingLeft: 1,
|
|
57
|
+
paddingRight: 1,
|
|
58
|
+
borderColor: border,
|
|
59
|
+
backgroundColor: bg,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const logoAspectRatio = 415 / 260;
|
|
63
|
+
const image = new ImageRenderable(renderer, {
|
|
64
|
+
id: 'logo',
|
|
65
|
+
src: logo,
|
|
66
|
+
height: 1,
|
|
67
|
+
aspectRatio: logoAspectRatio,
|
|
68
|
+
fit: 'contain',
|
|
69
|
+
backgroundColor: bg,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const title = new TextRenderable(renderer, {
|
|
73
|
+
id: 'title',
|
|
74
|
+
content: "LLxprt Code - I'm here to help",
|
|
75
|
+
marginLeft: 1,
|
|
76
|
+
fg,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
header.add(image);
|
|
80
|
+
header.add(title);
|
|
81
|
+
renderer.root.add(header);
|
|
82
|
+
|
|
83
|
+
renderer.start();
|
|
84
|
+
|
|
85
|
+
// Give pixel resolution a moment to arrive and settle any re-layout.
|
|
86
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
87
|
+
renderer.pause();
|
|
88
|
+
|
|
89
|
+
// Keep the frame visible for capture.
|
|
90
|
+
await new Promise((r) => setTimeout(r, 6000));
|
|
91
|
+
} finally {
|
|
92
|
+
renderer?.destroy();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await main().catch((err: unknown) => {
|
|
97
|
+
process.stderr.write(`visual-header-demo failed:\n${formatError(err)}\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
UNAME="$(uname)"
|
|
5
|
+
if [[ "${UNAME}" != "Darwin" ]]; then
|
|
6
|
+
echo "Error: visual regression capture requires macOS." >&2
|
|
7
|
+
exit 2
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
for cmd in osascript swift screencapture magick python3; do
|
|
11
|
+
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
|
12
|
+
echo "Error: required command not found: ${cmd}" >&2
|
|
13
|
+
exit 2
|
|
14
|
+
fi
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
ITERM_APP_PATH="${LLXPRT_UI_ITERM_APP_PATH:-/Applications/iTerm2.app}"
|
|
18
|
+
if [[ ! -d "${ITERM_APP_PATH}" ]]; then
|
|
19
|
+
echo "Error: iTerm2 is not installed at ${ITERM_APP_PATH}" >&2
|
|
20
|
+
echo "Install iTerm2 or set LLXPRT_UI_ITERM_APP_PATH to the correct path." >&2
|
|
21
|
+
exit 2
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
25
|
+
|
|
26
|
+
UPDATE_BASELINE=0
|
|
27
|
+
if [[ "${1:-}" == "--update-baseline" ]]; then
|
|
28
|
+
UPDATE_BASELINE=1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
ARTIFACTS_DIR="${ROOT_DIR}/packages/ui/visual-artifacts"
|
|
32
|
+
BASELINES_DIR="${ROOT_DIR}/packages/ui/visual-baselines"
|
|
33
|
+
BASELINE_PATH="${BASELINES_DIR}/header.png"
|
|
34
|
+
|
|
35
|
+
mkdir -p "${ARTIFACTS_DIR}"
|
|
36
|
+
mkdir -p "${BASELINES_DIR}"
|
|
37
|
+
|
|
38
|
+
RAW_PATH="${ARTIFACTS_DIR}/header-raw.png"
|
|
39
|
+
CROP_PATH="${ARTIFACTS_DIR}/header-crop.png"
|
|
40
|
+
DIFF_PATH="${ARTIFACTS_DIR}/header-diff.png"
|
|
41
|
+
|
|
42
|
+
# Window size (in points as used by macOS UI scripting).
|
|
43
|
+
WINDOW_SIZE="${LLXPRT_UI_VISUAL_WINDOW_SIZE:-1400,650}"
|
|
44
|
+
# Crop within the captured window: y,height
|
|
45
|
+
CROP_Y="${LLXPRT_UI_VISUAL_CROP_Y:-80}"
|
|
46
|
+
CROP_H="${LLXPRT_UI_VISUAL_CROP_H:-220}"
|
|
47
|
+
STARTUP_DELAY="${LLXPRT_UI_VISUAL_DELAY_SEC:-2}"
|
|
48
|
+
|
|
49
|
+
IFS=',' read -r WIN_W WIN_H <<< "${WINDOW_SIZE}"
|
|
50
|
+
|
|
51
|
+
if [[ -z "${WIN_W}" || -z "${WIN_H}" ]]; then
|
|
52
|
+
echo "Invalid LLXPRT_UI_VISUAL_WINDOW_SIZE: '${WINDOW_SIZE}'" >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
COMMAND="cd ${ROOT_DIR} && bun run packages/ui/scripts/visual-header-demo.ts"
|
|
57
|
+
|
|
58
|
+
WINDOW_ID="$(osascript <<APPLESCRIPT
|
|
59
|
+
on run
|
|
60
|
+
set winW to ${WIN_W}
|
|
61
|
+
set winH to ${WIN_H}
|
|
62
|
+
set cmd to "${COMMAND}"
|
|
63
|
+
|
|
64
|
+
tell application "iTerm2"
|
|
65
|
+
activate
|
|
66
|
+
set demoWindow to (create window with default profile)
|
|
67
|
+
tell current session of demoWindow
|
|
68
|
+
write text cmd
|
|
69
|
+
end tell
|
|
70
|
+
set demoId to id of demoWindow
|
|
71
|
+
end tell
|
|
72
|
+
|
|
73
|
+
delay ${STARTUP_DELAY}
|
|
74
|
+
|
|
75
|
+
tell application "System Events"
|
|
76
|
+
tell process "iTerm2"
|
|
77
|
+
set frontmost to true
|
|
78
|
+
set size of front window to {winW, winH}
|
|
79
|
+
end tell
|
|
80
|
+
end tell
|
|
81
|
+
|
|
82
|
+
return demoId
|
|
83
|
+
end run
|
|
84
|
+
APPLESCRIPT
|
|
85
|
+
)"
|
|
86
|
+
|
|
87
|
+
cleanup() {
|
|
88
|
+
if [[ -n "${WINDOW_ID:-}" ]]; then
|
|
89
|
+
osascript -e "tell application \"iTerm2\" to close (first window whose id is ${WINDOW_ID})" >/dev/null 2>&1 || true
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
trap cleanup EXIT
|
|
93
|
+
|
|
94
|
+
if [[ -z "${WINDOW_ID}" ]]; then
|
|
95
|
+
echo "Failed to create iTerm2 window for visual capture." >&2
|
|
96
|
+
exit 2
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
BOUNDS="$(swift - <<SWIFT
|
|
100
|
+
import Foundation
|
|
101
|
+
import CoreGraphics
|
|
102
|
+
|
|
103
|
+
let target = ${WINDOW_ID}
|
|
104
|
+
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
|
105
|
+
let infoList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? []
|
|
106
|
+
|
|
107
|
+
for info in infoList {
|
|
108
|
+
let owner = info[kCGWindowOwnerName as String] as? String ?? ""
|
|
109
|
+
if owner != "iTerm" { continue }
|
|
110
|
+
guard let number = info[kCGWindowNumber as String] as? Int, number == target else { continue }
|
|
111
|
+
guard let bounds = info[kCGWindowBounds as String] as? [String: Any] else { continue }
|
|
112
|
+
let x = bounds["X"] as? Int ?? 0
|
|
113
|
+
let y = bounds["Y"] as? Int ?? 0
|
|
114
|
+
let w = bounds["Width"] as? Int ?? 0
|
|
115
|
+
let h = bounds["Height"] as? Int ?? 0
|
|
116
|
+
print("\\(x),\\(y),\\(w),\\(h)")
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
SWIFT
|
|
120
|
+
)"
|
|
121
|
+
|
|
122
|
+
if [[ -z "${BOUNDS}" ]]; then
|
|
123
|
+
echo "Failed to locate iTerm window bounds for window id ${WINDOW_ID}." >&2
|
|
124
|
+
echo "Make sure iTerm2 is running and Accessibility permissions are granted." >&2
|
|
125
|
+
exit 2
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
IFS=',' read -r WIN_X WIN_Y WIN_W_FOUND WIN_H_FOUND <<< "${BOUNDS}"
|
|
129
|
+
if [[ -z "${WIN_X}" || -z "${WIN_Y}" || -z "${WIN_W_FOUND}" || -z "${WIN_H_FOUND}" ]]; then
|
|
130
|
+
echo "Failed to parse window bounds: '${BOUNDS}'" >&2
|
|
131
|
+
exit 2
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Capture the window region by coordinates (more reliable than -l on some setups).
|
|
135
|
+
screencapture -x -R "${WIN_X},${WIN_Y},${WIN_W_FOUND},${WIN_H_FOUND}" "${RAW_PATH}"
|
|
136
|
+
|
|
137
|
+
magick "${RAW_PATH}" -crop "${WIN_W_FOUND}x${CROP_H}+0+${CROP_Y}" +repage "${CROP_PATH}"
|
|
138
|
+
|
|
139
|
+
# Sanity-check the capture contains the expected green border (avoid saving wallpaper/blank captures).
|
|
140
|
+
GREEN_FRACTION="$(magick "${CROP_PATH}" -alpha off -fuzz 10% -fill white -opaque "#2d7d46" -fill black +opaque "#2d7d46" -colorspace Gray -format "%[fx:mean]" info:)"
|
|
141
|
+
if [[ -z "${GREEN_FRACTION}" ]]; then
|
|
142
|
+
echo "Failed to compute capture sanity check metric" >&2
|
|
143
|
+
exit 2
|
|
144
|
+
fi
|
|
145
|
+
python3 - <<PY
|
|
146
|
+
import sys
|
|
147
|
+
fraction=float("${GREEN_FRACTION}")
|
|
148
|
+
if fraction < 0.0005:
|
|
149
|
+
sys.stderr.write(f"Capture sanity check failed for ${CROP_PATH} (green border fraction={fraction}).\\n")
|
|
150
|
+
sys.stderr.write("This usually means macOS Screen Recording permission is missing for your terminal/shell.\\n")
|
|
151
|
+
sys.exit(2)
|
|
152
|
+
PY
|
|
153
|
+
|
|
154
|
+
if [[ "${UPDATE_BASELINE}" -eq 1 ]]; then
|
|
155
|
+
cp "${CROP_PATH}" "${BASELINE_PATH}"
|
|
156
|
+
echo "Updated baseline: ${BASELINE_PATH}"
|
|
157
|
+
trap - EXIT
|
|
158
|
+
cleanup
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
if [[ ! -f "${BASELINE_PATH}" ]]; then
|
|
163
|
+
echo "Baseline not found: ${BASELINE_PATH}" >&2
|
|
164
|
+
echo "Run: LLXPRT_UI_VISUAL_WINDOW_SIZE='${WINDOW_SIZE}' $(basename "${BASH_SOURCE[0]}") --update-baseline" >&2
|
|
165
|
+
exit 2
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
set +e
|
|
169
|
+
METRIC="$(magick compare -metric AE "${BASELINE_PATH}" "${CROP_PATH}" "${DIFF_PATH}" 2>&1)"
|
|
170
|
+
STATUS=$?
|
|
171
|
+
set -e
|
|
172
|
+
|
|
173
|
+
if [[ "${STATUS}" -eq 0 ]]; then
|
|
174
|
+
echo "Visual regression passed"
|
|
175
|
+
trap - EXIT
|
|
176
|
+
cleanup
|
|
177
|
+
exit 0
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
echo "Visual regression failed: ${METRIC} differing pixels"
|
|
181
|
+
echo "Artifacts:"
|
|
182
|
+
echo " baseline: ${BASELINE_PATH}"
|
|
183
|
+
echo " current : ${CROP_PATH}"
|
|
184
|
+
echo " diff : ${DIFF_PATH}"
|
|
185
|
+
trap - EXIT
|
|
186
|
+
cleanup
|
|
187
|
+
exit 1
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import { fileURLToPath } from 'node:url';
|
|
4
3
|
import React from 'react';
|
|
5
|
-
import { useRenderer } from '@vybestack/opentui-react';
|
|
6
|
-
import { useEffect, useState } from 'react';
|
|
7
4
|
import type { ThemeDefinition } from '../../features/theme';
|
|
8
|
-
import { getLogger } from '../../lib/logger';
|
|
9
5
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
15
|
-
const LOGO_PATH = path.resolve(__dirname, '../../../llxprt.png');
|
|
16
|
-
|
|
17
|
-
logger.debug('HeaderBar module loaded', {
|
|
18
|
-
__filename,
|
|
19
|
-
__dirname,
|
|
20
|
-
LOGO_PATH,
|
|
21
|
-
logoExists: existsSync(LOGO_PATH),
|
|
22
|
-
});
|
|
23
|
-
const LOGO_PX_WIDTH = 150;
|
|
24
|
-
const LOGO_PX_HEIGHT = 90;
|
|
6
|
+
const DEFAULT_LOGO_PATH = fileURLToPath(
|
|
7
|
+
new URL('../../../llxprt.png', import.meta.url),
|
|
8
|
+
);
|
|
9
|
+
const LOGO_ASPECT_RATIO = 415 / 260;
|
|
25
10
|
|
|
26
11
|
interface HeaderBarProps {
|
|
27
12
|
readonly text: string;
|
|
@@ -29,74 +14,13 @@ interface HeaderBarProps {
|
|
|
29
14
|
}
|
|
30
15
|
|
|
31
16
|
export function HeaderBar({ text, theme }: HeaderBarProps): React.ReactNode {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pixelResolution?: { width: number; height: number };
|
|
36
|
-
} | null;
|
|
37
|
-
const resolution = caps?.pixelResolution ?? renderer.resolution ?? null;
|
|
38
|
-
const cellMetrics = renderer.getCellMetrics() ?? null;
|
|
39
|
-
|
|
40
|
-
// Log graphics support and resolution detection
|
|
41
|
-
logger.debug('HeaderBar render', {
|
|
42
|
-
graphicsSupport: renderer.graphicsSupport,
|
|
43
|
-
termProgram: process.env.TERM_PROGRAM,
|
|
44
|
-
term: process.env.TERM,
|
|
45
|
-
resolution,
|
|
46
|
-
cellMetrics,
|
|
47
|
-
rendererResolution: renderer.resolution,
|
|
48
|
-
terminalWidth: renderer.terminalWidth,
|
|
49
|
-
terminalHeight: renderer.terminalHeight,
|
|
50
|
-
});
|
|
51
|
-
const [, setTick] = useState(0);
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
const refresh = () => setTick((t) => t + 1);
|
|
55
|
-
renderer.on('capabilities', refresh);
|
|
56
|
-
renderer.on('pixelResolution', refresh);
|
|
57
|
-
renderer.on('resize', refresh);
|
|
58
|
-
return () => {
|
|
59
|
-
renderer.off('capabilities', refresh);
|
|
60
|
-
renderer.off('pixelResolution', refresh);
|
|
61
|
-
renderer.off('resize', refresh);
|
|
62
|
-
};
|
|
63
|
-
}, [renderer]);
|
|
64
|
-
|
|
65
|
-
const pxPerCellX =
|
|
66
|
-
resolution && renderer.terminalWidth > 0
|
|
67
|
-
? resolution.width / renderer.terminalWidth
|
|
68
|
-
: null;
|
|
69
|
-
const pxPerCellY =
|
|
70
|
-
resolution && renderer.terminalHeight > 0
|
|
71
|
-
? resolution.height / renderer.terminalHeight
|
|
72
|
-
: null;
|
|
73
|
-
const desiredCellHeight = 2;
|
|
74
|
-
const scaleFactor = 0.9; // modest shrink to keep it inside the border
|
|
75
|
-
const fallbackPxPerCellX = 9;
|
|
76
|
-
const fallbackPxPerCellY = 20;
|
|
77
|
-
const scaledPixelHeight = Math.round(
|
|
78
|
-
pxPerCellY != null
|
|
79
|
-
? Math.min(
|
|
80
|
-
LOGO_PX_HEIGHT * scaleFactor,
|
|
81
|
-
pxPerCellY * desiredCellHeight * scaleFactor,
|
|
82
|
-
)
|
|
83
|
-
: LOGO_PX_HEIGHT * scaleFactor,
|
|
84
|
-
);
|
|
85
|
-
const scaledPixelWidth = Math.max(
|
|
86
|
-
1,
|
|
87
|
-
Math.round((scaledPixelHeight * LOGO_PX_WIDTH) / LOGO_PX_HEIGHT),
|
|
88
|
-
);
|
|
89
|
-
const effPxPerCellX = pxPerCellX ?? fallbackPxPerCellX;
|
|
90
|
-
const effPxPerCellY = pxPerCellY ?? fallbackPxPerCellY;
|
|
91
|
-
const logoWidthCells = Math.max(
|
|
92
|
-
1,
|
|
93
|
-
Math.ceil(scaledPixelWidth / effPxPerCellX),
|
|
94
|
-
);
|
|
95
|
-
const logoHeightCells = Math.max(
|
|
96
|
-
1,
|
|
97
|
-
Math.ceil(scaledPixelHeight / effPxPerCellY),
|
|
17
|
+
const headerHeight = 3;
|
|
18
|
+
const themeLogoPath = fileURLToPath(
|
|
19
|
+
new URL(`../../../logos/${theme.slug}.png`, import.meta.url),
|
|
98
20
|
);
|
|
99
|
-
const
|
|
21
|
+
const logoPath = existsSync(themeLogoPath)
|
|
22
|
+
? themeLogoPath
|
|
23
|
+
: DEFAULT_LOGO_PATH;
|
|
100
24
|
|
|
101
25
|
return (
|
|
102
26
|
<box
|
|
@@ -113,22 +37,20 @@ export function HeaderBar({ text, theme }: HeaderBarProps): React.ReactNode {
|
|
|
113
37
|
backgroundColor: theme.colors.panel.headerBg ?? theme.colors.panel.bg,
|
|
114
38
|
alignItems: 'center',
|
|
115
39
|
flexDirection: 'row',
|
|
116
|
-
gap: 0,
|
|
117
40
|
justifyContent: 'flex-start',
|
|
118
41
|
}}
|
|
119
42
|
>
|
|
120
43
|
<image
|
|
121
|
-
src={
|
|
44
|
+
src={logoPath}
|
|
122
45
|
alt="LLxprt Code"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pixelHeight={scaledPixelHeight}
|
|
46
|
+
height={1}
|
|
47
|
+
aspectRatio={LOGO_ASPECT_RATIO}
|
|
48
|
+
backgroundColor={theme.colors.panel.headerBg ?? theme.colors.panel.bg}
|
|
127
49
|
style={{ marginRight: 1 }}
|
|
128
50
|
/>
|
|
129
51
|
<text
|
|
130
52
|
fg={theme.colors.panel.headerFg ?? theme.colors.text.primary}
|
|
131
|
-
style={{
|
|
53
|
+
style={{ alignSelf: 'center' }}
|
|
132
54
|
>
|
|
133
55
|
{text}
|
|
134
56
|
</text>
|