@designfever/web-review-kit 0.6.0 → 0.7.1

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 (43) hide show
  1. package/.env.sample +32 -0
  2. package/README.md +35 -10
  3. package/dist/chunk-2ZLU5FTD.js +602 -0
  4. package/dist/chunk-2ZLU5FTD.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 +1037 -144
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +29 -3
  12. package/dist/index.d.ts +29 -3
  13. package/dist/index.js +50 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/react-shell.cjs +10339 -5168
  16. package/dist/react-shell.cjs.map +1 -1
  17. package/dist/react-shell.d.cts +39 -4
  18. package/dist/react-shell.d.ts +39 -4
  19. package/dist/react-shell.js +9687 -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 +1144 -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 +829 -5
  30. package/dist/vite.js.map +1 -1
  31. package/docs/README.md +13 -7
  32. package/docs/adapters.md +128 -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 +330 -0
  38. package/docs/figma-overlay.md +11 -4
  39. package/docs/installation.md +42 -7
  40. package/docs/release-notes-0.7.0.md +132 -0
  41. package/docs/release-notes-0.7.1.md +34 -0
  42. package/package.json +6 -2
  43. 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,498 @@ 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
+ figmaToken: readReviewFigmaImageToken(
554
+ options.token ?? getStoredReviewFigmaImageToken
555
+ )
556
+ });
557
+ },
558
+ updateImage(id, patch) {
559
+ return request(`${endpoint}/${encodeURIComponent(id)}`, {
560
+ method: "PATCH",
561
+ body: JSON.stringify(patch)
562
+ });
563
+ },
564
+ reorderImages(input) {
565
+ return request(`${endpoint}/reorder`, {
566
+ method: "PATCH",
567
+ body: JSON.stringify(input)
568
+ });
569
+ },
570
+ deleteImage(id) {
571
+ return request(`${endpoint}/${encodeURIComponent(id)}`, {
572
+ method: "DELETE"
573
+ });
574
+ }
575
+ };
576
+ }
577
+ async function createClientRenderedAddImageInput(input, clientRender, fetchOption) {
578
+ const options = normalizeClientRenderOptions(clientRender);
579
+ if (!options) return input;
580
+ const token = readClientRenderToken(options);
581
+ if (!token) return input;
582
+ try {
583
+ const asset = await withTimeout(
584
+ createClientRenderedFigmaAsset(input.figmaUrl, token, options, fetchOption),
585
+ options.timeoutMs ?? 1e4
586
+ );
587
+ return {
588
+ ...input,
589
+ imageFormat: asset.imageFormat,
590
+ asset
591
+ };
592
+ } catch {
593
+ return input;
594
+ }
595
+ }
596
+ function normalizeClientRenderOptions(clientRender) {
597
+ if (!clientRender) return null;
598
+ if (clientRender === true) return {};
599
+ return clientRender;
600
+ }
601
+ function readClientRenderToken(options) {
602
+ const token = typeof options.token === "function" ? options.token() : options.token;
603
+ return typeof token === "string" ? token.trim() : "";
604
+ }
605
+ async function createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOption) {
606
+ const ref = parseReviewFigmaNodeRef(figmaUrl);
607
+ if (!ref) throw new Error("A Figma node link is required.");
608
+ const requestFetch = fetchOption ?? globalThis.fetch;
609
+ if (!requestFetch) throw new Error("Figma client rendering requires fetch.");
610
+ const renderFormat = options.renderFormat ?? "png";
611
+ const response = await requestFetch(
612
+ createReviewFigmaImageApiUrl({
613
+ apiBaseUrl: options.apiBaseUrl,
614
+ fileKey: ref.fileKey,
615
+ nodeId: ref.nodeId,
616
+ format: renderFormat,
617
+ scale: options.renderScale,
618
+ useAbsoluteBounds: options.useAbsoluteBounds
619
+ }),
620
+ {
621
+ headers: {
622
+ "X-Figma-Token": token
623
+ }
624
+ }
625
+ );
626
+ const body = await response.json().catch(() => null);
627
+ if (!response.ok) {
628
+ throw new Error(body?.err || `Figma image render failed with ${response.status}`);
629
+ }
630
+ const imageUrl = body?.images?.[ref.nodeId];
631
+ if (!imageUrl) throw new Error(`Figma image render returned no URL for ${ref.nodeId}.`);
632
+ const imageResponse = await requestFetch(imageUrl);
633
+ if (!imageResponse.ok) {
634
+ throw new Error(`Figma image download failed with ${imageResponse.status}`);
635
+ }
636
+ const originalBlob = await imageResponse.blob();
637
+ const originalMimeType = normalizeClientImageMimeType(originalBlob.type) ?? getReviewFigmaImageMimeType(renderFormat === "jpg" ? "jpg" : "png");
638
+ const originalFormat = getReviewFigmaImageFormatFromMimeType(originalMimeType) ?? (renderFormat === "jpg" ? "jpg" : "png");
639
+ const dimensions = await readImageBlobDimensions(originalBlob);
640
+ const shouldConvertToWebp = options.convertToWebp ?? true;
641
+ const convertedBlob = shouldConvertToWebp ? await convertImageBlobToWebp(
642
+ originalBlob,
643
+ options.webpQuality ?? 0.9,
644
+ dimensions
645
+ ).catch(() => null) : null;
646
+ const finalBlob = convertedBlob?.type === "image/webp" ? convertedBlob : originalBlob;
647
+ const finalMimeType = normalizeClientImageMimeType(finalBlob.type) ?? originalMimeType;
648
+ const finalFormat = getReviewFigmaImageFormatFromMimeType(finalMimeType) ?? originalFormat;
649
+ return {
650
+ dataUrl: await blobToDataUrl(finalBlob),
651
+ imageFormat: finalFormat,
652
+ mimeType: finalMimeType,
653
+ byteSize: finalBlob.size,
654
+ width: dimensions.width,
655
+ height: dimensions.height
656
+ };
657
+ }
658
+ async function readImageBlobDimensions(blob) {
659
+ const image = await loadImageBlob(blob);
660
+ return {
661
+ width: image.naturalWidth || image.width,
662
+ height: image.naturalHeight || image.height
663
+ };
664
+ }
665
+ async function convertImageBlobToWebp(blob, quality, dimensions) {
666
+ if (typeof document === "undefined") return null;
667
+ if (!dimensions.width || !dimensions.height) return null;
668
+ const image = await loadImageBlob(blob);
669
+ const canvas = document.createElement("canvas");
670
+ canvas.width = dimensions.width;
671
+ canvas.height = dimensions.height;
672
+ const context = canvas.getContext("2d");
673
+ if (!context) return null;
674
+ context.drawImage(image, 0, 0);
675
+ return new Promise((resolve) => {
676
+ canvas.toBlob(resolve, "image/webp", quality);
677
+ });
678
+ }
679
+ function loadImageBlob(blob) {
680
+ return new Promise((resolve, reject) => {
681
+ if (typeof Image === "undefined" || typeof URL === "undefined") {
682
+ reject(new Error("Image decoding is unavailable."));
683
+ return;
684
+ }
685
+ const image = new Image();
686
+ const objectUrl = URL.createObjectURL(blob);
687
+ image.onload = () => {
688
+ URL.revokeObjectURL(objectUrl);
689
+ resolve(image);
690
+ };
691
+ image.onerror = () => {
692
+ URL.revokeObjectURL(objectUrl);
693
+ reject(new Error("Image decoding failed."));
694
+ };
695
+ image.src = objectUrl;
696
+ });
697
+ }
698
+ function blobToDataUrl(blob) {
699
+ return new Promise((resolve, reject) => {
700
+ const reader = new FileReader();
701
+ reader.onload = () => {
702
+ if (typeof reader.result === "string") {
703
+ resolve(reader.result);
704
+ return;
705
+ }
706
+ reject(new Error("Blob encoding failed."));
707
+ };
708
+ reader.onerror = () => reject(reader.error ?? new Error("Blob encoding failed."));
709
+ reader.readAsDataURL(blob);
710
+ });
711
+ }
712
+ function normalizeClientImageMimeType(value) {
713
+ const mimeType = value?.split(";")[0]?.trim().toLowerCase();
714
+ if (mimeType === "image/jpg") return "image/jpeg";
715
+ if (mimeType === "image/jpeg" || mimeType === "image/png" || mimeType === "image/webp") {
716
+ return mimeType;
717
+ }
718
+ return null;
719
+ }
720
+ async function withTimeout(promise, timeoutMs) {
721
+ let timeoutId;
722
+ try {
723
+ return await Promise.race([
724
+ promise,
725
+ new Promise((_, reject) => {
726
+ timeoutId = setTimeout(
727
+ () => reject(new Error("Figma client rendering timed out.")),
728
+ timeoutMs
729
+ );
730
+ })
731
+ ]);
732
+ } finally {
733
+ if (timeoutId) clearTimeout(timeoutId);
734
+ }
735
+ }
736
+ function getReviewFigmaImageTargetKey(target) {
737
+ return JSON.stringify(normalizeReviewFigmaImageTarget(target));
738
+ }
739
+ function getReviewFigmaImageMimeType(format) {
740
+ if (format === "jpg") return "image/jpeg";
741
+ if (format === "png") return "image/png";
742
+ return "image/webp";
743
+ }
744
+ function createReviewFigmaImageStoreRequest(endpoint, fetchOption) {
745
+ return async (input, init = {}) => {
746
+ const requestFetch = fetchOption ?? globalThis.fetch;
747
+ if (!requestFetch) throw new Error("Figma image store requires fetch.");
748
+ const figmaToken = init.figmaToken ?? "";
749
+ const { figmaToken: _figmaToken, ...requestInit } = init;
750
+ const response = await requestFetch(input, {
751
+ ...requestInit,
752
+ headers: {
753
+ "Content-Type": "application/json",
754
+ ...figmaToken ? { "X-Figma-Token": figmaToken } : {},
755
+ ...requestInit.headers ?? {}
756
+ }
757
+ });
758
+ const text = await response.text();
759
+ const body = text ? JSON.parse(text) : null;
760
+ if (!response.ok) {
761
+ const message = typeof body?.error === "string" ? body.error : `Figma image store request failed: ${response.status}`;
762
+ throw new Error(message);
763
+ }
764
+ return body;
765
+ };
766
+ }
767
+ function readReviewFigmaImageToken(provider) {
768
+ const token = typeof provider === "function" ? provider() : provider;
769
+ return typeof token === "string" ? token.trim() : "";
770
+ }
771
+ function getStoredReviewFigmaImageToken() {
772
+ if (typeof window === "undefined") return "";
773
+ try {
774
+ return window.localStorage.getItem("figma-token") ?? "";
775
+ } catch {
776
+ return "";
777
+ }
778
+ }
779
+ function normalizeReviewFigmaImageTarget(target) {
780
+ if (target.type === "figma-node") {
781
+ return {
782
+ type: target.type,
783
+ projectId: target.projectId,
784
+ fileKey: target.fileKey,
785
+ nodeId: target.nodeId
786
+ };
787
+ }
788
+ return {
789
+ type: target.type,
790
+ projectId: target.projectId,
791
+ pageUrl: target.pageUrl,
792
+ slot: target.slot ?? "",
793
+ viewport: target.viewport ? {
794
+ label: target.viewport.label ?? "",
795
+ width: target.viewport.width ?? null,
796
+ height: target.viewport.height ?? null,
797
+ scope: target.viewport.scope ?? ""
798
+ } : null
799
+ };
800
+ }
801
+
802
+ // src/figma/image.snapshot.ts
803
+ function createReviewFigmaImagesSnapshot(images, options = {}) {
804
+ const targetKeys = options.targets?.length ? new Set(options.targets.map(getReviewFigmaImageTargetKey)) : null;
805
+ return images.filter((image) => {
806
+ if (options.projectId && image.projectId !== options.projectId) {
807
+ return false;
808
+ }
809
+ if (targetKeys && !targetKeys.has(getReviewFigmaImageTargetKey(image.target))) {
810
+ return false;
811
+ }
812
+ return true;
813
+ }).map(cloneReviewFigmaImage).sort(compareReviewFigmaSnapshotImages);
814
+ }
815
+ function createReviewFigmaReleaseSnapshot({
816
+ images,
817
+ projectId,
818
+ releaseId,
819
+ label,
820
+ createdAt,
821
+ targets
822
+ }) {
823
+ return {
824
+ version: 1,
825
+ projectId,
826
+ ...releaseId ? { releaseId } : null,
827
+ ...label ? { label } : null,
828
+ createdAt: createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
829
+ figmaImagesSnapshot: createReviewFigmaImagesSnapshot(images, {
830
+ projectId,
831
+ targets
832
+ })
833
+ };
834
+ }
835
+ async function collectReviewFigmaReleaseSnapshot({
836
+ store,
837
+ targets,
838
+ ...snapshotOptions
839
+ }) {
840
+ const imagesByTarget = await Promise.all(
841
+ targets.map((target) => store.listImages(target))
842
+ );
843
+ return createReviewFigmaReleaseSnapshot({
844
+ ...snapshotOptions,
845
+ targets,
846
+ images: dedupeReviewFigmaImages(imagesByTarget.flat())
847
+ });
848
+ }
849
+ function dedupeReviewFigmaImages(images) {
850
+ return Array.from(new Map(images.map((image) => [image.id, image])).values());
851
+ }
852
+ function cloneReviewFigmaImage(image) {
853
+ return {
854
+ ...image,
855
+ target: cloneReviewFigmaImageTarget(image.target)
856
+ };
857
+ }
858
+ function cloneReviewFigmaImageTarget(target) {
859
+ if (target.type === "figma-node") {
860
+ return {
861
+ type: target.type,
862
+ projectId: target.projectId,
863
+ fileKey: target.fileKey,
864
+ nodeId: target.nodeId
865
+ };
866
+ }
867
+ return {
868
+ type: target.type,
869
+ projectId: target.projectId,
870
+ pageUrl: target.pageUrl,
871
+ slot: target.slot,
872
+ viewport: target.viewport ? {
873
+ label: target.viewport.label,
874
+ width: target.viewport.width,
875
+ height: target.viewport.height,
876
+ scope: target.viewport.scope
877
+ } : void 0
878
+ };
879
+ }
880
+ function compareReviewFigmaSnapshotImages(a, b) {
881
+ return a.projectId.localeCompare(b.projectId) || getReviewFigmaImageTargetKey(a.target).localeCompare(
882
+ getReviewFigmaImageTargetKey(b.target)
883
+ ) || a.order - b.order || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
884
+ }
885
+
368
886
  // src/core/geometry.ts
369
887
  function rectanglesIntersect(a, b) {
370
888
  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 +1065,13 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
547
1065
  const target = environment.document.elementFromPoint(point.x, point.y);
548
1066
  if (!target) return void 0;
549
1067
  const candidates = createAnchorCandidates(target, configuredAttribute);
550
- const primary = candidates[0];
551
- if (!primary) return void 0;
1068
+ const primaryCandidate = candidates[0];
1069
+ if (!primaryCandidate) return void 0;
552
1070
  return {
553
- ...primary,
1071
+ ...primaryCandidate,
554
1072
  candidates,
555
1073
  htmlSnippet: getElementHtmlSnippet(
556
- getAnchorSourceElement(target, primary, configuredAttribute) ?? target
1074
+ getAnchorSourceElement(target, primaryCandidate, configuredAttribute) ?? target
557
1075
  ),
558
1076
  source: getDomSourceHint(target)
559
1077
  };
@@ -561,13 +1079,13 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
561
1079
  function getDomAnchorFromElement(target, configuredAttribute = "data-qa-id", environment) {
562
1080
  if (target.ownerDocument !== environment.document) return void 0;
563
1081
  const candidates = createAnchorCandidates(target, configuredAttribute);
564
- const primary = candidates[0];
565
- if (!primary) return void 0;
1082
+ const primaryCandidate = candidates[0];
1083
+ if (!primaryCandidate) return void 0;
566
1084
  return {
567
- ...primary,
1085
+ ...primaryCandidate,
568
1086
  candidates,
569
1087
  htmlSnippet: getElementHtmlSnippet(
570
- getAnchorSourceElement(target, primary, configuredAttribute) ?? target
1088
+ getAnchorSourceElement(target, primaryCandidate, configuredAttribute) ?? target
571
1089
  ),
572
1090
  source: getDomSourceHint(target)
573
1091
  };
@@ -738,7 +1256,7 @@ function createAnchorCandidates(target, configuredAttribute) {
738
1256
  function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
739
1257
  for (const attributeName of attributeNames) {
740
1258
  const selector = `[${attributeName}]`;
741
- const element = safeClosest(target, selector);
1259
+ const element = tryClosest(target, selector);
742
1260
  if (!element) continue;
743
1261
  const value = getStableAttributeValue(element, attributeName);
744
1262
  if (!value) continue;
@@ -844,7 +1362,7 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
844
1362
  return target;
845
1363
  }
846
1364
  }
847
- function safeClosest(element, selector) {
1365
+ function tryClosest(element, selector) {
848
1366
  try {
849
1367
  return element.closest(selector);
850
1368
  } catch {
@@ -1444,18 +1962,90 @@ function setDocumentScrollInstantly(environment, position) {
1444
1962
  );
1445
1963
  }
1446
1964
 
1965
+ // src/core/draft.metrics.ts
1966
+ function getDraftViewportScale(viewport, presets) {
1967
+ const preset = findReviewViewportPreset(viewport, presets);
1968
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
1969
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
1970
+ return { scale, designWidth, presetLabel: preset.label };
1971
+ }
1972
+ function getDraftAdjustmentMetrics(draft, presets) {
1973
+ const adjustment = draft.adjustment;
1974
+ const x = adjustment?.x ?? 0;
1975
+ const y = adjustment?.y ?? 0;
1976
+ const scale = adjustment?.scale ?? 0;
1977
+ const {
1978
+ scale: viewportScale,
1979
+ designWidth,
1980
+ presetLabel
1981
+ } = getDraftViewportScale(draft.viewport, presets);
1982
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
1983
+ const scaleCssDelta = scale * viewportScale;
1984
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
1985
+ 1 / selection.width,
1986
+ (selection.width + scaleCssDelta) / selection.width
1987
+ ) : 1;
1988
+ return {
1989
+ x,
1990
+ y,
1991
+ scale,
1992
+ cssX: x * viewportScale,
1993
+ cssY: y * viewportScale,
1994
+ scaleFactor,
1995
+ viewportScale,
1996
+ designWidth,
1997
+ presetLabel,
1998
+ viewportWidth: draft.viewport.width
1999
+ };
2000
+ }
2001
+ function hasDraftAdjustment(draft, presets) {
2002
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
2003
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
2004
+ }
2005
+ function getAdjustedDraftPoint(point, draft, presets) {
2006
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
2007
+ return {
2008
+ x: point.x + metrics.cssX,
2009
+ y: point.y + metrics.cssY
2010
+ };
2011
+ }
2012
+ function getAdjustedDraftSelection(selection, draft, presets) {
2013
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
2014
+ return {
2015
+ ...selection,
2016
+ left: selection.left + metrics.cssX,
2017
+ top: selection.top + metrics.cssY,
2018
+ width: selection.width * metrics.scaleFactor,
2019
+ height: selection.height * metrics.scaleFactor
2020
+ };
2021
+ }
2022
+
2023
+ // src/core/typography.tokens.ts
2024
+ var reviewTypographyTokens = `
2025
+ --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2026
+ --df-review-font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
2027
+ --df-review-font-size-3xs: 9px;
2028
+ --df-review-font-size-2xs: 10px;
2029
+ --df-review-font-size-xs: 11px;
2030
+ --df-review-font-size-sm: 12px;
2031
+ --df-review-font-size-md: 13px;
2032
+ --df-review-font-size-lg: 14px;
2033
+ --df-review-font-size-xl: 15px;
2034
+ --df-review-font-size-2xl: 18px;
2035
+ --df-review-font-weight-normal: 400;
2036
+ --df-review-font-weight-emphasis: 500;
2037
+ --df-review-line-height-tight: 1.25;
2038
+ --df-review-line-height-base: 1.42;
2039
+ --df-review-line-height-relaxed: 1.55;
2040
+ `;
2041
+
1447
2042
  // src/core/overlay.style.ts
1448
2043
  function createStyleElement() {
1449
2044
  const style = document.createElement("style");
1450
2045
  style.textContent = `
1451
2046
  :host {
1452
2047
  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;
2048
+ ${reviewTypographyTokens}
1459
2049
  --df-review-space-1: 4px;
1460
2050
  --df-review-space-1-5: 6px;
1461
2051
  --df-review-space-2: 8px;
@@ -1554,8 +2144,8 @@ function createStyleElement() {
1554
2144
  }
1555
2145
 
1556
2146
  .dfwr-title {
1557
- font-size: 15px;
1558
- font-weight: 700;
2147
+ font-size: var(--df-review-font-size-xl);
2148
+ font-weight: var(--df-review-font-weight-emphasis);
1559
2149
  line-height: 1.25;
1560
2150
  }
1561
2151
 
@@ -1599,11 +2189,15 @@ function createStyleElement() {
1599
2189
  }
1600
2190
 
1601
2191
  .dfwr-button {
2192
+ display: inline-flex;
2193
+ align-items: center;
2194
+ justify-content: center;
2195
+ gap: 6px;
1602
2196
  min-height: var(--df-review-control-height-md);
1603
2197
  padding: 0 12px;
1604
2198
  border-radius: var(--df-review-radius-sm);
1605
2199
  font-size: var(--df-review-font-size-sm);
1606
- font-weight: 650;
2200
+ font-weight: var(--df-review-font-weight-emphasis);
1607
2201
  }
1608
2202
 
1609
2203
  .dfwr-button:hover,
@@ -1619,6 +2213,21 @@ function createStyleElement() {
1619
2213
  color: var(--df-review-color-accent-contrast);
1620
2214
  }
1621
2215
 
2216
+ .dfwr-button:disabled {
2217
+ cursor: default;
2218
+ opacity: 0.62;
2219
+ }
2220
+
2221
+ .dfwr-spinner {
2222
+ display: inline-block;
2223
+ width: 12px;
2224
+ height: 12px;
2225
+ border: 2px solid currentColor;
2226
+ border-right-color: transparent;
2227
+ border-radius: 999px;
2228
+ animation: dfwr-spin 720ms linear infinite;
2229
+ }
2230
+
1622
2231
  .dfwr-icon-button {
1623
2232
  display: inline-flex;
1624
2233
  align-items: center;
@@ -1628,7 +2237,7 @@ function createStyleElement() {
1628
2237
  padding: 0 8px;
1629
2238
  border-radius: var(--df-review-radius-sm);
1630
2239
  font-size: var(--df-review-font-size-xs);
1631
- font-weight: 700;
2240
+ font-weight: var(--df-review-font-weight-emphasis);
1632
2241
  line-height: 1;
1633
2242
  text-transform: uppercase;
1634
2243
  }
@@ -1783,7 +2392,7 @@ function createStyleElement() {
1783
2392
  0 8px 18px rgba(0, 0, 0, 0.28);
1784
2393
  color: #111820;
1785
2394
  font-size: var(--df-review-font-size-2xs);
1786
- font-weight: 900;
2395
+ font-weight: var(--df-review-font-weight-emphasis);
1787
2396
  line-height: 1;
1788
2397
  pointer-events: none;
1789
2398
  }
@@ -1809,7 +2418,7 @@ function createStyleElement() {
1809
2418
  box-shadow: 0 0 0 4px rgba(var(--dfwr-scope-rgb), 0.18);
1810
2419
  color: var(--dfwr-scope);
1811
2420
  font-size: var(--df-review-font-size-2xs);
1812
- font-weight: 800;
2421
+ font-weight: var(--df-review-font-weight-emphasis);
1813
2422
  }
1814
2423
 
1815
2424
  .dfwr-bound-marker.is-highlighted {
@@ -2151,10 +2760,18 @@ function createStyleElement() {
2151
2760
  gap: 10px;
2152
2761
  }
2153
2762
 
2763
+ .dfwr-form-error {
2764
+ margin: 0;
2765
+ color: #ff8f61;
2766
+ font-size: var(--df-review-font-size-sm);
2767
+ line-height: 1.4;
2768
+ overflow-wrap: anywhere;
2769
+ }
2770
+
2771
+ .dfwr-input,
2772
+ .dfwr-select,
2154
2773
  .dfwr-textarea {
2155
2774
  width: 100%;
2156
- min-height: 92px;
2157
- resize: vertical;
2158
2775
  border: 1px solid rgba(255, 255, 255, 0.16);
2159
2776
  border-radius: var(--df-review-radius-sm);
2160
2777
  padding: 10px;
@@ -2165,14 +2782,39 @@ function createStyleElement() {
2165
2782
  line-height: 1.45;
2166
2783
  }
2167
2784
 
2785
+ .dfwr-input,
2786
+ .dfwr-select {
2787
+ min-height: 38px;
2788
+ }
2789
+
2790
+ .dfwr-select {
2791
+ appearance: none;
2792
+ cursor: pointer;
2793
+ }
2794
+
2795
+ .dfwr-textarea {
2796
+ min-height: 92px;
2797
+ resize: vertical;
2798
+ }
2799
+
2800
+ .dfwr-input:focus,
2801
+ .dfwr-select:focus,
2168
2802
  .dfwr-textarea:focus {
2169
2803
  outline: 2px solid var(--df-review-color-accent-ring);
2170
2804
  outline-offset: 1px;
2171
2805
  }
2172
2806
 
2173
2807
  @media (hover: none) and (pointer: coarse) {
2808
+ .dfwr-input,
2809
+ .dfwr-select,
2174
2810
  .dfwr-textarea {
2175
- font-size: 16px;
2811
+ font-size: var(--df-review-font-size-xl);
2812
+ }
2813
+ }
2814
+
2815
+ @keyframes dfwr-spin {
2816
+ to {
2817
+ transform: rotate(360deg);
2176
2818
  }
2177
2819
  }
2178
2820
 
@@ -2230,8 +2872,8 @@ function createStyleElement() {
2230
2872
  color: var(--df-review-color-text);
2231
2873
  cursor: pointer;
2232
2874
  font: inherit;
2233
- font-size: 14px;
2234
- font-weight: 800;
2875
+ font-size: var(--df-review-font-size-lg);
2876
+ font-weight: var(--df-review-font-weight-emphasis);
2235
2877
  line-height: 1;
2236
2878
  }
2237
2879
 
@@ -2284,7 +2926,7 @@ function createStyleElement() {
2284
2926
  margin-bottom: 10px;
2285
2927
  color: rgba(247, 247, 242, 0.74);
2286
2928
  font-size: var(--df-review-font-size-sm);
2287
- font-weight: 700;
2929
+ font-weight: var(--df-review-font-weight-emphasis);
2288
2930
  }
2289
2931
 
2290
2932
  .dfwr-item {
@@ -2306,6 +2948,8 @@ function createStyleElement() {
2306
2948
  }
2307
2949
 
2308
2950
  .dfwr-item-body {
2951
+ display: grid;
2952
+ gap: 4px;
2309
2953
  min-width: 0;
2310
2954
  flex: 1;
2311
2955
  }
@@ -2325,7 +2969,7 @@ function createStyleElement() {
2325
2969
  border-radius: var(--df-review-radius-pill);
2326
2970
  padding: 0 7px;
2327
2971
  font-size: var(--df-review-font-size-2xs);
2328
- font-weight: 800;
2972
+ font-weight: var(--df-review-font-weight-emphasis);
2329
2973
  line-height: 1;
2330
2974
  letter-spacing: 0;
2331
2975
  text-transform: uppercase;
@@ -2343,13 +2987,29 @@ function createStyleElement() {
2343
2987
  color: rgba(247, 247, 242, 0.64);
2344
2988
  }
2345
2989
 
2990
+ .dfwr-item-title {
2991
+ margin: 4px 0 0;
2992
+ color: var(--df-review-color-text);
2993
+ font-size: var(--df-review-font-size-md);
2994
+ font-weight: var(--df-review-font-weight-normal);
2995
+ line-height: 1.35;
2996
+ overflow-wrap: anywhere;
2997
+ }
2998
+
2346
2999
  .dfwr-item-comment {
3000
+ margin: 0;
3001
+ color: var(--df-review-color-text-muted);
3002
+ font-size: var(--df-review-font-size-sm);
3003
+ line-height: 1.45;
3004
+ overflow-wrap: anywhere;
3005
+ white-space: pre-wrap;
3006
+ }
3007
+
3008
+ .dfwr-item-comment.is-primary {
2347
3009
  margin: 4px 0;
2348
3010
  color: var(--df-review-color-text);
2349
3011
  font-size: var(--df-review-font-size-md);
2350
3012
  line-height: 1.42;
2351
- overflow-wrap: anywhere;
2352
- white-space: pre-wrap;
2353
3013
  }
2354
3014
 
2355
3015
  .dfwr-item-date {
@@ -2593,7 +3253,7 @@ var WebReviewKitView = class {
2593
3253
  );
2594
3254
  }
2595
3255
  }
2596
- if (state.isOpen && state.mode === "area" && !state.areaDraft) {
3256
+ if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
2597
3257
  shell.append(this.createAreaLayer());
2598
3258
  }
2599
3259
  if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
@@ -2667,64 +3327,29 @@ var WebReviewKitView = class {
2667
3327
  this.config.actions.setSelectingArea(false);
2668
3328
  this.config.actions.render();
2669
3329
  }
3330
+ // Draft adjustment geometry lives in draft.metrics.ts; these thin wrappers
3331
+ // supply the configured viewport presets so call sites stay unchanged.
3332
+ get viewportPresets() {
3333
+ return this.config.options.viewports?.presets;
3334
+ }
2670
3335
  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
- };
3336
+ return getDraftAdjustmentMetrics(draft, this.viewportPresets);
2698
3337
  }
2699
3338
  hasDraftAdjustment(draft) {
2700
- const metrics = this.getDraftAdjustmentMetrics(draft);
2701
- return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
3339
+ return hasDraftAdjustment(draft, this.viewportPresets);
2702
3340
  }
2703
3341
  getAdjustedDraftPoint(point, draft) {
2704
- const metrics = this.getDraftAdjustmentMetrics(draft);
2705
- return {
2706
- x: point.x + metrics.cssX,
2707
- y: point.y + metrics.cssY
2708
- };
3342
+ return getAdjustedDraftPoint(point, draft, this.viewportPresets);
2709
3343
  }
2710
3344
  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
- };
3345
+ return getAdjustedDraftSelection(
3346
+ selection,
3347
+ draft,
3348
+ this.viewportPresets
3349
+ );
2719
3350
  }
2720
3351
  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 };
3352
+ return getDraftViewportScale(viewport, this.viewportPresets);
2728
3353
  }
2729
3354
  getDraftComposerWidth(environment) {
2730
3355
  const bounds = environment.overlayRect;
@@ -2870,6 +3495,67 @@ var WebReviewKitView = class {
2870
3495
  return trimmedComment ? `${trimmedComment}
2871
3496
  ${adjustment}` : adjustment;
2872
3497
  }
3498
+ getAssigneeOption(assigneeId) {
3499
+ if (!assigneeId) return void 0;
3500
+ return this.config.options.assigneeOptions?.find(
3501
+ (option) => option.value === assigneeId
3502
+ );
3503
+ }
3504
+ getAssigneeName(assigneeId) {
3505
+ return this.getAssigneeOption(assigneeId)?.label;
3506
+ }
3507
+ createDraftTitleInput(value, onInput) {
3508
+ const input = document.createElement("input");
3509
+ input.className = "dfwr-input";
3510
+ input.placeholder = "Title";
3511
+ input.type = "text";
3512
+ input.value = value ?? "";
3513
+ input.addEventListener("input", () => onInput(input.value));
3514
+ return input;
3515
+ }
3516
+ isTitleFieldEnabled() {
3517
+ return this.config.options.fields?.title === true;
3518
+ }
3519
+ createDraftAssigneeSelect(value, fallbackLabel, onChange) {
3520
+ const assigneeOptions = this.config.options.assigneeOptions ?? [];
3521
+ if (assigneeOptions.length === 0) return void 0;
3522
+ const assigneeTitle = this.config.options.assigneeTitle?.trim() || "Assignee";
3523
+ const select = document.createElement("select");
3524
+ select.className = "dfwr-select";
3525
+ const emptyOption = document.createElement("option");
3526
+ emptyOption.value = "";
3527
+ emptyOption.textContent = assigneeTitle;
3528
+ select.append(emptyOption);
3529
+ const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
3530
+ if (hasUnknownAssignee && value) {
3531
+ const option = document.createElement("option");
3532
+ option.value = value;
3533
+ option.textContent = fallbackLabel ?? value;
3534
+ select.append(option);
3535
+ }
3536
+ assigneeOptions.forEach((assigneeOption) => {
3537
+ const option = document.createElement("option");
3538
+ option.value = assigneeOption.value;
3539
+ option.textContent = assigneeOption.label;
3540
+ select.append(option);
3541
+ });
3542
+ select.value = value ?? "";
3543
+ select.addEventListener("change", () => {
3544
+ onChange(select.value || null, this.getAssigneeName(select.value));
3545
+ });
3546
+ return select;
3547
+ }
3548
+ getDraftFields(titleInput, textarea, assigneeSelect) {
3549
+ const title = titleInput?.value.trim();
3550
+ const comment = textarea.value.trim();
3551
+ const assigneeId = assigneeSelect?.value.trim() || void 0;
3552
+ return {
3553
+ title: title || void 0,
3554
+ comment,
3555
+ assigneeId,
3556
+ assigneeName: this.getAssigneeName(assigneeId)
3557
+ };
3558
+ }
2873
3559
  getStyleableDraftElement(draft, environment) {
2874
3560
  if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2875
3561
  return draft.previewElement;
@@ -3060,6 +3746,9 @@ ${adjustment}` : adjustment;
3060
3746
  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
3747
  return empty;
3062
3748
  }
3749
+ // Builds the note draft layer: the on-page marker/highlight plus its composer
3750
+ // popover. When dockComposer is set the composer renders into the side panel
3751
+ // instead of floating next to the marker (used for the docked review mode).
3063
3752
  createNotePopover(draft, options = {}) {
3064
3753
  const environment = this.config.getEnvironment();
3065
3754
  const group = document.createElement("div");
@@ -3123,6 +3812,14 @@ ${adjustment}` : adjustment;
3123
3812
  meta.className = "dfwr-item-date";
3124
3813
  meta.textContent = formatNoteDraftMeta(draft);
3125
3814
  }
3815
+ const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(draft.title, (title) => {
3816
+ const noteDraft = this.state.noteDraft;
3817
+ if (!noteDraft) return;
3818
+ this.config.actions.setNoteDraft({
3819
+ ...noteDraft,
3820
+ title
3821
+ });
3822
+ }) : void 0;
3126
3823
  const textarea = document.createElement("textarea");
3127
3824
  textarea.className = "dfwr-textarea";
3128
3825
  textarea.placeholder = "Review comment";
@@ -3136,13 +3833,30 @@ ${adjustment}` : adjustment;
3136
3833
  comment: textarea.value
3137
3834
  });
3138
3835
  });
3836
+ const assigneeSelect = this.createDraftAssigneeSelect(
3837
+ draft.assigneeId,
3838
+ draft.assigneeName,
3839
+ (assigneeId, assigneeName) => {
3840
+ const noteDraft = this.state.noteDraft;
3841
+ if (!noteDraft) return;
3842
+ this.config.actions.setNoteDraft({
3843
+ ...noteDraft,
3844
+ assigneeId,
3845
+ assigneeName
3846
+ });
3847
+ }
3848
+ );
3139
3849
  const saveDraft = () => {
3140
- const comment = textarea.value.trim();
3141
3850
  const currentDraft = this.state.noteDraft ?? draft;
3851
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3852
+ const comment = fields.comment;
3142
3853
  if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
3143
3854
  void this.config.actions.createItem({
3144
3855
  kind: "note",
3856
+ title: fields.title,
3145
3857
  comment: this.withDraftAdjustmentComment(comment, currentDraft),
3858
+ assigneeId: fields.assigneeId,
3859
+ assigneeName: fields.assigneeName,
3146
3860
  viewport: currentDraft.viewport,
3147
3861
  anchor: currentDraft.anchor,
3148
3862
  marker: currentDraft.marker,
@@ -3160,10 +3874,14 @@ ${adjustment}` : adjustment;
3160
3874
  const actions = this.createFormActions("Save note", saveDraft, {
3161
3875
  leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
3162
3876
  });
3877
+ const error = this.createDraftError();
3163
3878
  form.append(
3164
3879
  ...meta ? [meta] : [],
3165
3880
  ...adjustmentControls ? [adjustmentControls.panel] : [],
3881
+ ...titleInput ? [titleInput] : [],
3166
3882
  textarea,
3883
+ ...assigneeSelect ? [assigneeSelect] : [],
3884
+ ...error ? [error] : [],
3167
3885
  actions
3168
3886
  );
3169
3887
  const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
@@ -3290,6 +4008,9 @@ ${adjustment}` : adjustment;
3290
4008
  handle.addEventListener("pointerup", stopDrag);
3291
4009
  handle.addEventListener("pointercancel", stopDrag);
3292
4010
  }
4011
+ // Builds the element-adjustment controls (nudge the previewed element via
4012
+ // arrow keys / buttons). Wires keyboard deltas to the draft transform and
4013
+ // keeps the pin, popover, highlight and textarea in sync as the value changes.
3293
4014
  createAdjustmentControls({
3294
4015
  draft,
3295
4016
  pin,
@@ -3434,6 +4155,14 @@ ${adjustment}` : adjustment;
3434
4155
  return form;
3435
4156
  }
3436
4157
  form.append(this.createAreaMetricsPanel(areaDraft));
4158
+ const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(areaDraft.title, (title) => {
4159
+ const draft = this.state.areaDraft;
4160
+ if (!draft) return;
4161
+ this.config.actions.setAreaDraft({
4162
+ ...draft,
4163
+ title
4164
+ });
4165
+ }) : void 0;
3437
4166
  const textarea = document.createElement("textarea");
3438
4167
  textarea.className = "dfwr-textarea";
3439
4168
  textarea.placeholder = "Area comment";
@@ -3447,20 +4176,44 @@ ${adjustment}` : adjustment;
3447
4176
  comment: textarea.value
3448
4177
  });
3449
4178
  });
4179
+ const assigneeSelect = this.createDraftAssigneeSelect(
4180
+ areaDraft.assigneeId,
4181
+ areaDraft.assigneeName,
4182
+ (assigneeId, assigneeName) => {
4183
+ const draft = this.state.areaDraft;
4184
+ if (!draft) return;
4185
+ this.config.actions.setAreaDraft({
4186
+ ...draft,
4187
+ assigneeId,
4188
+ assigneeName
4189
+ });
4190
+ }
4191
+ );
3450
4192
  const actions = this.createFormActions("Save area", () => {
3451
- const comment = textarea.value.trim();
3452
4193
  const draft = this.state.areaDraft;
4194
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
4195
+ const comment = fields.comment;
3453
4196
  if (!comment || !draft) return;
3454
4197
  void this.config.actions.createItem({
3455
4198
  kind: "area",
4199
+ title: fields.title,
3456
4200
  comment,
4201
+ assigneeId: fields.assigneeId,
4202
+ assigneeName: fields.assigneeName,
3457
4203
  viewport: draft.viewport,
3458
4204
  anchor: draft.anchor,
3459
4205
  marker: draft.marker,
3460
4206
  selection: draft.selection
3461
4207
  });
3462
4208
  });
3463
- form.append(textarea, actions);
4209
+ const error = this.createDraftError();
4210
+ form.append(
4211
+ ...titleInput ? [titleInput] : [],
4212
+ textarea,
4213
+ ...assigneeSelect ? [assigneeSelect] : [],
4214
+ ...error ? [error] : [],
4215
+ actions
4216
+ );
3464
4217
  return form;
3465
4218
  }
3466
4219
  createAreaMetricsPanel(draft) {
@@ -3552,18 +4305,27 @@ ${adjustment}` : adjustment;
3552
4305
  createFormActions(saveLabel, onSave, options) {
3553
4306
  const actions = document.createElement("div");
3554
4307
  actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
4308
+ const isSaving = this.state.isCreatingItem;
3555
4309
  const save = document.createElement("button");
3556
4310
  save.className = "dfwr-button is-primary";
3557
4311
  save.type = "button";
3558
- save.textContent = saveLabel;
4312
+ save.disabled = isSaving;
4313
+ save.setAttribute("aria-busy", isSaving ? "true" : "false");
4314
+ if (isSaving) {
4315
+ save.append(this.createSpinner("dfwr-spinner"), "Saving...");
4316
+ } else {
4317
+ save.textContent = saveLabel;
4318
+ }
3559
4319
  save.addEventListener("click", (event) => {
3560
4320
  event.preventDefault();
3561
4321
  event.stopPropagation();
4322
+ if (this.state.isCreatingItem) return;
3562
4323
  onSave();
3563
4324
  });
3564
4325
  const cancel = document.createElement("button");
3565
4326
  cancel.className = "dfwr-button";
3566
4327
  cancel.type = "button";
4328
+ cancel.disabled = isSaving;
3567
4329
  cancel.textContent = "Cancel";
3568
4330
  cancel.addEventListener("click", (event) => {
3569
4331
  this.cancelDraft(event);
@@ -3586,6 +4348,20 @@ ${adjustment}` : adjustment;
3586
4348
  actions.append(save, cancel);
3587
4349
  return actions;
3588
4350
  }
4351
+ createSpinner(className) {
4352
+ const spinner = document.createElement("span");
4353
+ spinner.className = className;
4354
+ spinner.setAttribute("aria-hidden", "true");
4355
+ return spinner;
4356
+ }
4357
+ createDraftError() {
4358
+ if (!this.state.draftError) return void 0;
4359
+ const error = document.createElement("p");
4360
+ error.className = "dfwr-form-error";
4361
+ error.setAttribute("role", "alert");
4362
+ error.textContent = this.state.draftError;
4363
+ return error;
4364
+ }
3589
4365
  createList() {
3590
4366
  const section = document.createElement("div");
3591
4367
  section.className = "dfwr-list";
@@ -3638,14 +4414,20 @@ ${adjustment}` : adjustment;
3638
4414
  kind.className = "dfwr-item-kind";
3639
4415
  kind.textContent = item.kind;
3640
4416
  badges.append(scope, kind);
4417
+ const title = this.isTitleFieldEnabled() ? item.title?.trim() : "";
4418
+ const titleElement = title ? document.createElement("strong") : void 0;
4419
+ if (title && titleElement) {
4420
+ titleElement.className = "dfwr-item-title";
4421
+ titleElement.textContent = title;
4422
+ }
3641
4423
  const comment = document.createElement("p");
3642
- comment.className = "dfwr-item-comment";
4424
+ comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
3643
4425
  comment.textContent = item.comment;
3644
4426
  const date = document.createElement("time");
3645
4427
  date.className = "dfwr-item-date";
3646
4428
  date.dateTime = item.createdAt;
3647
4429
  date.textContent = formatItemMeta(item);
3648
- body.append(badges, comment, date);
4430
+ body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
3649
4431
  const actions = document.createElement("div");
3650
4432
  actions.className = "dfwr-item-actions";
3651
4433
  actions.addEventListener("click", (event) => event.stopPropagation());
@@ -3834,7 +4616,14 @@ ${formatItemMeta(item)}`;
3834
4616
  event,
3835
4617
  this.config.getEnvironment()
3836
4618
  );
3837
- void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, textarea.value) : this.config.actions.bindNoteDraftToPoint(nextPoint, textarea.value));
4619
+ const currentDraft = this.state.noteDraft;
4620
+ const fields = {
4621
+ title: currentDraft?.title,
4622
+ comment: textarea.value,
4623
+ assigneeId: currentDraft?.assigneeId,
4624
+ assigneeName: currentDraft?.assigneeName
4625
+ };
4626
+ void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, fields) : this.config.actions.bindNoteDraftToPoint(nextPoint, fields));
3838
4627
  });
3839
4628
  }
3840
4629
  createNoteLayer() {
@@ -3912,10 +4701,14 @@ ${formatItemMeta(item)}`;
3912
4701
  let startX = 0;
3913
4702
  let startY = 0;
3914
4703
  let selection;
4704
+ let activePointerId;
4705
+ let isDragging = false;
4706
+ const ownerWindow = layer.ownerDocument.defaultView ?? window;
3915
4707
  const updateBox = (event) => {
4708
+ const nextEnvironment = this.config.getEnvironment();
3916
4709
  const nextPoint = toTargetPointFromHostEvent(
3917
4710
  event,
3918
- this.config.getEnvironment()
4711
+ nextEnvironment
3919
4712
  );
3920
4713
  const left = Math.min(startX, nextPoint.x);
3921
4714
  const top = Math.min(startY, nextPoint.y);
@@ -3923,7 +4716,7 @@ ${formatItemMeta(item)}`;
3923
4716
  const height = Math.abs(nextPoint.y - startY);
3924
4717
  const hostPoint = toHostPoint(
3925
4718
  { x: left, y: top },
3926
- this.config.getEnvironment()
4719
+ nextEnvironment
3927
4720
  );
3928
4721
  selection = { left, top, width, height };
3929
4722
  box.style.left = `${hostPoint.x}px`;
@@ -3931,9 +4724,68 @@ ${formatItemMeta(item)}`;
3931
4724
  box.style.width = `${width}px`;
3932
4725
  box.style.height = `${height}px`;
3933
4726
  };
4727
+ const addDragListeners = () => {
4728
+ ownerWindow.addEventListener("pointermove", handlePointerMove, true);
4729
+ ownerWindow.addEventListener("pointerup", handlePointerUp, true);
4730
+ ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
4731
+ };
4732
+ const removeDragListeners = () => {
4733
+ ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
4734
+ ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
4735
+ ownerWindow.removeEventListener(
4736
+ "pointercancel",
4737
+ handlePointerCancel,
4738
+ true
4739
+ );
4740
+ };
4741
+ const releasePointerCapture = (event) => {
4742
+ try {
4743
+ if (layer.hasPointerCapture(event.pointerId)) {
4744
+ layer.releasePointerCapture(event.pointerId);
4745
+ }
4746
+ } catch {
4747
+ }
4748
+ };
4749
+ function isActivePointer(event) {
4750
+ return isDragging && event.pointerId === activePointerId;
4751
+ }
4752
+ const finishAreaSelection = (event) => {
4753
+ if (!isActivePointer(event)) return;
4754
+ event.preventDefault();
4755
+ updateBox(event);
4756
+ releasePointerCapture(event);
4757
+ removeDragListeners();
4758
+ isDragging = false;
4759
+ activePointerId = void 0;
4760
+ if (!selection || selection.width < 8 || selection.height < 8) return;
4761
+ this.config.actions.setSelectingArea(true);
4762
+ this.config.actions.render();
4763
+ void this.config.actions.createAreaDraft(selection);
4764
+ };
4765
+ function handlePointerMove(event) {
4766
+ if (!isActivePointer(event)) return;
4767
+ event.preventDefault();
4768
+ updateBox(event);
4769
+ }
4770
+ const handlePointerUp = (event) => {
4771
+ finishAreaSelection(event);
4772
+ };
4773
+ const handlePointerCancel = (event) => {
4774
+ if (!isActivePointer(event)) return;
4775
+ releasePointerCapture(event);
4776
+ removeDragListeners();
4777
+ isDragging = false;
4778
+ activePointerId = void 0;
4779
+ };
3934
4780
  layer.addEventListener("pointerdown", (event) => {
4781
+ if (event.button !== 0) return;
3935
4782
  event.preventDefault();
3936
- layer.setPointerCapture(event.pointerId);
4783
+ activePointerId = event.pointerId;
4784
+ isDragging = true;
4785
+ try {
4786
+ layer.setPointerCapture(event.pointerId);
4787
+ } catch {
4788
+ }
3937
4789
  const startPoint = toTargetPointFromHostEvent(
3938
4790
  event,
3939
4791
  this.config.getEnvironment()
@@ -3941,20 +4793,11 @@ ${formatItemMeta(item)}`;
3941
4793
  startX = startPoint.x;
3942
4794
  startY = startPoint.y;
3943
4795
  updateBox(event);
4796
+ addDragListeners();
3944
4797
  });
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
- });
4798
+ layer.addEventListener("pointermove", handlePointerMove);
4799
+ layer.addEventListener("pointerup", handlePointerUp);
4800
+ layer.addEventListener("pointercancel", handlePointerCancel);
3958
4801
  return layer;
3959
4802
  }
3960
4803
  };
@@ -3994,6 +4837,8 @@ var WebReviewKitApp = class {
3994
4837
  this.isOpen = false;
3995
4838
  this.mode = "idle";
3996
4839
  this.items = [];
4840
+ this.draftError = "";
4841
+ this.isCreatingItem = false;
3997
4842
  this.isSelectingArea = false;
3998
4843
  this.handleKeyDown = (event) => {
3999
4844
  if (event.key === "Escape" && this.cancelMode()) {
@@ -4024,6 +4869,8 @@ var WebReviewKitApp = class {
4024
4869
  items: this.items,
4025
4870
  noteDraft: this.noteDraft,
4026
4871
  areaDraft: this.areaDraft,
4872
+ draftError: this.draftError,
4873
+ isCreatingItem: this.isCreatingItem,
4027
4874
  isSelectingArea: this.isSelectingArea,
4028
4875
  highlightedItemId: this.highlightedItemId
4029
4876
  }),
@@ -4038,19 +4885,22 @@ var WebReviewKitApp = class {
4038
4885
  clearDrafts: () => {
4039
4886
  this.noteDraft = void 0;
4040
4887
  this.areaDraft = void 0;
4888
+ this.draftError = "";
4041
4889
  },
4042
4890
  setNoteDraft: (draft) => {
4043
4891
  this.noteDraft = draft;
4892
+ this.draftError = "";
4044
4893
  },
4045
4894
  setAreaDraft: (draft) => {
4046
4895
  this.areaDraft = draft;
4896
+ this.draftError = "";
4047
4897
  },
4048
4898
  setSelectingArea: (isSelectingArea) => {
4049
4899
  this.isSelectingArea = isSelectingArea;
4050
4900
  },
4051
4901
  createItem: (input) => this.createItem(input),
4052
- bindNoteDraftToPoint: (point, comment) => this.bindNoteDraftToPoint(point, comment),
4053
- bindElementDraftToPoint: (point, comment) => this.bindElementDraftToPoint(point, comment),
4902
+ bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
4903
+ bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
4054
4904
  createAreaDraft: (selection) => this.createAreaDraft(selection)
4055
4905
  }
4056
4906
  });
@@ -4119,7 +4969,7 @@ var WebReviewKitApp = class {
4119
4969
  this.noteDraft = void 0;
4120
4970
  this.areaDraft = void 0;
4121
4971
  this.isSelectingArea = false;
4122
- await this.bindElementDraftToElement(element, comment);
4972
+ await this.bindElementDraftToElement(element, { comment });
4123
4973
  }
4124
4974
  getMode() {
4125
4975
  return this.mode;
@@ -4262,7 +5112,7 @@ var WebReviewKitApp = class {
4262
5112
  if (!this.shadow) return;
4263
5113
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
4264
5114
  }
4265
- async bindNoteDraftToPoint(point, comment) {
5115
+ async bindNoteDraftToPoint(point, fields = {}) {
4266
5116
  const environment = this.getEnvironment();
4267
5117
  if (!environment) return;
4268
5118
  const viewport = getViewportSize(environment);
@@ -4282,13 +5132,13 @@ var WebReviewKitApp = class {
4282
5132
  viewport,
4283
5133
  anchor,
4284
5134
  marker,
4285
- comment
5135
+ ...fields
4286
5136
  };
4287
5137
  });
4288
5138
  this.noteDraft = draft;
4289
5139
  this.render();
4290
5140
  }
4291
- async bindElementDraftToPoint(point, comment) {
5141
+ async bindElementDraftToPoint(point, fields = {}) {
4292
5142
  const environment = this.getEnvironment();
4293
5143
  if (!environment) return;
4294
5144
  const viewport = getViewportSize(environment);
@@ -4332,14 +5182,14 @@ var WebReviewKitApp = class {
4332
5182
  anchor,
4333
5183
  marker,
4334
5184
  selection: reviewSelection,
4335
- comment,
5185
+ ...fields,
4336
5186
  previewElement
4337
5187
  };
4338
5188
  });
4339
5189
  this.noteDraft = draft;
4340
5190
  this.render();
4341
5191
  }
4342
- async bindElementDraftToElement(element, comment) {
5192
+ async bindElementDraftToElement(element, fields = {}) {
4343
5193
  const environment = this.getEnvironment();
4344
5194
  if (!environment || element.ownerDocument !== environment.document) return;
4345
5195
  const viewport = getViewportSize(environment);
@@ -4372,7 +5222,7 @@ var WebReviewKitApp = class {
4372
5222
  anchor,
4373
5223
  marker,
4374
5224
  selection: reviewSelection,
4375
- comment,
5225
+ ...fields,
4376
5226
  previewElement
4377
5227
  };
4378
5228
  });
@@ -4382,26 +5232,33 @@ var WebReviewKitApp = class {
4382
5232
  }
4383
5233
  async createAreaDraft(selection) {
4384
5234
  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();
5235
+ if (!environment) {
5236
+ this.isSelectingArea = false;
5237
+ this.render();
5238
+ return;
5239
+ }
5240
+ try {
5241
+ const viewport = getViewportSize(environment);
5242
+ this.areaDraft = await this.withOverlayHidden(() => {
5243
+ const marker = createSelectionCenterMarker(
5244
+ selection,
5245
+ void 0,
5246
+ environment
5247
+ );
5248
+ const reviewSelection = {
5249
+ viewport: toPublicSelection(selection)
5250
+ };
5251
+ return {
5252
+ viewport,
5253
+ marker,
5254
+ selection: reviewSelection
5255
+ };
5256
+ });
5257
+ this.setModeState("area");
5258
+ } finally {
5259
+ this.isSelectingArea = false;
5260
+ this.render();
5261
+ }
4405
5262
  }
4406
5263
  async withOverlayHidden(callback) {
4407
5264
  if (!this.root) return callback();
@@ -4415,11 +5272,16 @@ var WebReviewKitApp = class {
4415
5272
  }
4416
5273
  async createItem(input) {
4417
5274
  const environment = this.getEnvironment();
4418
- if (!environment) return;
5275
+ if (!environment || this.isCreatingItem) return;
4419
5276
  const now = (/* @__PURE__ */ new Date()).toISOString();
4420
5277
  const routeKey = getRouteKey(environment);
4421
5278
  const viewport = input.viewport ?? getViewportSize(environment);
4422
5279
  const createdBy = this.options.userId?.trim();
5280
+ const title = input.title?.trim();
5281
+ const assigneeId = input.assigneeId?.trim() || void 0;
5282
+ const assigneeOption = this.options.assigneeOptions?.find(
5283
+ (option) => option.value === assigneeId
5284
+ );
4423
5285
  const item = {
4424
5286
  id: createId(),
4425
5287
  projectId: this.options.projectId,
@@ -4429,8 +5291,10 @@ var WebReviewKitApp = class {
4429
5291
  normalizedPath: routeKey,
4430
5292
  scope: input.scope ?? getReviewViewportScope(viewport, this.options.viewports?.presets),
4431
5293
  kind: input.kind,
4432
- title: input.comment.split("\n")[0]?.slice(0, 80),
5294
+ title: title || void 0,
4433
5295
  comment: input.comment,
5296
+ assigneeId,
5297
+ assigneeName: input.assigneeName ?? assigneeOption?.label,
4434
5298
  createdBy: createdBy || void 0,
4435
5299
  status: "todo",
4436
5300
  viewport,
@@ -4445,13 +5309,23 @@ var WebReviewKitApp = class {
4445
5309
  createdAt: now,
4446
5310
  updatedAt: now
4447
5311
  };
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);
5312
+ this.draftError = "";
5313
+ this.isCreatingItem = true;
5314
+ this.render();
5315
+ try {
5316
+ const createdItem = await this.adapter.create(item);
5317
+ this.setModeState("idle");
5318
+ this.noteDraft = void 0;
5319
+ this.areaDraft = void 0;
5320
+ this.highlightItem(createdItem.id);
5321
+ await this.reload();
5322
+ await this.options.onCreateItem?.(createdItem);
5323
+ } catch (error) {
5324
+ this.draftError = error instanceof Error ? error.message : "Failed to save QA.";
5325
+ } finally {
5326
+ this.isCreatingItem = false;
5327
+ this.render();
5328
+ }
4455
5329
  }
4456
5330
  async restoreItem(item) {
4457
5331
  this.setModeState("idle");
@@ -4505,16 +5379,35 @@ function createNoopController() {
4505
5379
  }
4506
5380
  // Annotate the CommonJS export names for ESM import in node:
4507
5381
  0 && (module.exports = {
5382
+ DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
5383
+ DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
5384
+ DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
4508
5385
  DEFAULT_REVIEW_VIEWPORTS,
5386
+ FIGMA_NODE_REF_SEPARATOR,
5387
+ REVIEW_FIGMA_TOKEN_MISSING_CODE,
4509
5388
  REVIEW_WORKFLOW_STATUS_OPTIONS,
5389
+ ReviewFigmaTokenError,
5390
+ collectReviewFigmaReleaseSnapshot,
5391
+ createReviewFigmaFrameUrl,
5392
+ createReviewFigmaImageStoreClient,
5393
+ createReviewFigmaImagesSnapshot,
5394
+ createReviewFigmaNodeValue,
5395
+ createReviewFigmaReleaseSnapshot,
4510
5396
  createWebReviewKit,
4511
5397
  findReviewViewportPreset,
4512
5398
  getNumberedReviewItems,
5399
+ getReviewFigmaImageMimeType,
5400
+ getReviewFigmaImageTargetKey,
4513
5401
  getReviewItemScope,
4514
5402
  getReviewItemScopeLabel,
4515
5403
  getReviewViewportScope,
5404
+ isReviewFigmaTokenError,
4516
5405
  localAdapter,
4517
5406
  normalizeReviewItemStatus,
5407
+ parseReviewFigmaNodeRef,
5408
+ readReviewFigmaToken,
5409
+ requireReviewFigmaNodeRef,
5410
+ requireReviewFigmaToken,
4518
5411
  supabaseAdapter
4519
5412
  });
4520
5413
  //# sourceMappingURL=index.cjs.map