zunokit-builder
Version:
🎨 Figma-style drag-and-drop UI builder for React developers. Create UI visually, export clean Tailwind CSS code. Perfect for rapid prototyping and component creation.
965 lines (954 loc) • 41.9 kB
JavaScript
"use client";
// src/components/ZunoBuilder.tsx
import { useState as useState2 } from "react";
// src/components/builder/BuilderProvider.tsx
import { createContext, useContext, useReducer, useEffect } from "react";
import { jsx } from "react/jsx-runtime";
var initialState = {
layout: null,
selectedBlockId: null,
hoveredBlockId: null,
draggedBlockId: null,
clipboard: null,
isLoading: true,
isDirty: false,
mode: "design",
tool: "select",
viewport: {
width: 1200,
height: 800,
scale: 1,
offsetX: 0,
offsetY: 0,
showGrid: false,
gridSize: 10,
snapToGrid: false,
showRulers: false
},
history: {
past: [],
present: null,
future: [],
maxHistorySize: 50
}
};
function builderReducer(state, action) {
switch (action.type) {
case "SET_LAYOUT":
return {
...state,
layout: action.payload,
isLoading: false
};
case "ADD_BLOCK":
if (!state.layout)
return state;
return {
...state,
layout: {
...state.layout,
blocks: [...state.layout.blocks, action.payload]
},
isDirty: true
};
case "UPDATE_BLOCK":
if (!state.layout)
return state;
return {
...state,
layout: {
...state.layout,
blocks: state.layout.blocks.map(
(block) => block.id === action.payload.id ? { ...block, ...action.payload.updates } : block
)
},
isDirty: true
};
case "REMOVE_BLOCK":
if (!state.layout)
return state;
return {
...state,
layout: {
...state.layout,
blocks: state.layout.blocks.filter((block) => block.id !== action.payload)
},
selectedBlockId: state.selectedBlockId === action.payload ? null : state.selectedBlockId,
isDirty: true
};
case "SELECT_BLOCK":
return {
...state,
selectedBlockId: action.payload
};
case "SET_LOADING":
return {
...state,
isLoading: action.payload
};
case "SET_TOOL":
return {
...state,
tool: action.payload
};
default:
return state;
}
}
var BuilderContext = createContext(null);
function BuilderProvider({ children }) {
const [state, dispatch] = useReducer(builderReducer, initialState);
useEffect(() => {
const loadLayout = async () => {
try {
const defaultLayout = {
version: "1.0.0",
id: "default-layout",
name: "Default Layout",
blocks: [],
metadata: {
version: "1.0.0",
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
targetPath: ""
},
viewport: {
width: 1200,
height: 800,
scale: 1,
offsetX: 0,
offsetY: 0
}
};
dispatch({ type: "SET_LAYOUT", payload: defaultLayout });
} catch (error) {
console.error("Failed to load layout:", error);
dispatch({ type: "SET_LOADING", payload: false });
}
};
if (process.env.NODE_ENV === "development") {
loadLayout();
} else {
dispatch({ type: "SET_LOADING", payload: false });
}
}, []);
return /* @__PURE__ */ jsx(BuilderContext.Provider, { value: { state, dispatch }, children });
}
function useBuilder() {
const context = useContext(BuilderContext);
if (!context) {
throw new Error("useBuilder must be used within BuilderProvider");
}
return context;
}
// src/components/blocks/BlockRenderer.tsx
import React2 from "react";
// src/utils/index.ts
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs) {
return twMerge(clsx(inputs));
}
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
// src/components/blocks/BlockRenderer.tsx
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
function BlockRenderer({ block, isSelected, isHovered }) {
const { dispatch } = useBuilder();
const handleClick = (e) => {
e.stopPropagation();
dispatch({ type: "SELECT_BLOCK", payload: block.id });
};
const commonProps = {
onClick: handleClick,
className: cn(
"absolute cursor-pointer transition-all",
block.style.className,
isSelected && "ring-2 ring-blue-500",
isHovered && "ring-1 ring-gray-400"
),
style: {
left: block.position.x,
top: block.position.y,
width: block.position.width,
height: block.position.height,
...block.style.customStyles
}
};
switch (block.type) {
case "text":
return /* @__PURE__ */ jsx2("div", { ...commonProps, children: React2.createElement(
block.props.tag || "p",
{
className: cn(
"w-full h-full",
`text-${block.props.fontSize || "base"}`,
`font-${block.props.fontWeight || "normal"}`,
`text-${block.props.textAlign || "left"}`
)
},
block.props.content || "Text"
) });
case "button":
return /* @__PURE__ */ jsx2("button", { ...commonProps, children: block.props.text || "Button" });
case "input":
return /* @__PURE__ */ jsx2(
"input",
{
...commonProps,
type: block.props.type || "text",
placeholder: block.props.placeholder,
disabled: block.props.disabled,
readOnly: block.props.readOnly
}
);
case "image":
return /* @__PURE__ */ jsx2(
"img",
{
...commonProps,
src: block.props.src,
alt: block.props.alt || "",
style: {
...commonProps.style,
objectFit: block.props.objectFit || "cover"
}
}
);
case "section":
case "card":
case "div":
return /* @__PURE__ */ jsx2("div", { ...commonProps, children: block.children?.length ? /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-500 p-2", children: [
"Container with ",
block.children.length,
" children"
] }) : /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-400 p-2", children: [
"Empty ",
block.type
] }) });
default:
return /* @__PURE__ */ jsx2("div", { ...commonProps, children: /* @__PURE__ */ jsxs("div", { className: "text-xs text-red-500 p-2", children: [
"Unknown block type: ",
block.type
] }) });
}
}
// src/components/builder/Canvas.tsx
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
function Canvas() {
const { state, dispatch } = useBuilder();
const { layout, viewport } = state;
const handleCanvasClick = (e) => {
if (e.target === e.currentTarget) {
dispatch({ type: "SELECT_BLOCK", payload: null });
}
};
if (!layout) {
return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
/* @__PURE__ */ jsx3("div", { className: "w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx3("svg", { className: "w-8 h-8 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" }) }) }),
/* @__PURE__ */ jsx3("p", { className: "text-gray-500 text-sm", children: "Loading canvas..." })
] }) });
}
return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsxs2(
"div",
{
className: "relative bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden",
style: { width: 800, height: 600 },
children: [
/* @__PURE__ */ jsxs2(
"div",
{
className: "relative w-full h-full bg-white cursor-default",
onClick: handleCanvasClick,
style: {
backgroundImage: viewport.showGrid ? `
radial-gradient(circle, #e5e7eb 1px, transparent 1px)
` : void 0,
backgroundSize: viewport.showGrid ? `${viewport.gridSize || 20}px ${viewport.gridSize || 20}px` : void 0
},
children: [
layout.blocks.length === 0 && /* @__PURE__ */ jsx3("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
/* @__PURE__ */ jsx3("div", { className: "w-20 h-20 mx-auto mb-4 bg-gray-50 rounded-2xl flex items-center justify-center", children: /* @__PURE__ */ jsx3("svg", { className: "w-10 h-10 text-gray-300", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M12 4v16m8-8H4" }) }) }),
/* @__PURE__ */ jsx3("h3", { className: "text-lg font-medium text-gray-900 mb-2", children: "Start designing" }),
/* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 mb-4", children: "Drag components from the left sidebar to get started" }),
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-center gap-2 text-xs text-gray-400", children: [
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx3("div", { className: "w-3 h-3 border border-gray-300 rounded" }),
/* @__PURE__ */ jsx3("span", { children: "Text" })
] }),
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx3("div", { className: "w-3 h-3 bg-blue-500 rounded" }),
/* @__PURE__ */ jsx3("span", { children: "Button" })
] }),
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx3("div", { className: "w-3 h-3 border border-gray-300 rounded" }),
/* @__PURE__ */ jsx3("span", { children: "Input" })
] })
] })
] }) }),
layout.blocks.map((block) => /* @__PURE__ */ jsx3(
BlockRenderer,
{
block,
isSelected: state.selectedBlockId === block.id,
isHovered: state.hoveredBlockId === block.id
},
block.id
)),
state.selectedBlockId && /* @__PURE__ */ jsx3(SelectionOverlay, { blockId: state.selectedBlockId })
]
}
),
/* @__PURE__ */ jsxs2("div", { className: "absolute bottom-0 left-0 right-0 h-8 bg-gray-50 border-t border-gray-200 flex items-center justify-between px-4 text-xs text-gray-600", children: [
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-4", children: [
/* @__PURE__ */ jsx3("span", { children: "800 \xD7 600" }),
/* @__PURE__ */ jsxs2("span", { children: [
layout.blocks.length,
" elements"
] })
] }),
/* @__PURE__ */ jsx3("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx3("span", { children: "100%" }) })
] })
]
}
) });
}
function SelectionOverlay({ blockId }) {
const { state } = useBuilder();
const block = state.layout?.blocks.find((b) => b.id === blockId);
if (!block)
return null;
return /* @__PURE__ */ jsxs2(Fragment, { children: [
/* @__PURE__ */ jsx3(
"div",
{
className: "absolute border-2 border-blue-500 pointer-events-none z-10",
style: {
left: block.position.x - 1,
top: block.position.y - 1,
width: block.position.width + 2,
height: block.position.height + 2
}
}
),
/* @__PURE__ */ jsxs2(
"div",
{
className: "absolute pointer-events-none z-20",
style: {
left: block.position.x - 4,
top: block.position.y - 4,
width: block.position.width + 8,
height: block.position.height + 8
},
children: [
/* @__PURE__ */ jsx3("div", { className: "absolute -top-1 -left-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -top-1 -right-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -bottom-1 -left-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -bottom-1 -right-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -bottom-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -left-1 top-1/2 transform -translate-y-1/2 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" }),
/* @__PURE__ */ jsx3("div", { className: "absolute -right-1 top-1/2 transform -translate-y-1/2 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm" })
]
}
),
/* @__PURE__ */ jsx3(
"div",
{
className: "absolute -top-6 left-0 bg-blue-500 text-white px-2 py-0.5 rounded text-xs font-medium capitalize z-30",
style: { left: block.position.x },
children: block.type
}
)
] });
}
// src/components/builder/Sidebar.tsx
import { useState } from "react";
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
var BLOCK_TEMPLATES = [
{
type: "text",
name: "Text",
description: "Add headings, paragraphs, and labels",
icon: /* @__PURE__ */ jsx4("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" }) }),
category: "Content",
create: () => ({
id: generateId(),
type: "text",
props: {
id: generateId(),
content: "Edit this text",
tag: "p",
fontSize: "base",
fontWeight: "normal",
textAlign: "left"
},
style: {
className: "text-gray-900 leading-relaxed"
},
position: {
x: 100,
y: 100,
width: 200,
height: 40
}
})
},
{
type: "button",
name: "Button",
description: "Interactive button component",
icon: /* @__PURE__ */ jsx4("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" }) }),
category: "Interactive",
create: () => ({
id: generateId(),
type: "button",
props: {
id: generateId(),
text: "Button",
variant: "default",
size: "default"
},
style: {
className: "bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded-lg font-medium transition-colors"
},
position: {
x: 100,
y: 200,
width: 120,
height: 44
}
})
},
{
type: "input",
name: "Input",
description: "Text input field for forms",
icon: /* @__PURE__ */ jsx4("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) }),
category: "Interactive",
create: () => ({
id: generateId(),
type: "input",
props: {
id: generateId(),
type: "text",
placeholder: "Enter text...",
size: "default",
variant: "default"
},
style: {
className: "border border-gray-300 rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
},
position: {
x: 100,
y: 300,
width: 240,
height: 44
}
})
},
{
type: "section",
name: "Container",
description: "Layout container for grouping elements",
icon: /* @__PURE__ */ jsx4("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" }) }),
category: "Layout",
create: () => ({
id: generateId(),
type: "section",
props: {
id: generateId(),
tag: "div",
display: "block"
},
style: {
className: "bg-white border border-gray-200 rounded-lg p-6 shadow-sm"
},
position: {
x: 100,
y: 400,
width: 320,
height: 200
}
})
}
];
var TABS = [
{ id: "components", name: "Design", icon: "\u{1F3A8}" },
{ id: "layers", name: "Layers", icon: "\u{1F4CB}" }
];
function Sidebar() {
const [activeTab, setActiveTab] = useState("components");
const { dispatch, state } = useBuilder();
const handleAddBlock = (template) => {
const block = template.create();
dispatch({ type: "ADD_BLOCK", payload: block });
dispatch({ type: "SELECT_BLOCK", payload: block.id });
};
const categories = Array.from(new Set(BLOCK_TEMPLATES.map((t) => t.category)));
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col h-full", children: [
/* @__PURE__ */ jsx4("div", { className: "flex border-b border-gray-200", children: TABS.map((tab) => /* @__PURE__ */ jsxs3(
"button",
{
onClick: () => setActiveTab(tab.id),
className: `flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${activeTab === tab.id ? "text-blue-600 border-b-2 border-blue-600" : "text-gray-600 hover:text-gray-900"}`,
children: [
/* @__PURE__ */ jsx4("span", { children: tab.icon }),
tab.name
]
},
tab.id
)) }),
/* @__PURE__ */ jsxs3("div", { className: "flex-1 overflow-auto", children: [
activeTab === "components" && /* @__PURE__ */ jsx4("div", { className: "p-4", children: categories.map((category) => /* @__PURE__ */ jsxs3("div", { className: "mb-6", children: [
/* @__PURE__ */ jsx4("h3", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3", children: category }),
/* @__PURE__ */ jsx4("div", { className: "space-y-2", children: BLOCK_TEMPLATES.filter((t) => t.category === category).map((template) => /* @__PURE__ */ jsxs3(
"button",
{
onClick: () => handleAddBlock(template),
className: "w-full flex items-start gap-3 p-3 bg-white border border-gray-200 rounded-lg hover:border-blue-200 hover:bg-blue-50 transition-all text-left group",
children: [
/* @__PURE__ */ jsx4("div", { className: "p-2 bg-gray-100 rounded-lg group-hover:bg-blue-100 transition-colors", children: template.icon }),
/* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
/* @__PURE__ */ jsx4("div", { className: "text-sm font-medium text-gray-900", children: template.name }),
/* @__PURE__ */ jsx4("div", { className: "text-xs text-gray-500 mt-0.5", children: template.description })
] })
]
},
template.type
)) })
] }, category)) }),
activeTab === "layers" && /* @__PURE__ */ jsxs3("div", { className: "p-4", children: [
/* @__PURE__ */ jsx4("h3", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3", children: "Layers" }),
state.layout?.blocks.length === 0 ? /* @__PURE__ */ jsxs3("div", { className: "text-center py-8 text-gray-500", children: [
/* @__PURE__ */ jsx4("svg", { className: "w-8 h-8 mx-auto mb-2 opacity-50", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" }) }),
/* @__PURE__ */ jsx4("p", { className: "text-sm", children: "No layers yet" }),
/* @__PURE__ */ jsx4("p", { className: "text-xs", children: "Add components to see them here" })
] }) : /* @__PURE__ */ jsx4("div", { className: "space-y-1", children: state.layout?.blocks.map((block) => /* @__PURE__ */ jsxs3(
"button",
{
onClick: () => dispatch({ type: "SELECT_BLOCK", payload: block.id }),
className: `w-full flex items-center gap-2 p-2 rounded text-left text-sm transition-colors ${state.selectedBlockId === block.id ? "bg-blue-100 text-blue-900" : "hover:bg-gray-100"}`,
children: [
/* @__PURE__ */ jsx4("div", { className: "w-4 h-4 bg-gray-300 rounded" }),
/* @__PURE__ */ jsx4("span", { className: "capitalize", children: block.type })
]
},
block.id
)) })
] })
] })
] });
}
// src/components/builder/PropertyPanel.tsx
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
function PropertyPanel() {
const { state, dispatch } = useBuilder();
const selectedBlock = state.layout?.blocks.find((b) => b.id === state.selectedBlockId);
if (!selectedBlock) {
return /* @__PURE__ */ jsxs4("div", { className: "w-64 bg-gray-50 border-l border-gray-200 p-4", children: [
/* @__PURE__ */ jsx5("h2", { className: "font-semibold text-sm text-gray-900 mb-4", children: "Properties" }),
/* @__PURE__ */ jsx5("div", { className: "text-sm text-gray-500", children: "Select a block to edit its properties" })
] });
}
const updateBlockProps = (updates) => {
dispatch({
type: "UPDATE_BLOCK",
payload: {
id: selectedBlock.id,
updates: {
props: { ...selectedBlock.props, ...updates }
}
}
});
};
const updateBlockStyle = (updates) => {
dispatch({
type: "UPDATE_BLOCK",
payload: {
id: selectedBlock.id,
updates: {
style: { ...selectedBlock.style, ...updates }
}
}
});
};
return /* @__PURE__ */ jsxs4("div", { className: "w-64 bg-gray-50 border-l border-gray-200 p-4", children: [
/* @__PURE__ */ jsx5("h2", { className: "font-semibold text-sm text-gray-900 mb-4", children: "Properties" }),
/* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-xs font-medium text-gray-700 mb-1", children: "Block Type" }),
/* @__PURE__ */ jsx5("div", { className: "text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded", children: selectedBlock.type })
] }),
selectedBlock.type === "text" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-xs font-medium text-gray-700 mb-1", children: "Content" }),
/* @__PURE__ */ jsx5(
"textarea",
{
value: selectedBlock.props.content || "",
onChange: (e) => updateBlockProps({ content: e.target.value }),
className: "w-full text-sm border border-gray-300 rounded px-2 py-1",
rows: 3
}
)
] }),
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-xs font-medium text-gray-700 mb-1", children: "Tag" }),
/* @__PURE__ */ jsxs4(
"select",
{
value: selectedBlock.props.tag || "p",
onChange: (e) => updateBlockProps({ tag: e.target.value }),
className: "w-full text-sm border border-gray-300 rounded px-2 py-1",
children: [
/* @__PURE__ */ jsx5("option", { value: "p", children: "Paragraph" }),
/* @__PURE__ */ jsx5("option", { value: "h1", children: "Heading 1" }),
/* @__PURE__ */ jsx5("option", { value: "h2", children: "Heading 2" }),
/* @__PURE__ */ jsx5("option", { value: "h3", children: "Heading 3" }),
/* @__PURE__ */ jsx5("option", { value: "span", children: "Span" }),
/* @__PURE__ */ jsx5("option", { value: "div", children: "Div" })
]
}
)
] })
] }),
selectedBlock.type === "button" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-xs font-medium text-gray-700 mb-1", children: "Text" }),
/* @__PURE__ */ jsx5(
"input",
{
type: "text",
value: selectedBlock.props.text || "",
onChange: (e) => updateBlockProps({ text: e.target.value }),
className: "w-full text-sm border border-gray-300 rounded px-2 py-1"
}
)
] }),
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-xs font-medium text-gray-700 mb-1", children: "Variant" }),
/* @__PURE__ */ jsxs4(
"select",
{
value: selectedBlock.props.variant || "default",
onChange: (e) => updateBlockProps({ variant: e.target.value }),
className: "w-full text-sm border border-gray-300 rounded px-2 py-1",
children: [
/* @__PURE__ */ jsx5("option", { value: "default", children: "Default" }),
/* @__PURE__ */ jsx5("option", { value: "destructive", children: "Destructive" }),
/* @__PURE__ */ jsx5("option", { value: "outline", children: "Outline" }),
/* @__PURE__ */ jsx5("option", { value: "secondary", children: "Secondary" }),
/* @__PURE__ */ jsx5("option", { value: "ghost", children: "Ghost" }),
/* @__PURE__ */ jsx5("option", { value: "link", children: "Link" })
]
}
)
] })
] }),
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-xs font-medium text-gray-700 mb-2", children: "Position & Size" }),
/* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-2 gap-2 text-xs", children: [
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-gray-600", children: "X" }),
/* @__PURE__ */ jsx5(
"input",
{
type: "number",
value: selectedBlock.position.x,
onChange: (e) => dispatch({
type: "UPDATE_BLOCK",
payload: {
id: selectedBlock.id,
updates: {
position: { ...selectedBlock.position, x: parseInt(e.target.value) || 0 }
}
}
}),
className: "w-full border border-gray-300 rounded px-1 py-1"
}
)
] }),
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-gray-600", children: "Y" }),
/* @__PURE__ */ jsx5(
"input",
{
type: "number",
value: selectedBlock.position.y,
onChange: (e) => dispatch({
type: "UPDATE_BLOCK",
payload: {
id: selectedBlock.id,
updates: {
position: { ...selectedBlock.position, y: parseInt(e.target.value) || 0 }
}
}
}),
className: "w-full border border-gray-300 rounded px-1 py-1"
}
)
] }),
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-gray-600", children: "Width" }),
/* @__PURE__ */ jsx5(
"input",
{
type: "number",
value: selectedBlock.position.width,
onChange: (e) => dispatch({
type: "UPDATE_BLOCK",
payload: {
id: selectedBlock.id,
updates: {
position: { ...selectedBlock.position, width: parseInt(e.target.value) || 0 }
}
}
}),
className: "w-full border border-gray-300 rounded px-1 py-1"
}
)
] }),
/* @__PURE__ */ jsxs4("div", { children: [
/* @__PURE__ */ jsx5("label", { className: "block text-gray-600", children: "Height" }),
/* @__PURE__ */ jsx5(
"input",
{
type: "number",
value: selectedBlock.position.height,
onChange: (e) => dispatch({
type: "UPDATE_BLOCK",
payload: {
id: selectedBlock.id,
updates: {
position: { ...selectedBlock.position, height: parseInt(e.target.value) || 0 }
}
}
}),
className: "w-full border border-gray-300 rounded px-1 py-1"
}
)
] })
] })
] }),
/* @__PURE__ */ jsx5(
"button",
{
onClick: () => dispatch({ type: "REMOVE_BLOCK", payload: selectedBlock.id }),
className: "w-full bg-red-600 hover:bg-red-700 text-white text-sm py-2 rounded",
children: "Delete Block"
}
)
] })
] });
}
// src/components/builder/Toolbar.tsx
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
function Toolbar() {
const { state, dispatch } = useBuilder();
const tools = [
{
id: "select",
icon: /* @__PURE__ */ jsx6("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" }) }),
name: "Select",
active: state.tool === "select"
},
{
id: "hand",
icon: /* @__PURE__ */ jsx6("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11" }) }),
name: "Hand",
active: state.tool === "hand"
},
{
id: "text",
icon: /* @__PURE__ */ jsx6("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" }) }),
name: "Text",
active: state.tool === "text"
}
];
return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1", children: [
tools.map((tool) => /* @__PURE__ */ jsx6(
"button",
{
onClick: () => dispatch({ type: "SET_TOOL", payload: tool.id }),
className: `p-2 rounded transition-colors ${tool.active ? "bg-blue-100 text-blue-600" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`,
title: tool.name,
children: tool.icon
},
tool.id
)),
/* @__PURE__ */ jsx6("div", { className: "h-6 w-px bg-gray-300 mx-2" }),
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx6("button", { className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded", children: /* @__PURE__ */ jsx6("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10h-3m-3 0h3m0 0V7m0 3v3" }) }) }),
/* @__PURE__ */ jsx6("span", { className: "text-xs text-gray-600 px-1", children: "100%" }),
/* @__PURE__ */ jsx6("button", { className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded", children: /* @__PURE__ */ jsx6("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM16 10h-6" }) }) })
] })
] });
}
// src/components/ZunoBuilder.tsx
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
function ZunoBuilder({ config, children }) {
const [isBuilderOpen, setIsBuilderOpen] = useState2(false);
const isEnabled = config?.enabled ?? process.env.NODE_ENV === "development";
if (!isEnabled) {
return /* @__PURE__ */ jsx7(Fragment3, { children });
}
if (!isBuilderOpen) {
return /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
children,
/* @__PURE__ */ jsx7("div", { className: "fixed bottom-6 right-6 z-50", children: /* @__PURE__ */ jsxs6(
"button",
{
onClick: () => setIsBuilderOpen(true),
className: "group bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-6 py-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center gap-2 font-medium",
children: [
/* @__PURE__ */ jsx7("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx7("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" }) }),
"Open Builder"
]
}
) })
] });
}
return /* @__PURE__ */ jsx7(BuilderProvider, { children: /* @__PURE__ */ jsxs6("div", { className: "fixed inset-0 z-50 bg-gray-50 flex flex-col", children: [
/* @__PURE__ */ jsxs6("div", { className: "h-12 bg-white border-b border-gray-200 flex items-center justify-between px-4 shadow-sm", children: [
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-4", children: [
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsx7("div", { className: "w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded flex items-center justify-center", children: /* @__PURE__ */ jsx7("span", { className: "text-white text-xs font-bold", children: "Z" }) }),
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-gray-900", children: "Zuno Builder" })
] }),
/* @__PURE__ */ jsx7("div", { className: "h-6 w-px bg-gray-300" }),
/* @__PURE__ */ jsx7(Toolbar, {})
] }),
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-3", children: [
/* @__PURE__ */ jsx7("button", { className: "px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded transition-colors", children: "Preview" }),
/* @__PURE__ */ jsx7("button", { className: "px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors", children: "Publish" }),
/* @__PURE__ */ jsx7("div", { className: "h-6 w-px bg-gray-300" }),
/* @__PURE__ */ jsx7(
"button",
{
onClick: () => setIsBuilderOpen(false),
className: "p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition-colors",
children: /* @__PURE__ */ jsx7("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx7("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
}
)
] })
] }),
/* @__PURE__ */ jsxs6("div", { className: "flex flex-1 overflow-hidden", children: [
/* @__PURE__ */ jsx7("div", { className: "w-64 bg-white border-r border-gray-200 flex flex-col", children: /* @__PURE__ */ jsx7(Sidebar, {}) }),
/* @__PURE__ */ jsxs6("div", { className: "flex-1 bg-gray-50 flex flex-col overflow-hidden", children: [
/* @__PURE__ */ jsx7("div", { className: "h-10 bg-white border-b border-gray-200 flex items-center justify-center px-4", children: /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 text-sm text-gray-600", children: [
/* @__PURE__ */ jsx7("span", { children: "100%" }),
/* @__PURE__ */ jsx7("div", { className: "h-4 w-px bg-gray-300" }),
/* @__PURE__ */ jsx7("span", { children: "Frame 1" })
] }) }),
/* @__PURE__ */ jsx7("div", { className: "flex-1 overflow-auto p-8", children: /* @__PURE__ */ jsx7(Canvas, {}) })
] }),
/* @__PURE__ */ jsx7("div", { className: "w-72 bg-white border-l border-gray-200", children: /* @__PURE__ */ jsx7(PropertyPanel, {}) })
] })
] }) });
}
// src/hooks/useZunoBuilder.ts
import { useState as useState3, useEffect as useEffect2 } from "react";
function useZunoBuilder() {
const [layout, setLayout] = useState3(null);
const [isLoading, setIsLoading] = useState3(true);
useEffect2(() => {
if (process.env.NODE_ENV === "development") {
loadLayout();
} else {
setIsLoading(false);
}
}, []);
const loadLayout = async () => {
try {
setLayout({
version: "1.0.0",
id: "default-layout",
name: "Default Layout",
blocks: [],
metadata: {
version: "1.0.0",
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
targetPath: ""
},
viewport: {
width: 1200,
height: 800,
scale: 1,
offsetX: 0,
offsetY: 0
}
});
} catch (error) {
console.warn("Failed to load Zuno layout:", error);
} finally {
setIsLoading(false);
}
};
const saveLayout = async (newLayout) => {
try {
setLayout(newLayout);
} catch (error) {
console.error("Failed to save Zuno layout:", error);
}
};
const addBlock = (block) => {
if (!layout)
return;
const updatedLayout = {
...layout,
blocks: [...layout.blocks, block],
metadata: {
...layout.metadata,
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
}
};
saveLayout(updatedLayout);
};
const updateBlock = (blockId, updates) => {
if (!layout)
return;
const updatedBlocks = layout.blocks.map(
(block) => block.id === blockId ? { ...block, ...updates } : block
);
const updatedLayout = {
...layout,
blocks: updatedBlocks,
metadata: {
...layout.metadata,
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
}
};
saveLayout(updatedLayout);
};
const removeBlock = (blockId) => {
if (!layout)
return;
const updatedBlocks = layout.blocks.filter((block) => block.id !== blockId);
const updatedLayout = {
...layout,
blocks: updatedBlocks,
metadata: {
...layout.metadata,
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
}
};
saveLayout(updatedLayout);
};
return {
layout,
isLoading,
addBlock,
updateBlock,
removeBlock,
saveLayout
};
}
export {
ZunoBuilder,
useZunoBuilder
};
//# sourceMappingURL=index.mjs.map