feat(preview): add interactive address bar and visual editing tools to preview header

This commit is contained in:
2026-06-12 10:52:38 -07:00
parent 2a7e87c790
commit 2ee68c7ac2
2 changed files with 340 additions and 111 deletions

View File

@@ -91,7 +91,14 @@ export function ProjectIconRail({ workspace, projectId }: Props) {
);
}
import { Monitor, Smartphone, RotateCw } from "lucide-react";
import {
Monitor,
Smartphone,
RotateCw,
Wand2,
Palette,
Maximize2,
} from "lucide-react";
import { usePreviewToolbarStore } from "./preview-toolbar/preview-toolbar-state";
function PreviewDeviceToggles() {
@@ -100,93 +107,207 @@ function PreviewDeviceToggles() {
const triggerRefresh = usePreviewToolbarStore((s) => s.triggerRefresh);
return (
<div
style={{
display: "flex",
gap: 4,
background: "#f1ebe3",
padding: 4,
borderRadius: 8,
border: "1px solid #e8e4dc",
}}
>
<button
onClick={triggerRefresh}
title="Reload Preview"
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 26,
borderRadius: 6,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: "transparent",
color: "#8c8580",
marginRight: 4,
gap: 4,
background: "#f4f4f5",
padding: 4,
borderRadius: 8,
border: "1px solid #e4e4e7",
}}
onMouseEnter={(e) => (e.currentTarget.style.color = "#1a1a1a")}
onMouseLeave={(e) => (e.currentTarget.style.color = "#8c8580")}
>
<RotateCw size={14} />
</button>
<button
onClick={triggerRefresh}
title="Reload Preview"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 26,
borderRadius: 6,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: "transparent",
color: "#71717a",
marginRight: 4,
}}
onMouseEnter={(e) => (e.currentTarget.style.color = "#18181b")}
onMouseLeave={(e) => (e.currentTarget.style.color = "#71717a")}
>
<RotateCw size={14} />
</button>
<div
style={{
width: 1,
height: 16,
background: "#e4e4e7",
alignSelf: "center",
marginRight: 4,
}}
/>
<button
onClick={() => setDeviceMode("desktop")}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "4px 10px",
borderRadius: 6,
fontSize: "0.75rem",
fontWeight: 500,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "desktop" ? "#ffffff" : "transparent",
color: deviceMode === "desktop" ? "#18181b" : "#71717a",
boxShadow:
deviceMode === "desktop" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
}}
>
<Monitor size={14} />
Desktop
</button>
<button
onClick={() => setDeviceMode("mobile")}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "4px 10px",
borderRadius: 6,
fontSize: "0.75rem",
fontWeight: 500,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "mobile" ? "#ffffff" : "transparent",
color: deviceMode === "mobile" ? "#18181b" : "#71717a",
boxShadow:
deviceMode === "mobile" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
}}
>
<Smartphone size={14} />
Mobile
</button>
</div>
<div
style={{
width: 1,
height: 16,
background: "#d9d2c5",
alignSelf: "center",
marginRight: 4,
display: "flex",
alignItems: "center",
gap: 6,
background: "#fafafa",
border: "1px solid #e4e4e7",
padding: "4px 12px",
borderRadius: 8,
height: 34,
minWidth: 200,
color: "#71717a",
fontSize: "0.75rem",
}}
/>
>
<span style={{ opacity: 0.5 }}>/</span>
<input
type="text"
defaultValue=""
placeholder="Path (e.g. /dashboard)"
style={{
background: "transparent",
border: "none",
outline: "none",
width: "100%",
color: "#18181b",
fontSize: "0.75rem",
}}
/>
</div>
<button
onClick={() => setDeviceMode("desktop")}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "4px 10px",
borderRadius: 6,
fontSize: "0.75rem",
fontWeight: 500,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "desktop" ? "#ffffff" : "transparent",
color: deviceMode === "desktop" ? "#1a1a1a" : "#8c8580",
boxShadow:
deviceMode === "desktop" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
}}
>
<Monitor size={14} />
Desktop
</button>
<button
onClick={() => setDeviceMode("mobile")}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "4px 10px",
borderRadius: 6,
fontSize: "0.75rem",
fontWeight: 500,
border: "none",
cursor: "pointer",
transition: "all 0.15s",
background: deviceMode === "mobile" ? "#ffffff" : "transparent",
color: deviceMode === "mobile" ? "#1a1a1a" : "#8c8580",
boxShadow:
deviceMode === "mobile" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
}}
>
<Smartphone size={14} />
Mobile
</button>
<div style={{ display: "flex", gap: 4, marginLeft: 4 }}>
<button
title="Visual Edit"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
borderRadius: 8,
border: "1px solid transparent",
background: "transparent",
color: "#71717a",
cursor: "pointer",
transition: "all 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#f4f4f5";
e.currentTarget.style.color = "#18181b";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "#71717a";
}}
>
<Wand2 size={15} />
</button>
<button
title="Theme"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
borderRadius: 8,
border: "1px solid transparent",
background: "transparent",
color: "#71717a",
cursor: "pointer",
transition: "all 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#f4f4f5";
e.currentTarget.style.color = "#18181b";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "#71717a";
}}
>
<Palette size={15} />
</button>
<button
title="Fullscreen / Pop-out"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
borderRadius: 8,
border: "1px solid transparent",
background: "transparent",
color: "#71717a",
cursor: "pointer",
transition: "all 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#f4f4f5";
e.currentTarget.style.color = "#18181b";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "#71717a";
}}
>
<Maximize2 size={15} />
</button>
</div>
</div>
);
}

View File

@@ -337,11 +337,11 @@ function renderMarkdown(text: string): string {
const containerStyle = `
margin: 12px 0;
border: 1px solid #e8e4dc;
border: 1px solid #e4e4e7;
border-radius: 8px;
overflow: hidden;
background: #faf8f5;
box-shadow: 0 1px 3px rgba(1a,1a,1a,0.02);
background: #fafafa;
box-shadow: 0 1px 3px rgba(0,0,0,0.02);
font-family: var(--font-ibm-plex-mono), SFMono-Regular, Consolas, monospace;
`
.trim()
@@ -352,11 +352,11 @@ function renderMarkdown(text: string): string {
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: #f0ede8;
border-bottom: 1px solid #e8e4dc;
background: #f4f4f5;
border-bottom: 1px solid #e4e4e7;
font-size: 0.68rem;
font-weight: 600;
color: #6b6560;
color: #52525b;
`
.trim()
.replace(/\s+/g, " ");
@@ -376,12 +376,12 @@ function renderMarkdown(text: string): string {
const buttonStyle = `
background: #ffffff;
border: 1px solid #e8e4dc;
border: 1px solid #e4e4e7;
border-radius: 4px;
padding: 2px 6px;
font-size: 0.65rem;
cursor: pointer;
color: #6b6560;
color: #52525b;
display: flex;
align-items: center;
gap: 4px;
@@ -430,7 +430,7 @@ function renderMarkdown(text: string): string {
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(
/`([^`]+)`/g,
'<code style="background:#f0ede8;padding:1px 5px;border-radius:3px;font-family:var(--font-ibm-plex-mono),monospace;font-size:0.85em;overflow-wrap:anywhere;word-break:break-word">$1</code>',
'<code style="background:#f4f4f5;padding:1px 5px;border-radius:3px;font-family:var(--font-ibm-plex-mono),monospace;font-size:0.85em;overflow-wrap:anywhere;word-break:break-word">$1</code>',
)
.replace(
/^### (.+)$/gm,
@@ -513,24 +513,26 @@ const MessageBubble = React.memo(function MessageBubble({
width: 24,
height: 24,
borderRadius: "50%",
background: "#1a1a1a",
background: "#f4f4f5", // Zinc-100 instead of black
display: "flex",
alignItems: "center",
justifyContent: "center",
marginRight: 8,
flexShrink: 0,
marginTop: 2,
border: "1px solid #e4e4e7",
}}
>
<span
style={{
color: "#fff",
color: "#18181b", // Dark gray instead of white
fontSize: "0.6rem",
fontWeight: 700,
fontFamily: "var(--font-lora),serif",
fontStyle: "italic",
}}
>
V
V.
</span>
</div>
)}
@@ -561,14 +563,13 @@ const MessageBubble = React.memo(function MessageBubble({
(!msg.timeline || msg.timeline.length === 0))) && (
<div
style={{
padding: isUser ? "9px 14px" : "10px 14px",
borderRadius: isUser
? "14px 14px 4px 14px"
: "4px 14px 14px 14px",
background: isUser ? "#1a1a1a" : "#f7f4ef",
color: isUser ? "#fff" : "#1a1a1a",
padding: isUser ? "9px 14px" : "4px 0px",
borderRadius: isUser ? "14px 14px 4px 14px" : "0",
background: isUser ? "#111827" : "transparent",
color: isUser ? "#fff" : "#111827",
fontSize: "0.84rem",
lineHeight: 1.6,
letterSpacing: "-0.01em",
fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
...proseWrap,
}}
@@ -669,12 +670,13 @@ function TimelineText({ text }: { text: string }) {
return (
<div
style={{
padding: "10px 14px",
borderRadius: "4px 14px 14px 14px",
background: "#f7f4ef",
color: "#1a1a1a",
padding: "4px 0px",
borderRadius: "0",
background: "transparent",
color: "#111827",
fontSize: "0.84rem",
lineHeight: 1.6,
letterSpacing: "-0.01em",
fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
marginBottom: 6,
...proseWrap,
@@ -2044,11 +2046,33 @@ export function ChatPanel({
gap: 8,
background: "#fff",
borderRadius: 12,
border: "1px solid #e8e4dc",
border: "1px solid #e4e4e7",
padding: "10px 12px",
boxShadow: "0 2px 6px #1a1a1a08",
boxShadow: "0 2px 6px rgba(0,0,0,0.03)",
}}
>
{/* Fake Suggested Next Steps Chips (for design review) */}
{!sending &&
messages.length > 0 &&
messages[messages.length - 1].role === "assistant" && (
<div
style={{
display: "flex",
gap: 6,
marginBottom: 4,
overflowX: "auto",
paddingBottom: 4,
}}
>
<button style={suggestionChipStyle}>
Implement bulk checkout
</button>
<button style={suggestionChipStyle}>
Make mobile responsive
</button>
<button style={suggestionChipStyle}>Connect Stripe</button>
</div>
)}
<textarea
ref={inputRef}
value={input}
@@ -2091,6 +2115,27 @@ export function ChatPanel({
paddingTop: 4,
}}
>
<div style={{ display: "flex", gap: 6, flex: 1 }}>
{/* Mode Toggles */}
<select
value={chatMode}
onChange={(e) => setChatMode(e.target.value as any)}
style={{
appearance: "none",
background: "transparent",
border: "none",
fontSize: "0.75rem",
fontWeight: 500,
color: "#71717a",
cursor: "pointer",
outline: "none",
padding: "4px 8px",
}}
>
<option value="vibe">Discuss</option>
<option value="collaborate">Plan</option>
</select>
</div>
<div style={{ display: "flex", gap: 6 }}>
<button
type="button"
@@ -2105,23 +2150,58 @@ export function ChatPanel({
style={{
...COMPOSER_ACTION_BTN_BASE,
background: "transparent",
color: "#8c8580",
color: "#a1a1aa",
border: "1px solid transparent",
cursor: "pointer",
transition: "all 0.1s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#f0ede8";
e.currentTarget.style.color = "#1a1a1a";
e.currentTarget.style.background = "#f4f4f5";
e.currentTarget.style.color = "#18181b";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "#8c8580";
e.currentTarget.style.color = "#a1a1aa";
}}
title="Attach file to context"
>
<Paperclip style={{ width: 14, height: 14 }} />
</button>
<button
type="button"
style={{
...COMPOSER_ACTION_BTN_BASE,
background: "transparent",
color: "#a1a1aa",
border: "1px solid transparent",
cursor: "pointer",
transition: "all 0.1s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#f4f4f5";
e.currentTarget.style.color = "#18181b";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "#a1a1aa";
}}
title="Voice dictation"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" x2="12" y1="19" y2="22"></line>
</svg>
</button>
{selectToggle}
</div>
{(() => {
@@ -2140,11 +2220,11 @@ export function ChatPanel({
style={{
...COMPOSER_ACTION_BTN_BASE,
background: isActive
? "#1a1a1a"
? "#111827"
: canSend
? "#1a1a1a"
: "#e8e4dc",
color: isActive || canSend ? "#fff" : "#a09a90",
? "#111827"
: "#e4e4e7",
color: isActive || canSend ? "#fff" : "#a1a1aa",
border: "none",
cursor: isActive || canSend ? "pointer" : "not-allowed",
transition: "all 0.15s ease",
@@ -2668,6 +2748,34 @@ export function ChatPanel({
</button>
)}
<div style={{ display: "flex", gap: 4 }}>
{/* Top-Level Publish Button */}
<button
type="button"
onClick={() => {
if (activeThread && !sending) {
sendMessage("ship it");
}
}}
disabled={!activeThread || sending}
style={{
background: "#18181b",
border: "none",
cursor: activeThread && !sending ? "pointer" : "not-allowed",
padding: structural ? "4px 12px" : "5px 14px",
borderRadius: 6,
color: "#fff",
display: "flex",
alignItems: "center",
fontSize: "0.75rem",
fontWeight: 600,
opacity: activeThread && !sending ? 1 : 0.5,
transition: "opacity 0.15s ease",
}}
title="Ship to production"
>
Publish
</button>
<button
type="button"
onClick={newThread}