@designfever/web-review-kit 0.6.0 → 0.7.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.
Files changed (41) hide show
  1. package/.env.sample +29 -0
  2. package/README.md +35 -10
  3. package/dist/chunk-AB5B6O77.js +584 -0
  4. package/dist/chunk-AB5B6O77.js.map +1 -0
  5. package/dist/{chunk-IN36JHEU.js → chunk-RPVLRULC.js} +504 -143
  6. package/dist/chunk-RPVLRULC.js.map +1 -0
  7. package/dist/image.types-BmzkFSPX.d.cts +71 -0
  8. package/dist/image.types-BmzkFSPX.d.ts +71 -0
  9. package/dist/index.cjs +1019 -144
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +27 -3
  12. package/dist/index.d.ts +27 -3
  13. package/dist/index.js +50 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/react-shell.cjs +10293 -5185
  16. package/dist/react-shell.cjs.map +1 -1
  17. package/dist/react-shell.d.cts +37 -4
  18. package/dist/react-shell.d.ts +37 -4
  19. package/dist/react-shell.js +9624 -4856
  20. package/dist/react-shell.js.map +1 -1
  21. package/dist/token-Dt-ZH-YO.d.cts +88 -0
  22. package/dist/token-nJXPPdYX.d.ts +88 -0
  23. package/dist/{types-DFHHVRBc.d.cts → types-DT9Z66mV.d.cts} +13 -1
  24. package/dist/{types-DFHHVRBc.d.ts → types-DT9Z66mV.d.ts} +13 -1
  25. package/dist/vite.cjs +1116 -5
  26. package/dist/vite.cjs.map +1 -1
  27. package/dist/vite.d.cts +45 -1
  28. package/dist/vite.d.ts +45 -1
  29. package/dist/vite.js +800 -5
  30. package/dist/vite.js.map +1 -1
  31. package/docs/README.md +11 -7
  32. package/docs/adapters.md +126 -0
  33. package/docs/adaptor.sample.ts +13 -1
  34. package/docs/architecture.md +2 -1
  35. package/docs/code-review-0.6.0.md +232 -0
  36. package/docs/db-setup.md +4 -2
  37. package/docs/figma-image-mvp-0.7.0.md +327 -0
  38. package/docs/installation.md +39 -7
  39. package/docs/release-notes-0.7.0.md +128 -0
  40. package/package.json +6 -2
  41. package/dist/chunk-IN36JHEU.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -20,16 +20,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT: () => DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
24
+ DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT: () => DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
25
+ DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY: () => DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
23
26
  DEFAULT_REVIEW_VIEWPORTS: () => DEFAULT_REVIEW_VIEWPORTS,
27
+ FIGMA_NODE_REF_SEPARATOR: () => FIGMA_NODE_REF_SEPARATOR,
28
+ REVIEW_FIGMA_TOKEN_MISSING_CODE: () => REVIEW_FIGMA_TOKEN_MISSING_CODE,
24
29
  REVIEW_WORKFLOW_STATUS_OPTIONS: () => REVIEW_WORKFLOW_STATUS_OPTIONS,
30
+ ReviewFigmaTokenError: () => ReviewFigmaTokenError,
31
+ collectReviewFigmaReleaseSnapshot: () => collectReviewFigmaReleaseSnapshot,
32
+ createReviewFigmaFrameUrl: () => createReviewFigmaFrameUrl,
33
+ createReviewFigmaImageStoreClient: () => createReviewFigmaImageStoreClient,
34
+ createReviewFigmaImagesSnapshot: () => createReviewFigmaImagesSnapshot,
35
+ createReviewFigmaNodeValue: () => createReviewFigmaNodeValue,
36
+ createReviewFigmaReleaseSnapshot: () => createReviewFigmaReleaseSnapshot,
25
37
  createWebReviewKit: () => createWebReviewKit,
26
38
  findReviewViewportPreset: () => findReviewViewportPreset,
27
39
  getNumberedReviewItems: () => getNumberedReviewItems,
40
+ getReviewFigmaImageMimeType: () => getReviewFigmaImageMimeType,
41
+ getReviewFigmaImageTargetKey: () => getReviewFigmaImageTargetKey,
28
42
  getReviewItemScope: () => getReviewItemScope,
29
43
  getReviewItemScopeLabel: () => getReviewItemScopeLabel,
30
44
  getReviewViewportScope: () => getReviewViewportScope,
45
+ isReviewFigmaTokenError: () => isReviewFigmaTokenError,
31
46
  localAdapter: () => localAdapter,
32
47
  normalizeReviewItemStatus: () => normalizeReviewItemStatus,
48
+ parseReviewFigmaNodeRef: () => parseReviewFigmaNodeRef,
49
+ readReviewFigmaToken: () => readReviewFigmaToken,
50
+ requireReviewFigmaNodeRef: () => requireReviewFigmaNodeRef,
51
+ requireReviewFigmaToken: () => requireReviewFigmaToken,
33
52
  supabaseAdapter: () => supabaseAdapter
34
53
  });
35
54
  module.exports = __toCommonJS(index_exports);
@@ -345,8 +364,10 @@ async function unwrapResponse(request, label) {
345
364
  }
346
365
  function buildSupabaseReviewUrl(item, source, options, itemId) {
347
366
  if (typeof window === "undefined") return void 0;
348
- const prefix = options.reviewPathPrefix ?? "/review";
349
- const url = new URL(prefix, window.location.origin);
367
+ const url = new URL(
368
+ normalizeReviewUrlPath(options.reviewPathPrefix),
369
+ window.location.origin
370
+ );
350
371
  url.searchParams.set("source", source);
351
372
  url.searchParams.set("target", item.routeKey || item.normalizedPath || "/");
352
373
  url.searchParams.set("w", String(Math.round(item.viewport.width)));
@@ -354,6 +375,11 @@ function buildSupabaseReviewUrl(item, source, options, itemId) {
354
375
  if (itemId) url.searchParams.set("item", itemId);
355
376
  return url.toString();
356
377
  }
378
+ function normalizeReviewUrlPath(value = "/review") {
379
+ const raw = value.trim() || "/review";
380
+ const path = raw.startsWith("/") ? raw : `/${raw}`;
381
+ return path.endsWith("/") ? path : `${path}/`;
382
+ }
357
383
  function toAbsoluteReviewUrl(path) {
358
384
  if (typeof window === "undefined") return path;
359
385
  return new URL(path, window.location.origin).toString();
@@ -365,6 +391,480 @@ function createSupabaseReviewItemId() {
365
391
  return `review-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
366
392
  }
367
393
 
394
+ // src/figma/image.types.ts
395
+ var DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT = "webp";
396
+
397
+ // src/vite/figma-asset.ts
398
+ function getReviewFigmaImageFormatFromMimeType(mimeType) {
399
+ if (mimeType === "image/webp") return "webp";
400
+ if (mimeType === "image/png") return "png";
401
+ if (mimeType === "image/jpeg") return "jpg";
402
+ return null;
403
+ }
404
+
405
+ // src/figma/parse.ts
406
+ var FIGMA_NODE_REF_SEPARATOR = "->";
407
+ function parseReviewFigmaNodeRef(value) {
408
+ if (typeof value !== "string") return normalizeReviewFigmaNodeRef(value);
409
+ const input = value.trim();
410
+ if (!input) return null;
411
+ return parseReviewFigmaNodeRefValue(input) ?? parseReviewFigmaUrl(input);
412
+ }
413
+ function requireReviewFigmaNodeRef(value) {
414
+ const ref = parseReviewFigmaNodeRef(value);
415
+ if (!ref) {
416
+ throw new Error("A Figma node link or fileKey->nodeId value is required.");
417
+ }
418
+ return ref;
419
+ }
420
+ function createReviewFigmaNodeValue(ref) {
421
+ return `${ref.fileKey}${FIGMA_NODE_REF_SEPARATOR}${ref.nodeId}`;
422
+ }
423
+ function createReviewFigmaFrameUrl(value) {
424
+ const ref = parseReviewFigmaNodeRef(value);
425
+ if (!ref) return null;
426
+ return `https://www.figma.com/design/${encodeURIComponent(
427
+ ref.fileKey
428
+ )}?node-id=${encodeURIComponent(ref.nodeId.replace(/:/g, "-"))}`;
429
+ }
430
+ function parseReviewFigmaNodeRefValue(value) {
431
+ const [fileKey, nodeId, extra] = value.split(FIGMA_NODE_REF_SEPARATOR).map((part) => part.trim());
432
+ if (!fileKey || !nodeId || extra !== void 0) return null;
433
+ return normalizeReviewFigmaNodeRef({ fileKey, nodeId });
434
+ }
435
+ function parseReviewFigmaUrl(value) {
436
+ let url;
437
+ try {
438
+ url = new URL(value);
439
+ } catch {
440
+ return null;
441
+ }
442
+ const fileKey = getFigmaFileKey(url);
443
+ const nodeId = normalizeFigmaNodeId(url.searchParams.get("node-id"));
444
+ if (!fileKey || !nodeId) return null;
445
+ return { fileKey, nodeId, sourceUrl: value };
446
+ }
447
+ function getFigmaFileKey(url) {
448
+ if (!/(^|\.)figma\.com$/i.test(url.hostname)) return null;
449
+ const parts = url.pathname.split("/").filter(Boolean);
450
+ const fileKeyIndex = parts.findIndex(
451
+ (part) => ["design", "file", "proto", "board"].includes(part)
452
+ );
453
+ const fileKey = fileKeyIndex >= 0 ? parts[fileKeyIndex + 1] : null;
454
+ return normalizeFigmaFileKey(fileKey);
455
+ }
456
+ function normalizeReviewFigmaNodeRef(value) {
457
+ const fileKey = normalizeFigmaFileKey(value?.fileKey);
458
+ const nodeId = normalizeFigmaNodeId(value?.nodeId);
459
+ if (!fileKey || !nodeId) return null;
460
+ const sourceUrl = normalizeOptionalString(value?.sourceUrl);
461
+ return sourceUrl ? { fileKey, nodeId, sourceUrl } : { fileKey, nodeId };
462
+ }
463
+ function normalizeFigmaFileKey(value) {
464
+ return normalizeOptionalString(value)?.replace(/[?#].*$/, "") ?? null;
465
+ }
466
+ function normalizeFigmaNodeId(value) {
467
+ const nodeId = normalizeOptionalString(value);
468
+ if (!nodeId) return null;
469
+ if (nodeId.includes(":")) return nodeId;
470
+ return nodeId.replace(/-/g, ":");
471
+ }
472
+ function normalizeOptionalString(value) {
473
+ if (typeof value !== "string") return null;
474
+ return value.trim() || null;
475
+ }
476
+
477
+ // src/figma/token.ts
478
+ var DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY = "FIGMA_TOKEN";
479
+ var REVIEW_FIGMA_TOKEN_MISSING_CODE = "DFWR_FIGMA_TOKEN_MISSING";
480
+ var ReviewFigmaTokenError = class extends Error {
481
+ constructor(envKey = DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY) {
482
+ super(
483
+ `Figma image rendering requires server env ${envKey}. Set ${envKey} in the dev/server environment; do not expose it as VITE_${envKey}.`
484
+ );
485
+ this.code = REVIEW_FIGMA_TOKEN_MISSING_CODE;
486
+ this.name = "ReviewFigmaTokenError";
487
+ this.envKey = envKey;
488
+ }
489
+ };
490
+ function readReviewFigmaToken(options = {}) {
491
+ if (options.enabled === false) return null;
492
+ const envKey = options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;
493
+ return normalizeReviewFigmaToken(options.token ?? options.env?.[envKey]);
494
+ }
495
+ function requireReviewFigmaToken(options = {}) {
496
+ const envKey = options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;
497
+ const token = readReviewFigmaToken(options);
498
+ if (!token) throw new ReviewFigmaTokenError(envKey);
499
+ return token;
500
+ }
501
+ function isReviewFigmaTokenError(error) {
502
+ if (!error || typeof error !== "object") return false;
503
+ return error instanceof ReviewFigmaTokenError || "code" in error && error.code === REVIEW_FIGMA_TOKEN_MISSING_CODE;
504
+ }
505
+ function normalizeReviewFigmaToken(value) {
506
+ if (typeof value !== "string") return null;
507
+ return value.trim() || null;
508
+ }
509
+
510
+ // src/figma/render.ts
511
+ var DEFAULT_FIGMA_API_BASE_URL = "https://api.figma.com";
512
+ function createReviewFigmaImageApiUrl({
513
+ apiBaseUrl = DEFAULT_FIGMA_API_BASE_URL,
514
+ fileKey,
515
+ nodeId,
516
+ format = "png",
517
+ scale,
518
+ useAbsoluteBounds
519
+ }) {
520
+ const url = new URL(`/v1/images/${encodeURIComponent(fileKey)}`, apiBaseUrl);
521
+ url.searchParams.set("ids", nodeId);
522
+ url.searchParams.set("format", format);
523
+ if (typeof scale === "number" && Number.isFinite(scale) && scale > 0) {
524
+ url.searchParams.set("scale", String(scale));
525
+ }
526
+ if (useAbsoluteBounds !== void 0) {
527
+ url.searchParams.set("use_absolute_bounds", String(useAbsoluteBounds));
528
+ }
529
+ return url.toString();
530
+ }
531
+
532
+ // src/figma/image.store.ts
533
+ var DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT = "/__dfwr/figma-images";
534
+ function createReviewFigmaImageStoreClient(options = {}) {
535
+ const endpoint = options.endpoint ?? DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT;
536
+ const request = createReviewFigmaImageStoreRequest(endpoint, options.fetch);
537
+ return {
538
+ listImages(target) {
539
+ const url = `${endpoint}?target=${encodeURIComponent(
540
+ JSON.stringify(target)
541
+ )}`;
542
+ return request(url);
543
+ },
544
+ async addImage(input) {
545
+ const nextInput = await createClientRenderedAddImageInput(
546
+ input,
547
+ options.clientRender,
548
+ options.fetch
549
+ );
550
+ return request(endpoint, {
551
+ method: "POST",
552
+ body: JSON.stringify(nextInput)
553
+ });
554
+ },
555
+ updateImage(id, patch) {
556
+ return request(`${endpoint}/${encodeURIComponent(id)}`, {
557
+ method: "PATCH",
558
+ body: JSON.stringify(patch)
559
+ });
560
+ },
561
+ reorderImages(input) {
562
+ return request(`${endpoint}/reorder`, {
563
+ method: "PATCH",
564
+ body: JSON.stringify(input)
565
+ });
566
+ },
567
+ deleteImage(id) {
568
+ return request(`${endpoint}/${encodeURIComponent(id)}`, {
569
+ method: "DELETE"
570
+ });
571
+ }
572
+ };
573
+ }
574
+ async function createClientRenderedAddImageInput(input, clientRender, fetchOption) {
575
+ const options = normalizeClientRenderOptions(clientRender);
576
+ if (!options) return input;
577
+ const token = readClientRenderToken(options);
578
+ if (!token) return input;
579
+ try {
580
+ const asset = await withTimeout(
581
+ createClientRenderedFigmaAsset(input.figmaUrl, token, options, fetchOption),
582
+ options.timeoutMs ?? 1e4
583
+ );
584
+ return {
585
+ ...input,
586
+ imageFormat: asset.imageFormat,
587
+ asset
588
+ };
589
+ } catch {
590
+ return input;
591
+ }
592
+ }
593
+ function normalizeClientRenderOptions(clientRender) {
594
+ if (!clientRender) return null;
595
+ if (clientRender === true) return {};
596
+ return clientRender;
597
+ }
598
+ function readClientRenderToken(options) {
599
+ const token = typeof options.token === "function" ? options.token() : options.token;
600
+ return typeof token === "string" ? token.trim() : "";
601
+ }
602
+ async function createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOption) {
603
+ const ref = parseReviewFigmaNodeRef(figmaUrl);
604
+ if (!ref) throw new Error("A Figma node link is required.");
605
+ const requestFetch = fetchOption ?? globalThis.fetch;
606
+ if (!requestFetch) throw new Error("Figma client rendering requires fetch.");
607
+ const renderFormat = options.renderFormat ?? "png";
608
+ const response = await requestFetch(
609
+ createReviewFigmaImageApiUrl({
610
+ apiBaseUrl: options.apiBaseUrl,
611
+ fileKey: ref.fileKey,
612
+ nodeId: ref.nodeId,
613
+ format: renderFormat,
614
+ scale: options.renderScale,
615
+ useAbsoluteBounds: options.useAbsoluteBounds
616
+ }),
617
+ {
618
+ headers: {
619
+ "X-Figma-Token": token
620
+ }
621
+ }
622
+ );
623
+ const body = await response.json().catch(() => null);
624
+ if (!response.ok) {
625
+ throw new Error(body?.err || `Figma image render failed with ${response.status}`);
626
+ }
627
+ const imageUrl = body?.images?.[ref.nodeId];
628
+ if (!imageUrl) throw new Error(`Figma image render returned no URL for ${ref.nodeId}.`);
629
+ const imageResponse = await requestFetch(imageUrl);
630
+ if (!imageResponse.ok) {
631
+ throw new Error(`Figma image download failed with ${imageResponse.status}`);
632
+ }
633
+ const originalBlob = await imageResponse.blob();
634
+ const originalMimeType = normalizeClientImageMimeType(originalBlob.type) ?? getReviewFigmaImageMimeType(renderFormat === "jpg" ? "jpg" : "png");
635
+ const originalFormat = getReviewFigmaImageFormatFromMimeType(originalMimeType) ?? (renderFormat === "jpg" ? "jpg" : "png");
636
+ const dimensions = await readImageBlobDimensions(originalBlob);
637
+ const shouldConvertToWebp = options.convertToWebp ?? true;
638
+ const convertedBlob = shouldConvertToWebp ? await convertImageBlobToWebp(
639
+ originalBlob,
640
+ options.webpQuality ?? 0.9,
641
+ dimensions
642
+ ).catch(() => null) : null;
643
+ const finalBlob = convertedBlob?.type === "image/webp" ? convertedBlob : originalBlob;
644
+ const finalMimeType = normalizeClientImageMimeType(finalBlob.type) ?? originalMimeType;
645
+ const finalFormat = getReviewFigmaImageFormatFromMimeType(finalMimeType) ?? originalFormat;
646
+ return {
647
+ dataUrl: await blobToDataUrl(finalBlob),
648
+ imageFormat: finalFormat,
649
+ mimeType: finalMimeType,
650
+ byteSize: finalBlob.size,
651
+ width: dimensions.width,
652
+ height: dimensions.height
653
+ };
654
+ }
655
+ async function readImageBlobDimensions(blob) {
656
+ const image = await loadImageBlob(blob);
657
+ return {
658
+ width: image.naturalWidth || image.width,
659
+ height: image.naturalHeight || image.height
660
+ };
661
+ }
662
+ async function convertImageBlobToWebp(blob, quality, dimensions) {
663
+ if (typeof document === "undefined") return null;
664
+ if (!dimensions.width || !dimensions.height) return null;
665
+ const image = await loadImageBlob(blob);
666
+ const canvas = document.createElement("canvas");
667
+ canvas.width = dimensions.width;
668
+ canvas.height = dimensions.height;
669
+ const context = canvas.getContext("2d");
670
+ if (!context) return null;
671
+ context.drawImage(image, 0, 0);
672
+ return new Promise((resolve) => {
673
+ canvas.toBlob(resolve, "image/webp", quality);
674
+ });
675
+ }
676
+ function loadImageBlob(blob) {
677
+ return new Promise((resolve, reject) => {
678
+ if (typeof Image === "undefined" || typeof URL === "undefined") {
679
+ reject(new Error("Image decoding is unavailable."));
680
+ return;
681
+ }
682
+ const image = new Image();
683
+ const objectUrl = URL.createObjectURL(blob);
684
+ image.onload = () => {
685
+ URL.revokeObjectURL(objectUrl);
686
+ resolve(image);
687
+ };
688
+ image.onerror = () => {
689
+ URL.revokeObjectURL(objectUrl);
690
+ reject(new Error("Image decoding failed."));
691
+ };
692
+ image.src = objectUrl;
693
+ });
694
+ }
695
+ function blobToDataUrl(blob) {
696
+ return new Promise((resolve, reject) => {
697
+ const reader = new FileReader();
698
+ reader.onload = () => {
699
+ if (typeof reader.result === "string") {
700
+ resolve(reader.result);
701
+ return;
702
+ }
703
+ reject(new Error("Blob encoding failed."));
704
+ };
705
+ reader.onerror = () => reject(reader.error ?? new Error("Blob encoding failed."));
706
+ reader.readAsDataURL(blob);
707
+ });
708
+ }
709
+ function normalizeClientImageMimeType(value) {
710
+ const mimeType = value?.split(";")[0]?.trim().toLowerCase();
711
+ if (mimeType === "image/jpg") return "image/jpeg";
712
+ if (mimeType === "image/jpeg" || mimeType === "image/png" || mimeType === "image/webp") {
713
+ return mimeType;
714
+ }
715
+ return null;
716
+ }
717
+ async function withTimeout(promise, timeoutMs) {
718
+ let timeoutId;
719
+ try {
720
+ return await Promise.race([
721
+ promise,
722
+ new Promise((_, reject) => {
723
+ timeoutId = setTimeout(
724
+ () => reject(new Error("Figma client rendering timed out.")),
725
+ timeoutMs
726
+ );
727
+ })
728
+ ]);
729
+ } finally {
730
+ if (timeoutId) clearTimeout(timeoutId);
731
+ }
732
+ }
733
+ function getReviewFigmaImageTargetKey(target) {
734
+ return JSON.stringify(normalizeReviewFigmaImageTarget(target));
735
+ }
736
+ function getReviewFigmaImageMimeType(format) {
737
+ if (format === "jpg") return "image/jpeg";
738
+ if (format === "png") return "image/png";
739
+ return "image/webp";
740
+ }
741
+ function createReviewFigmaImageStoreRequest(endpoint, fetchOption) {
742
+ return async (input, init = {}) => {
743
+ const requestFetch = fetchOption ?? globalThis.fetch;
744
+ if (!requestFetch) throw new Error("Figma image store requires fetch.");
745
+ const response = await requestFetch(input, {
746
+ ...init,
747
+ headers: {
748
+ "Content-Type": "application/json",
749
+ ...init.headers ?? {}
750
+ }
751
+ });
752
+ const text = await response.text();
753
+ const body = text ? JSON.parse(text) : null;
754
+ if (!response.ok) {
755
+ const message = typeof body?.error === "string" ? body.error : `Figma image store request failed: ${response.status}`;
756
+ throw new Error(message);
757
+ }
758
+ return body;
759
+ };
760
+ }
761
+ function normalizeReviewFigmaImageTarget(target) {
762
+ if (target.type === "figma-node") {
763
+ return {
764
+ type: target.type,
765
+ projectId: target.projectId,
766
+ fileKey: target.fileKey,
767
+ nodeId: target.nodeId
768
+ };
769
+ }
770
+ return {
771
+ type: target.type,
772
+ projectId: target.projectId,
773
+ pageUrl: target.pageUrl,
774
+ slot: target.slot ?? "",
775
+ viewport: target.viewport ? {
776
+ label: target.viewport.label ?? "",
777
+ width: target.viewport.width ?? null,
778
+ height: target.viewport.height ?? null,
779
+ scope: target.viewport.scope ?? ""
780
+ } : null
781
+ };
782
+ }
783
+
784
+ // src/figma/image.snapshot.ts
785
+ function createReviewFigmaImagesSnapshot(images, options = {}) {
786
+ const targetKeys = options.targets?.length ? new Set(options.targets.map(getReviewFigmaImageTargetKey)) : null;
787
+ return images.filter((image) => {
788
+ if (options.projectId && image.projectId !== options.projectId) {
789
+ return false;
790
+ }
791
+ if (targetKeys && !targetKeys.has(getReviewFigmaImageTargetKey(image.target))) {
792
+ return false;
793
+ }
794
+ return true;
795
+ }).map(cloneReviewFigmaImage).sort(compareReviewFigmaSnapshotImages);
796
+ }
797
+ function createReviewFigmaReleaseSnapshot({
798
+ images,
799
+ projectId,
800
+ releaseId,
801
+ label,
802
+ createdAt,
803
+ targets
804
+ }) {
805
+ return {
806
+ version: 1,
807
+ projectId,
808
+ ...releaseId ? { releaseId } : null,
809
+ ...label ? { label } : null,
810
+ createdAt: createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
811
+ figmaImagesSnapshot: createReviewFigmaImagesSnapshot(images, {
812
+ projectId,
813
+ targets
814
+ })
815
+ };
816
+ }
817
+ async function collectReviewFigmaReleaseSnapshot({
818
+ store,
819
+ targets,
820
+ ...snapshotOptions
821
+ }) {
822
+ const imagesByTarget = await Promise.all(
823
+ targets.map((target) => store.listImages(target))
824
+ );
825
+ return createReviewFigmaReleaseSnapshot({
826
+ ...snapshotOptions,
827
+ targets,
828
+ images: dedupeReviewFigmaImages(imagesByTarget.flat())
829
+ });
830
+ }
831
+ function dedupeReviewFigmaImages(images) {
832
+ return Array.from(new Map(images.map((image) => [image.id, image])).values());
833
+ }
834
+ function cloneReviewFigmaImage(image) {
835
+ return {
836
+ ...image,
837
+ target: cloneReviewFigmaImageTarget(image.target)
838
+ };
839
+ }
840
+ function cloneReviewFigmaImageTarget(target) {
841
+ if (target.type === "figma-node") {
842
+ return {
843
+ type: target.type,
844
+ projectId: target.projectId,
845
+ fileKey: target.fileKey,
846
+ nodeId: target.nodeId
847
+ };
848
+ }
849
+ return {
850
+ type: target.type,
851
+ projectId: target.projectId,
852
+ pageUrl: target.pageUrl,
853
+ slot: target.slot,
854
+ viewport: target.viewport ? {
855
+ label: target.viewport.label,
856
+ width: target.viewport.width,
857
+ height: target.viewport.height,
858
+ scope: target.viewport.scope
859
+ } : void 0
860
+ };
861
+ }
862
+ function compareReviewFigmaSnapshotImages(a, b) {
863
+ return a.projectId.localeCompare(b.projectId) || getReviewFigmaImageTargetKey(a.target).localeCompare(
864
+ getReviewFigmaImageTargetKey(b.target)
865
+ ) || a.order - b.order || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
866
+ }
867
+
368
868
  // src/core/geometry.ts
369
869
  function rectanglesIntersect(a, b) {
370
870
  return a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
@@ -547,13 +1047,13 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
547
1047
  const target = environment.document.elementFromPoint(point.x, point.y);
548
1048
  if (!target) return void 0;
549
1049
  const candidates = createAnchorCandidates(target, configuredAttribute);
550
- const primary = candidates[0];
551
- if (!primary) return void 0;
1050
+ const primaryCandidate = candidates[0];
1051
+ if (!primaryCandidate) return void 0;
552
1052
  return {
553
- ...primary,
1053
+ ...primaryCandidate,
554
1054
  candidates,
555
1055
  htmlSnippet: getElementHtmlSnippet(
556
- getAnchorSourceElement(target, primary, configuredAttribute) ?? target
1056
+ getAnchorSourceElement(target, primaryCandidate, configuredAttribute) ?? target
557
1057
  ),
558
1058
  source: getDomSourceHint(target)
559
1059
  };
@@ -561,13 +1061,13 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
561
1061
  function getDomAnchorFromElement(target, configuredAttribute = "data-qa-id", environment) {
562
1062
  if (target.ownerDocument !== environment.document) return void 0;
563
1063
  const candidates = createAnchorCandidates(target, configuredAttribute);
564
- const primary = candidates[0];
565
- if (!primary) return void 0;
1064
+ const primaryCandidate = candidates[0];
1065
+ if (!primaryCandidate) return void 0;
566
1066
  return {
567
- ...primary,
1067
+ ...primaryCandidate,
568
1068
  candidates,
569
1069
  htmlSnippet: getElementHtmlSnippet(
570
- getAnchorSourceElement(target, primary, configuredAttribute) ?? target
1070
+ getAnchorSourceElement(target, primaryCandidate, configuredAttribute) ?? target
571
1071
  ),
572
1072
  source: getDomSourceHint(target)
573
1073
  };
@@ -738,7 +1238,7 @@ function createAnchorCandidates(target, configuredAttribute) {
738
1238
  function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
739
1239
  for (const attributeName of attributeNames) {
740
1240
  const selector = `[${attributeName}]`;
741
- const element = safeClosest(target, selector);
1241
+ const element = tryClosest(target, selector);
742
1242
  if (!element) continue;
743
1243
  const value = getStableAttributeValue(element, attributeName);
744
1244
  if (!value) continue;
@@ -844,7 +1344,7 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
844
1344
  return target;
845
1345
  }
846
1346
  }
847
- function safeClosest(element, selector) {
1347
+ function tryClosest(element, selector) {
848
1348
  try {
849
1349
  return element.closest(selector);
850
1350
  } catch {
@@ -1444,18 +1944,90 @@ function setDocumentScrollInstantly(environment, position) {
1444
1944
  );
1445
1945
  }
1446
1946
 
1947
+ // src/core/draft.metrics.ts
1948
+ function getDraftViewportScale(viewport, presets) {
1949
+ const preset = findReviewViewportPreset(viewport, presets);
1950
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
1951
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
1952
+ return { scale, designWidth, presetLabel: preset.label };
1953
+ }
1954
+ function getDraftAdjustmentMetrics(draft, presets) {
1955
+ const adjustment = draft.adjustment;
1956
+ const x = adjustment?.x ?? 0;
1957
+ const y = adjustment?.y ?? 0;
1958
+ const scale = adjustment?.scale ?? 0;
1959
+ const {
1960
+ scale: viewportScale,
1961
+ designWidth,
1962
+ presetLabel
1963
+ } = getDraftViewportScale(draft.viewport, presets);
1964
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
1965
+ const scaleCssDelta = scale * viewportScale;
1966
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
1967
+ 1 / selection.width,
1968
+ (selection.width + scaleCssDelta) / selection.width
1969
+ ) : 1;
1970
+ return {
1971
+ x,
1972
+ y,
1973
+ scale,
1974
+ cssX: x * viewportScale,
1975
+ cssY: y * viewportScale,
1976
+ scaleFactor,
1977
+ viewportScale,
1978
+ designWidth,
1979
+ presetLabel,
1980
+ viewportWidth: draft.viewport.width
1981
+ };
1982
+ }
1983
+ function hasDraftAdjustment(draft, presets) {
1984
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
1985
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
1986
+ }
1987
+ function getAdjustedDraftPoint(point, draft, presets) {
1988
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
1989
+ return {
1990
+ x: point.x + metrics.cssX,
1991
+ y: point.y + metrics.cssY
1992
+ };
1993
+ }
1994
+ function getAdjustedDraftSelection(selection, draft, presets) {
1995
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
1996
+ return {
1997
+ ...selection,
1998
+ left: selection.left + metrics.cssX,
1999
+ top: selection.top + metrics.cssY,
2000
+ width: selection.width * metrics.scaleFactor,
2001
+ height: selection.height * metrics.scaleFactor
2002
+ };
2003
+ }
2004
+
2005
+ // src/core/typography.tokens.ts
2006
+ var reviewTypographyTokens = `
2007
+ --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2008
+ --df-review-font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
2009
+ --df-review-font-size-3xs: 9px;
2010
+ --df-review-font-size-2xs: 10px;
2011
+ --df-review-font-size-xs: 11px;
2012
+ --df-review-font-size-sm: 12px;
2013
+ --df-review-font-size-md: 13px;
2014
+ --df-review-font-size-lg: 14px;
2015
+ --df-review-font-size-xl: 15px;
2016
+ --df-review-font-size-2xl: 18px;
2017
+ --df-review-font-weight-normal: 400;
2018
+ --df-review-font-weight-emphasis: 500;
2019
+ --df-review-line-height-tight: 1.25;
2020
+ --df-review-line-height-base: 1.42;
2021
+ --df-review-line-height-relaxed: 1.55;
2022
+ `;
2023
+
1447
2024
  // src/core/overlay.style.ts
1448
2025
  function createStyleElement() {
1449
2026
  const style = document.createElement("style");
1450
2027
  style.textContent = `
1451
2028
  :host {
1452
2029
  color-scheme: dark;
1453
- --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1454
- --df-review-font-size-2xs: 10px;
1455
- --df-review-font-size-xs: 11px;
1456
- --df-review-font-size-sm: 12px;
1457
- --df-review-font-size-md: 13px;
1458
- --df-review-font-size-xl: 15px;
2030
+ ${reviewTypographyTokens}
1459
2031
  --df-review-space-1: 4px;
1460
2032
  --df-review-space-1-5: 6px;
1461
2033
  --df-review-space-2: 8px;
@@ -1554,8 +2126,8 @@ function createStyleElement() {
1554
2126
  }
1555
2127
 
1556
2128
  .dfwr-title {
1557
- font-size: 15px;
1558
- font-weight: 700;
2129
+ font-size: var(--df-review-font-size-xl);
2130
+ font-weight: var(--df-review-font-weight-emphasis);
1559
2131
  line-height: 1.25;
1560
2132
  }
1561
2133
 
@@ -1599,11 +2171,15 @@ function createStyleElement() {
1599
2171
  }
1600
2172
 
1601
2173
  .dfwr-button {
2174
+ display: inline-flex;
2175
+ align-items: center;
2176
+ justify-content: center;
2177
+ gap: 6px;
1602
2178
  min-height: var(--df-review-control-height-md);
1603
2179
  padding: 0 12px;
1604
2180
  border-radius: var(--df-review-radius-sm);
1605
2181
  font-size: var(--df-review-font-size-sm);
1606
- font-weight: 650;
2182
+ font-weight: var(--df-review-font-weight-emphasis);
1607
2183
  }
1608
2184
 
1609
2185
  .dfwr-button:hover,
@@ -1619,6 +2195,21 @@ function createStyleElement() {
1619
2195
  color: var(--df-review-color-accent-contrast);
1620
2196
  }
1621
2197
 
2198
+ .dfwr-button:disabled {
2199
+ cursor: default;
2200
+ opacity: 0.62;
2201
+ }
2202
+
2203
+ .dfwr-spinner {
2204
+ display: inline-block;
2205
+ width: 12px;
2206
+ height: 12px;
2207
+ border: 2px solid currentColor;
2208
+ border-right-color: transparent;
2209
+ border-radius: 999px;
2210
+ animation: dfwr-spin 720ms linear infinite;
2211
+ }
2212
+
1622
2213
  .dfwr-icon-button {
1623
2214
  display: inline-flex;
1624
2215
  align-items: center;
@@ -1628,7 +2219,7 @@ function createStyleElement() {
1628
2219
  padding: 0 8px;
1629
2220
  border-radius: var(--df-review-radius-sm);
1630
2221
  font-size: var(--df-review-font-size-xs);
1631
- font-weight: 700;
2222
+ font-weight: var(--df-review-font-weight-emphasis);
1632
2223
  line-height: 1;
1633
2224
  text-transform: uppercase;
1634
2225
  }
@@ -1783,7 +2374,7 @@ function createStyleElement() {
1783
2374
  0 8px 18px rgba(0, 0, 0, 0.28);
1784
2375
  color: #111820;
1785
2376
  font-size: var(--df-review-font-size-2xs);
1786
- font-weight: 900;
2377
+ font-weight: var(--df-review-font-weight-emphasis);
1787
2378
  line-height: 1;
1788
2379
  pointer-events: none;
1789
2380
  }
@@ -1809,7 +2400,7 @@ function createStyleElement() {
1809
2400
  box-shadow: 0 0 0 4px rgba(var(--dfwr-scope-rgb), 0.18);
1810
2401
  color: var(--dfwr-scope);
1811
2402
  font-size: var(--df-review-font-size-2xs);
1812
- font-weight: 800;
2403
+ font-weight: var(--df-review-font-weight-emphasis);
1813
2404
  }
1814
2405
 
1815
2406
  .dfwr-bound-marker.is-highlighted {
@@ -2151,10 +2742,18 @@ function createStyleElement() {
2151
2742
  gap: 10px;
2152
2743
  }
2153
2744
 
2745
+ .dfwr-form-error {
2746
+ margin: 0;
2747
+ color: #ff8f61;
2748
+ font-size: var(--df-review-font-size-sm);
2749
+ line-height: 1.4;
2750
+ overflow-wrap: anywhere;
2751
+ }
2752
+
2753
+ .dfwr-input,
2754
+ .dfwr-select,
2154
2755
  .dfwr-textarea {
2155
2756
  width: 100%;
2156
- min-height: 92px;
2157
- resize: vertical;
2158
2757
  border: 1px solid rgba(255, 255, 255, 0.16);
2159
2758
  border-radius: var(--df-review-radius-sm);
2160
2759
  padding: 10px;
@@ -2165,14 +2764,39 @@ function createStyleElement() {
2165
2764
  line-height: 1.45;
2166
2765
  }
2167
2766
 
2767
+ .dfwr-input,
2768
+ .dfwr-select {
2769
+ min-height: 38px;
2770
+ }
2771
+
2772
+ .dfwr-select {
2773
+ appearance: none;
2774
+ cursor: pointer;
2775
+ }
2776
+
2777
+ .dfwr-textarea {
2778
+ min-height: 92px;
2779
+ resize: vertical;
2780
+ }
2781
+
2782
+ .dfwr-input:focus,
2783
+ .dfwr-select:focus,
2168
2784
  .dfwr-textarea:focus {
2169
2785
  outline: 2px solid var(--df-review-color-accent-ring);
2170
2786
  outline-offset: 1px;
2171
2787
  }
2172
2788
 
2173
2789
  @media (hover: none) and (pointer: coarse) {
2790
+ .dfwr-input,
2791
+ .dfwr-select,
2174
2792
  .dfwr-textarea {
2175
- font-size: 16px;
2793
+ font-size: var(--df-review-font-size-xl);
2794
+ }
2795
+ }
2796
+
2797
+ @keyframes dfwr-spin {
2798
+ to {
2799
+ transform: rotate(360deg);
2176
2800
  }
2177
2801
  }
2178
2802
 
@@ -2230,8 +2854,8 @@ function createStyleElement() {
2230
2854
  color: var(--df-review-color-text);
2231
2855
  cursor: pointer;
2232
2856
  font: inherit;
2233
- font-size: 14px;
2234
- font-weight: 800;
2857
+ font-size: var(--df-review-font-size-lg);
2858
+ font-weight: var(--df-review-font-weight-emphasis);
2235
2859
  line-height: 1;
2236
2860
  }
2237
2861
 
@@ -2284,7 +2908,7 @@ function createStyleElement() {
2284
2908
  margin-bottom: 10px;
2285
2909
  color: rgba(247, 247, 242, 0.74);
2286
2910
  font-size: var(--df-review-font-size-sm);
2287
- font-weight: 700;
2911
+ font-weight: var(--df-review-font-weight-emphasis);
2288
2912
  }
2289
2913
 
2290
2914
  .dfwr-item {
@@ -2306,6 +2930,8 @@ function createStyleElement() {
2306
2930
  }
2307
2931
 
2308
2932
  .dfwr-item-body {
2933
+ display: grid;
2934
+ gap: 4px;
2309
2935
  min-width: 0;
2310
2936
  flex: 1;
2311
2937
  }
@@ -2325,7 +2951,7 @@ function createStyleElement() {
2325
2951
  border-radius: var(--df-review-radius-pill);
2326
2952
  padding: 0 7px;
2327
2953
  font-size: var(--df-review-font-size-2xs);
2328
- font-weight: 800;
2954
+ font-weight: var(--df-review-font-weight-emphasis);
2329
2955
  line-height: 1;
2330
2956
  letter-spacing: 0;
2331
2957
  text-transform: uppercase;
@@ -2343,13 +2969,29 @@ function createStyleElement() {
2343
2969
  color: rgba(247, 247, 242, 0.64);
2344
2970
  }
2345
2971
 
2972
+ .dfwr-item-title {
2973
+ margin: 4px 0 0;
2974
+ color: var(--df-review-color-text);
2975
+ font-size: var(--df-review-font-size-md);
2976
+ font-weight: var(--df-review-font-weight-normal);
2977
+ line-height: 1.35;
2978
+ overflow-wrap: anywhere;
2979
+ }
2980
+
2346
2981
  .dfwr-item-comment {
2982
+ margin: 0;
2983
+ color: var(--df-review-color-text-muted);
2984
+ font-size: var(--df-review-font-size-sm);
2985
+ line-height: 1.45;
2986
+ overflow-wrap: anywhere;
2987
+ white-space: pre-wrap;
2988
+ }
2989
+
2990
+ .dfwr-item-comment.is-primary {
2347
2991
  margin: 4px 0;
2348
2992
  color: var(--df-review-color-text);
2349
2993
  font-size: var(--df-review-font-size-md);
2350
2994
  line-height: 1.42;
2351
- overflow-wrap: anywhere;
2352
- white-space: pre-wrap;
2353
2995
  }
2354
2996
 
2355
2997
  .dfwr-item-date {
@@ -2593,7 +3235,7 @@ var WebReviewKitView = class {
2593
3235
  );
2594
3236
  }
2595
3237
  }
2596
- if (state.isOpen && state.mode === "area" && !state.areaDraft) {
3238
+ if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
2597
3239
  shell.append(this.createAreaLayer());
2598
3240
  }
2599
3241
  if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
@@ -2667,64 +3309,29 @@ var WebReviewKitView = class {
2667
3309
  this.config.actions.setSelectingArea(false);
2668
3310
  this.config.actions.render();
2669
3311
  }
3312
+ // Draft adjustment geometry lives in draft.metrics.ts; these thin wrappers
3313
+ // supply the configured viewport presets so call sites stay unchanged.
3314
+ get viewportPresets() {
3315
+ return this.config.options.viewports?.presets;
3316
+ }
2670
3317
  getDraftAdjustmentMetrics(draft) {
2671
- const adjustment = draft.adjustment;
2672
- const x = adjustment?.x ?? 0;
2673
- const y = adjustment?.y ?? 0;
2674
- const scale = adjustment?.scale ?? 0;
2675
- const {
2676
- scale: viewportScale,
2677
- designWidth,
2678
- presetLabel
2679
- } = this.getDraftViewportScale(draft.viewport);
2680
- const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2681
- const scaleCssDelta = scale * viewportScale;
2682
- const scaleFactor = selection && selection.width > 0 ? Math.max(
2683
- 1 / selection.width,
2684
- (selection.width + scaleCssDelta) / selection.width
2685
- ) : 1;
2686
- return {
2687
- x,
2688
- y,
2689
- scale,
2690
- cssX: x * viewportScale,
2691
- cssY: y * viewportScale,
2692
- scaleFactor,
2693
- viewportScale,
2694
- designWidth,
2695
- presetLabel,
2696
- viewportWidth: draft.viewport.width
2697
- };
3318
+ return getDraftAdjustmentMetrics(draft, this.viewportPresets);
2698
3319
  }
2699
3320
  hasDraftAdjustment(draft) {
2700
- const metrics = this.getDraftAdjustmentMetrics(draft);
2701
- return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
3321
+ return hasDraftAdjustment(draft, this.viewportPresets);
2702
3322
  }
2703
3323
  getAdjustedDraftPoint(point, draft) {
2704
- const metrics = this.getDraftAdjustmentMetrics(draft);
2705
- return {
2706
- x: point.x + metrics.cssX,
2707
- y: point.y + metrics.cssY
2708
- };
3324
+ return getAdjustedDraftPoint(point, draft, this.viewportPresets);
2709
3325
  }
2710
3326
  getAdjustedDraftSelection(selection, draft) {
2711
- const metrics = this.getDraftAdjustmentMetrics(draft);
2712
- return {
2713
- ...selection,
2714
- left: selection.left + metrics.cssX,
2715
- top: selection.top + metrics.cssY,
2716
- width: selection.width * metrics.scaleFactor,
2717
- height: selection.height * metrics.scaleFactor
2718
- };
3327
+ return getAdjustedDraftSelection(
3328
+ selection,
3329
+ draft,
3330
+ this.viewportPresets
3331
+ );
2719
3332
  }
2720
3333
  getDraftViewportScale(viewport) {
2721
- const preset = findReviewViewportPreset(
2722
- viewport,
2723
- this.config.options.viewports?.presets
2724
- );
2725
- const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2726
- const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2727
- return { scale, designWidth, presetLabel: preset.label };
3334
+ return getDraftViewportScale(viewport, this.viewportPresets);
2728
3335
  }
2729
3336
  getDraftComposerWidth(environment) {
2730
3337
  const bounds = environment.overlayRect;
@@ -2870,6 +3477,67 @@ var WebReviewKitView = class {
2870
3477
  return trimmedComment ? `${trimmedComment}
2871
3478
  ${adjustment}` : adjustment;
2872
3479
  }
3480
+ getAssigneeOption(assigneeId) {
3481
+ if (!assigneeId) return void 0;
3482
+ return this.config.options.assigneeOptions?.find(
3483
+ (option) => option.value === assigneeId
3484
+ );
3485
+ }
3486
+ getAssigneeName(assigneeId) {
3487
+ return this.getAssigneeOption(assigneeId)?.label;
3488
+ }
3489
+ createDraftTitleInput(value, onInput) {
3490
+ const input = document.createElement("input");
3491
+ input.className = "dfwr-input";
3492
+ input.placeholder = "Title";
3493
+ input.type = "text";
3494
+ input.value = value ?? "";
3495
+ input.addEventListener("input", () => onInput(input.value));
3496
+ return input;
3497
+ }
3498
+ isTitleFieldEnabled() {
3499
+ return this.config.options.fields?.title === true;
3500
+ }
3501
+ createDraftAssigneeSelect(value, fallbackLabel, onChange) {
3502
+ const assigneeOptions = this.config.options.assigneeOptions ?? [];
3503
+ if (assigneeOptions.length === 0) return void 0;
3504
+ const assigneeTitle = this.config.options.assigneeTitle?.trim() || "Assignee";
3505
+ const select = document.createElement("select");
3506
+ select.className = "dfwr-select";
3507
+ const emptyOption = document.createElement("option");
3508
+ emptyOption.value = "";
3509
+ emptyOption.textContent = assigneeTitle;
3510
+ select.append(emptyOption);
3511
+ const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
3512
+ if (hasUnknownAssignee && value) {
3513
+ const option = document.createElement("option");
3514
+ option.value = value;
3515
+ option.textContent = fallbackLabel ?? value;
3516
+ select.append(option);
3517
+ }
3518
+ assigneeOptions.forEach((assigneeOption) => {
3519
+ const option = document.createElement("option");
3520
+ option.value = assigneeOption.value;
3521
+ option.textContent = assigneeOption.label;
3522
+ select.append(option);
3523
+ });
3524
+ select.value = value ?? "";
3525
+ select.addEventListener("change", () => {
3526
+ onChange(select.value || null, this.getAssigneeName(select.value));
3527
+ });
3528
+ return select;
3529
+ }
3530
+ getDraftFields(titleInput, textarea, assigneeSelect) {
3531
+ const title = titleInput?.value.trim();
3532
+ const comment = textarea.value.trim();
3533
+ const assigneeId = assigneeSelect?.value.trim() || void 0;
3534
+ return {
3535
+ title: title || void 0,
3536
+ comment,
3537
+ assigneeId,
3538
+ assigneeName: this.getAssigneeName(assigneeId)
3539
+ };
3540
+ }
2873
3541
  getStyleableDraftElement(draft, environment) {
2874
3542
  if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2875
3543
  return draft.previewElement;
@@ -3060,6 +3728,9 @@ ${adjustment}` : adjustment;
3060
3728
  empty.textContent = this.state.noteDraft ? "Write the note in the page box." : this.state.mode === "element" ? "Click an element to add QA." : "Click on the page to place a note.";
3061
3729
  return empty;
3062
3730
  }
3731
+ // Builds the note draft layer: the on-page marker/highlight plus its composer
3732
+ // popover. When dockComposer is set the composer renders into the side panel
3733
+ // instead of floating next to the marker (used for the docked review mode).
3063
3734
  createNotePopover(draft, options = {}) {
3064
3735
  const environment = this.config.getEnvironment();
3065
3736
  const group = document.createElement("div");
@@ -3123,6 +3794,14 @@ ${adjustment}` : adjustment;
3123
3794
  meta.className = "dfwr-item-date";
3124
3795
  meta.textContent = formatNoteDraftMeta(draft);
3125
3796
  }
3797
+ const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(draft.title, (title) => {
3798
+ const noteDraft = this.state.noteDraft;
3799
+ if (!noteDraft) return;
3800
+ this.config.actions.setNoteDraft({
3801
+ ...noteDraft,
3802
+ title
3803
+ });
3804
+ }) : void 0;
3126
3805
  const textarea = document.createElement("textarea");
3127
3806
  textarea.className = "dfwr-textarea";
3128
3807
  textarea.placeholder = "Review comment";
@@ -3136,13 +3815,30 @@ ${adjustment}` : adjustment;
3136
3815
  comment: textarea.value
3137
3816
  });
3138
3817
  });
3818
+ const assigneeSelect = this.createDraftAssigneeSelect(
3819
+ draft.assigneeId,
3820
+ draft.assigneeName,
3821
+ (assigneeId, assigneeName) => {
3822
+ const noteDraft = this.state.noteDraft;
3823
+ if (!noteDraft) return;
3824
+ this.config.actions.setNoteDraft({
3825
+ ...noteDraft,
3826
+ assigneeId,
3827
+ assigneeName
3828
+ });
3829
+ }
3830
+ );
3139
3831
  const saveDraft = () => {
3140
- const comment = textarea.value.trim();
3141
3832
  const currentDraft = this.state.noteDraft ?? draft;
3833
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3834
+ const comment = fields.comment;
3142
3835
  if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
3143
3836
  void this.config.actions.createItem({
3144
3837
  kind: "note",
3838
+ title: fields.title,
3145
3839
  comment: this.withDraftAdjustmentComment(comment, currentDraft),
3840
+ assigneeId: fields.assigneeId,
3841
+ assigneeName: fields.assigneeName,
3146
3842
  viewport: currentDraft.viewport,
3147
3843
  anchor: currentDraft.anchor,
3148
3844
  marker: currentDraft.marker,
@@ -3160,10 +3856,14 @@ ${adjustment}` : adjustment;
3160
3856
  const actions = this.createFormActions("Save note", saveDraft, {
3161
3857
  leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
3162
3858
  });
3859
+ const error = this.createDraftError();
3163
3860
  form.append(
3164
3861
  ...meta ? [meta] : [],
3165
3862
  ...adjustmentControls ? [adjustmentControls.panel] : [],
3863
+ ...titleInput ? [titleInput] : [],
3166
3864
  textarea,
3865
+ ...assigneeSelect ? [assigneeSelect] : [],
3866
+ ...error ? [error] : [],
3167
3867
  actions
3168
3868
  );
3169
3869
  const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
@@ -3290,6 +3990,9 @@ ${adjustment}` : adjustment;
3290
3990
  handle.addEventListener("pointerup", stopDrag);
3291
3991
  handle.addEventListener("pointercancel", stopDrag);
3292
3992
  }
3993
+ // Builds the element-adjustment controls (nudge the previewed element via
3994
+ // arrow keys / buttons). Wires keyboard deltas to the draft transform and
3995
+ // keeps the pin, popover, highlight and textarea in sync as the value changes.
3293
3996
  createAdjustmentControls({
3294
3997
  draft,
3295
3998
  pin,
@@ -3434,6 +4137,14 @@ ${adjustment}` : adjustment;
3434
4137
  return form;
3435
4138
  }
3436
4139
  form.append(this.createAreaMetricsPanel(areaDraft));
4140
+ const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(areaDraft.title, (title) => {
4141
+ const draft = this.state.areaDraft;
4142
+ if (!draft) return;
4143
+ this.config.actions.setAreaDraft({
4144
+ ...draft,
4145
+ title
4146
+ });
4147
+ }) : void 0;
3437
4148
  const textarea = document.createElement("textarea");
3438
4149
  textarea.className = "dfwr-textarea";
3439
4150
  textarea.placeholder = "Area comment";
@@ -3447,20 +4158,44 @@ ${adjustment}` : adjustment;
3447
4158
  comment: textarea.value
3448
4159
  });
3449
4160
  });
4161
+ const assigneeSelect = this.createDraftAssigneeSelect(
4162
+ areaDraft.assigneeId,
4163
+ areaDraft.assigneeName,
4164
+ (assigneeId, assigneeName) => {
4165
+ const draft = this.state.areaDraft;
4166
+ if (!draft) return;
4167
+ this.config.actions.setAreaDraft({
4168
+ ...draft,
4169
+ assigneeId,
4170
+ assigneeName
4171
+ });
4172
+ }
4173
+ );
3450
4174
  const actions = this.createFormActions("Save area", () => {
3451
- const comment = textarea.value.trim();
3452
4175
  const draft = this.state.areaDraft;
4176
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
4177
+ const comment = fields.comment;
3453
4178
  if (!comment || !draft) return;
3454
4179
  void this.config.actions.createItem({
3455
4180
  kind: "area",
4181
+ title: fields.title,
3456
4182
  comment,
4183
+ assigneeId: fields.assigneeId,
4184
+ assigneeName: fields.assigneeName,
3457
4185
  viewport: draft.viewport,
3458
4186
  anchor: draft.anchor,
3459
4187
  marker: draft.marker,
3460
4188
  selection: draft.selection
3461
4189
  });
3462
4190
  });
3463
- form.append(textarea, actions);
4191
+ const error = this.createDraftError();
4192
+ form.append(
4193
+ ...titleInput ? [titleInput] : [],
4194
+ textarea,
4195
+ ...assigneeSelect ? [assigneeSelect] : [],
4196
+ ...error ? [error] : [],
4197
+ actions
4198
+ );
3464
4199
  return form;
3465
4200
  }
3466
4201
  createAreaMetricsPanel(draft) {
@@ -3552,18 +4287,27 @@ ${adjustment}` : adjustment;
3552
4287
  createFormActions(saveLabel, onSave, options) {
3553
4288
  const actions = document.createElement("div");
3554
4289
  actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
4290
+ const isSaving = this.state.isCreatingItem;
3555
4291
  const save = document.createElement("button");
3556
4292
  save.className = "dfwr-button is-primary";
3557
4293
  save.type = "button";
3558
- save.textContent = saveLabel;
4294
+ save.disabled = isSaving;
4295
+ save.setAttribute("aria-busy", isSaving ? "true" : "false");
4296
+ if (isSaving) {
4297
+ save.append(this.createSpinner("dfwr-spinner"), "Saving...");
4298
+ } else {
4299
+ save.textContent = saveLabel;
4300
+ }
3559
4301
  save.addEventListener("click", (event) => {
3560
4302
  event.preventDefault();
3561
4303
  event.stopPropagation();
4304
+ if (this.state.isCreatingItem) return;
3562
4305
  onSave();
3563
4306
  });
3564
4307
  const cancel = document.createElement("button");
3565
4308
  cancel.className = "dfwr-button";
3566
4309
  cancel.type = "button";
4310
+ cancel.disabled = isSaving;
3567
4311
  cancel.textContent = "Cancel";
3568
4312
  cancel.addEventListener("click", (event) => {
3569
4313
  this.cancelDraft(event);
@@ -3586,6 +4330,20 @@ ${adjustment}` : adjustment;
3586
4330
  actions.append(save, cancel);
3587
4331
  return actions;
3588
4332
  }
4333
+ createSpinner(className) {
4334
+ const spinner = document.createElement("span");
4335
+ spinner.className = className;
4336
+ spinner.setAttribute("aria-hidden", "true");
4337
+ return spinner;
4338
+ }
4339
+ createDraftError() {
4340
+ if (!this.state.draftError) return void 0;
4341
+ const error = document.createElement("p");
4342
+ error.className = "dfwr-form-error";
4343
+ error.setAttribute("role", "alert");
4344
+ error.textContent = this.state.draftError;
4345
+ return error;
4346
+ }
3589
4347
  createList() {
3590
4348
  const section = document.createElement("div");
3591
4349
  section.className = "dfwr-list";
@@ -3638,14 +4396,20 @@ ${adjustment}` : adjustment;
3638
4396
  kind.className = "dfwr-item-kind";
3639
4397
  kind.textContent = item.kind;
3640
4398
  badges.append(scope, kind);
4399
+ const title = this.isTitleFieldEnabled() ? item.title?.trim() : "";
4400
+ const titleElement = title ? document.createElement("strong") : void 0;
4401
+ if (title && titleElement) {
4402
+ titleElement.className = "dfwr-item-title";
4403
+ titleElement.textContent = title;
4404
+ }
3641
4405
  const comment = document.createElement("p");
3642
- comment.className = "dfwr-item-comment";
4406
+ comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
3643
4407
  comment.textContent = item.comment;
3644
4408
  const date = document.createElement("time");
3645
4409
  date.className = "dfwr-item-date";
3646
4410
  date.dateTime = item.createdAt;
3647
4411
  date.textContent = formatItemMeta(item);
3648
- body.append(badges, comment, date);
4412
+ body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
3649
4413
  const actions = document.createElement("div");
3650
4414
  actions.className = "dfwr-item-actions";
3651
4415
  actions.addEventListener("click", (event) => event.stopPropagation());
@@ -3834,7 +4598,14 @@ ${formatItemMeta(item)}`;
3834
4598
  event,
3835
4599
  this.config.getEnvironment()
3836
4600
  );
3837
- void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, textarea.value) : this.config.actions.bindNoteDraftToPoint(nextPoint, textarea.value));
4601
+ const currentDraft = this.state.noteDraft;
4602
+ const fields = {
4603
+ title: currentDraft?.title,
4604
+ comment: textarea.value,
4605
+ assigneeId: currentDraft?.assigneeId,
4606
+ assigneeName: currentDraft?.assigneeName
4607
+ };
4608
+ void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, fields) : this.config.actions.bindNoteDraftToPoint(nextPoint, fields));
3838
4609
  });
3839
4610
  }
3840
4611
  createNoteLayer() {
@@ -3912,10 +4683,14 @@ ${formatItemMeta(item)}`;
3912
4683
  let startX = 0;
3913
4684
  let startY = 0;
3914
4685
  let selection;
4686
+ let activePointerId;
4687
+ let isDragging = false;
4688
+ const ownerWindow = layer.ownerDocument.defaultView ?? window;
3915
4689
  const updateBox = (event) => {
4690
+ const nextEnvironment = this.config.getEnvironment();
3916
4691
  const nextPoint = toTargetPointFromHostEvent(
3917
4692
  event,
3918
- this.config.getEnvironment()
4693
+ nextEnvironment
3919
4694
  );
3920
4695
  const left = Math.min(startX, nextPoint.x);
3921
4696
  const top = Math.min(startY, nextPoint.y);
@@ -3923,7 +4698,7 @@ ${formatItemMeta(item)}`;
3923
4698
  const height = Math.abs(nextPoint.y - startY);
3924
4699
  const hostPoint = toHostPoint(
3925
4700
  { x: left, y: top },
3926
- this.config.getEnvironment()
4701
+ nextEnvironment
3927
4702
  );
3928
4703
  selection = { left, top, width, height };
3929
4704
  box.style.left = `${hostPoint.x}px`;
@@ -3931,9 +4706,68 @@ ${formatItemMeta(item)}`;
3931
4706
  box.style.width = `${width}px`;
3932
4707
  box.style.height = `${height}px`;
3933
4708
  };
4709
+ const addDragListeners = () => {
4710
+ ownerWindow.addEventListener("pointermove", handlePointerMove, true);
4711
+ ownerWindow.addEventListener("pointerup", handlePointerUp, true);
4712
+ ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
4713
+ };
4714
+ const removeDragListeners = () => {
4715
+ ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
4716
+ ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
4717
+ ownerWindow.removeEventListener(
4718
+ "pointercancel",
4719
+ handlePointerCancel,
4720
+ true
4721
+ );
4722
+ };
4723
+ const releasePointerCapture = (event) => {
4724
+ try {
4725
+ if (layer.hasPointerCapture(event.pointerId)) {
4726
+ layer.releasePointerCapture(event.pointerId);
4727
+ }
4728
+ } catch {
4729
+ }
4730
+ };
4731
+ function isActivePointer(event) {
4732
+ return isDragging && event.pointerId === activePointerId;
4733
+ }
4734
+ const finishAreaSelection = (event) => {
4735
+ if (!isActivePointer(event)) return;
4736
+ event.preventDefault();
4737
+ updateBox(event);
4738
+ releasePointerCapture(event);
4739
+ removeDragListeners();
4740
+ isDragging = false;
4741
+ activePointerId = void 0;
4742
+ if (!selection || selection.width < 8 || selection.height < 8) return;
4743
+ this.config.actions.setSelectingArea(true);
4744
+ this.config.actions.render();
4745
+ void this.config.actions.createAreaDraft(selection);
4746
+ };
4747
+ function handlePointerMove(event) {
4748
+ if (!isActivePointer(event)) return;
4749
+ event.preventDefault();
4750
+ updateBox(event);
4751
+ }
4752
+ const handlePointerUp = (event) => {
4753
+ finishAreaSelection(event);
4754
+ };
4755
+ const handlePointerCancel = (event) => {
4756
+ if (!isActivePointer(event)) return;
4757
+ releasePointerCapture(event);
4758
+ removeDragListeners();
4759
+ isDragging = false;
4760
+ activePointerId = void 0;
4761
+ };
3934
4762
  layer.addEventListener("pointerdown", (event) => {
4763
+ if (event.button !== 0) return;
3935
4764
  event.preventDefault();
3936
- layer.setPointerCapture(event.pointerId);
4765
+ activePointerId = event.pointerId;
4766
+ isDragging = true;
4767
+ try {
4768
+ layer.setPointerCapture(event.pointerId);
4769
+ } catch {
4770
+ }
3937
4771
  const startPoint = toTargetPointFromHostEvent(
3938
4772
  event,
3939
4773
  this.config.getEnvironment()
@@ -3941,20 +4775,11 @@ ${formatItemMeta(item)}`;
3941
4775
  startX = startPoint.x;
3942
4776
  startY = startPoint.y;
3943
4777
  updateBox(event);
4778
+ addDragListeners();
3944
4779
  });
3945
- layer.addEventListener("pointermove", (event) => {
3946
- if (!layer.hasPointerCapture(event.pointerId)) return;
3947
- updateBox(event);
3948
- });
3949
- layer.addEventListener("pointerup", (event) => {
3950
- if (!layer.hasPointerCapture(event.pointerId)) return;
3951
- layer.releasePointerCapture(event.pointerId);
3952
- updateBox(event);
3953
- if (!selection || selection.width < 8 || selection.height < 8) return;
3954
- this.config.actions.setSelectingArea(true);
3955
- this.config.actions.render();
3956
- void this.config.actions.createAreaDraft(selection);
3957
- });
4780
+ layer.addEventListener("pointermove", handlePointerMove);
4781
+ layer.addEventListener("pointerup", handlePointerUp);
4782
+ layer.addEventListener("pointercancel", handlePointerCancel);
3958
4783
  return layer;
3959
4784
  }
3960
4785
  };
@@ -3994,6 +4819,8 @@ var WebReviewKitApp = class {
3994
4819
  this.isOpen = false;
3995
4820
  this.mode = "idle";
3996
4821
  this.items = [];
4822
+ this.draftError = "";
4823
+ this.isCreatingItem = false;
3997
4824
  this.isSelectingArea = false;
3998
4825
  this.handleKeyDown = (event) => {
3999
4826
  if (event.key === "Escape" && this.cancelMode()) {
@@ -4024,6 +4851,8 @@ var WebReviewKitApp = class {
4024
4851
  items: this.items,
4025
4852
  noteDraft: this.noteDraft,
4026
4853
  areaDraft: this.areaDraft,
4854
+ draftError: this.draftError,
4855
+ isCreatingItem: this.isCreatingItem,
4027
4856
  isSelectingArea: this.isSelectingArea,
4028
4857
  highlightedItemId: this.highlightedItemId
4029
4858
  }),
@@ -4038,19 +4867,22 @@ var WebReviewKitApp = class {
4038
4867
  clearDrafts: () => {
4039
4868
  this.noteDraft = void 0;
4040
4869
  this.areaDraft = void 0;
4870
+ this.draftError = "";
4041
4871
  },
4042
4872
  setNoteDraft: (draft) => {
4043
4873
  this.noteDraft = draft;
4874
+ this.draftError = "";
4044
4875
  },
4045
4876
  setAreaDraft: (draft) => {
4046
4877
  this.areaDraft = draft;
4878
+ this.draftError = "";
4047
4879
  },
4048
4880
  setSelectingArea: (isSelectingArea) => {
4049
4881
  this.isSelectingArea = isSelectingArea;
4050
4882
  },
4051
4883
  createItem: (input) => this.createItem(input),
4052
- bindNoteDraftToPoint: (point, comment) => this.bindNoteDraftToPoint(point, comment),
4053
- bindElementDraftToPoint: (point, comment) => this.bindElementDraftToPoint(point, comment),
4884
+ bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
4885
+ bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
4054
4886
  createAreaDraft: (selection) => this.createAreaDraft(selection)
4055
4887
  }
4056
4888
  });
@@ -4119,7 +4951,7 @@ var WebReviewKitApp = class {
4119
4951
  this.noteDraft = void 0;
4120
4952
  this.areaDraft = void 0;
4121
4953
  this.isSelectingArea = false;
4122
- await this.bindElementDraftToElement(element, comment);
4954
+ await this.bindElementDraftToElement(element, { comment });
4123
4955
  }
4124
4956
  getMode() {
4125
4957
  return this.mode;
@@ -4262,7 +5094,7 @@ var WebReviewKitApp = class {
4262
5094
  if (!this.shadow) return;
4263
5095
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
4264
5096
  }
4265
- async bindNoteDraftToPoint(point, comment) {
5097
+ async bindNoteDraftToPoint(point, fields = {}) {
4266
5098
  const environment = this.getEnvironment();
4267
5099
  if (!environment) return;
4268
5100
  const viewport = getViewportSize(environment);
@@ -4282,13 +5114,13 @@ var WebReviewKitApp = class {
4282
5114
  viewport,
4283
5115
  anchor,
4284
5116
  marker,
4285
- comment
5117
+ ...fields
4286
5118
  };
4287
5119
  });
4288
5120
  this.noteDraft = draft;
4289
5121
  this.render();
4290
5122
  }
4291
- async bindElementDraftToPoint(point, comment) {
5123
+ async bindElementDraftToPoint(point, fields = {}) {
4292
5124
  const environment = this.getEnvironment();
4293
5125
  if (!environment) return;
4294
5126
  const viewport = getViewportSize(environment);
@@ -4332,14 +5164,14 @@ var WebReviewKitApp = class {
4332
5164
  anchor,
4333
5165
  marker,
4334
5166
  selection: reviewSelection,
4335
- comment,
5167
+ ...fields,
4336
5168
  previewElement
4337
5169
  };
4338
5170
  });
4339
5171
  this.noteDraft = draft;
4340
5172
  this.render();
4341
5173
  }
4342
- async bindElementDraftToElement(element, comment) {
5174
+ async bindElementDraftToElement(element, fields = {}) {
4343
5175
  const environment = this.getEnvironment();
4344
5176
  if (!environment || element.ownerDocument !== environment.document) return;
4345
5177
  const viewport = getViewportSize(environment);
@@ -4372,7 +5204,7 @@ var WebReviewKitApp = class {
4372
5204
  anchor,
4373
5205
  marker,
4374
5206
  selection: reviewSelection,
4375
- comment,
5207
+ ...fields,
4376
5208
  previewElement
4377
5209
  };
4378
5210
  });
@@ -4382,26 +5214,33 @@ var WebReviewKitApp = class {
4382
5214
  }
4383
5215
  async createAreaDraft(selection) {
4384
5216
  const environment = this.getEnvironment();
4385
- if (!environment) return;
4386
- const viewport = getViewportSize(environment);
4387
- this.areaDraft = await this.withOverlayHidden(() => {
4388
- const marker = createSelectionCenterMarker(
4389
- selection,
4390
- void 0,
4391
- environment
4392
- );
4393
- const reviewSelection = {
4394
- viewport: toPublicSelection(selection)
4395
- };
4396
- return {
4397
- viewport,
4398
- marker,
4399
- selection: reviewSelection
4400
- };
4401
- });
4402
- this.isSelectingArea = false;
4403
- this.setModeState("area");
4404
- this.render();
5217
+ if (!environment) {
5218
+ this.isSelectingArea = false;
5219
+ this.render();
5220
+ return;
5221
+ }
5222
+ try {
5223
+ const viewport = getViewportSize(environment);
5224
+ this.areaDraft = await this.withOverlayHidden(() => {
5225
+ const marker = createSelectionCenterMarker(
5226
+ selection,
5227
+ void 0,
5228
+ environment
5229
+ );
5230
+ const reviewSelection = {
5231
+ viewport: toPublicSelection(selection)
5232
+ };
5233
+ return {
5234
+ viewport,
5235
+ marker,
5236
+ selection: reviewSelection
5237
+ };
5238
+ });
5239
+ this.setModeState("area");
5240
+ } finally {
5241
+ this.isSelectingArea = false;
5242
+ this.render();
5243
+ }
4405
5244
  }
4406
5245
  async withOverlayHidden(callback) {
4407
5246
  if (!this.root) return callback();
@@ -4415,11 +5254,16 @@ var WebReviewKitApp = class {
4415
5254
  }
4416
5255
  async createItem(input) {
4417
5256
  const environment = this.getEnvironment();
4418
- if (!environment) return;
5257
+ if (!environment || this.isCreatingItem) return;
4419
5258
  const now = (/* @__PURE__ */ new Date()).toISOString();
4420
5259
  const routeKey = getRouteKey(environment);
4421
5260
  const viewport = input.viewport ?? getViewportSize(environment);
4422
5261
  const createdBy = this.options.userId?.trim();
5262
+ const title = input.title?.trim();
5263
+ const assigneeId = input.assigneeId?.trim() || void 0;
5264
+ const assigneeOption = this.options.assigneeOptions?.find(
5265
+ (option) => option.value === assigneeId
5266
+ );
4423
5267
  const item = {
4424
5268
  id: createId(),
4425
5269
  projectId: this.options.projectId,
@@ -4429,8 +5273,10 @@ var WebReviewKitApp = class {
4429
5273
  normalizedPath: routeKey,
4430
5274
  scope: input.scope ?? getReviewViewportScope(viewport, this.options.viewports?.presets),
4431
5275
  kind: input.kind,
4432
- title: input.comment.split("\n")[0]?.slice(0, 80),
5276
+ title: title || void 0,
4433
5277
  comment: input.comment,
5278
+ assigneeId,
5279
+ assigneeName: input.assigneeName ?? assigneeOption?.label,
4434
5280
  createdBy: createdBy || void 0,
4435
5281
  status: "todo",
4436
5282
  viewport,
@@ -4445,13 +5291,23 @@ var WebReviewKitApp = class {
4445
5291
  createdAt: now,
4446
5292
  updatedAt: now
4447
5293
  };
4448
- const createdItem = await this.adapter.create(item);
4449
- this.setModeState("idle");
4450
- this.noteDraft = void 0;
4451
- this.areaDraft = void 0;
4452
- this.highlightItem(createdItem.id);
4453
- await this.reload();
4454
- await this.options.onCreateItem?.(createdItem);
5294
+ this.draftError = "";
5295
+ this.isCreatingItem = true;
5296
+ this.render();
5297
+ try {
5298
+ const createdItem = await this.adapter.create(item);
5299
+ this.setModeState("idle");
5300
+ this.noteDraft = void 0;
5301
+ this.areaDraft = void 0;
5302
+ this.highlightItem(createdItem.id);
5303
+ await this.reload();
5304
+ await this.options.onCreateItem?.(createdItem);
5305
+ } catch (error) {
5306
+ this.draftError = error instanceof Error ? error.message : "Failed to save QA.";
5307
+ } finally {
5308
+ this.isCreatingItem = false;
5309
+ this.render();
5310
+ }
4455
5311
  }
4456
5312
  async restoreItem(item) {
4457
5313
  this.setModeState("idle");
@@ -4505,16 +5361,35 @@ function createNoopController() {
4505
5361
  }
4506
5362
  // Annotate the CommonJS export names for ESM import in node:
4507
5363
  0 && (module.exports = {
5364
+ DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
5365
+ DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
5366
+ DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
4508
5367
  DEFAULT_REVIEW_VIEWPORTS,
5368
+ FIGMA_NODE_REF_SEPARATOR,
5369
+ REVIEW_FIGMA_TOKEN_MISSING_CODE,
4509
5370
  REVIEW_WORKFLOW_STATUS_OPTIONS,
5371
+ ReviewFigmaTokenError,
5372
+ collectReviewFigmaReleaseSnapshot,
5373
+ createReviewFigmaFrameUrl,
5374
+ createReviewFigmaImageStoreClient,
5375
+ createReviewFigmaImagesSnapshot,
5376
+ createReviewFigmaNodeValue,
5377
+ createReviewFigmaReleaseSnapshot,
4510
5378
  createWebReviewKit,
4511
5379
  findReviewViewportPreset,
4512
5380
  getNumberedReviewItems,
5381
+ getReviewFigmaImageMimeType,
5382
+ getReviewFigmaImageTargetKey,
4513
5383
  getReviewItemScope,
4514
5384
  getReviewItemScopeLabel,
4515
5385
  getReviewViewportScope,
5386
+ isReviewFigmaTokenError,
4516
5387
  localAdapter,
4517
5388
  normalizeReviewItemStatus,
5389
+ parseReviewFigmaNodeRef,
5390
+ readReviewFigmaToken,
5391
+ requireReviewFigmaNodeRef,
5392
+ requireReviewFigmaToken,
4518
5393
  supabaseAdapter
4519
5394
  });
4520
5395
  //# sourceMappingURL=index.cjs.map