Skip to content

Commit 840496a

Browse files
committed
feat(webview): liquid toggle in header, star-button Initialize, hover states, sticky footer
1 parent b4f51e9 commit 840496a

8 files changed

Lines changed: 325 additions & 76 deletions

File tree

maestro-extension/webview-ui/src/App.tsx

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useState } from "react";
22
import { Header } from "@/components/header";
3-
import { ModeToggle } from "@/components/mode-toggle";
43
import { CommandList } from "@/components/command-list";
54
import { ContextStatus } from "@/components/context-status";
65
import { Separator } from "@/components/ui/separator";
@@ -13,17 +12,14 @@ export default function App() {
1312

1413
return (
1514
<div className="flex h-screen flex-col overflow-hidden">
16-
<Header />
15+
<Header
16+
zeroDefectActive={state.zeroDefectActive}
17+
onToggle={() => toggleMode("zero-defect")}
18+
onHover={setHoveredDescription}
19+
onLeave={() => setHoveredDescription(null)}
20+
/>
1721

1822
<div className="flex flex-1 flex-col gap-3 overflow-y-auto pb-4">
19-
{/* Zero-Defect Mode Toggle */}
20-
<ModeToggle
21-
active={state.zeroDefectActive}
22-
onToggle={() => toggleMode("zero-defect")}
23-
/>
24-
25-
<Separator />
26-
2723
{/* Command List */}
2824
<CommandList
2925
skills={state.skills}
@@ -41,36 +37,37 @@ export default function App() {
4137
info={state.contextInfo}
4238
onInit={initContext}
4339
/>
40+
</div>
4441

45-
<Separator />
46-
47-
{/* Footer Links */}
48-
<div className="flex flex-col gap-1 px-4">
42+
{/* Sticky bottom section */}
43+
<div className="mt-auto shrink-0 border-t border-border/50">
44+
{/* Footer Links — sticky bottom */}
45+
<div className="flex flex-col gap-0.5 px-4 py-2">
4946
<button
5047
type="button"
5148
onClick={() => openLink("vscode:settings/maestro")}
52-
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
49+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
5350
>
5451
<Settings className="h-3.5 w-3.5" />
5552
Preferences
5653
</button>
5754
<button
5855
type="button"
5956
onClick={() => openLink("https://maestroskills.dev")}
60-
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
57+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
6158
>
6259
<ExternalLink className="h-3.5 w-3.5" />
6360
maestroskills.dev
6461
</button>
6562
</div>
66-
</div>
6763

68-
{/* Global Hover Description Footer */}
69-
<div className="border-t px-3 h-[80px] flex items-center transition-colors shrink-0">
70-
<MessageCircleQuestionMark className="min-w-[14px] h-3.5 w-3.5 mr-2 shrink-0 text-muted-foreground" />
71-
<p className="text-[11px] leading-relaxed text-muted-foreground line-clamp-4">
72-
{hoveredDescription || "Hover over any command to see its description."}
73-
</p>
64+
{/* Global Hover Description Footer */}
65+
<div className="border-t border-border/30 px-3 h-[80px] flex items-center transition-colors shrink-0">
66+
<MessageCircleQuestionMark className="min-w-[14px] h-3.5 w-3.5 mr-2 shrink-0 text-muted-foreground" />
67+
<p className="text-[11px] leading-relaxed text-muted-foreground line-clamp-4">
68+
{hoveredDescription || "Hover over any command to see its description."}
69+
</p>
70+
</div>
7471
</div>
7572
</div>
7673
);

maestro-extension/webview-ui/src/components/command-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function CommandList({ skills, usedCommands = [], onRunCommand, onHover,
9090
<button
9191
type="button"
9292
onClick={() => toggle(catId)}
93-
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
93+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
9494
>
9595
<ChevronRight
9696
className={cn(

maestro-extension/webview-ui/src/components/context-status.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FileText, AlertCircle } from "lucide-react";
22
import { Badge } from "@/components/ui/badge";
3-
import { cn } from "@/lib/utils";
3+
import { StarButton } from "@/components/ui/star-button";
44

55
interface ContextStatusProps {
66
detected: boolean;
@@ -24,16 +24,17 @@ export function ContextStatus({ detected, info, onInit }: ContextStatusProps) {
2424
<>
2525
<AlertCircle className="h-3.5 w-3.5 text-muted-foreground" />
2626
<span className="text-muted-foreground">No context file</span>
27-
<button
28-
type="button"
27+
<StarButton
2928
onClick={onInit}
30-
className={cn(
31-
"ml-auto rounded-md border border-border/50 bg-card px-2 py-0.5 text-[10px] font-medium text-muted-foreground transition-colors",
32-
"hover:border-primary/40 hover:text-primary"
33-
)}
29+
className="ml-auto h-6 px-2.5 border border-border/50 bg-card"
30+
lightColor="var(--color-primary)"
31+
backgroundColor="var(--color-card)"
32+
duration={4}
33+
lightWidth={80}
34+
borderWidth={1}
3435
>
3536
Initialize
36-
</button>
37+
</StarButton>
3738
</>
3839
)}
3940
</div>
Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,56 @@
11
import { useEffect, useRef } from "react";
22
import { AudioLinesIcon, type AudioLinesIconHandle } from "@/components/ui/audio-lines";
3+
import { Toggle, GooeyFilter } from "@/components/ui/liquid-toggle";
4+
import { Shield } from "lucide-react";
5+
import { cn } from "@/lib/utils";
36

4-
export function Header() {
7+
interface HeaderProps {
8+
zeroDefectActive: boolean;
9+
onToggle: () => void;
10+
onHover: (desc: string) => void;
11+
onLeave: () => void;
12+
}
13+
14+
export function Header({ zeroDefectActive, onToggle, onHover, onLeave }: HeaderProps) {
515
const iconRef = useRef<AudioLinesIconHandle>(null);
616

717
useEffect(() => {
818
iconRef.current?.startAnimation();
919
}, []);
1020

1121
return (
12-
<div className="flex items-center gap-2 px-4 py-3">
22+
<div className="flex items-center gap-2 px-4 py-3 shrink-0">
23+
<GooeyFilter />
1324
<AudioLinesIcon ref={iconRef} size={20} className="text-primary" />
1425
<span className="font-heading text-sm font-bold tracking-tight">
1526
Maestro
1627
</span>
28+
29+
{/* Zero-Defect toggle pushed to the right */}
30+
<div
31+
className="ml-auto flex items-center gap-1.5"
32+
onMouseEnter={() =>
33+
onHover(
34+
zeroDefectActive
35+
? "Zero-Defect Mode is ON — 8 precision rules are injected into every AI prompt. Click to deactivate."
36+
: "Zero-Defect Mode — Activate maximum precision. Injects 8 strict rules into every AI prompt to prevent mistakes."
37+
)
38+
}
39+
onMouseLeave={onLeave}
40+
>
41+
<Shield
42+
className={cn(
43+
"h-3 w-3 transition-colors duration-300",
44+
zeroDefectActive ? "text-primary" : "text-muted-foreground"
45+
)}
46+
/>
47+
<Toggle
48+
checked={zeroDefectActive}
49+
onCheckedChange={() => onToggle()}
50+
variant={zeroDefectActive ? "warning" : "default"}
51+
size="sm"
52+
/>
53+
</div>
1754
</div>
1855
);
1956
}

maestro-extension/webview-ui/src/components/mode-toggle.tsx

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"use client";
2+
3+
import React from 'react';
4+
import { cn } from '@/lib/utils';
5+
6+
const variantColors = {
7+
default: 'var(--color-primary)',
8+
success: '#10B981',
9+
warning: 'var(--color-primary)',
10+
danger: '#EF4444',
11+
};
12+
13+
const sizeMap = {
14+
sm: { w: 34, h: 20, cx1: 10, cy: 10, r: 6.5, cx2: 24, dropCx: 23, dropCy: -0.5, dropR: 1.5, travel: 8 },
15+
md: { w: 52, h: 32, cx1: 16, cy: 16, r: 10, cx2: 36, dropCx: 35, dropCy: -1, dropR: 2.5, travel: 12 },
16+
lg: { w: 64, h: 40, cx1: 20, cy: 20, r: 12.5, cx2: 44, dropCx: 43, dropCy: -1, dropR: 3, travel: 14 },
17+
};
18+
19+
interface ToggleProps {
20+
checked?: boolean;
21+
onCheckedChange?: (checked: boolean) => void;
22+
className?: string;
23+
variant?: 'default' | 'success' | 'warning' | 'danger';
24+
size?: 'sm' | 'md' | 'lg';
25+
}
26+
27+
export function Toggle({
28+
checked = false,
29+
onCheckedChange,
30+
className,
31+
variant = 'default',
32+
size = 'md'
33+
}: ToggleProps) {
34+
const [isChecked, setIsChecked] = React.useState(checked);
35+
36+
React.useEffect(() => {
37+
setIsChecked(checked);
38+
}, [checked]);
39+
40+
const handleClick = () => {
41+
const next = !isChecked;
42+
setIsChecked(next);
43+
onCheckedChange?.(next);
44+
};
45+
46+
const s = sizeMap[size];
47+
const activeColor = variantColors[variant];
48+
const trackColor = isChecked ? activeColor : 'var(--color-muted, #D2D6E9)';
49+
50+
return (
51+
<button
52+
type="button"
53+
role="switch"
54+
aria-checked={isChecked}
55+
onClick={handleClick}
56+
className={cn("relative block cursor-pointer shrink-0", className)}
57+
style={{ width: s.w, height: s.h }}
58+
>
59+
{/* Track background */}
60+
<div
61+
className="absolute inset-0 rounded-full transition-colors duration-500"
62+
style={{ backgroundColor: trackColor }}
63+
/>
64+
65+
{/* Gooey SVG circles */}
66+
<svg
67+
viewBox={`0 0 ${s.w} ${s.h}`}
68+
filter="url(#goo)"
69+
className="pointer-events-none absolute inset-0 fill-white"
70+
style={{ width: s.w, height: s.h }}
71+
>
72+
<circle
73+
cx={s.cx1}
74+
cy={s.cy}
75+
r={s.r}
76+
style={{
77+
transformOrigin: `${s.cx1}px ${s.cy}px`,
78+
transform: `translateX(${isChecked ? `${s.travel}px` : '0px'}) scale(${isChecked ? 0 : 1})`,
79+
transition: 'transform 500ms',
80+
}}
81+
/>
82+
<circle
83+
cx={s.cx2}
84+
cy={s.cy}
85+
r={s.r}
86+
style={{
87+
transformOrigin: `${s.cx2}px ${s.cy}px`,
88+
transform: `translateX(${isChecked ? '0px' : `-${s.travel}px`}) scale(${isChecked ? 1 : 0})`,
89+
transition: 'transform 500ms',
90+
}}
91+
/>
92+
{isChecked && (
93+
<circle
94+
cx={s.dropCx}
95+
cy={s.dropCy}
96+
r={s.dropR}
97+
style={{ transition: 'transform 700ms' }}
98+
/>
99+
)}
100+
</svg>
101+
</button>
102+
);
103+
}
104+
105+
export function GooeyFilter() {
106+
return (
107+
<svg className="fixed" style={{ width: 0, height: 0 }}>
108+
<defs>
109+
<filter id="goo">
110+
<feGaussianBlur
111+
in="SourceGraphic"
112+
stdDeviation="2"
113+
result="blur"
114+
/>
115+
<feColorMatrix
116+
in="blur"
117+
mode="matrix"
118+
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"
119+
result="goo"
120+
/>
121+
<feComposite
122+
in="SourceGraphic"
123+
in2="goo"
124+
operator="atop"
125+
/>
126+
</filter>
127+
</defs>
128+
</svg>
129+
);
130+
}

0 commit comments

Comments
 (0)