feat(preview): add interactive address bar and visual editing tools to preview header
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user