@drawnagency/primitives 0.1.40 → 0.1.41

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.
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ siteId: string;
3
+ }
4
+ export declare function BugReportFAB({ siteId }: Props): import("react/jsx-runtime").JSX.Element | null;
5
+ export {};
6
+ //# sourceMappingURL=BugReportFAB.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BugReportFAB.d.ts","sourceRoot":"","sources":["../../../src/components/shell/BugReportFAB.tsx"],"names":[],"mappings":"AAMA,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAyBD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,kDAsT7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAqDjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAsoBP"}
1
+ {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAsDjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAwoBP"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -0,0 +1,344 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { EditorModal } from "./EditorModal";
4
+ import { useEditorContext } from "./EditorContext";
5
+ import { env } from "../../lib/env";
6
+
7
+ interface Props {
8
+ siteId: string;
9
+ }
10
+
11
+ type Category = "Visual" | "Unexpected Behavior" | "Media / Media Library" | "Save / Publish" | "Other";
12
+
13
+ const CATEGORIES: Category[] = [
14
+ "Visual",
15
+ "Unexpected Behavior",
16
+ "Media / Media Library",
17
+ "Save / Publish",
18
+ "Other",
19
+ ];
20
+
21
+ interface CapturedContext {
22
+ url: string;
23
+ section_id: null;
24
+ user_agent: string;
25
+ viewport_width: number;
26
+ viewport_height: number;
27
+ }
28
+
29
+ interface ImagePreview {
30
+ dataUri: string;
31
+ name: string;
32
+ }
33
+
34
+ export function BugReportFAB({ siteId }: Props) {
35
+ const { isEditMode, historyState } = useEditorContext();
36
+ const [isOpen, setIsOpen] = useState(false);
37
+ const [category, setCategory] = useState<Category>("Visual");
38
+ const [isCritical, setIsCritical] = useState(false);
39
+ const [description, setDescription] = useState("");
40
+ const [images, setImages] = useState<ImagePreview[]>([]);
41
+ const [isDragOver, setIsDragOver] = useState(false);
42
+ const [isSubmitting, setIsSubmitting] = useState(false);
43
+ const [submitError, setSubmitError] = useState<string | null>(null);
44
+ const [descriptionError, setDescriptionError] = useState<string | null>(null);
45
+ const [toast, setToast] = useState<string | null>(null);
46
+ const capturedContextRef = useRef<CapturedContext | null>(null);
47
+ const dropZoneRef = useRef<HTMLDivElement>(null);
48
+
49
+ const resetForm = useCallback(() => {
50
+ setCategory("Visual");
51
+ setIsCritical(false);
52
+ setDescription("");
53
+ setImages([]);
54
+ setSubmitError(null);
55
+ setDescriptionError(null);
56
+ capturedContextRef.current = null;
57
+ }, []);
58
+
59
+ const handleOpen = useCallback(() => {
60
+ capturedContextRef.current = {
61
+ url: window.location.pathname,
62
+ section_id: null,
63
+ user_agent: navigator.userAgent,
64
+ viewport_width: window.innerWidth,
65
+ viewport_height: window.innerHeight,
66
+ };
67
+ setIsOpen(true);
68
+ }, []);
69
+
70
+ const handleClose = useCallback(() => {
71
+ setIsOpen(false);
72
+ resetForm();
73
+ }, [resetForm]);
74
+
75
+ const fileToDataUri = useCallback((file: File): Promise<string> => {
76
+ return new Promise((resolve, reject) => {
77
+ const reader = new FileReader();
78
+ reader.onload = () => resolve(reader.result as string);
79
+ reader.onerror = reject;
80
+ reader.readAsDataURL(file);
81
+ });
82
+ }, []);
83
+
84
+ const addImageFiles = useCallback(async (files: File[]) => {
85
+ const imageFiles = files.filter((f) => f.type.startsWith("image/"));
86
+ if (imageFiles.length === 0) return;
87
+ const newPreviews = await Promise.all(
88
+ imageFiles.map(async (file) => ({
89
+ dataUri: await fileToDataUri(file),
90
+ name: file.name,
91
+ })),
92
+ );
93
+ setImages((prev) => [...prev, ...newPreviews].slice(0, 5));
94
+ }, [fileToDataUri]);
95
+
96
+ const handleRemoveImage = useCallback((index: number) => {
97
+ setImages((prev) => prev.filter((_, i) => i !== index));
98
+ }, []);
99
+
100
+ const handleDragOver = useCallback((e: React.DragEvent) => {
101
+ e.preventDefault();
102
+ setIsDragOver(true);
103
+ }, []);
104
+
105
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
106
+ if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) {
107
+ setIsDragOver(false);
108
+ }
109
+ }, []);
110
+
111
+ const handleDrop = useCallback(async (e: React.DragEvent) => {
112
+ e.preventDefault();
113
+ setIsDragOver(false);
114
+ const files = Array.from(e.dataTransfer.files);
115
+ await addImageFiles(files);
116
+ }, [addImageFiles]);
117
+
118
+ // Document-level paste listener when modal is open
119
+ useEffect(() => {
120
+ if (!isOpen) return;
121
+
122
+ const handlePaste = async (e: ClipboardEvent) => {
123
+ const items = e.clipboardData?.items;
124
+ if (!items) return;
125
+ const files: File[] = [];
126
+ for (const item of Array.from(items)) {
127
+ if (item.type.startsWith("image/")) {
128
+ const file = item.getAsFile();
129
+ if (file) files.push(file);
130
+ }
131
+ }
132
+ if (files.length > 0) {
133
+ await addImageFiles(files);
134
+ }
135
+ };
136
+
137
+ document.addEventListener("paste", handlePaste);
138
+ return () => document.removeEventListener("paste", handlePaste);
139
+ }, [isOpen, addImageFiles]);
140
+
141
+ const handleSubmit = useCallback(async () => {
142
+ if (!description.trim()) {
143
+ setDescriptionError("Please describe the issue.");
144
+ return;
145
+ }
146
+ setDescriptionError(null);
147
+ setSubmitError(null);
148
+ setIsSubmitting(true);
149
+
150
+ try {
151
+ const supabase = createClient(
152
+ env("SUPABASE_URL"),
153
+ env("SUPABASE_ANON_KEY"),
154
+ );
155
+
156
+ const { data: userData } = await supabase.auth.getUser();
157
+ const userId = userData?.user?.id ?? null;
158
+
159
+ const { error } = await supabase.from("bug_reports").insert({
160
+ site_id: siteId,
161
+ user_id: userId,
162
+ category,
163
+ is_critical: isCritical,
164
+ description: description.trim(),
165
+ images: images.map((img) => img.dataUri),
166
+ context: capturedContextRef.current,
167
+ });
168
+
169
+ if (error) {
170
+ setSubmitError(error.message);
171
+ return;
172
+ }
173
+
174
+ handleClose();
175
+ setToast("Bug report submitted");
176
+ setTimeout(() => setToast(null), 3000);
177
+ } catch (err) {
178
+ setSubmitError(err instanceof Error ? err.message : "Submission failed");
179
+ } finally {
180
+ setIsSubmitting(false);
181
+ }
182
+ }, [siteId, category, isCritical, description, images, handleClose]);
183
+
184
+ const visible = isEditMode && historyState === null;
185
+
186
+ if (!visible && !isOpen && !toast) return null;
187
+
188
+ return (
189
+ <>
190
+ {/* FAB button */}
191
+ {visible && (
192
+ <button
193
+ type="button"
194
+ onClick={handleOpen}
195
+ aria-label="Report a bug"
196
+ className="cursor-pointer fixed bottom-16 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
197
+ >
198
+ {/* Bug / beetle SVG icon */}
199
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
200
+ <path d="M19 8h-1.26A6.003 6.003 0 0012 4a6.003 6.003 0 00-5.74 4H5a1 1 0 000 2h1.05A6.04 6.04 0 006 12H5a1 1 0 000 2h1c0 .34.03.67.08 1H5a1 1 0 000 2h1.26a6 6 0 0011.48 0H19a1 1 0 000-2h-1.08c.05-.33.08-.66.08-1h1a1 1 0 000-2h-1a6.04 6.04 0 00-.05-2H19a1 1 0 000-2zM12 6c1.7 0 3.16.88 4 2.19V8a1 1 0 10-2 0v.04A3.97 3.97 0 0012 8a3.97 3.97 0 00-2 .04V8a1 1 0 10-2 0v.19A4.69 4.69 0 018 6a4 4 0 014 0zm0 14a4 4 0 01-4-4v-4a4 4 0 018 0v4a4 4 0 01-4 4z"/>
201
+ </svg>
202
+ </button>
203
+ )}
204
+
205
+ {/* Modal */}
206
+ <EditorModal
207
+ isOpen={isOpen}
208
+ onClose={handleClose}
209
+ title="Report a Bug"
210
+ >
211
+ <div className="space-y-4">
212
+ {/* Category */}
213
+ <div>
214
+ <label className="mb-1 block text-sm font-medium text-base-contrast" htmlFor="bug-category">
215
+ Category
216
+ </label>
217
+ <select
218
+ id="bug-category"
219
+ value={category}
220
+ onChange={(e) => setCategory(e.target.value as Category)}
221
+ className="w-full rounded-md border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast"
222
+ >
223
+ {CATEGORIES.map((cat) => (
224
+ <option key={cat} value={cat}>{cat}</option>
225
+ ))}
226
+ </select>
227
+ </div>
228
+
229
+ {/* Critical toggle */}
230
+ <div className="flex items-center gap-3">
231
+ <button
232
+ type="button"
233
+ role="switch"
234
+ aria-checked={isCritical}
235
+ onClick={() => setIsCritical((prev) => !prev)}
236
+ className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
237
+ isCritical
238
+ ? "bg-red-600 focus-visible:ring-red-600"
239
+ : "bg-base-200 focus-visible:ring-base-200"
240
+ }`}
241
+ >
242
+ <span
243
+ className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform ${
244
+ isCritical ? "translate-x-5" : "translate-x-0.5"
245
+ } mt-0.5`}
246
+ />
247
+ </button>
248
+ <span className="text-sm text-base-contrast">This is blocking my work</span>
249
+ </div>
250
+
251
+ {/* Description */}
252
+ <div>
253
+ <label className="mb-1 block text-sm font-medium text-base-contrast" htmlFor="bug-description">
254
+ Description
255
+ </label>
256
+ <textarea
257
+ id="bug-description"
258
+ rows={4}
259
+ value={description}
260
+ onChange={(e) => {
261
+ setDescription(e.target.value);
262
+ if (descriptionError && e.target.value.trim()) {
263
+ setDescriptionError(null);
264
+ }
265
+ }}
266
+ placeholder="Describe the issue..."
267
+ className="w-full rounded-md border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast placeholder:text-base-contrast/50 resize-none"
268
+ />
269
+ {descriptionError && (
270
+ <p className="mt-1 text-xs text-red-600">{descriptionError}</p>
271
+ )}
272
+ </div>
273
+
274
+ {/* Screenshots drop zone */}
275
+ <div>
276
+ <label className="mb-1 block text-sm font-medium text-base-contrast">
277
+ Screenshots
278
+ </label>
279
+ <div
280
+ ref={dropZoneRef}
281
+ onDragOver={handleDragOver}
282
+ onDragLeave={handleDragLeave}
283
+ onDrop={handleDrop}
284
+ className={`rounded-md border-2 border-dashed px-4 py-6 text-center transition-colors ${
285
+ isDragOver
286
+ ? "border-primary bg-primary/5"
287
+ : "border-base-200 bg-base-accent/30"
288
+ }`}
289
+ >
290
+ <p className="text-sm text-base-contrast/60">
291
+ Drop images here or paste from clipboard
292
+ </p>
293
+ </div>
294
+
295
+ {/* Thumbnail previews */}
296
+ {images.length > 0 && (
297
+ <div className="mt-2 flex flex-wrap gap-2">
298
+ {images.map((img, index) => (
299
+ <div key={index} className="group relative h-16 w-16">
300
+ <img
301
+ src={img.dataUri}
302
+ alt={img.name}
303
+ className="h-16 w-16 rounded object-cover border border-base-200"
304
+ />
305
+ <button
306
+ type="button"
307
+ onClick={() => handleRemoveImage(index)}
308
+ aria-label={`Remove screenshot ${img.name}`}
309
+ className="absolute -top-1.5 -right-1.5 hidden h-5 w-5 items-center justify-center rounded-full bg-red-600 text-white text-xs group-hover:flex"
310
+ >
311
+ ×
312
+ </button>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ )}
317
+ </div>
318
+
319
+ {/* Submit error */}
320
+ {submitError && (
321
+ <p className="text-sm text-red-600">{submitError}</p>
322
+ )}
323
+
324
+ {/* Submit button */}
325
+ <button
326
+ type="button"
327
+ onClick={handleSubmit}
328
+ disabled={isSubmitting}
329
+ className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-contrast hover:opacity-90 transition-opacity disabled:opacity-60"
330
+ >
331
+ {isSubmitting ? "Submitting..." : "Submit Report"}
332
+ </button>
333
+ </div>
334
+ </EditorModal>
335
+
336
+ {/* Toast */}
337
+ {toast && (
338
+ <div className="fixed bottom-4 left-1/2 z-[60] -translate-x-1/2 rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-lg">
339
+ {toast}
340
+ </div>
341
+ )}
342
+ </>
343
+ );
344
+ }
@@ -21,6 +21,7 @@ import { ensureSectionsRegistered } from "../sections/register";
21
21
  import { getSection, getAllSections } from "../../lib/registry";
22
22
 
23
23
  ensureSectionsRegistered();
24
+ import { BugReportFAB } from "./BugReportFAB";
24
25
  import { SectionWrapper } from "../editor/SectionWrapper";
25
26
  import { SectionOrderingModal } from "../editor/SectionOrderingModal";
26
27
  import { SectionLayout } from "../sections/SectionLayout";
@@ -633,6 +634,8 @@ export default function EditorShell({
633
634
  onOrderingClick={() => setShowOrderingModal(true)}
634
635
  />
635
636
 
637
+ <BugReportFAB siteId={siteId} />
638
+
636
639
  <HistoryOrEditorContent sections={sections}>
637
640
  <EditorContent
638
641
  sections={sections}