@taterboom/shiteki 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1237 @@
1
+ // src/components/ShitekiWidget.tsx
2
+ import { useCallback as useCallback9, useEffect as useEffect7, useState as useState11 } from "react";
3
+ import ReactDOM from "react-dom";
4
+ import { AnimatePresence as AnimatePresence3 } from "motion/react";
5
+
6
+ // src/hooks/useAnnotations.ts
7
+ import { useCallback, useRef, useState } from "react";
8
+ var STORAGE_KEY = "shiteki:annotations";
9
+ function readStored() {
10
+ try {
11
+ const raw = localStorage.getItem(STORAGE_KEY);
12
+ return raw ? JSON.parse(raw) : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function writeStored(annotations, nextId) {
18
+ try {
19
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ annotations, nextId }));
20
+ } catch {
21
+ }
22
+ }
23
+ function removeStored() {
24
+ try {
25
+ localStorage.removeItem(STORAGE_KEY);
26
+ } catch {
27
+ }
28
+ }
29
+ function useAnnotations() {
30
+ const stored = readStored();
31
+ const [annotations, setAnnotations] = useState(stored?.annotations ?? []);
32
+ const nextIdRef = useRef(stored?.nextId ?? 1);
33
+ const add = useCallback((elementInfo, comment) => {
34
+ const annotation = {
35
+ id: nextIdRef.current++,
36
+ elementInfo,
37
+ comment,
38
+ createdAt: Date.now()
39
+ };
40
+ setAnnotations((prev) => {
41
+ const next = [...prev, annotation];
42
+ writeStored(next, nextIdRef.current);
43
+ return next;
44
+ });
45
+ return annotation;
46
+ }, []);
47
+ const remove = useCallback((id) => {
48
+ setAnnotations((prev) => {
49
+ const next = prev.filter((a) => a.id !== id);
50
+ writeStored(next, nextIdRef.current);
51
+ return next;
52
+ });
53
+ }, []);
54
+ const clear = useCallback(() => {
55
+ setAnnotations([]);
56
+ nextIdRef.current = 1;
57
+ removeStored();
58
+ }, []);
59
+ return { annotations, add, remove, clear };
60
+ }
61
+
62
+ // src/hooks/useConfig.ts
63
+ import { useCallback as useCallback2, useState as useState2 } from "react";
64
+ var STORAGE_KEY2 = "shiteki:config";
65
+ function readStored2() {
66
+ try {
67
+ const raw = localStorage.getItem(STORAGE_KEY2);
68
+ return raw ? JSON.parse(raw) : null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function merge(defaults, stored) {
74
+ if (!stored) return defaults;
75
+ return {
76
+ mode: stored.mode || defaults.mode,
77
+ endpoint: stored.endpoint || defaults.endpoint,
78
+ githubToken: stored.githubToken || defaults.githubToken,
79
+ owner: stored.owner || defaults.owner,
80
+ repo: stored.repo || defaults.repo,
81
+ labels: stored.labels ?? defaults.labels
82
+ };
83
+ }
84
+ function useConfig(defaults) {
85
+ const [config, setConfig] = useState2(() => merge(defaults, readStored2()));
86
+ const updateConfig = useCallback2(
87
+ (partial) => {
88
+ setConfig((prev) => {
89
+ const next = { ...prev, ...partial };
90
+ try {
91
+ localStorage.setItem(STORAGE_KEY2, JSON.stringify(next));
92
+ } catch {
93
+ }
94
+ return next;
95
+ });
96
+ },
97
+ []
98
+ );
99
+ return { config, updateConfig };
100
+ }
101
+
102
+ // src/hooks/useElementPicker.ts
103
+ import { useCallback as useCallback3, useEffect, useRef as useRef2, useState as useState3 } from "react";
104
+
105
+ // src/utils/getElementSelector.ts
106
+ function getElementSelector(el) {
107
+ if (el.id) {
108
+ return `#${CSS.escape(el.id)}`;
109
+ }
110
+ const parts = [];
111
+ let current = el;
112
+ while (current && current !== document.documentElement) {
113
+ let selector = current.tagName.toLowerCase();
114
+ if (current.id) {
115
+ parts.unshift(`#${CSS.escape(current.id)}`);
116
+ break;
117
+ }
118
+ const parent = current.parentElement;
119
+ if (parent) {
120
+ const siblings = Array.from(parent.children).filter(
121
+ (c) => c.tagName === current.tagName
122
+ );
123
+ if (siblings.length > 1) {
124
+ const index = siblings.indexOf(current) + 1;
125
+ selector += `:nth-of-type(${index})`;
126
+ }
127
+ }
128
+ parts.unshift(selector);
129
+ current = parent;
130
+ }
131
+ return parts.join(" > ");
132
+ }
133
+
134
+ // src/utils/getElementInfo.ts
135
+ var CAPTURED_ATTRS = ["id", "class", "role", "data-testid", "href", "type", "name", "aria-label"];
136
+ function getElementInfo(el) {
137
+ const rect = el.getBoundingClientRect();
138
+ const attributes = {};
139
+ for (const attr of CAPTURED_ATTRS) {
140
+ const value = el.getAttribute(attr);
141
+ if (value) {
142
+ attributes[attr] = value;
143
+ }
144
+ }
145
+ return {
146
+ selector: getElementSelector(el),
147
+ tagName: el.tagName.toLowerCase(),
148
+ textContent: (el.textContent ?? "").trim().slice(0, 80),
149
+ rect: {
150
+ top: rect.top,
151
+ left: rect.left,
152
+ width: rect.width,
153
+ height: rect.height
154
+ },
155
+ attributes
156
+ };
157
+ }
158
+
159
+ // src/utils/isShitekiElement.ts
160
+ function isShitekiElement(el) {
161
+ return !!el.closest(".shiteki-root");
162
+ }
163
+
164
+ // src/hooks/useElementPicker.ts
165
+ function useElementPicker({ enabled, onElementSelected }) {
166
+ const [hoveredRect, setHoveredRect] = useState3(null);
167
+ const onElementSelectedRef = useRef2(onElementSelected);
168
+ onElementSelectedRef.current = onElementSelected;
169
+ const handleMouseMove = useCallback3((e) => {
170
+ const el = document.elementFromPoint(e.clientX, e.clientY);
171
+ if (!el || isShitekiElement(el)) {
172
+ setHoveredRect(null);
173
+ return;
174
+ }
175
+ const rect = el.getBoundingClientRect();
176
+ setHoveredRect({
177
+ top: rect.top,
178
+ left: rect.left,
179
+ width: rect.width,
180
+ height: rect.height
181
+ });
182
+ }, []);
183
+ const handleClick = useCallback3((e) => {
184
+ const el = document.elementFromPoint(e.clientX, e.clientY);
185
+ if (!el || isShitekiElement(el)) return;
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ e.stopImmediatePropagation();
189
+ const info = getElementInfo(el);
190
+ setHoveredRect(null);
191
+ onElementSelectedRef.current(info);
192
+ }, []);
193
+ useEffect(() => {
194
+ if (!enabled) {
195
+ setHoveredRect(null);
196
+ return;
197
+ }
198
+ document.body.style.cursor = "crosshair";
199
+ document.addEventListener("mousemove", handleMouseMove, true);
200
+ document.addEventListener("click", handleClick, true);
201
+ return () => {
202
+ document.body.style.cursor = "";
203
+ document.removeEventListener("mousemove", handleMouseMove, true);
204
+ document.removeEventListener("click", handleClick, true);
205
+ };
206
+ }, [enabled, handleMouseMove, handleClick]);
207
+ return { hoveredRect };
208
+ }
209
+
210
+ // src/hooks/useSubmit.ts
211
+ import { useCallback as useCallback4, useState as useState4 } from "react";
212
+ var SHITEKI_FOOTER = "\n\n---\n*Submitted via [Shiteki](https://github.com/taterboom/shiteki)*";
213
+ async function submitViaEndpoint(config, title, body) {
214
+ const res = await fetch(`${config.endpoint}/actions/send`, {
215
+ method: "POST",
216
+ headers: { "Content-Type": "application/json" },
217
+ body: JSON.stringify({
218
+ owner: config.owner,
219
+ repo: config.repo,
220
+ data: { title, body, labels: config.labels }
221
+ })
222
+ });
223
+ const json = await res.json();
224
+ if (!res.ok || !json.success) {
225
+ throw new Error(json.error ?? "Failed to submit");
226
+ }
227
+ return { issueUrl: json.issueUrl, issueNumber: json.issueNumber };
228
+ }
229
+ async function submitDirect(config, title, body) {
230
+ const issueBody = body + SHITEKI_FOOTER;
231
+ const res = await fetch(
232
+ `https://api.github.com/repos/${config.owner}/${config.repo}/issues`,
233
+ {
234
+ method: "POST",
235
+ headers: {
236
+ Authorization: `Bearer ${config.githubToken}`,
237
+ Accept: "application/vnd.github+json",
238
+ "User-Agent": "shiteki-widget",
239
+ "Content-Type": "application/json"
240
+ },
241
+ body: JSON.stringify({
242
+ title,
243
+ body: issueBody,
244
+ labels: config.labels
245
+ })
246
+ }
247
+ );
248
+ if (!res.ok) {
249
+ const json2 = await res.json().catch(() => null);
250
+ throw new Error(json2?.message ?? `GitHub API error (${res.status})`);
251
+ }
252
+ const json = await res.json();
253
+ return { issueUrl: json.html_url, issueNumber: json.number };
254
+ }
255
+ function useSubmit(config) {
256
+ const [state, setState] = useState4({ status: "idle" });
257
+ const submit = useCallback4(
258
+ async (title, body) => {
259
+ setState({ status: "loading" });
260
+ try {
261
+ const result = config.mode === "direct" ? await submitDirect(config, title, body) : await submitViaEndpoint(config, title, body);
262
+ setState({ status: "success", result });
263
+ } catch (err) {
264
+ setState({
265
+ status: "error",
266
+ error: err instanceof Error ? err.message : "Unknown error"
267
+ });
268
+ }
269
+ },
270
+ [config.mode, config.endpoint, config.githubToken, config.owner, config.repo, config.labels]
271
+ );
272
+ const reset = useCallback4(() => setState({ status: "idle" }), []);
273
+ return { state, submit, reset };
274
+ }
275
+
276
+ // src/hooks/useClipboard.ts
277
+ import { useCallback as useCallback5, useRef as useRef3, useState as useState5 } from "react";
278
+ function useClipboard(timeout = 2e3) {
279
+ const [copied, setCopied] = useState5(false);
280
+ const timerRef = useRef3();
281
+ const copy = useCallback5(
282
+ async (text) => {
283
+ try {
284
+ await navigator.clipboard.writeText(text);
285
+ } catch {
286
+ const textarea = document.createElement("textarea");
287
+ textarea.value = text;
288
+ textarea.style.position = "fixed";
289
+ textarea.style.opacity = "0";
290
+ document.body.appendChild(textarea);
291
+ textarea.select();
292
+ document.execCommand("copy");
293
+ document.body.removeChild(textarea);
294
+ }
295
+ setCopied(true);
296
+ clearTimeout(timerRef.current);
297
+ timerRef.current = setTimeout(() => setCopied(false), timeout);
298
+ },
299
+ [timeout]
300
+ );
301
+ return { copied, copy };
302
+ }
303
+
304
+ // src/hooks/useKeyboardShortcuts.ts
305
+ import { useEffect as useEffect2, useRef as useRef4 } from "react";
306
+ function isInputTarget(target) {
307
+ if (!target || !(target instanceof HTMLElement)) return false;
308
+ const tag = target.tagName;
309
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
310
+ if (target.isContentEditable) return true;
311
+ return false;
312
+ }
313
+ var DOUBLE_TAP_THRESHOLD = 300;
314
+ function useKeyboardShortcuts(opts) {
315
+ const lastDRef = useRef4(0);
316
+ useEffect2(() => {
317
+ function handleKeyDown(e) {
318
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
319
+ if (e.key.toLowerCase() === "x" && !isInputTarget(e.target)) {
320
+ e.preventDefault();
321
+ opts.onToggleOpen();
322
+ return;
323
+ }
324
+ if (!opts.open) return;
325
+ if (e.key === "Escape") {
326
+ e.preventDefault();
327
+ if (opts.mode === "annotating") {
328
+ opts.onCancelAnnotation();
329
+ } else if (opts.sendDialogOpen) {
330
+ opts.onCloseSendDialog();
331
+ } else if (opts.settingsOpen) {
332
+ opts.onCloseSettings();
333
+ } else {
334
+ opts.onClose();
335
+ }
336
+ return;
337
+ }
338
+ if (isInputTarget(e.target)) return;
339
+ const key = e.key.toLowerCase();
340
+ if (key === "c" && opts.annotationCount > 0) {
341
+ e.preventDefault();
342
+ opts.onCopy();
343
+ } else if (key === "s" && opts.annotationCount > 0) {
344
+ e.preventDefault();
345
+ opts.onSend();
346
+ } else if (key === "d" && opts.annotationCount > 0) {
347
+ const now = Date.now();
348
+ if (now - lastDRef.current < DOUBLE_TAP_THRESHOLD) {
349
+ e.preventDefault();
350
+ opts.onClear();
351
+ lastDRef.current = 0;
352
+ } else {
353
+ lastDRef.current = now;
354
+ }
355
+ }
356
+ }
357
+ document.addEventListener("keydown", handleKeyDown);
358
+ return () => document.removeEventListener("keydown", handleKeyDown);
359
+ }, [
360
+ opts.open,
361
+ opts.mode,
362
+ opts.annotationCount,
363
+ opts.settingsOpen,
364
+ opts.sendDialogOpen,
365
+ opts.onCopy,
366
+ opts.onSend,
367
+ opts.onClear,
368
+ opts.onToggleOpen,
369
+ opts.onClose,
370
+ opts.onCancelAnnotation,
371
+ opts.onCloseSettings,
372
+ opts.onCloseSendDialog
373
+ ]);
374
+ }
375
+
376
+ // src/hooks/useShortcutHint.ts
377
+ import { useCallback as useCallback6, useState as useState6 } from "react";
378
+ var STORAGE_KEY3 = "shiteki:shortcut-hint-dismissed";
379
+ function useShortcutHint() {
380
+ const [showHint, setShowHint] = useState6(() => {
381
+ try {
382
+ return !localStorage.getItem(STORAGE_KEY3);
383
+ } catch {
384
+ return true;
385
+ }
386
+ });
387
+ const dismissHint = useCallback6(() => {
388
+ setShowHint(false);
389
+ try {
390
+ localStorage.setItem(STORAGE_KEY3, "1");
391
+ } catch {
392
+ }
393
+ }, []);
394
+ return { showHint, dismissHint };
395
+ }
396
+
397
+ // src/utils/generatePrompt.ts
398
+ function generatePrompt(annotations) {
399
+ const now = (/* @__PURE__ */ new Date()).toISOString();
400
+ const pageTitle = document.title || "Untitled";
401
+ const pageUrl = location.href;
402
+ const lines = [
403
+ "# Visual Annotations",
404
+ "",
405
+ `**Page:** [${pageTitle}](${pageUrl})`,
406
+ `**Annotations:** ${annotations.length}`,
407
+ `**Captured:** ${now}`
408
+ ];
409
+ for (const ann of annotations) {
410
+ const { elementInfo: el } = ann;
411
+ lines.push(
412
+ "",
413
+ "---",
414
+ "",
415
+ `## Annotation #${ann.id}`,
416
+ "",
417
+ "**What should change:**",
418
+ `> ${ann.comment}`,
419
+ "",
420
+ "**Element:**",
421
+ `- Tag: \`<${el.tagName}>\``,
422
+ `- Text: "${el.textContent}"`,
423
+ `- Selector: \`${el.selector}\``
424
+ );
425
+ const attrEntries = Object.entries(el.attributes);
426
+ if (attrEntries.length > 0) {
427
+ lines.push(`- Attributes: ${attrEntries.map(([k, v]) => `\`${k}="${v}"\``).join(", ")}`);
428
+ }
429
+ }
430
+ lines.push("");
431
+ return lines.join("\n");
432
+ }
433
+
434
+ // src/components/Toolbar.tsx
435
+ import { motion, AnimatePresence } from "motion/react";
436
+
437
+ // src/utils/spring.ts
438
+ var spring = { type: "spring", bounce: 0.1, duration: 0.3 };
439
+
440
+ // src/components/Toolbar.tsx
441
+ import { jsx, jsxs } from "react/jsx-runtime";
442
+ function Toolbar({
443
+ open,
444
+ annotationCount,
445
+ copied,
446
+ sending,
447
+ settingsOpen,
448
+ onOpen,
449
+ onCopy,
450
+ onSend,
451
+ onClear,
452
+ onSettings,
453
+ onClose,
454
+ showHint,
455
+ onDismissHint
456
+ }) {
457
+ return /* @__PURE__ */ jsxs(
458
+ motion.div,
459
+ {
460
+ layout: true,
461
+ className: "shiteki-toolbar",
462
+ initial: { opacity: 0, scale: 0 },
463
+ animate: {
464
+ opacity: 1,
465
+ scale: 1,
466
+ gap: open ? 4 : 0,
467
+ padding: "6px",
468
+ background: open ? "#ffffff" : "rgb(37, 99, 235)",
469
+ borderColor: open ? "#e5e7eb" : "transparent",
470
+ boxShadow: open ? "0 4px 12px rgba(0, 0, 0, 0.08)" : "0 2px 16px rgba(37, 99, 235, 0.3)"
471
+ },
472
+ transition: spring,
473
+ onClick: !open ? onOpen : void 0,
474
+ role: !open ? "button" : void 0,
475
+ "aria-label": !open ? "Open Shiteki" : void 0,
476
+ style: { cursor: !open ? "pointer" : void 0 },
477
+ whileHover: !open ? { scale: 1.08, boxShadow: "0 4px 20px rgba(37, 99, 235, 0.4)" } : void 0,
478
+ whileTap: !open ? { scale: 0.95 } : void 0,
479
+ children: [
480
+ /* @__PURE__ */ jsxs("div", { className: "shiteki-toolbar-picker", children: [
481
+ /* @__PURE__ */ jsx(
482
+ motion.div,
483
+ {
484
+ className: `shiteki-toolbar-btn ${open ? "shiteki-toolbar-btn--active" : ""}`,
485
+ whileHover: { scale: 1.1 },
486
+ whileTap: { scale: 0.9 },
487
+ style: !open ? { color: "#fff", pointerEvents: "none" } : { pointerEvents: "none" },
488
+ children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
489
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
490
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "12", y2: "6" }),
491
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "12", y2: "22" }),
492
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "12", x2: "6", y2: "12" }),
493
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "12", x2: "22", y2: "12" })
494
+ ] })
495
+ }
496
+ ),
497
+ open && annotationCount > 0 && /* @__PURE__ */ jsx(
498
+ motion.span,
499
+ {
500
+ className: "shiteki-toolbar-badge",
501
+ initial: { scale: 0 },
502
+ animate: { scale: 1 },
503
+ transition: spring,
504
+ children: annotationCount
505
+ }
506
+ )
507
+ ] }),
508
+ /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs(
509
+ motion.div,
510
+ {
511
+ initial: { opacity: 0, width: 0 },
512
+ animate: { opacity: 1, width: "auto" },
513
+ exit: { opacity: 0, width: 0 },
514
+ transition: spring,
515
+ style: { overflow: "hidden", display: "flex", alignItems: "center", gap: 4 },
516
+ children: [
517
+ annotationCount > 0 && /* @__PURE__ */ jsx("div", { className: "shiteki-toolbar-sep" }),
518
+ annotationCount > 0 && /* @__PURE__ */ jsx("button", { className: "shiteki-toolbar-btn", onClick: onCopy, title: "Copy to clipboard", children: copied ? /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }) : /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
519
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
520
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
521
+ ] }) }),
522
+ annotationCount > 0 && /* @__PURE__ */ jsx(
523
+ "button",
524
+ {
525
+ className: "shiteki-toolbar-btn",
526
+ onClick: onSend,
527
+ disabled: sending,
528
+ title: "Send as GitHub Issue",
529
+ children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
530
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
531
+ /* @__PURE__ */ jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
532
+ ] })
533
+ }
534
+ ),
535
+ annotationCount > 0 && /* @__PURE__ */ jsx("button", { className: "shiteki-toolbar-btn", onClick: onClear, title: "Clear all annotations", children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
536
+ /* @__PURE__ */ jsx("polyline", { points: "3 6 5 6 21 6" }),
537
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
538
+ ] }) }),
539
+ /* @__PURE__ */ jsx(
540
+ "button",
541
+ {
542
+ className: `shiteki-toolbar-btn ${settingsOpen ? "shiteki-toolbar-btn--active" : ""}`,
543
+ onClick: onSettings,
544
+ title: "Settings",
545
+ children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
546
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
547
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
548
+ ] })
549
+ }
550
+ ),
551
+ /* @__PURE__ */ jsx("div", { className: "shiteki-toolbar-sep" }),
552
+ /* @__PURE__ */ jsx("button", { className: "shiteki-toolbar-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
553
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
554
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
555
+ ] }) })
556
+ ]
557
+ },
558
+ "expand"
559
+ ) }),
560
+ /* @__PURE__ */ jsx(AnimatePresence, { children: open && showHint && /* @__PURE__ */ jsxs(
561
+ motion.div,
562
+ {
563
+ className: "shiteki-shortcut-hint",
564
+ initial: { opacity: 0, y: 4 },
565
+ animate: { opacity: 1, y: 0 },
566
+ exit: { opacity: 0, y: 4 },
567
+ transition: { duration: 0.15 },
568
+ children: [
569
+ /* @__PURE__ */ jsxs("span", { children: [
570
+ /* @__PURE__ */ jsx("kbd", { children: "X" }),
571
+ " Toggle",
572
+ /* @__PURE__ */ jsx("kbd", { children: "C" }),
573
+ " Copy",
574
+ /* @__PURE__ */ jsx("kbd", { children: "S" }),
575
+ " Send",
576
+ /* @__PURE__ */ jsx("kbd", { children: "DD" }),
577
+ " Clear"
578
+ ] }),
579
+ /* @__PURE__ */ jsx("button", { className: "shiteki-shortcut-hint-close", onClick: onDismissHint, "aria-label": "Dismiss", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
580
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
581
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
582
+ ] }) })
583
+ ]
584
+ },
585
+ "hint"
586
+ ) })
587
+ ]
588
+ }
589
+ );
590
+ }
591
+
592
+ // src/components/ElementHighlight.tsx
593
+ import { jsx as jsx2 } from "react/jsx-runtime";
594
+ function ElementHighlight({ rect }) {
595
+ if (!rect) return null;
596
+ return /* @__PURE__ */ jsx2(
597
+ "div",
598
+ {
599
+ className: "shiteki-highlight",
600
+ style: {
601
+ top: rect.top - 1.5,
602
+ left: rect.left - 1.5,
603
+ width: rect.width + 3,
604
+ height: rect.height + 3
605
+ }
606
+ }
607
+ );
608
+ }
609
+
610
+ // src/components/AnnotationPopover.tsx
611
+ import { useEffect as useEffect3, useRef as useRef5, useState as useState7 } from "react";
612
+ import { motion as motion2 } from "motion/react";
613
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
614
+ function AnnotationPopover({ elementInfo, onAdd, onCancel }) {
615
+ const [comment, setComment] = useState7("");
616
+ const textareaRef = useRef5(null);
617
+ const [dragOffset, setDragOffset] = useState7({ x: 0, y: 0 });
618
+ const dragRef = useRef5(null);
619
+ useEffect3(() => {
620
+ textareaRef.current?.focus();
621
+ }, []);
622
+ const { rect } = elementInfo;
623
+ const spaceBelow = window.innerHeight - (rect.top + rect.height);
624
+ const above = spaceBelow < 250;
625
+ const top = above ? rect.top - 180 : rect.top + rect.height + 8;
626
+ let left = rect.left + rect.width / 2 - 160;
627
+ left = Math.max(12, Math.min(left, window.innerWidth - 332));
628
+ const yOffset = above ? -8 : 8;
629
+ const handlePointerDown = (e) => {
630
+ dragRef.current = { startX: e.clientX, startY: e.clientY, origX: dragOffset.x, origY: dragOffset.y };
631
+ e.target.setPointerCapture(e.pointerId);
632
+ };
633
+ const handlePointerMove = (e) => {
634
+ if (!dragRef.current) return;
635
+ setDragOffset({
636
+ x: dragRef.current.origX + e.clientX - dragRef.current.startX,
637
+ y: dragRef.current.origY + e.clientY - dragRef.current.startY
638
+ });
639
+ };
640
+ const handlePointerUp = () => {
641
+ dragRef.current = null;
642
+ };
643
+ const handleSubmit = (e) => {
644
+ e.preventDefault();
645
+ if (!comment.trim()) return;
646
+ onAdd(comment.trim());
647
+ };
648
+ return /* @__PURE__ */ jsxs2(
649
+ motion2.div,
650
+ {
651
+ className: "shiteki-popover",
652
+ style: { top: top + dragOffset.y, left: left + dragOffset.x, transformOrigin: above ? "bottom center" : "top center" },
653
+ initial: { opacity: 0, scale: 0.95, y: yOffset },
654
+ animate: { opacity: 1, scale: 1, y: 0 },
655
+ exit: { opacity: 0, scale: 0.95, y: yOffset },
656
+ transition: spring,
657
+ children: [
658
+ /* @__PURE__ */ jsxs2(
659
+ "div",
660
+ {
661
+ className: "shiteki-popover-info",
662
+ style: { cursor: "grab" },
663
+ onPointerDown: handlePointerDown,
664
+ onPointerMove: handlePointerMove,
665
+ onPointerUp: handlePointerUp,
666
+ children: [
667
+ /* @__PURE__ */ jsxs2("span", { className: "shiteki-popover-tag", children: [
668
+ "<",
669
+ elementInfo.tagName,
670
+ ">"
671
+ ] }),
672
+ elementInfo.textContent && /* @__PURE__ */ jsxs2("span", { className: "shiteki-popover-text", children: [
673
+ '"',
674
+ elementInfo.textContent,
675
+ '"'
676
+ ] })
677
+ ]
678
+ }
679
+ ),
680
+ /* @__PURE__ */ jsxs2("form", { className: "shiteki-popover-form", onSubmit: handleSubmit, children: [
681
+ /* @__PURE__ */ jsx3(
682
+ "textarea",
683
+ {
684
+ ref: textareaRef,
685
+ className: "shiteki-popover-textarea",
686
+ placeholder: "What should change?",
687
+ value: comment,
688
+ onChange: (e) => setComment(e.target.value),
689
+ rows: 3
690
+ }
691
+ ),
692
+ /* @__PURE__ */ jsxs2("div", { className: "shiteki-popover-actions", children: [
693
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "shiteki-btn shiteki-btn--ghost", onClick: onCancel, children: "Cancel" }),
694
+ /* @__PURE__ */ jsx3("button", { type: "submit", className: "shiteki-btn shiteki-btn--primary", disabled: !comment.trim(), children: "Add" })
695
+ ] })
696
+ ] })
697
+ ]
698
+ }
699
+ );
700
+ }
701
+
702
+ // src/components/AnnotationMarkers.tsx
703
+ import { AnimatePresence as AnimatePresence2 } from "motion/react";
704
+
705
+ // src/hooks/useMarkerPositions.ts
706
+ import { useEffect as useEffect4, useState as useState8 } from "react";
707
+ function useMarkerPositions(annotations) {
708
+ const [positions, setPositions] = useState8([]);
709
+ useEffect4(() => {
710
+ function update() {
711
+ const newPositions = annotations.map((ann) => {
712
+ const el = document.querySelector(ann.elementInfo.selector);
713
+ if (el) {
714
+ const rect = el.getBoundingClientRect();
715
+ return { id: ann.id, top: rect.top, left: rect.left + rect.width };
716
+ }
717
+ const r = ann.elementInfo.rect;
718
+ return { id: ann.id, top: r.top, left: r.left + r.width };
719
+ });
720
+ setPositions(newPositions);
721
+ }
722
+ update();
723
+ window.addEventListener("scroll", update, true);
724
+ window.addEventListener("resize", update);
725
+ return () => {
726
+ window.removeEventListener("scroll", update, true);
727
+ window.removeEventListener("resize", update);
728
+ };
729
+ }, [annotations]);
730
+ return positions;
731
+ }
732
+
733
+ // src/components/AnnotationMarker.tsx
734
+ import { motion as motion3 } from "motion/react";
735
+ import { jsx as jsx4 } from "react/jsx-runtime";
736
+ function AnnotationMarker({ id, top, left, onRemove }) {
737
+ return /* @__PURE__ */ jsx4(
738
+ motion3.div,
739
+ {
740
+ className: "shiteki-marker",
741
+ style: { top: top - 10, left: left - 10 },
742
+ title: `Annotation #${id} (click to remove)`,
743
+ onClick: () => onRemove(id),
744
+ initial: { opacity: 0, scale: 0 },
745
+ animate: { opacity: 1, scale: 1 },
746
+ exit: { opacity: 0, scale: 0 },
747
+ transition: spring,
748
+ whileHover: { scale: 1.25 },
749
+ children: id
750
+ }
751
+ );
752
+ }
753
+
754
+ // src/components/AnnotationMarkers.tsx
755
+ import { jsx as jsx5 } from "react/jsx-runtime";
756
+ function AnnotationMarkers({ annotations, onRemove }) {
757
+ const positions = useMarkerPositions(annotations);
758
+ return /* @__PURE__ */ jsx5(AnimatePresence2, { children: positions.map((pos) => /* @__PURE__ */ jsx5(
759
+ AnnotationMarker,
760
+ {
761
+ id: pos.id,
762
+ top: pos.top,
763
+ left: pos.left,
764
+ onRemove
765
+ },
766
+ pos.id
767
+ )) });
768
+ }
769
+
770
+ // src/components/StatusMessage.tsx
771
+ import { useEffect as useEffect5 } from "react";
772
+ import { motion as motion4 } from "motion/react";
773
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
774
+ function StatusMessage({ state, onDismiss }) {
775
+ useEffect5(() => {
776
+ if (state.status === "success" || state.status === "error") {
777
+ const timer = setTimeout(onDismiss, 4e3);
778
+ return () => clearTimeout(timer);
779
+ }
780
+ }, [state.status, onDismiss]);
781
+ if (state.status === "success") {
782
+ return /* @__PURE__ */ jsxs3(
783
+ motion4.div,
784
+ {
785
+ className: "shiteki-status shiteki-status--success",
786
+ initial: { opacity: 0, y: 12, scale: 0.95 },
787
+ animate: { opacity: 1, y: 0, scale: 1 },
788
+ exit: { opacity: 0, y: 12, scale: 0.95 },
789
+ transition: spring,
790
+ children: [
791
+ "Issue created!",
792
+ " ",
793
+ /* @__PURE__ */ jsxs3("a", { href: state.result.issueUrl, target: "_blank", rel: "noopener noreferrer", children: [
794
+ "#",
795
+ state.result.issueNumber
796
+ ] })
797
+ ]
798
+ }
799
+ );
800
+ }
801
+ if (state.status === "error") {
802
+ return /* @__PURE__ */ jsx6(
803
+ motion4.div,
804
+ {
805
+ className: "shiteki-status shiteki-status--error",
806
+ initial: { opacity: 0, y: 12, scale: 0.95 },
807
+ animate: { opacity: 1, y: 0, scale: 1 },
808
+ exit: { opacity: 0, y: 12, scale: 0.95 },
809
+ transition: spring,
810
+ children: state.error
811
+ }
812
+ );
813
+ }
814
+ if (state.status === "loading") {
815
+ return /* @__PURE__ */ jsx6(
816
+ motion4.div,
817
+ {
818
+ className: "shiteki-status",
819
+ initial: { opacity: 0, y: 12, scale: 0.95 },
820
+ animate: { opacity: 1, y: 0, scale: 1 },
821
+ exit: { opacity: 0, y: 12, scale: 0.95 },
822
+ transition: spring,
823
+ children: "Sending..."
824
+ }
825
+ );
826
+ }
827
+ return null;
828
+ }
829
+
830
+ // src/components/SettingsPanel.tsx
831
+ import { useCallback as useCallback7, useState as useState9 } from "react";
832
+ import { motion as motion5 } from "motion/react";
833
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
834
+ function SettingsPanel({ config, onSave, onCancel }) {
835
+ const [mode, setMode] = useState9(config.mode);
836
+ const [endpoint, setEndpoint] = useState9(config.endpoint);
837
+ const [githubToken, setGithubToken] = useState9(config.githubToken);
838
+ const [owner, setOwner] = useState9(config.owner);
839
+ const [repo, setRepo] = useState9(config.repo);
840
+ const [labels, setLabels] = useState9((config.labels ?? []).join(", "));
841
+ const handleSave = useCallback7(() => {
842
+ onSave({
843
+ mode,
844
+ endpoint: endpoint.trim(),
845
+ githubToken: githubToken.trim(),
846
+ owner: owner.trim(),
847
+ repo: repo.trim(),
848
+ labels: labels.split(",").map((l) => l.trim()).filter(Boolean)
849
+ });
850
+ }, [mode, endpoint, githubToken, owner, repo, labels, onSave]);
851
+ return /* @__PURE__ */ jsxs4(
852
+ motion5.div,
853
+ {
854
+ className: "shiteki-settings",
855
+ initial: { opacity: 0, y: 12 },
856
+ animate: { opacity: 1, y: 0 },
857
+ exit: { opacity: 0, y: 12 },
858
+ transition: spring,
859
+ children: [
860
+ /* @__PURE__ */ jsx7("div", { className: "shiteki-settings-header", children: "Settings" }),
861
+ /* @__PURE__ */ jsxs4("div", { className: "shiteki-settings-body", children: [
862
+ /* @__PURE__ */ jsxs4("div", { className: "shiteki-settings-field", children: [
863
+ /* @__PURE__ */ jsx7("span", { className: "shiteki-settings-label", children: "Submit mode" }),
864
+ /* @__PURE__ */ jsxs4("div", { className: "shiteki-radio-group", children: [
865
+ /* @__PURE__ */ jsxs4("label", { className: "shiteki-radio", children: [
866
+ /* @__PURE__ */ jsx7(
867
+ "input",
868
+ {
869
+ type: "radio",
870
+ name: "shiteki-mode",
871
+ checked: mode === "endpoint",
872
+ onChange: () => setMode("endpoint")
873
+ }
874
+ ),
875
+ /* @__PURE__ */ jsx7("span", { children: "Endpoint" })
876
+ ] }),
877
+ /* @__PURE__ */ jsxs4("label", { className: "shiteki-radio", children: [
878
+ /* @__PURE__ */ jsx7(
879
+ "input",
880
+ {
881
+ type: "radio",
882
+ name: "shiteki-mode",
883
+ checked: mode === "direct",
884
+ onChange: () => setMode("direct")
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx7("span", { children: "Direct" })
888
+ ] })
889
+ ] })
890
+ ] }),
891
+ mode === "endpoint" ? /* @__PURE__ */ jsxs4("label", { className: "shiteki-settings-field", children: [
892
+ /* @__PURE__ */ jsx7("span", { className: "shiteki-settings-label", children: "Endpoint" }),
893
+ /* @__PURE__ */ jsx7(
894
+ "input",
895
+ {
896
+ className: "shiteki-settings-input",
897
+ type: "text",
898
+ value: endpoint,
899
+ onChange: (e) => setEndpoint(e.target.value),
900
+ placeholder: "https://..."
901
+ }
902
+ )
903
+ ] }) : /* @__PURE__ */ jsxs4("label", { className: "shiteki-settings-field", children: [
904
+ /* @__PURE__ */ jsx7("span", { className: "shiteki-settings-label", children: "GitHub Token" }),
905
+ /* @__PURE__ */ jsx7(
906
+ "input",
907
+ {
908
+ className: "shiteki-settings-input",
909
+ type: "password",
910
+ value: githubToken,
911
+ onChange: (e) => setGithubToken(e.target.value),
912
+ placeholder: "github_pat_..."
913
+ }
914
+ )
915
+ ] }),
916
+ /* @__PURE__ */ jsxs4("label", { className: "shiteki-settings-field", children: [
917
+ /* @__PURE__ */ jsx7("span", { className: "shiteki-settings-label", children: "Owner" }),
918
+ /* @__PURE__ */ jsx7(
919
+ "input",
920
+ {
921
+ className: "shiteki-settings-input",
922
+ type: "text",
923
+ value: owner,
924
+ onChange: (e) => setOwner(e.target.value),
925
+ placeholder: "github-username"
926
+ }
927
+ )
928
+ ] }),
929
+ /* @__PURE__ */ jsxs4("label", { className: "shiteki-settings-field", children: [
930
+ /* @__PURE__ */ jsx7("span", { className: "shiteki-settings-label", children: "Repo" }),
931
+ /* @__PURE__ */ jsx7(
932
+ "input",
933
+ {
934
+ className: "shiteki-settings-input",
935
+ type: "text",
936
+ value: repo,
937
+ onChange: (e) => setRepo(e.target.value),
938
+ placeholder: "repository-name"
939
+ }
940
+ )
941
+ ] }),
942
+ /* @__PURE__ */ jsxs4("label", { className: "shiteki-settings-field", children: [
943
+ /* @__PURE__ */ jsx7("span", { className: "shiteki-settings-label", children: "Labels" }),
944
+ /* @__PURE__ */ jsx7(
945
+ "input",
946
+ {
947
+ className: "shiteki-settings-input",
948
+ type: "text",
949
+ value: labels,
950
+ onChange: (e) => setLabels(e.target.value),
951
+ placeholder: "bug, feedback"
952
+ }
953
+ )
954
+ ] })
955
+ ] }),
956
+ /* @__PURE__ */ jsxs4("div", { className: "shiteki-settings-actions", children: [
957
+ /* @__PURE__ */ jsx7("button", { className: "shiteki-btn shiteki-btn--ghost", onClick: onCancel, children: "Cancel" }),
958
+ /* @__PURE__ */ jsx7("button", { className: "shiteki-btn shiteki-btn--primary", onClick: handleSave, children: "Save" })
959
+ ] })
960
+ ]
961
+ }
962
+ );
963
+ }
964
+
965
+ // src/components/SendDialog.tsx
966
+ import { useCallback as useCallback8, useEffect as useEffect6, useRef as useRef6, useState as useState10 } from "react";
967
+ import { motion as motion6 } from "motion/react";
968
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
969
+ function SendDialog({ defaultTitle, sending, onConfirm, onCancel }) {
970
+ const [title, setTitle] = useState10(defaultTitle);
971
+ const inputRef = useRef6(null);
972
+ useEffect6(() => {
973
+ inputRef.current?.focus();
974
+ inputRef.current?.select();
975
+ }, []);
976
+ const handleSubmit = useCallback8(
977
+ (e) => {
978
+ e.preventDefault();
979
+ if (!title.trim() || sending) return;
980
+ onConfirm(title.trim());
981
+ },
982
+ [title, sending, onConfirm]
983
+ );
984
+ return /* @__PURE__ */ jsxs5(
985
+ motion6.div,
986
+ {
987
+ className: "shiteki-popover",
988
+ style: {
989
+ position: "fixed",
990
+ bottom: 76,
991
+ right: 20,
992
+ width: 320
993
+ },
994
+ initial: { opacity: 0, y: 12 },
995
+ animate: { opacity: 1, y: 0 },
996
+ exit: { opacity: 0, y: 12 },
997
+ transition: spring,
998
+ children: [
999
+ /* @__PURE__ */ jsx8("div", { className: "shiteki-popover-info", children: /* @__PURE__ */ jsx8("span", { className: "shiteki-popover-tag", children: "Create Issue" }) }),
1000
+ /* @__PURE__ */ jsxs5("form", { className: "shiteki-popover-form", onSubmit: handleSubmit, children: [
1001
+ /* @__PURE__ */ jsx8(
1002
+ "input",
1003
+ {
1004
+ ref: inputRef,
1005
+ className: "shiteki-settings-input",
1006
+ type: "text",
1007
+ value: title,
1008
+ onChange: (e) => setTitle(e.target.value),
1009
+ placeholder: "Issue title"
1010
+ }
1011
+ ),
1012
+ /* @__PURE__ */ jsxs5("div", { className: "shiteki-popover-actions", children: [
1013
+ /* @__PURE__ */ jsx8(
1014
+ "button",
1015
+ {
1016
+ type: "button",
1017
+ className: "shiteki-btn shiteki-btn--ghost",
1018
+ onClick: onCancel,
1019
+ disabled: sending,
1020
+ children: "Cancel"
1021
+ }
1022
+ ),
1023
+ /* @__PURE__ */ jsx8(
1024
+ "button",
1025
+ {
1026
+ type: "submit",
1027
+ className: "shiteki-btn shiteki-btn--primary",
1028
+ disabled: !title.trim() || sending,
1029
+ children: sending ? "Sending\u2026" : "Send"
1030
+ }
1031
+ )
1032
+ ] })
1033
+ ] })
1034
+ ]
1035
+ }
1036
+ );
1037
+ }
1038
+
1039
+ // #style-inject:#style-inject
1040
+ function styleInject(css, { insertAt } = {}) {
1041
+ if (!css || typeof document === "undefined") return;
1042
+ const head = document.head || document.getElementsByTagName("head")[0];
1043
+ const style = document.createElement("style");
1044
+ style.type = "text/css";
1045
+ if (insertAt === "top") {
1046
+ if (head.firstChild) {
1047
+ head.insertBefore(style, head.firstChild);
1048
+ } else {
1049
+ head.appendChild(style);
1050
+ }
1051
+ } else {
1052
+ head.appendChild(style);
1053
+ }
1054
+ if (style.styleSheet) {
1055
+ style.styleSheet.cssText = css;
1056
+ } else {
1057
+ style.appendChild(document.createTextNode(css));
1058
+ }
1059
+ }
1060
+
1061
+ // src/styles/widget.css
1062
+ styleInject('.shiteki-root {\n --shiteki-primary: #2563eb;\n --shiteki-primary-hover: #1d4ed8;\n --shiteki-bg: #ffffff;\n --shiteki-border: #e5e7eb;\n --shiteki-text: #111827;\n --shiteki-text-secondary: #6b7280;\n --shiteki-radius: 12px;\n --shiteki-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n sans-serif;\n font-size: 14px;\n line-height: 1.5;\n color: var(--shiteki-text);\n box-sizing: border-box;\n}\n.shiteki-toolbar {\n position: fixed;\n bottom: 20px;\n right: 20px;\n display: flex;\n align-items: center;\n border-radius: 9999px;\n z-index: 99999;\n background: var(--shiteki-bg);\n border: 1px solid var(--shiteki-border);\n box-shadow: var(--shiteki-shadow);\n}\n.shiteki-toolbar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--shiteki-text);\n cursor: pointer;\n transition: background 0.15s, color 0.15s;\n}\n.shiteki-toolbar-btn:hover {\n background: #f3f4f6;\n}\n.shiteki-toolbar-btn:active {\n transform: scale(0.9);\n}\n.shiteki-toolbar-btn--active {\n background: var(--shiteki-primary);\n color: #fff;\n}\n.shiteki-toolbar-btn--active:hover {\n background: var(--shiteki-primary-hover);\n}\n.shiteki-toolbar-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.shiteki-toolbar-picker {\n position: relative;\n}\n.shiteki-toolbar-badge {\n position: absolute;\n top: -4px;\n right: -6px;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n border-radius: 8px;\n background: #ef4444;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n line-height: 1;\n pointer-events: none;\n box-sizing: border-box;\n}\n.shiteki-toolbar-sep {\n width: 1px;\n height: 24px;\n background: var(--shiteki-border);\n margin: 0 4px;\n}\n.shiteki-highlight {\n position: fixed;\n pointer-events: none;\n border: 1.5px solid var(--shiteki-primary, #2563eb);\n background: rgba(37, 99, 235, 0.08);\n z-index: 99997;\n transition: all 0.05s ease-out;\n box-sizing: border-box;\n}\n.shiteki-marker {\n position: fixed;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n background: var(--shiteki-primary, #2563eb);\n color: #fff;\n font-size: 11px;\n font-weight: 700;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: 99998;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);\n user-select: none;\n}\n.shiteki-popover {\n position: fixed;\n width: 320px;\n background: var(--shiteki-bg);\n border: 1px solid var(--shiteki-border);\n border-radius: var(--shiteki-radius);\n box-shadow: var(--shiteki-shadow);\n z-index: 99999;\n overflow: hidden;\n}\n.shiteki-popover-info {\n padding: 10px 14px;\n background: #f9fafb;\n border-bottom: 1px solid var(--shiteki-border);\n display: flex;\n align-items: baseline;\n gap: 8px;\n overflow: hidden;\n}\n.shiteki-popover-tag {\n font-family:\n ui-monospace,\n SFMono-Regular,\n "SF Mono",\n Menlo,\n monospace;\n font-size: 12px;\n font-weight: 600;\n color: var(--shiteki-primary);\n white-space: nowrap;\n}\n.shiteki-popover-text {\n font-size: 12px;\n color: var(--shiteki-text-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.shiteki-popover-form {\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.shiteki-popover-textarea {\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--shiteki-border);\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n color: var(--shiteki-text);\n background: transparent;\n outline: none;\n resize: vertical;\n min-height: 60px;\n box-sizing: border-box;\n transition: border-color 0.15s;\n}\n.shiteki-popover-textarea:focus {\n border-color: var(--shiteki-primary);\n}\n.shiteki-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.shiteki-btn {\n padding: 6px 14px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s;\n}\n.shiteki-btn:active {\n transform: scale(0.95);\n}\n.shiteki-btn--primary {\n background: var(--shiteki-primary);\n color: #fff;\n}\n.shiteki-btn--primary:hover:not(:disabled) {\n background: var(--shiteki-primary-hover);\n}\n.shiteki-btn--primary:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.shiteki-btn--ghost {\n background: transparent;\n color: var(--shiteki-text-secondary);\n}\n.shiteki-btn--ghost:hover {\n background: #f3f4f6;\n}\n.shiteki-status {\n position: fixed;\n bottom: 76px;\n right: 20px;\n padding: 8px 18px;\n background: var(--shiteki-bg);\n border: 1px solid var(--shiteki-border);\n border-radius: 8px;\n box-shadow: var(--shiteki-shadow);\n font-size: 13px;\n white-space: nowrap;\n z-index: 99998;\n}\n.shiteki-status--success {\n color: #059669;\n}\n.shiteki-status--error {\n color: #dc2626;\n}\n.shiteki-status a {\n color: inherit;\n text-decoration: underline;\n}\n.shiteki-settings {\n position: fixed;\n bottom: 76px;\n right: 20px;\n width: 280px;\n background: var(--shiteki-bg);\n border: 1px solid var(--shiteki-border);\n border-radius: var(--shiteki-radius);\n box-shadow: var(--shiteki-shadow);\n z-index: 99999;\n overflow: hidden;\n}\n.shiteki-settings-header {\n padding: 10px 14px;\n font-size: 13px;\n font-weight: 600;\n border-bottom: 1px solid var(--shiteki-border);\n background: #f9fafb;\n}\n.shiteki-settings-body {\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.shiteki-settings-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.shiteki-settings-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--shiteki-text-secondary);\n}\n.shiteki-settings-input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--shiteki-border);\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n color: var(--shiteki-text);\n background: transparent;\n outline: none;\n box-sizing: border-box;\n transition: border-color 0.15s;\n}\n.shiteki-settings-input:focus {\n border-color: var(--shiteki-primary);\n}\n.shiteki-radio-group {\n display: flex;\n gap: 12px;\n}\n.shiteki-radio {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 13px;\n cursor: pointer;\n}\n.shiteki-radio input[type=radio] {\n margin: 0;\n accent-color: var(--shiteki-primary);\n cursor: pointer;\n}\n.shiteki-settings-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 0 14px 12px;\n}\n.shiteki-shortcut-hint {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 10px;\n background: var(--shiteki-bg);\n border: 1px solid var(--shiteki-border);\n border-radius: 8px;\n box-shadow: var(--shiteki-shadow);\n font-size: 12px;\n color: var(--shiteki-text-secondary);\n white-space: nowrap;\n}\n.shiteki-shortcut-hint kbd {\n display: inline-block;\n padding: 1px 5px;\n margin-right: 2px;\n background: #f3f4f6;\n border: 1px solid var(--shiteki-border);\n border-radius: 4px;\n font-family: inherit;\n font-size: 11px;\n font-weight: 600;\n color: var(--shiteki-text);\n line-height: 1.4;\n min-width: 19.5px;\n text-align: center;\n}\n.shiteki-shortcut-hint span {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.shiteki-shortcut-hint-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--shiteki-text-secondary);\n cursor: pointer;\n padding: 0;\n}\n.shiteki-shortcut-hint-close:hover {\n background: #f3f4f6;\n}\n');
1063
+
1064
+ // src/components/ShitekiWidget.tsx
1065
+ import { Fragment, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1066
+ function ShitekiWidget(props) {
1067
+ const { config, updateConfig } = useConfig(props);
1068
+ const [open, setOpen] = useState11(false);
1069
+ const [mode, setMode] = useState11("idle");
1070
+ const [settingsOpen, setSettingsOpen] = useState11(false);
1071
+ const [sendDialogOpen, setSendDialogOpen] = useState11(false);
1072
+ const [selectedElement, setSelectedElement] = useState11(null);
1073
+ const { annotations, add, remove, clear } = useAnnotations();
1074
+ const { state: submitState, submit, reset: resetSubmit } = useSubmit(config);
1075
+ const { copied, copy } = useClipboard();
1076
+ const { showHint, dismissHint } = useShortcutHint();
1077
+ useEffect7(() => {
1078
+ if (submitState.status === "success") {
1079
+ clear();
1080
+ }
1081
+ }, [submitState.status, clear]);
1082
+ const handleElementSelected = useCallback9((info) => {
1083
+ setSelectedElement(info);
1084
+ setMode("annotating");
1085
+ }, []);
1086
+ const { hoveredRect } = useElementPicker({
1087
+ enabled: open && mode === "picking",
1088
+ onElementSelected: handleElementSelected
1089
+ });
1090
+ const handleOpen = useCallback9(() => {
1091
+ setOpen(true);
1092
+ setMode("picking");
1093
+ }, []);
1094
+ const handleToggleOpen = useCallback9(() => {
1095
+ setOpen((prev) => {
1096
+ if (prev) {
1097
+ setMode("idle");
1098
+ setSelectedElement(null);
1099
+ setSettingsOpen(false);
1100
+ } else {
1101
+ setMode("picking");
1102
+ }
1103
+ return !prev;
1104
+ });
1105
+ }, []);
1106
+ const handleAddAnnotation = useCallback9(
1107
+ (comment) => {
1108
+ if (selectedElement) {
1109
+ add(selectedElement, comment);
1110
+ }
1111
+ setSelectedElement(null);
1112
+ setMode("picking");
1113
+ },
1114
+ [selectedElement, add]
1115
+ );
1116
+ const handleCancelAnnotation = useCallback9(() => {
1117
+ setSelectedElement(null);
1118
+ setMode("picking");
1119
+ }, []);
1120
+ const handleCopy = useCallback9(() => {
1121
+ const prompt = generatePrompt(annotations);
1122
+ copy(prompt);
1123
+ }, [annotations, copy]);
1124
+ const handleSend = useCallback9(() => {
1125
+ setSendDialogOpen(true);
1126
+ }, []);
1127
+ const handleSendConfirm = useCallback9(
1128
+ (title) => {
1129
+ const prompt = generatePrompt(annotations);
1130
+ submit(title, prompt);
1131
+ setSendDialogOpen(false);
1132
+ },
1133
+ [annotations, submit]
1134
+ );
1135
+ const handleSendCancel = useCallback9(() => {
1136
+ setSendDialogOpen(false);
1137
+ }, []);
1138
+ const handleClear = useCallback9(() => {
1139
+ clear();
1140
+ setSelectedElement(null);
1141
+ }, [clear]);
1142
+ const handleClose = useCallback9(() => {
1143
+ setOpen(false);
1144
+ setMode("idle");
1145
+ setSelectedElement(null);
1146
+ setSettingsOpen(false);
1147
+ }, []);
1148
+ const handleToggleSettings = useCallback9(() => {
1149
+ setSettingsOpen((prev) => !prev);
1150
+ }, []);
1151
+ const handleSettingsSave = useCallback9(
1152
+ (partial) => {
1153
+ updateConfig(partial);
1154
+ setSettingsOpen(false);
1155
+ },
1156
+ [updateConfig]
1157
+ );
1158
+ const handleSettingsCancel = useCallback9(() => {
1159
+ setSettingsOpen(false);
1160
+ }, []);
1161
+ useKeyboardShortcuts({
1162
+ open,
1163
+ mode,
1164
+ annotationCount: annotations.length,
1165
+ settingsOpen,
1166
+ sendDialogOpen,
1167
+ onCopy: handleCopy,
1168
+ onSend: handleSend,
1169
+ onClear: handleClear,
1170
+ onToggleOpen: handleToggleOpen,
1171
+ onClose: handleClose,
1172
+ onCancelAnnotation: handleCancelAnnotation,
1173
+ onCloseSettings: handleSettingsCancel,
1174
+ onCloseSendDialog: handleSendCancel
1175
+ });
1176
+ return ReactDOM.createPortal(
1177
+ /* @__PURE__ */ jsxs6("div", { className: "shiteki-root", children: [
1178
+ /* @__PURE__ */ jsx9(
1179
+ Toolbar,
1180
+ {
1181
+ open,
1182
+ annotationCount: annotations.length,
1183
+ copied,
1184
+ sending: submitState.status === "loading",
1185
+ settingsOpen,
1186
+ onOpen: handleOpen,
1187
+ onCopy: handleCopy,
1188
+ onSend: handleSend,
1189
+ onClear: handleClear,
1190
+ onSettings: handleToggleSettings,
1191
+ onClose: handleClose,
1192
+ showHint,
1193
+ onDismissHint: dismissHint
1194
+ }
1195
+ ),
1196
+ open && /* @__PURE__ */ jsxs6(Fragment, { children: [
1197
+ /* @__PURE__ */ jsx9(ElementHighlight, { rect: hoveredRect }),
1198
+ /* @__PURE__ */ jsx9(AnimatePresence3, { children: mode === "annotating" && selectedElement && /* @__PURE__ */ jsx9(
1199
+ AnnotationPopover,
1200
+ {
1201
+ elementInfo: selectedElement,
1202
+ onAdd: handleAddAnnotation,
1203
+ onCancel: handleCancelAnnotation
1204
+ },
1205
+ "popover"
1206
+ ) }),
1207
+ /* @__PURE__ */ jsx9(AnnotationMarkers, { annotations, onRemove: remove }),
1208
+ /* @__PURE__ */ jsxs6(AnimatePresence3, { children: [
1209
+ settingsOpen && /* @__PURE__ */ jsx9(
1210
+ SettingsPanel,
1211
+ {
1212
+ config,
1213
+ onSave: handleSettingsSave,
1214
+ onCancel: handleSettingsCancel
1215
+ },
1216
+ "settings"
1217
+ ),
1218
+ sendDialogOpen && /* @__PURE__ */ jsx9(
1219
+ SendDialog,
1220
+ {
1221
+ defaultTitle: `Visual Annotations (${annotations.length}) \u2014 ${document.title || location.href}`,
1222
+ sending: submitState.status === "loading",
1223
+ onConfirm: handleSendConfirm,
1224
+ onCancel: handleSendCancel
1225
+ },
1226
+ "send-dialog"
1227
+ )
1228
+ ] }),
1229
+ /* @__PURE__ */ jsx9(AnimatePresence3, { children: submitState.status !== "idle" && /* @__PURE__ */ jsx9(StatusMessage, { state: submitState, onDismiss: resetSubmit }, "status") })
1230
+ ] })
1231
+ ] }),
1232
+ document.body
1233
+ );
1234
+ }
1235
+ export {
1236
+ ShitekiWidget
1237
+ };