@vybestack/llxprt-ui 0.7.0-nightly.251214.7c8736a50 → 0.7.0-nightly.251216.6bcf96e64
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 +75 -150
- 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 +41 -92
|
@@ -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,102 +1,53 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
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
|
-
// Get the directory of this source file, then navigate to the logo
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
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 LOGO_ASPECT_RATIO = 415 / 260;
|
|
25
7
|
|
|
26
8
|
interface HeaderBarProps {
|
|
27
9
|
readonly text: string;
|
|
28
10
|
readonly theme: ThemeDefinition;
|
|
29
11
|
}
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
function getPackageRoot(): string | undefined {
|
|
14
|
+
// Bun: import.meta.dir
|
|
15
|
+
// Node 20.11+: import.meta.dirname
|
|
16
|
+
const meta = import.meta as ImportMeta & {
|
|
17
|
+
readonly dir: string;
|
|
18
|
+
readonly dirname: string;
|
|
19
|
+
};
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} | null;
|
|
37
|
-
const resolution = caps?.pixelResolution ?? renderer.resolution ?? null;
|
|
38
|
-
const cellMetrics = renderer.getCellMetrics() ?? null;
|
|
21
|
+
return meta.dir || meta.dirname;
|
|
22
|
+
}
|
|
39
23
|
|
|
40
|
-
|
|
41
|
-
|
|
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);
|
|
24
|
+
export function HeaderBar({ text, theme }: HeaderBarProps) {
|
|
25
|
+
const headerHeight = 3;
|
|
52
26
|
|
|
53
|
-
|
|
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]);
|
|
27
|
+
const packageRoot = getPackageRoot();
|
|
64
28
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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),
|
|
98
|
-
);
|
|
99
|
-
const headerHeight = Math.max(logoHeightCells + 1, 3);
|
|
29
|
+
let logoPath: string;
|
|
30
|
+
try {
|
|
31
|
+
const root = packageRoot ?? process.cwd();
|
|
32
|
+
|
|
33
|
+
// Try theme-specific logo first
|
|
34
|
+
const themeLogoPath = resolve(
|
|
35
|
+
root,
|
|
36
|
+
'..',
|
|
37
|
+
'..',
|
|
38
|
+
'logos',
|
|
39
|
+
`${theme.slug}.png`,
|
|
40
|
+
);
|
|
41
|
+
if (existsSync(themeLogoPath)) {
|
|
42
|
+
logoPath = themeLogoPath;
|
|
43
|
+
} else {
|
|
44
|
+
// Fall back to default logo
|
|
45
|
+
logoPath = resolve(root, '..', '..', 'llxprt.png');
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Last resort fallback - use relative path for edge cases
|
|
49
|
+
logoPath = `../../../logos/${theme.slug}.png`;
|
|
50
|
+
}
|
|
100
51
|
|
|
101
52
|
return (
|
|
102
53
|
<box
|
|
@@ -113,22 +64,20 @@ export function HeaderBar({ text, theme }: HeaderBarProps): React.ReactNode {
|
|
|
113
64
|
backgroundColor: theme.colors.panel.headerBg ?? theme.colors.panel.bg,
|
|
114
65
|
alignItems: 'center',
|
|
115
66
|
flexDirection: 'row',
|
|
116
|
-
gap: 0,
|
|
117
67
|
justifyContent: 'flex-start',
|
|
118
68
|
}}
|
|
119
69
|
>
|
|
120
70
|
<image
|
|
121
|
-
src={
|
|
71
|
+
src={logoPath}
|
|
122
72
|
alt="LLxprt Code"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pixelHeight={scaledPixelHeight}
|
|
73
|
+
height={1}
|
|
74
|
+
aspectRatio={LOGO_ASPECT_RATIO}
|
|
75
|
+
backgroundColor={theme.colors.panel.headerBg ?? theme.colors.panel.bg}
|
|
127
76
|
style={{ marginRight: 1 }}
|
|
128
77
|
/>
|
|
129
78
|
<text
|
|
130
79
|
fg={theme.colors.panel.headerFg ?? theme.colors.text.primary}
|
|
131
|
-
style={{
|
|
80
|
+
style={{ alignSelf: 'center' }}
|
|
132
81
|
>
|
|
133
82
|
{text}
|
|
134
83
|
</text>
|