@calca/canvas-ui 0.0.0-stage → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
- # Temporary Holding Version
1
+ # @calca/canvas-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Headless canvas UI components for Calca — presentational React pieces for canvas surfaces: `DesignFrame` (generated-frame chrome), `DesignCard`, `PipelineStatusOverlay`, `RubberBandOverlay` (marquee selection), `useMountEffect`. No React Flow or document-model dependency — pair with `@calca/canvas-flow` for the full surface.
4
+
5
+ Part of [Calca](https://calca.illo.fyi) — open source.
6
+
7
+ ## License
8
+
9
+ Apache-2.0
@@ -0,0 +1,64 @@
1
+ import { Comment, DesignIteration, PipelineStatus, Point } from "@calca/shared";
2
+ //#region src/components/DesignFrame.d.ts
3
+ export declare const DEFAULT_FRAME_WIDTH = 480;
4
+ interface DesignFrameProps {
5
+ iteration: DesignIteration;
6
+ width?: number;
7
+ height?: number;
8
+ className?: string;
9
+ style?: React.CSSProperties;
10
+ isCommentMode?: boolean;
11
+ isSelectMode?: boolean;
12
+ isSelected?: boolean;
13
+ isDragging?: boolean;
14
+ scale?: number;
15
+ onClick?: (e: React.MouseEvent) => void;
16
+ onMouseDown?: (e: React.MouseEvent) => void;
17
+ onAddComment?: (iterationId: string, position: {
18
+ x: number;
19
+ y: number;
20
+ }, screenX: number, screenY: number) => void;
21
+ onClickComment?: (comment: Comment, iterationId: string) => void;
22
+ }
23
+ export declare function DesignFrame({ iteration, width, height: _height, className, style, isCommentMode, isSelectMode, isSelected, isDragging, scale, onClick, onMouseDown, onAddComment, onClickComment }: DesignFrameProps): import("react/jsx-runtime").JSX.Element;
24
+ //#endregion
25
+ //#region src/components/DesignCard.d.ts
26
+ interface DesignCardProps {
27
+ iteration: DesignIteration;
28
+ isCommentMode: boolean;
29
+ isSelectMode: boolean;
30
+ isDragging: boolean;
31
+ isSelected?: boolean;
32
+ onSelect?: (e?: React.MouseEvent) => void;
33
+ onAddComment: (iterationId: string, position: Point, screenX: number, screenY: number) => void;
34
+ onClickComment: (comment: Comment, iterationId: string) => void;
35
+ onDragStart: (e: React.MouseEvent) => void;
36
+ scale: number;
37
+ }
38
+ export declare function DesignCard({ iteration, isCommentMode, isSelectMode, isDragging, isSelected, onSelect, onAddComment, onClickComment, onDragStart, scale }: DesignCardProps): import("react/jsx-runtime").JSX.Element;
39
+ //#endregion
40
+ //#region src/components/PipelineStatusOverlay.d.ts
41
+ interface PipelineStatusBarProps {
42
+ status: PipelineStatus;
43
+ x: number;
44
+ y: number;
45
+ width: number;
46
+ frameHeight: number;
47
+ }
48
+ export declare function PipelineStatusOverlay({ status, x, y, width, frameHeight }: PipelineStatusBarProps): import("react/jsx-runtime").JSX.Element | null;
49
+ //#endregion
50
+ //#region src/components/RubberBandOverlay.d.ts
51
+ interface RubberBandOverlayProps {
52
+ rubberBand: {
53
+ startX: number;
54
+ startY: number;
55
+ currentX: number;
56
+ currentY: number;
57
+ } | null;
58
+ }
59
+ export declare function RubberBandOverlay({ rubberBand }: RubberBandOverlayProps): import("react/jsx-runtime").JSX.Element | null;
60
+ //#endregion
61
+ //#region src/utils/use-mount-effect.d.ts
62
+ export declare function useMountEffect(effect: () => void | (() => void)): void;
63
+ //#endregion
64
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":["CommentType","CommentType"],"sources":["../src/components/DesignFrame.tsx","../src/components/DesignCard.tsx","../src/components/PipelineStatusOverlay.tsx","../src/components/RubberBandOverlay.tsx","../src/utils/use-mount-effect.ts"],"mappings":";;qBAgBa;UAIH;EACR,WAAW;EACX;EACA;EACA;EACA,QAAQ,MAAM;EACd;EACA;EACA;EACA;EACA;EACA,WAAW,GAAG,MAAM;EACpB,eAAe,GAAG,MAAM;EACxB,gBACE,qBACA;IAAY;IAAW;KACvB,iBACA;EAEF,kBAAkB,SAASA,SAAa;;wBAG1B,cACd,WACA,OACA,QAAQ,SACR,WACA,OACA,eACA,cACA,YACA,YACA,OACA,SACA,aACA,cACA,kBACC,+CAAgB,IAAA;;;UCnDT;EACR,WAAW;EACX;EACA;EACA;EACA;EACA,YAAY,IAAI,MAAM;EACtB,eAAe,qBAAqB,UAAU,OAAO,iBAAiB;EACtE,iBAAiB,SAASC,SAAa;EACvC,cAAc,GAAG,MAAM;EACvB;;wBAGc,aACd,WACA,eACA,cACA,YACA,YACA,UACA,cACA,gBACA,aACA,SACC,8CAAe,IAAA;;;UC5BR;EACR,QAAQ;EACR;EACA;EACA;EACA;;wBAGc,wBACd,QACA,GACA,GACA,OACA,eACC,qDAAsB,IAAA;;;UChBf;EACR;IACE;IACA;IACA;IACA;;;wBAIY,oBAAoB,cAAc,qDAAsB,IAAA;;;wBCNxD,eAAe"}
package/dist/index.mjs ADDED
@@ -0,0 +1,280 @@
1
+ import { Loader } from "lucide-react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { STAGE_CONFIG } from "@calca/shared";
5
+
6
+ //#region src/utils/use-mount-effect.ts
7
+ function useMountEffect(effect) {
8
+ useEffect(effect, []);
9
+ }
10
+
11
+ //#endregion
12
+ //#region src/components/DesignFrame.tsx
13
+ const getTailwindScriptSrc = () => {
14
+ if (typeof window !== "undefined" && window.__CALCA_DESKTOP__) return `${window.location.origin}/tailwindcss.js`;
15
+ return "https://cdn.tailwindcss.com";
16
+ };
17
+ const DEFAULT_FRAME_WIDTH = 480;
18
+ const FRAME_WIDTH = 480;
19
+ const INITIAL_IFRAME_HEIGHT = 2e3;
20
+ function DesignFrame({ iteration, width, height: _height, className = "", style, isCommentMode, isSelectMode, isSelected, isDragging, scale = 1, onClick, onMouseDown, onAddComment, onClickComment }) {
21
+ const iframeRef = useRef(null);
22
+ const wrapperRef = useRef(null);
23
+ const [contentHeight, setContentHeight] = useState(320);
24
+ const measuredRef = useRef(false);
25
+ const measurementGenRef = useRef(0);
26
+ const frameW = width || iteration.width || FRAME_WIDTH;
27
+ const srcdoc = iteration.html && !iteration.isLoading ? `<!DOCTYPE html>
28
+ <html style="height:auto;overflow:hidden;"><head>
29
+ <meta charset="utf-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1">
31
+ <script src="${getTailwindScriptSrc()}"><\/script>
32
+ <style>
33
+ html, body { margin: 0; padding: 0; height: auto !important; min-height: 0 !important; max-height: none !important; overflow: hidden; }
34
+ body { background: white; width: ${frameW}px; }
35
+ #calca-measure { width: ${frameW}px; overflow: hidden; }
36
+ img, video, svg { max-width: 100%; height: auto; display: block; object-fit: cover; }
37
+ * { animation: none !important; transition: none !important; }
38
+ [style*="100vh"], [style*="min-height: 100vh"], [style*="height: 100vh"] { height: auto !important; min-height: 0 !important; }
39
+ </style>
40
+ </head><body><div id="calca-measure" data-gen="${measurementGenRef.current}">${iteration.html}</div>
41
+ <script>
42
+ var GEN = ${measurementGenRef.current};
43
+ function reportHeight() {
44
+ var el = document.getElementById('calca-measure');
45
+ if (!el) return;
46
+ var h = el.offsetHeight || el.scrollHeight || 100;
47
+ parent.postMessage({ type: 'calca-frame-height', id: '${iteration.id}', height: h, gen: GEN }, '*');
48
+ }
49
+ setTimeout(reportHeight, 300);
50
+ setTimeout(reportHeight, 800);
51
+ setTimeout(reportHeight, 2000);
52
+ <\/script></body></html>` : void 0;
53
+ useEffect(() => {
54
+ if (!iteration.html || iteration.isLoading) return;
55
+ measuredRef.current = false;
56
+ measurementGenRef.current += 1;
57
+ const currentGen = measurementGenRef.current;
58
+ if (iteration.height) setContentHeight(iteration.height);
59
+ const onMessage = (e) => {
60
+ if (e.data?.type === "calca-frame-height" && e.data.id === iteration.id && e.data.gen === currentGen) {
61
+ const h = Math.min(Math.max(e.data.height, 50), 12e3);
62
+ if (!iteration.height || iteration.height === 0 || Math.abs(h - iteration.height) > 30) {
63
+ setContentHeight(h);
64
+ measuredRef.current = true;
65
+ }
66
+ }
67
+ };
68
+ window.addEventListener("message", onMessage);
69
+ return () => window.removeEventListener("message", onMessage);
70
+ }, [
71
+ iteration.html,
72
+ iteration.isLoading,
73
+ iteration.id,
74
+ iteration.height
75
+ ]);
76
+ const handleClick = (e) => {
77
+ if (isCommentMode && onAddComment && wrapperRef.current) {
78
+ e.stopPropagation();
79
+ const rect = wrapperRef.current.getBoundingClientRect();
80
+ const x = (e.clientX - rect.left) / scale;
81
+ const y = (e.clientY - rect.top) / scale;
82
+ onAddComment(iteration.id, {
83
+ x,
84
+ y
85
+ }, e.clientX, e.clientY);
86
+ }
87
+ onClick?.(e);
88
+ };
89
+ const frameHeight = iteration.isLoading ? 320 : contentHeight;
90
+ return /* @__PURE__ */ jsxs("div", {
91
+ ref: wrapperRef,
92
+ "data-tour": "design-frame",
93
+ onClick: handleClick,
94
+ onMouseDown,
95
+ className: `relative bg-white rounded-xl shadow-md border overflow-hidden transition-shadow ${isSelected ? "ring-2 ring-blue-500 border-blue-400/50 shadow-lg" : "border-gray-200/80"} ${isCommentMode ? "cursor-crosshair ring-2 ring-blue-400/20 hover:ring-blue-400/40" : isSelectMode ? isDragging ? "cursor-grabbing shadow-xl ring-2 ring-blue-400/30" : "cursor-grab hover:shadow-lg" : ""} ${className}`,
96
+ style: {
97
+ ...style,
98
+ height: frameHeight,
99
+ width: frameW
100
+ },
101
+ children: [iteration.isLoading ? /* @__PURE__ */ jsxs("div", {
102
+ className: "w-full h-full flex flex-col items-center justify-center gap-4",
103
+ children: [/* @__PURE__ */ jsx("div", {
104
+ className: "relative w-10 h-10",
105
+ children: /* @__PURE__ */ jsx(Loader, { className: "w-10 h-10 animate-spin" })
106
+ }), /* @__PURE__ */ jsx("span", {
107
+ className: "text-[12px] font-medium text-gray-400",
108
+ children: "Generating…"
109
+ })]
110
+ }) : /* @__PURE__ */ jsx("iframe", {
111
+ ref: iframeRef,
112
+ title: iteration.label,
113
+ sandbox: "allow-scripts",
114
+ srcDoc: srcdoc,
115
+ style: {
116
+ border: "none",
117
+ display: "block",
118
+ height: measuredRef.current ? contentHeight : iteration.height || INITIAL_IFRAME_HEIGHT,
119
+ pointerEvents: "none",
120
+ width: frameW
121
+ }
122
+ }), isCommentMode && onClickComment && onAddComment && iteration.comments.map((comment) => /* @__PURE__ */ jsx(CommentPin, {
123
+ comment,
124
+ onClick: () => onClickComment(comment, iteration.id)
125
+ }, comment.id))]
126
+ });
127
+ }
128
+ const STATUS_COLORS = {
129
+ done: {
130
+ bg: "bg-emerald-500",
131
+ shadow: "rgba(16,185,129,0.4)",
132
+ anchor: "bg-emerald-400/60",
133
+ ping: "bg-emerald-400/30"
134
+ },
135
+ waiting: {
136
+ bg: "bg-gray-400",
137
+ shadow: "rgba(156,163,175,0.4)",
138
+ anchor: "bg-gray-400/60",
139
+ ping: "bg-gray-400/30"
140
+ },
141
+ working: {
142
+ bg: "bg-amber-500",
143
+ shadow: "rgba(245,158,11,0.4)",
144
+ anchor: "bg-amber-400/60",
145
+ ping: "bg-amber-400/30"
146
+ }
147
+ };
148
+ function CommentPin({ comment, onClick }) {
149
+ const [isNew, setIsNew] = useState(true);
150
+ useMountEffect(() => {
151
+ const timer = setTimeout(() => setIsNew(false), 2e3);
152
+ return () => clearTimeout(timer);
153
+ });
154
+ const status = comment.status || "waiting";
155
+ const colors = STATUS_COLORS[status];
156
+ const isWorking = status === "working";
157
+ return /* @__PURE__ */ jsxs("div", {
158
+ className: "absolute z-20",
159
+ style: {
160
+ left: comment.position.x - 14,
161
+ top: comment.position.y - 14
162
+ },
163
+ children: [
164
+ (isNew || isWorking) && /* @__PURE__ */ jsx("span", { className: `absolute inset-0 rounded-full ${colors.ping} animate-ping` }),
165
+ /* @__PURE__ */ jsx("span", { className: `absolute left-1/2 -translate-x-1/2 top-full w-0.5 h-2 ${colors.anchor}` }),
166
+ /* @__PURE__ */ jsx("button", {
167
+ className: `relative w-7 h-7 rounded-full ${colors.bg} text-white text-[11px] font-bold flex items-center justify-center hover:scale-110 transition-all cursor-pointer border-2 border-white`,
168
+ style: { boxShadow: `0 2px 8px ${colors.shadow}` },
169
+ onClick: (e) => {
170
+ e.stopPropagation();
171
+ onClick();
172
+ },
173
+ title: comment.aiResponse || comment.text,
174
+ children: status === "done" ? "✓" : comment.number
175
+ })
176
+ ]
177
+ });
178
+ }
179
+
180
+ //#endregion
181
+ //#region src/components/DesignCard.tsx
182
+ function DesignCard({ iteration, isCommentMode, isSelectMode, isDragging, isSelected, onSelect, onAddComment, onClickComment, onDragStart, scale }) {
183
+ return /* @__PURE__ */ jsxs("div", {
184
+ className: `absolute ${isDragging ? "z-50" : ""}`,
185
+ style: {
186
+ left: iteration.position.x,
187
+ top: iteration.position.y,
188
+ width: iteration.width || 480
189
+ },
190
+ children: [/* @__PURE__ */ jsx("div", {
191
+ className: "mb-2 flex items-center gap-2 group/label",
192
+ children: /* @__PURE__ */ jsx("span", {
193
+ className: "text-xs font-medium text-gray-500/80 bg-white/60 backdrop-blur-sm px-2.5 py-0.5 rounded-lg border border-white/40",
194
+ children: iteration.label
195
+ })
196
+ }), /* @__PURE__ */ jsx(DesignFrame, {
197
+ iteration,
198
+ isCommentMode,
199
+ isSelectMode,
200
+ isSelected,
201
+ isDragging,
202
+ scale,
203
+ onClick: (e) => {
204
+ if (isSelectMode && onSelect) {
205
+ e.stopPropagation();
206
+ onSelect(e);
207
+ }
208
+ },
209
+ onMouseDown: (e) => {
210
+ if (isSelectMode) {
211
+ e.stopPropagation();
212
+ onDragStart(e);
213
+ }
214
+ },
215
+ onAddComment: (iterationId, position, screenX, screenY) => onAddComment(iterationId, position, screenX, screenY),
216
+ onClickComment
217
+ })]
218
+ });
219
+ }
220
+
221
+ //#endregion
222
+ //#region src/components/PipelineStatusOverlay.tsx
223
+ function PipelineStatusOverlay({ status, x, y, width, frameHeight }) {
224
+ const config = STAGE_CONFIG[status.stage];
225
+ const isDone = status.stage === "done";
226
+ const isError = status.stage === "error";
227
+ const isQueued = status.stage === "queued";
228
+ if (isDone) return null;
229
+ const topOffset = y + frameHeight + 8;
230
+ const value = Math.max(status.progress * 100, 5);
231
+ const indicatorClass = isError ? "bg-destructive" : "bg-gradient-to-r from-primary to-secondary";
232
+ const pulseClass = status.stage === "layout" || status.stage === "images" ? "animate-pulse" : "";
233
+ return /* @__PURE__ */ jsxs("div", {
234
+ className: "absolute pointer-events-none",
235
+ style: {
236
+ left: x,
237
+ top: topOffset,
238
+ width
239
+ },
240
+ children: [!isQueued && /* @__PURE__ */ jsx("div", {
241
+ className: "h-1 w-full overflow-hidden rounded-full bg-primary/20",
242
+ children: /* @__PURE__ */ jsx("div", {
243
+ className: `h-full transition-all ${indicatorClass} ${pulseClass}`,
244
+ style: { width: `${value}%` }
245
+ })
246
+ }), /* @__PURE__ */ jsxs("div", {
247
+ className: "flex items-center justify-between mt-1.5",
248
+ children: [/* @__PURE__ */ jsxs("span", {
249
+ className: "text-[10px] font-medium text-muted-foreground whitespace-nowrap",
250
+ children: [
251
+ config.icon,
252
+ " ",
253
+ config.label
254
+ ]
255
+ }), status.skipped && /* @__PURE__ */ jsxs("span", {
256
+ className: "text-[10px] font-medium text-destructive/80 whitespace-nowrap",
257
+ children: ["⏭ ", status.reason || "Skipped"]
258
+ })]
259
+ })]
260
+ });
261
+ }
262
+
263
+ //#endregion
264
+ //#region src/components/RubberBandOverlay.tsx
265
+ function RubberBandOverlay({ rubberBand }) {
266
+ if (!rubberBand) return null;
267
+ return /* @__PURE__ */ jsx("div", {
268
+ className: "fixed pointer-events-none z-[60] border border-blue-400/50 bg-blue-400/10 rounded-sm",
269
+ style: {
270
+ height: Math.abs(rubberBand.currentY - rubberBand.startY),
271
+ left: Math.min(rubberBand.startX, rubberBand.currentX),
272
+ top: Math.min(rubberBand.startY, rubberBand.currentY),
273
+ width: Math.abs(rubberBand.currentX - rubberBand.startX)
274
+ }
275
+ });
276
+ }
277
+
278
+ //#endregion
279
+ export { DEFAULT_FRAME_WIDTH, DesignCard, DesignFrame, PipelineStatusOverlay, RubberBandOverlay, useMountEffect };
280
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/use-mount-effect.ts","../src/components/DesignFrame.tsx","../src/components/DesignCard.tsx","../src/components/PipelineStatusOverlay.tsx","../src/components/RubberBandOverlay.tsx"],"sourcesContent":["import { useEffect } from \"react\";\n\n// oxlint-disable-next-line\nexport function useMountEffect(effect: () => void | (() => void)) {\n useEffect(effect, []);\n}\n","import type { Comment as CommentType, DesignIteration } from \"@calca/shared\";\nimport { Loader } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { useMountEffect } from \"../utils/use-mount-effect\";\n\nconst getTailwindScriptSrc = (): string => {\n if (\n typeof window !== \"undefined\" &&\n (window as unknown as { __CALCA_DESKTOP__?: boolean }).__CALCA_DESKTOP__\n ) {\n return `${window.location.origin}/tailwindcss.js`;\n }\n return \"https://cdn.tailwindcss.com\";\n};\n\nexport const DEFAULT_FRAME_WIDTH = 480;\nconst FRAME_WIDTH = DEFAULT_FRAME_WIDTH;\nconst INITIAL_IFRAME_HEIGHT = 2000;\n\ninterface DesignFrameProps {\n iteration: DesignIteration;\n width?: number;\n height?: number;\n className?: string;\n style?: React.CSSProperties;\n isCommentMode?: boolean;\n isSelectMode?: boolean;\n isSelected?: boolean;\n isDragging?: boolean;\n scale?: number;\n onClick?: (e: React.MouseEvent) => void;\n onMouseDown?: (e: React.MouseEvent) => void;\n onAddComment?: (\n iterationId: string,\n position: { x: number; y: number },\n screenX: number,\n screenY: number,\n ) => void;\n onClickComment?: (comment: CommentType, iterationId: string) => void;\n}\n\nexport function DesignFrame({\n iteration,\n width,\n height: _height,\n className = \"\",\n style,\n isCommentMode,\n isSelectMode,\n isSelected,\n isDragging,\n scale = 1,\n onClick,\n onMouseDown,\n onAddComment,\n onClickComment,\n}: DesignFrameProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const [contentHeight, setContentHeight] = useState(320);\n const measuredRef = useRef(false);\n const measurementGenRef = useRef(0);\n\n const frameW = width || iteration.width || FRAME_WIDTH;\n const srcdoc =\n iteration.html && !iteration.isLoading\n ? `<!DOCTYPE html>\n<html style=\"height:auto;overflow:hidden;\"><head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <script src=\"${getTailwindScriptSrc()}\"></script>\n <style>\n html, body { margin: 0; padding: 0; height: auto !important; min-height: 0 !important; max-height: none !important; overflow: hidden; }\n body { background: white; width: ${frameW}px; }\n #calca-measure { width: ${frameW}px; overflow: hidden; }\n img, video, svg { max-width: 100%; height: auto; display: block; object-fit: cover; }\n * { animation: none !important; transition: none !important; }\n [style*=\"100vh\"], [style*=\"min-height: 100vh\"], [style*=\"height: 100vh\"] { height: auto !important; min-height: 0 !important; }\n </style>\n</head><body><div id=\"calca-measure\" data-gen=\"${measurementGenRef.current}\">${iteration.html}</div>\n<script>\nvar GEN = ${measurementGenRef.current};\nfunction reportHeight() {\n var el = document.getElementById('calca-measure');\n if (!el) return;\n var h = el.offsetHeight || el.scrollHeight || 100;\n parent.postMessage({ type: 'calca-frame-height', id: '${iteration.id}', height: h, gen: GEN }, '*');\n}\nsetTimeout(reportHeight, 300);\nsetTimeout(reportHeight, 800);\nsetTimeout(reportHeight, 2000);\n</script></body></html>`\n : undefined;\n\n // oxlint-disable-next-line\n useEffect(() => {\n if (!iteration.html || iteration.isLoading) {\n return;\n }\n measuredRef.current = false;\n measurementGenRef.current += 1;\n const currentGen = measurementGenRef.current;\n\n if (iteration.height) {\n setContentHeight(iteration.height);\n }\n\n const onMessage = (e: MessageEvent) => {\n if (\n e.data?.type === \"calca-frame-height\" &&\n e.data.id === iteration.id &&\n e.data.gen === currentGen\n ) {\n const h = Math.min(Math.max(e.data.height, 50), 12_000);\n if (!iteration.height || iteration.height === 0 || Math.abs(h - iteration.height) > 30) {\n setContentHeight(h);\n measuredRef.current = true;\n }\n }\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [iteration.html, iteration.isLoading, iteration.id, iteration.height]);\n\n const handleClick = (e: React.MouseEvent) => {\n if (isCommentMode && onAddComment && wrapperRef.current) {\n e.stopPropagation();\n const rect = wrapperRef.current.getBoundingClientRect();\n const x = (e.clientX - rect.left) / scale;\n const y = (e.clientY - rect.top) / scale;\n onAddComment(iteration.id, { x, y }, e.clientX, e.clientY);\n }\n onClick?.(e);\n };\n\n const frameHeight = iteration.isLoading ? 320 : contentHeight;\n\n return (\n <div\n ref={wrapperRef}\n data-tour=\"design-frame\"\n onClick={handleClick}\n onMouseDown={onMouseDown}\n className={`relative bg-white rounded-xl shadow-md border overflow-hidden transition-shadow ${\n isSelected ? \"ring-2 ring-blue-500 border-blue-400/50 shadow-lg\" : \"border-gray-200/80\"\n } ${\n isCommentMode\n ? \"cursor-crosshair ring-2 ring-blue-400/20 hover:ring-blue-400/40\"\n : isSelectMode\n ? isDragging\n ? \"cursor-grabbing shadow-xl ring-2 ring-blue-400/30\"\n : \"cursor-grab hover:shadow-lg\"\n : \"\"\n } ${className}`}\n style={{ ...style, height: frameHeight, width: frameW }}\n >\n {iteration.isLoading ? (\n <div className=\"w-full h-full flex flex-col items-center justify-center gap-4\">\n <div className=\"relative w-10 h-10\">\n <Loader className=\"w-10 h-10 animate-spin\" />\n </div>\n <span className=\"text-[12px] font-medium text-gray-400\">Generating…</span>\n </div>\n ) : (\n <iframe\n ref={iframeRef}\n title={iteration.label}\n sandbox=\"allow-scripts\"\n srcDoc={srcdoc}\n style={{\n border: \"none\",\n display: \"block\",\n height: measuredRef.current ? contentHeight : iteration.height || INITIAL_IFRAME_HEIGHT,\n pointerEvents: \"none\",\n width: frameW,\n }}\n />\n )}\n\n {isCommentMode &&\n onClickComment &&\n onAddComment &&\n iteration.comments.map((comment) => (\n <CommentPin\n key={comment.id}\n comment={comment}\n onClick={() => onClickComment(comment, iteration.id)}\n />\n ))}\n </div>\n );\n}\n\nconst STATUS_COLORS = {\n done: {\n bg: \"bg-emerald-500\",\n shadow: \"rgba(16,185,129,0.4)\",\n anchor: \"bg-emerald-400/60\",\n ping: \"bg-emerald-400/30\",\n },\n waiting: {\n bg: \"bg-gray-400\",\n shadow: \"rgba(156,163,175,0.4)\",\n anchor: \"bg-gray-400/60\",\n ping: \"bg-gray-400/30\",\n },\n working: {\n bg: \"bg-amber-500\",\n shadow: \"rgba(245,158,11,0.4)\",\n anchor: \"bg-amber-400/60\",\n ping: \"bg-amber-400/30\",\n },\n} as const;\n\nfunction CommentPin({ comment, onClick }: { comment: CommentType; onClick: () => void }) {\n const [isNew, setIsNew] = useState(true);\n\n useMountEffect(() => {\n const timer = setTimeout(() => setIsNew(false), 2000);\n return () => clearTimeout(timer);\n });\n\n const status = comment.status || \"waiting\";\n const colors = STATUS_COLORS[status];\n const isWorking = status === \"working\";\n\n return (\n <div\n className=\"absolute z-20\"\n style={{\n left: comment.position.x - 14,\n top: comment.position.y - 14,\n }}\n >\n {(isNew || isWorking) && (\n <span className={`absolute inset-0 rounded-full ${colors.ping} animate-ping`} />\n )}\n <span className={`absolute left-1/2 -translate-x-1/2 top-full w-0.5 h-2 ${colors.anchor}`} />\n <button\n className={`relative w-7 h-7 rounded-full ${colors.bg} text-white text-[11px] font-bold flex items-center justify-center hover:scale-110 transition-all cursor-pointer border-2 border-white`}\n style={{ boxShadow: `0 2px 8px ${colors.shadow}` }}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n title={comment.aiResponse || comment.text}\n >\n {status === \"done\" ? \"✓\" : comment.number}\n </button>\n </div>\n );\n}\n","import type { Comment as CommentType, DesignIteration, Point } from \"@calca/shared\";\n\nimport { DesignFrame, DEFAULT_FRAME_WIDTH } from \"./DesignFrame\";\n\nexport { DEFAULT_FRAME_WIDTH } from \"./DesignFrame\";\n\ninterface DesignCardProps {\n iteration: DesignIteration;\n isCommentMode: boolean;\n isSelectMode: boolean;\n isDragging: boolean;\n isSelected?: boolean;\n onSelect?: (e?: React.MouseEvent) => void;\n onAddComment: (iterationId: string, position: Point, screenX: number, screenY: number) => void;\n onClickComment: (comment: CommentType, iterationId: string) => void;\n onDragStart: (e: React.MouseEvent) => void;\n scale: number;\n}\n\nexport function DesignCard({\n iteration,\n isCommentMode,\n isSelectMode,\n isDragging,\n isSelected,\n onSelect,\n onAddComment,\n onClickComment,\n onDragStart,\n scale,\n}: DesignCardProps) {\n return (\n <div\n className={`absolute ${isDragging ? \"z-50\" : \"\"}`}\n style={{\n left: iteration.position.x,\n top: iteration.position.y,\n width: iteration.width || DEFAULT_FRAME_WIDTH,\n }}\n >\n <div className=\"mb-2 flex items-center gap-2 group/label\">\n <span className=\"text-xs font-medium text-gray-500/80 bg-white/60 backdrop-blur-sm px-2.5 py-0.5 rounded-lg border border-white/40\">\n {iteration.label}\n </span>\n </div>\n\n <DesignFrame\n iteration={iteration}\n isCommentMode={isCommentMode}\n isSelectMode={isSelectMode}\n isSelected={isSelected}\n isDragging={isDragging}\n scale={scale}\n onClick={(e) => {\n if (isSelectMode && onSelect) {\n e.stopPropagation();\n onSelect(e);\n }\n }}\n onMouseDown={(e) => {\n if (isSelectMode) {\n e.stopPropagation();\n onDragStart(e);\n }\n }}\n onAddComment={(iterationId, position, screenX, screenY) =>\n onAddComment(iterationId, position, screenX, screenY)\n }\n onClickComment={onClickComment}\n />\n </div>\n );\n}\n","import { type PipelineStatus, STAGE_CONFIG } from \"@calca/shared\";\n\ninterface PipelineStatusBarProps {\n status: PipelineStatus;\n x: number;\n y: number;\n width: number;\n frameHeight: number;\n}\n\nexport function PipelineStatusOverlay({\n status,\n x,\n y,\n width,\n frameHeight,\n}: PipelineStatusBarProps) {\n const config = STAGE_CONFIG[status.stage];\n const isDone = status.stage === \"done\";\n const isError = status.stage === \"error\";\n const isQueued = status.stage === \"queued\";\n\n if (isDone) {\n return null;\n }\n\n const topOffset = y + frameHeight + 8;\n const value = Math.max(status.progress * 100, 5);\n\n const indicatorClass = isError ? \"bg-destructive\" : \"bg-gradient-to-r from-primary to-secondary\";\n const pulseClass = status.stage === \"layout\" || status.stage === \"images\" ? \"animate-pulse\" : \"\";\n\n return (\n <div className=\"absolute pointer-events-none\" style={{ left: x, top: topOffset, width }}>\n {!isQueued && (\n <div className=\"h-1 w-full overflow-hidden rounded-full bg-primary/20\">\n <div\n className={`h-full transition-all ${indicatorClass} ${pulseClass}`}\n style={{ width: `${value}%` }}\n />\n </div>\n )}\n <div className=\"flex items-center justify-between mt-1.5\">\n <span className=\"text-[10px] font-medium text-muted-foreground whitespace-nowrap\">\n {config.icon} {config.label}\n </span>\n {status.skipped && (\n <span className=\"text-[10px] font-medium text-destructive/80 whitespace-nowrap\">\n ⏭ {status.reason || \"Skipped\"}\n </span>\n )}\n </div>\n </div>\n );\n}\n","interface RubberBandOverlayProps {\n rubberBand: {\n startX: number;\n startY: number;\n currentX: number;\n currentY: number;\n } | null;\n}\n\nexport function RubberBandOverlay({ rubberBand }: RubberBandOverlayProps) {\n if (!rubberBand) {\n return null;\n }\n\n return (\n <div\n className=\"fixed pointer-events-none z-[60] border border-blue-400/50 bg-blue-400/10 rounded-sm\"\n style={{\n height: Math.abs(rubberBand.currentY - rubberBand.startY),\n left: Math.min(rubberBand.startX, rubberBand.currentX),\n top: Math.min(rubberBand.startY, rubberBand.currentY),\n width: Math.abs(rubberBand.currentX - rubberBand.startX),\n }}\n />\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@calca/canvas-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.7.5",
4
+ "private": false,
5
+ "license": "Apache-2.0",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "sideEffects": false,
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.mts",
14
+ "import": "./dist/index.mjs"
15
+ }
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsdown",
22
+ "typecheck": "bunx tsc --noEmit",
23
+ "lint": "oxlint --fix .",
24
+ "format": "oxfmt --write .",
25
+ "test": "vitest run --passWithNoTests",
26
+ "clean": "rm -rf dist",
27
+ "validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD",
28
+ "check:pack": "publint && attw --pack . --profile esm-only"
29
+ },
30
+ "dependencies": {
31
+ "@calca/shared": "0.7.5",
32
+ "lucide-react": "^1.8.0"
33
+ },
34
+ "devDependencies": {
35
+ "@arethetypeswrong/cli": "^0.18.5",
36
+ "@types/react": "^19.2.14",
37
+ "@types/react-dom": "^19.2.3",
38
+ "publint": "^0.3.25",
39
+ "react": "^19.2.4",
40
+ "react-dom": "^19.2.4",
41
+ "tsdown": "^0.23.0",
42
+ "typescript": "^6.0.3",
43
+ "vitest": "^4.1.5"
44
+ },
45
+ "peerDependencies": {
46
+ "lucide-react": "^1.8.0",
47
+ "react": "^19.2.4",
48
+ "react-dom": "^19.2.4"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/espetro/calca",
53
+ "directory": "packages/canvas-ui"
54
+ }
55
+ }