Skip to content

Commit 854e6bb

Browse files
Align compare UI and theme switcher with the React version
Bring several server-rendered components in line with the React site they replaced, both visually and behaviorally: - Macro compare table: restore the per-row grouping (alternating muted backgrounds and group-separating borders) that mirrored the React classNameMap; safelist the dynamically-built row classes. - Icons: replace Font Awesome glyphs with the exact lucide/Radix SVGs the React site used (Copy/Check, PlusCircled, MagnifyingGlass, X, Sun/Moon), collected in a reusable icons.html partial with their ISC/MIT notices inlined. - Compare page Old/New fields: replace the native datalist with a command-palette dialog (Branches/Releases groups, keyboard shortcuts), matching the React VitessRefsCommand. - Header search: restyle as the React "Search a commit..." box and make it navigate to /history?gitRef=... instead of acting as a nav-link finder. - Compare Versions widget: rework into the React CompareActionsWrapper card with the same ref pickers, an X close button, and drag handling. - Theme switcher: replace the Light/Dark/System dropdown with a direct dark/light toggle using animated Sun/Moon icons (transition-transform). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ba38734 commit 854e6bb

8 files changed

Lines changed: 467 additions & 181 deletions

File tree

go/server/static/web/app.js

Lines changed: 135 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Top navigation shared with the Go template (see web.go webNavItems).
18-
var NAV_ITEMS = [
19-
{ to: "/status", title: "Status" },
20-
{ to: "/daily", title: "Daily" },
21-
{ to: "/compare", title: "Compare" },
22-
{ to: "/fk", title: "Foreign Keys" },
23-
{ to: "/pr", title: "PR" },
24-
{ to: "/history", title: "History" },
25-
{ to: "/", title: "Home" },
26-
];
27-
2817
// setTheme applies and persists the light/dark/system theme. "system" clears the
2918
// stored preference and follows the OS setting.
3019
function setTheme(mode) {
@@ -39,29 +28,96 @@ function setTheme(mode) {
3928
}
4029
}
4130

31+
// toggleTheme flips between dark and light based on the current document state,
32+
// mirroring the React ModeToggle. The Sun/Moon icons animate via CSS
33+
// transition-transform when the `dark` class on <html> changes.
34+
function toggleTheme() {
35+
var isDark = document.documentElement.classList.contains("dark");
36+
setTheme(isDark ? "light" : "dark");
37+
}
38+
4239
// commandPalette is the Alpine.js data factory for the Cmd/Ctrl+K palette.
40+
// commandPalette backs the header "Search a commit…" box (Cmd/Ctrl+K), mirroring
41+
// the React CommandMenu: typing a commit SHA / git ref and pressing Enter jumps
42+
// to the History page filtered by that ref (/history?gitRef=…).
4343
function commandPalette() {
4444
return {
4545
open: false,
4646
query: "",
47-
items: NAV_ITEMS,
4847
show() {
4948
this.open = true;
5049
this.query = "";
5150
this.$nextTick(() => {
5251
if (this.$refs.search) this.$refs.search.focus();
5352
});
5453
},
55-
filtered() {
54+
enter() {
55+
var q = this.query.trim();
56+
if (!q) return;
57+
window.location.href = "/history?gitRef=" + encodeURIComponent(q);
58+
},
59+
};
60+
}
61+
62+
// compareForm backs the Compare page's Old/New ref pickers. Each field is a
63+
// button that opens a shared command-palette dialog listing the vitess refs
64+
// (grouped into Branches/Releases), mirroring the React VitessRefsCommand. The
65+
// ref list and initial values are read from data-* attributes on the <form>;
66+
// the chosen names feed hidden inputs that the form submits (server resolves
67+
// names -> SHAs). Typing in the dialog and pressing Enter commits the raw text,
68+
// so a pasted commit SHA works too.
69+
function compareForm() {
70+
return {
71+
refs: [],
72+
oldVal: "",
73+
newVal: "",
74+
open: false, // false | "old" | "new"
75+
query: "",
76+
init() {
77+
try {
78+
this.refs = JSON.parse(this.$el.dataset.refs || "[]");
79+
} catch (e) {
80+
this.refs = [];
81+
}
82+
this.oldVal = this.$el.dataset.old || "";
83+
this.newVal = this.$el.dataset.new || "";
84+
},
85+
show(field) {
86+
this.open = field;
87+
this.query = "";
88+
this.$nextTick(
89+
function () {
90+
if (this.$refs.search) this.$refs.search.focus();
91+
}.bind(this)
92+
);
93+
},
94+
matches(kind) {
5695
var q = this.query.trim().toLowerCase();
57-
if (!q) return this.items;
58-
return this.items.filter(function (i) {
59-
return i.title.toLowerCase().includes(q) || i.to.toLowerCase().includes(q);
96+
return this.refs.filter(function (r) {
97+
return r.kind === kind && (!q || r.name.toLowerCase().indexOf(q) !== -1);
6098
});
6199
},
100+
branches() {
101+
return this.matches("branch");
102+
},
103+
releases() {
104+
return this.matches("release");
105+
},
106+
commit(value) {
107+
if (this.open === "new") this.newVal = value;
108+
else this.oldVal = value;
109+
this.open = false;
110+
},
111+
select(name) {
112+
this.commit(name);
113+
},
62114
enter() {
63-
var f = this.filtered();
64-
if (f.length) window.location.href = f[0].to;
115+
var q = this.query.trim();
116+
if (!q) {
117+
this.open = false;
118+
return;
119+
}
120+
this.commit(q);
65121
},
66122
};
67123
}
@@ -220,18 +276,15 @@ function copyCompareMarkdown(btn) {
220276
if (!ta) return;
221277
navigator.clipboard.writeText(ta.value).then(function () {
222278
var label = btn.querySelector("[data-copy-label]");
223-
var icon = btn.querySelector("i");
279+
var copyIcon = btn.querySelector("[data-copy-icon]");
280+
var checkIcon = btn.querySelector("[data-check-icon]");
224281
if (label) label.textContent = "Copied!";
225-
if (icon) {
226-
icon.classList.remove("fa-copy");
227-
icon.classList.add("fa-check");
228-
}
282+
if (copyIcon) copyIcon.classList.add("hidden");
283+
if (checkIcon) checkIcon.classList.remove("hidden");
229284
setTimeout(function () {
230285
if (label) label.textContent = "Copy as markdown";
231-
if (icon) {
232-
icon.classList.remove("fa-check");
233-
icon.classList.add("fa-copy");
234-
}
286+
if (copyIcon) copyIcon.classList.remove("hidden");
287+
if (checkIcon) checkIcon.classList.add("hidden");
235288
}, 2000);
236289
});
237290
}
@@ -446,6 +499,12 @@ function tooltip() {
446499
// global "Compare Versions" widget (see partials/compare_widget.html). It tracks
447500
// a translate offset updated on mouse drag; the widget's visibility and the
448501
// old/new refs themselves live in the global $store.compare.
502+
// compareWidget backs the draggable "Compare Versions" card (see
503+
// partials/compare_widget.html), mirroring the React CompareActionsWrapper. The
504+
// staged old/new refs live in the $store.compare; this component owns the drag
505+
// state and the shared ref-picker dialog (open/query) used by the Old/New
506+
// fields. Interactive controls carry the .cancel-drag class so a click on them
507+
// does not start a drag.
449508
function compareWidget() {
450509
return {
451510
dragging: false,
@@ -455,7 +514,11 @@ function compareWidget() {
455514
oy: 0,
456515
sx: 0,
457516
sy: 0,
517+
open: false, // false | "old" | "new"
518+
query: "",
458519
startDrag(e) {
520+
if (e.target.closest && e.target.closest(".cancel-drag")) return;
521+
if (e.preventDefault) e.preventDefault();
459522
this.dragging = true;
460523
this.sx = e.clientX;
461524
this.sy = e.clientY;
@@ -473,14 +536,53 @@ function compareWidget() {
473536
style() {
474537
return "transform: translate(" + this.x + "px," + this.y + "px)";
475538
},
539+
showPicker(field) {
540+
if (!this.$store.compare.visible) return;
541+
this.$store.compare.loadRefs();
542+
this.open = field;
543+
this.query = "";
544+
this.$nextTick(
545+
function () {
546+
if (this.$refs.search) this.$refs.search.focus();
547+
}.bind(this)
548+
);
549+
},
550+
matches(kind) {
551+
var q = this.query.trim().toLowerCase();
552+
return (this.$store.compare.refs || []).filter(function (r) {
553+
return r.kind === kind && (!q || r.name.toLowerCase().indexOf(q) !== -1);
554+
});
555+
},
556+
branches() {
557+
return this.matches("branch");
558+
},
559+
releases() {
560+
return this.matches("release");
561+
},
562+
commit(value) {
563+
if (this.open === "new") this.$store.compare.new = value;
564+
else this.$store.compare.old = value;
565+
this.open = false;
566+
},
567+
select(name) {
568+
this.commit(name);
569+
},
570+
enter() {
571+
var q = this.query.trim();
572+
if (!q) {
573+
this.open = false;
574+
return;
575+
}
576+
this.commit(q);
577+
},
476578
};
477579
}
478580

479581
// The global compare store backs the cross-page "Compare Versions" widget,
480-
// mirroring the React CompareContext. Row actions on the History (and, later,
481-
// Status) tables call addOld/addNew to stage commits; go() navigates to the
482-
// /compare page. Vitess ref names are fetched lazily from the JSON API to
483-
// populate the input datalist, failing silently if unavailable.
582+
// mirroring the React CompareContext. Row actions on the History and Status
583+
// tables call addOld/addNew to stage commits; go() navigates to the /compare
584+
// page. Vitess refs ({name, kind}) are fetched lazily from the JSON API to feed
585+
// the widget's ref pickers, failing silently if unavailable.
484586
document.addEventListener("alpine:init", function () {
485587
Alpine.store("compare", {
486588
old: "",
@@ -523,10 +625,10 @@ document.addEventListener("alpine:init", function () {
523625
if (!d) return;
524626
var out = [];
525627
(d.branches || []).forEach(function (b) {
526-
if (b && b.name) out.push(b.name);
628+
if (b && b.name) out.push({ name: b.name, kind: "branch" });
527629
});
528630
(d.tags || []).forEach(function (t) {
529-
if (t && t.name) out.push(t.name);
631+
if (t && t.name) out.push({ name: t.name, kind: "release" });
530632
});
531633
self.refs = out;
532634
})

go/server/tailwind.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ module.exports = {
1919
"bg-destructive", "text-destructive-foreground",
2020
"bg-warning", "text-warning-foreground",
2121
"bg-progress", "text-progress-foreground",
22+
// Per-row grouping classes for the macro compare table, built by
23+
// compareRows() in web_pages.go and never present literally in a template.
24+
"bg-muted/80", "bg-background", "border-foreground", "dark:border-none",
25+
"transition-colors", "hover:bg-muted/50",
2226
],
2327
theme: {
2428
container: { center: true, padding: "2rem", screens: { "2xl": "1400px" } },

go/server/templates/web/pages/compare.html

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,79 @@
33
<div class="flex flex-col items-center gap-4 max-w-screen-lg">
44
<h2 class="text-4xl md:text-6xl font-semibold text-primary mb-4">Compare Versions</h2>
55

6-
<form method="get" action="/compare" hx-get="/compare" hx-target="#compare-results" hx-swap="innerHTML" hx-push-url="true" class="flex flex-col md:flex-row gap-4 items-end">
6+
<form method="get" action="/compare" hx-get="/compare" hx-target="#compare-results" hx-swap="innerHTML" hx-push-url="true"
7+
x-data="compareForm()" data-refs="{{ .RefsJSON }}" data-old="{{ .OldRef }}" data-new="{{ .NewRef }}"
8+
@keydown.window.meta.o.prevent="show('old')" @keydown.window.ctrl.o.prevent="show('old')"
9+
@keydown.window.meta.j.prevent="show('new')" @keydown.window.ctrl.j.prevent="show('new')"
10+
class="flex flex-col md:flex-row gap-4 items-end">
711
<div class="flex flex-col">
8-
<label class="text-primary mb-2" for="old">Old</label>
9-
<input id="old" name="old" list="vitess-refs" value="{{ .OldRef }}" placeholder="Search commit or releases..." class="h-9 w-64 rounded-md border border-border bg-background px-3 text-sm" />
12+
<label class="text-primary mb-2">Old</label>
13+
<button type="button" @click="show('old')"
14+
class="relative inline-flex items-center justify-between whitespace-nowrap rounded-[0.5rem] border border-input bg-muted/50 h-10 px-4 py-2 text-sm font-normal text-muted-foreground shadow-none hover:bg-accent hover:text-accent-foreground w-full md:w-40 lg:w-64">
15+
<span class="overflow-hidden text-ellipsis" x-text="oldVal || 'Search commit or releases...'"></span>
16+
<kbd class="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
17+
<span class="text-xs"></span>o
18+
</kbd>
19+
</button>
20+
<input type="hidden" name="old" :value="oldVal" />
1021
</div>
1122
<div class="flex flex-col">
12-
<label class="text-primary mb-2" for="new">New</label>
13-
<input id="new" name="new" list="vitess-refs" value="{{ .NewRef }}" placeholder="Search commit or releases..." class="h-9 w-64 rounded-md border border-border bg-background px-3 text-sm" />
23+
<label class="text-primary mb-2">New</label>
24+
<button type="button" @click="show('new')"
25+
class="relative inline-flex items-center justify-between whitespace-nowrap rounded-[0.5rem] border border-input bg-muted/50 h-10 px-4 py-2 text-sm font-normal text-muted-foreground shadow-none hover:bg-accent hover:text-accent-foreground w-full md:w-40 lg:w-64">
26+
<span class="overflow-hidden text-ellipsis" x-text="newVal || 'Search commit or releases...'"></span>
27+
<kbd class="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
28+
<span class="text-xs"></span>j
29+
</kbd>
30+
</button>
31+
<input type="hidden" name="new" :value="newVal" />
32+
</div>
33+
<div class="flex md:items-end items-center justify-center mt-4 md:mt-0">
34+
<button type="submit" :disabled="!oldVal || !newVal"
35+
class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground h-10 px-4 py-2 text-sm font-medium hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none">Compare</button>
36+
</div>
37+
38+
<!-- Shared command dialog (one open at a time; mirrors the React VitessRefsCommand CommandDialog) -->
39+
<div
40+
x-show="open"
41+
x-cloak
42+
@keydown.escape.window="open = false"
43+
class="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh] bg-black/50"
44+
@click.self="open = false"
45+
>
46+
<div class="w-full max-w-lg rounded-lg border border-border bg-popover text-popover-foreground shadow-xl overflow-hidden">
47+
<div class="flex items-center border-b border-border px-3">
48+
{{ template "iconMagnifyingGlass" "mr-2 h-4 w-4 shrink-0 opacity-50" }}
49+
<input
50+
x-ref="search"
51+
x-model="query"
52+
@keydown.enter.prevent="enter()"
53+
type="text"
54+
placeholder="Search commit or releases..."
55+
class="flex h-11 w-full bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
56+
/>
57+
</div>
58+
<ul class="max-h-72 overflow-y-auto p-1">
59+
<template x-if="branches().length">
60+
<li class="px-2 py-1.5 text-xs font-medium text-muted-foreground">Branches</li>
61+
</template>
62+
<template x-for="ref in branches()" :key="'b-' + ref.name">
63+
<li>
64+
<button type="button" @click="select(ref.name)" class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-left hover:bg-accent hover:text-accent-foreground" x-text="ref.name"></button>
65+
</li>
66+
</template>
67+
<template x-if="releases().length">
68+
<li class="px-2 py-1.5 text-xs font-medium text-muted-foreground">Releases</li>
69+
</template>
70+
<template x-for="ref in releases()" :key="'r-' + ref.name">
71+
<li>
72+
<button type="button" @click="select(ref.name)" class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-left hover:bg-accent hover:text-accent-foreground" x-text="ref.name"></button>
73+
</li>
74+
</template>
75+
<li x-show="!branches().length && !releases().length" class="py-6 text-center text-sm text-muted-foreground">No results found.</li>
76+
</ul>
77+
</div>
1478
</div>
15-
<button type="submit" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground h-9 px-4 text-sm font-medium hover:bg-primary/90">Compare</button>
16-
<datalist id="vitess-refs">
17-
{{ range .Refs }}<option value="{{ .Name }}"></option>{{ end }}
18-
</datalist>
1979
</form>
2080
</div>
2181
</section>
@@ -31,7 +91,9 @@ <h2 class="text-4xl md:text-6xl font-semibold text-primary mb-4">Compare Version
3191
{{ else }}
3292
<div class="flex justify-end w-[80vw] xl:w-[60vw] mt-8">
3393
<button type="button" onclick="copyCompareMarkdown(this)" class="inline-flex items-center rounded-md border border-dashed border-border h-8 px-3 text-sm hover:bg-muted">
34-
<i class="fa-solid fa-copy mr-2 text-primary"></i><span data-copy-label>Copy as markdown</span>
94+
<span data-copy-icon>{{ template "iconCopy" "mr-2 h-4 w-4 text-primary" }}</span>
95+
<span data-check-icon class="hidden">{{ template "iconCheck" "mr-2 h-4 w-4 text-primary" }}</span>
96+
<span data-copy-label>Copy as markdown</span>
3597
</button>
3698
</div>
3799
<textarea id="compareMarkdown" class="hidden" aria-hidden="true">{{ .Markdown }}</textarea>
@@ -43,11 +105,11 @@ <h2 class="text-4xl md:text-6xl font-semibold text-primary mb-4">Compare Version
43105
<h3 class="text-2xl md:text-4xl">{{ .Workload }}</h3>
44106
{{ if .MissingResults }}
45107
<span class="inline-flex items-center rounded-md border border-dashed border-border h-8 px-3 text-sm text-muted-foreground cursor-not-allowed" aria-disabled="true">
46-
<i class="fa-solid fa-circle-plus mr-2 text-primary"></i>See Query Plan
108+
{{ template "iconPlusCircled" "mr-2 h-4 w-4 text-primary" }}See Query Plan
47109
</span>
48110
{{ else }}
49111
<a href="{{ .QueryPlanHref }}" class="inline-flex items-center rounded-md border border-dashed border-border h-8 px-3 text-sm hover:bg-muted">
50-
<i class="fa-solid fa-circle-plus mr-2 text-primary"></i>See Query Plan
112+
{{ template "iconPlusCircled" "mr-2 h-4 w-4 text-primary" }}See Query Plan
51113
</a>
52114
{{ end }}
53115
</div>

0 commit comments

Comments
 (0)