Optimize binary detail page layout

- Move version history to collapsible sidebar
- Group versions by major version (iOS 15.x, 16.x, etc.)
- Move copy button inside XML code block
- Improve loading skeleton
- Side-by-side layout on larger screens
This commit is contained in:
cc
2026-04-14 17:39:47 +02:00
parent 52b377b6a7
commit 872dca0fb9
+163 -111
View File
@@ -104,72 +104,78 @@ export default function BinaryDetail() {
(h) => h.os.build === build || `${h.os.version}_${h.os.build}` === build,
);
// Group versions by major version
const groupedHistory = useMemo(() => {
const groups: { [major: string]: typeof availableHistory } = {};
for (const h of availableHistory) {
const major = h.os.version.split(".")[0];
if (!groups[major]) groups[major] = [];
groups[major].push(h);
}
return Object.entries(groups).sort(([a], [b]) => Number(b) - Number(a));
}, [availableHistory]);
const renderVersionLink = (h: typeof availableHistory[0]) => {
const isCurrent =
h.os.build === build || `${h.os.version}_${h.os.build}` === build;
const isComparing = compareWith === `${h.os.version}_${h.os.build}`;
const versionTag = `${h.os.version}_${h.os.build}`;
if (isCurrent) {
return (
<span
key={h.os.build}
className="block px-2 py-1 text-xs rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 font-medium"
>
{h.os.version} (current)
</span>
);
}
const href = isComparing
? addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}`)
: addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}&compare=${encodeURIComponent(versionTag)}`);
return (
<a
key={h.os.build}
href={href}
className={`block px-2 py-1 text-xs rounded transition-colors ${
isComparing
? "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
: "hover:bg-accent"
}`}
>
{h.os.version}
{isComparing && " (comparing)"}
</a>
);
};
const hasVersionHistory = availableHistory.length > 1;
return (
<div>
<main className="space-y-6">
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-xl font-semibold">Entitlements of</h2>
<p>
<code className="text-red-800 dark:text-red-400 break-all font-thin text-sm">
<div className="flex flex-col lg:flex-row gap-6">
{/* Main content */}
<main className="flex-1 min-w-0 space-y-4">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<h2 className="text-lg font-semibold">Entitlements</h2>
<p className="truncate" title={path || ""}>
<code className="text-red-800 dark:text-red-400 text-sm">
{path}
</code>
</p>
</div>
{!loading && xml && <CopyButton text={xml} />}
</div>
{availableHistory.length > 1 && (
<div className="border rounded-lg p-4 bg-gray-50 dark:bg-gray-900">
<h3 className="font-semibold mb-2">Version History</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
This binary exists in {availableHistory.length} OS versions.
Select a version to compare:
</p>
<div className="flex flex-wrap gap-2">
{availableHistory.map((h) => {
const isCurrent =
h.os.build === build ||
`${h.os.version}_${h.os.build}` === build;
const isComparing = compareWith === `${h.os.version}_${h.os.build}`;
const versionTag = `${h.os.version}_${h.os.build}`;
if (isCurrent) {
return (
<span
key={h.os.build}
className="px-2 py-1 text-sm rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 font-medium"
>
{h.os.version} (current)
</span>
);
}
const href = isComparing
? addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}`)
: addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}&compare=${encodeURIComponent(versionTag)}`);
return (
<a
key={h.os.build}
href={href}
className={`px-2 py-1 text-sm rounded transition-colors ${
isComparing
? "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
: "bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600"
}`}
>
{h.os.version}
{isComparing && " (comparing)"}
</a>
);
})}
</div>
{loading && (
<div className="space-y-2">
<div className="h-6 w-32 bg-muted rounded animate-pulse" />
<div className="h-64 bg-muted rounded animate-pulse" />
</div>
)}
{loading && <p>Loading...</p>}
{!loading && compareWith && compareError && (
<div className="border border-red-300 bg-red-50 dark:bg-red-900/20 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
<p className="font-medium">Comparison failed</p>
@@ -187,67 +193,113 @@ export default function BinaryDetail() {
)}
{!loading && !compareWith && xml && (
<SyntaxHighlighter
language="xml"
showLineNumbers={true}
style={tomorrow}
customStyle={{
margin: 0,
borderRadius: "0.5rem",
fontSize: "0.875rem",
}}
renderer={({ rows, stylesheet, useInlineStyles }) => {
function addLink(node: rendererNode) {
if (node.type === "text" && xmlKeys.has(node.value as string)) {
return {
type: "element",
tagName: "span",
children: [
{
type: "element",
tagName: "a",
children: [
{
type: "text",
value: node.value as string,
} as rendererNode,
],
properties: {
className: ["text-blue-200", "hover:underline"],
href: addBasePath(
`/os/find?key=${encodeURIComponent(
node.value as string,
)}&os=${encodeURIComponent(os!)}`,
),
},
} as rendererNode,
],
properties: { className: ["linked-key"] },
} as rendererNode;
<div className="relative">
<div className="absolute right-2 top-2 z-10">
<CopyButton text={xml} />
</div>
<SyntaxHighlighter
language="xml"
showLineNumbers={true}
style={tomorrow}
customStyle={{
margin: 0,
borderRadius: "0.5rem",
fontSize: "0.875rem",
paddingTop: "2.5rem",
}}
renderer={({ rows, stylesheet, useInlineStyles }) => {
function addLink(node: rendererNode) {
if (node.type === "text" && xmlKeys.has(node.value as string)) {
return {
type: "element",
tagName: "span",
children: [
{
type: "element",
tagName: "a",
children: [
{
type: "text",
value: node.value as string,
} as rendererNode,
],
properties: {
className: ["text-blue-200", "hover:underline"],
href: addBasePath(
`/os/find?key=${encodeURIComponent(
node.value as string,
)}&os=${encodeURIComponent(os!)}`,
),
},
} as rendererNode,
],
properties: { className: ["linked-key"] },
} as rendererNode;
}
if (node.children) {
node.children = node.children.map(addLink);
}
return node;
}
if (node.children) {
node.children = node.children.map(addLink);
}
return node;
}
return rows.map((row, i) => {
return createElement({
node: addLink(row),
stylesheet,
useInlineStyles,
key: `code-segment-${i}`,
return rows.map((row, i) => {
return createElement({
node: addLink(row),
stylesheet,
useInlineStyles,
key: `code-segment-${i}`,
});
});
});
}}
>
{xml}
</SyntaxHighlighter>
}}
>
{xml}
</SyntaxHighlighter>
</div>
)}
{compareLoading && <p>Loading comparison...</p>}
{compareLoading && (
<div className="h-64 bg-muted rounded animate-pulse" />
)}
</main>
{/* Version history sidebar */}
{hasVersionHistory && (
<aside className="lg:w-48 shrink-0">
<div className="lg:sticky lg:top-4">
<h3 className="text-sm font-semibold mb-2 text-muted-foreground">
History ({availableHistory.length})
</h3>
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1">
{groupedHistory.map(([major, versions]) => (
<details
key={major}
open={versions.some(
(h) =>
h.os.build === build ||
`${h.os.version}_${h.os.build}` === build ||
compareWith === `${h.os.version}_${h.os.build}`
)}
className="group"
>
<summary className="cursor-pointer text-xs font-medium text-muted-foreground hover:text-foreground flex items-center gap-1 py-1">
<span className="group-open:rotate-90 transition-transform">
</span>
iOS {major}.x
<span className="ml-auto text-muted-foreground/60">
{versions.length}
</span>
</summary>
<div className="ml-3 mt-1 space-y-0.5 border-l pl-2">
{versions.map(renderVersionLink)}
</div>
</details>
))}
</div>
</div>
</aside>
)}
</div>
);
}