@designfever/web-review-kit 0.5.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 (42) hide show
  1. package/.env.sample +29 -0
  2. package/README.md +36 -10
  3. package/dist/chunk-AB5B6O77.js +584 -0
  4. package/dist/chunk-AB5B6O77.js.map +1 -0
  5. package/dist/{chunk-TWCSIBMY.js → chunk-RPVLRULC.js} +736 -201
  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 +1251 -202
  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 +10135 -4070
  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 +9258 -3707
  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-RvVa5ns-.d.cts → types-DT9Z66mV.d.cts} +14 -1
  24. package/dist/{types-RvVa5ns-.d.ts → types-DT9Z66mV.d.ts} +14 -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 -5
  32. package/docs/adapters.md +126 -0
  33. package/docs/adaptor.sample.ts +13 -1
  34. package/docs/architecture.md +6 -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 +44 -8
  39. package/docs/release-notes-0.6.0.md +108 -0
  40. package/docs/release-notes-0.7.0.md +128 -0
  41. package/package.json +6 -2
  42. package/dist/chunk-TWCSIBMY.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;
@@ -1520,6 +2092,14 @@ function createStyleElement() {
1520
2092
  cursor: default;
1521
2093
  }
1522
2094
 
2095
+ .dfwr-shell.is-docked-composer {
2096
+ position: relative;
2097
+ inset: auto;
2098
+ z-index: auto;
2099
+ padding: 0;
2100
+ pointer-events: auto;
2101
+ }
2102
+
1523
2103
  .dfwr-panel {
1524
2104
  position: fixed;
1525
2105
  right: 16px;
@@ -1546,8 +2126,8 @@ function createStyleElement() {
1546
2126
  }
1547
2127
 
1548
2128
  .dfwr-title {
1549
- font-size: 15px;
1550
- font-weight: 700;
2129
+ font-size: var(--df-review-font-size-xl);
2130
+ font-weight: var(--df-review-font-weight-emphasis);
1551
2131
  line-height: 1.25;
1552
2132
  }
1553
2133
 
@@ -1591,11 +2171,15 @@ function createStyleElement() {
1591
2171
  }
1592
2172
 
1593
2173
  .dfwr-button {
2174
+ display: inline-flex;
2175
+ align-items: center;
2176
+ justify-content: center;
2177
+ gap: 6px;
1594
2178
  min-height: var(--df-review-control-height-md);
1595
2179
  padding: 0 12px;
1596
2180
  border-radius: var(--df-review-radius-sm);
1597
2181
  font-size: var(--df-review-font-size-sm);
1598
- font-weight: 650;
2182
+ font-weight: var(--df-review-font-weight-emphasis);
1599
2183
  }
1600
2184
 
1601
2185
  .dfwr-button:hover,
@@ -1611,6 +2195,21 @@ function createStyleElement() {
1611
2195
  color: var(--df-review-color-accent-contrast);
1612
2196
  }
1613
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
+
1614
2213
  .dfwr-icon-button {
1615
2214
  display: inline-flex;
1616
2215
  align-items: center;
@@ -1620,7 +2219,7 @@ function createStyleElement() {
1620
2219
  padding: 0 8px;
1621
2220
  border-radius: var(--df-review-radius-sm);
1622
2221
  font-size: var(--df-review-font-size-xs);
1623
- font-weight: 700;
2222
+ font-weight: var(--df-review-font-weight-emphasis);
1624
2223
  line-height: 1;
1625
2224
  text-transform: uppercase;
1626
2225
  }
@@ -1775,7 +2374,7 @@ function createStyleElement() {
1775
2374
  0 8px 18px rgba(0, 0, 0, 0.28);
1776
2375
  color: #111820;
1777
2376
  font-size: var(--df-review-font-size-2xs);
1778
- font-weight: 900;
2377
+ font-weight: var(--df-review-font-weight-emphasis);
1779
2378
  line-height: 1;
1780
2379
  pointer-events: none;
1781
2380
  }
@@ -1801,7 +2400,7 @@ function createStyleElement() {
1801
2400
  box-shadow: 0 0 0 4px rgba(var(--dfwr-scope-rgb), 0.18);
1802
2401
  color: var(--dfwr-scope);
1803
2402
  font-size: var(--df-review-font-size-2xs);
1804
- font-weight: 800;
2403
+ font-weight: var(--df-review-font-weight-emphasis);
1805
2404
  }
1806
2405
 
1807
2406
  .dfwr-bound-marker.is-highlighted {
@@ -2020,6 +2619,20 @@ function createStyleElement() {
2020
2619
  border-color: rgba(99, 215, 199, 0.56);
2021
2620
  }
2022
2621
 
2622
+ .dfwr-shell.is-docked-composer .dfwr-note-popover.is-docked-composer,
2623
+ .dfwr-shell.is-docked-composer .dfwr-area-draft.is-docked-composer {
2624
+ position: relative;
2625
+ left: auto;
2626
+ right: auto;
2627
+ top: auto;
2628
+ z-index: auto;
2629
+ max-height: none;
2630
+ }
2631
+
2632
+ .dfwr-shell.is-docked-composer .dfwr-textarea {
2633
+ min-height: 184px;
2634
+ }
2635
+
2023
2636
  .dfwr-note-popover.is-dragging,
2024
2637
  .dfwr-area-draft.is-dragging {
2025
2638
  user-select: none;
@@ -2068,6 +2681,50 @@ function createStyleElement() {
2068
2681
  padding: 0;
2069
2682
  }
2070
2683
 
2684
+ .dfwr-actions.has-leading {
2685
+ align-items: center;
2686
+ justify-content: space-between;
2687
+ gap: 12px;
2688
+ }
2689
+
2690
+ .dfwr-actions-leading,
2691
+ .dfwr-actions-primary {
2692
+ display: flex;
2693
+ align-items: center;
2694
+ gap: 8px;
2695
+ min-width: 0;
2696
+ }
2697
+
2698
+ .dfwr-actions-primary {
2699
+ margin-left: auto;
2700
+ }
2701
+
2702
+ .dfwr-shell.is-docked-composer .dfwr-actions.has-leading {
2703
+ align-items: stretch;
2704
+ }
2705
+
2706
+ .dfwr-shell.is-docked-composer .dfwr-actions.has-leading .dfwr-button,
2707
+ .dfwr-shell.is-docked-composer .dfwr-actions.has-leading .dfwr-adjust-toggle {
2708
+ height: var(--df-review-control-height-md);
2709
+ min-height: var(--df-review-control-height-md);
2710
+ border-radius: var(--df-review-radius-sm);
2711
+ }
2712
+
2713
+ .dfwr-shell.is-docked-composer .dfwr-actions.has-leading .dfwr-button {
2714
+ min-width: 96px;
2715
+ padding: 0 12px;
2716
+ font-size: var(--df-review-font-size-sm);
2717
+ }
2718
+
2719
+ .dfwr-shell.is-docked-composer .dfwr-actions.has-leading .dfwr-adjust-toggle {
2720
+ width: var(--df-review-control-height-md);
2721
+ }
2722
+
2723
+ .dfwr-shell.is-docked-composer .dfwr-actions.has-leading .dfwr-adjust-toggle svg {
2724
+ width: 18px;
2725
+ height: 18px;
2726
+ }
2727
+
2071
2728
  .dfwr-note-actions {
2072
2729
  justify-content: flex-end;
2073
2730
  }
@@ -2085,10 +2742,18 @@ function createStyleElement() {
2085
2742
  gap: 10px;
2086
2743
  }
2087
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,
2088
2755
  .dfwr-textarea {
2089
2756
  width: 100%;
2090
- min-height: 92px;
2091
- resize: vertical;
2092
2757
  border: 1px solid rgba(255, 255, 255, 0.16);
2093
2758
  border-radius: var(--df-review-radius-sm);
2094
2759
  padding: 10px;
@@ -2099,14 +2764,39 @@ function createStyleElement() {
2099
2764
  line-height: 1.45;
2100
2765
  }
2101
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,
2102
2784
  .dfwr-textarea:focus {
2103
2785
  outline: 2px solid var(--df-review-color-accent-ring);
2104
2786
  outline-offset: 1px;
2105
2787
  }
2106
2788
 
2107
2789
  @media (hover: none) and (pointer: coarse) {
2790
+ .dfwr-input,
2791
+ .dfwr-select,
2108
2792
  .dfwr-textarea {
2109
- 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);
2110
2800
  }
2111
2801
  }
2112
2802
 
@@ -2164,8 +2854,8 @@ function createStyleElement() {
2164
2854
  color: var(--df-review-color-text);
2165
2855
  cursor: pointer;
2166
2856
  font: inherit;
2167
- font-size: 14px;
2168
- font-weight: 800;
2857
+ font-size: var(--df-review-font-size-lg);
2858
+ font-weight: var(--df-review-font-weight-emphasis);
2169
2859
  line-height: 1;
2170
2860
  }
2171
2861
 
@@ -2218,7 +2908,7 @@ function createStyleElement() {
2218
2908
  margin-bottom: 10px;
2219
2909
  color: rgba(247, 247, 242, 0.74);
2220
2910
  font-size: var(--df-review-font-size-sm);
2221
- font-weight: 700;
2911
+ font-weight: var(--df-review-font-weight-emphasis);
2222
2912
  }
2223
2913
 
2224
2914
  .dfwr-item {
@@ -2240,6 +2930,8 @@ function createStyleElement() {
2240
2930
  }
2241
2931
 
2242
2932
  .dfwr-item-body {
2933
+ display: grid;
2934
+ gap: 4px;
2243
2935
  min-width: 0;
2244
2936
  flex: 1;
2245
2937
  }
@@ -2259,7 +2951,7 @@ function createStyleElement() {
2259
2951
  border-radius: var(--df-review-radius-pill);
2260
2952
  padding: 0 7px;
2261
2953
  font-size: var(--df-review-font-size-2xs);
2262
- font-weight: 800;
2954
+ font-weight: var(--df-review-font-weight-emphasis);
2263
2955
  line-height: 1;
2264
2956
  letter-spacing: 0;
2265
2957
  text-transform: uppercase;
@@ -2277,13 +2969,29 @@ function createStyleElement() {
2277
2969
  color: rgba(247, 247, 242, 0.64);
2278
2970
  }
2279
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
+
2280
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 {
2281
2991
  margin: 4px 0;
2282
2992
  color: var(--df-review-color-text);
2283
2993
  font-size: var(--df-review-font-size-md);
2284
2994
  line-height: 1.42;
2285
- overflow-wrap: anywhere;
2286
- white-space: pre-wrap;
2287
2995
  }
2288
2996
 
2289
2997
  .dfwr-item-date {
@@ -2392,10 +3100,12 @@ function createStyleElement() {
2392
3100
 
2393
3101
  @media (max-width: 520px) {
2394
3102
  .dfwr-panel {
3103
+ left: 8px;
2395
3104
  right: 8px;
2396
- top: 8px;
2397
- width: calc(100vw - 16px);
2398
- max-height: calc(100vh - 16px);
3105
+ top: auto;
3106
+ bottom: 8px;
3107
+ width: auto;
3108
+ max-height: min(70vh, calc(100vh - 16px));
2399
3109
  }
2400
3110
  }
2401
3111
  `;
@@ -2475,6 +3185,7 @@ var WebReviewKitView = class {
2475
3185
  }
2476
3186
  clearDraftPreview() {
2477
3187
  this.restoreDraftPreview();
3188
+ this.clearShellComposer();
2478
3189
  }
2479
3190
  render(shadow, hiddenItemsStyle) {
2480
3191
  const state = this.state;
@@ -2485,11 +3196,13 @@ var WebReviewKitView = class {
2485
3196
  shadow.append(createStyleElement());
2486
3197
  shadow.append(hiddenItemsStyle);
2487
3198
  const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
3199
+ const shouldDockComposer = this.config.options.ui?.panel === false && hasDismissableDraft && Boolean(this.getShellComposerHost());
3200
+ let dockedComposer;
2488
3201
  const shell = document.createElement("div");
2489
3202
  shell.className = [
2490
3203
  "dfwr-shell",
2491
3204
  state.isOpen ? "is-open" : "",
2492
- hasDismissableDraft ? "has-dismissible-draft" : ""
3205
+ hasDismissableDraft && !shouldDockComposer ? "has-dismissible-draft" : ""
2493
3206
  ].filter(Boolean).join(" ");
2494
3207
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
2495
3208
  if (this.config.options.ui?.panel !== false) {
@@ -2506,105 +3219,119 @@ var WebReviewKitView = class {
2506
3219
  shell.append(panel);
2507
3220
  }
2508
3221
  shell.append(this.createMarkerLayer());
2509
- if (state.isOpen && hasDismissableDraft) {
3222
+ if (state.isOpen && hasDismissableDraft && !shouldDockComposer) {
2510
3223
  shell.append(this.createDraftCancelLayer());
2511
3224
  }
2512
3225
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
2513
- shell.append(
2514
- state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
2515
- );
3226
+ if (state.noteDraft) {
3227
+ const noteDraft = this.createNotePopover(state.noteDraft, {
3228
+ dockComposer: shouldDockComposer
3229
+ });
3230
+ shell.append(noteDraft.layer);
3231
+ dockedComposer = noteDraft.composer;
3232
+ } else {
3233
+ shell.append(
3234
+ state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
3235
+ );
3236
+ }
2516
3237
  }
2517
- if (state.isOpen && state.mode === "area" && !state.areaDraft) {
3238
+ if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
2518
3239
  shell.append(this.createAreaLayer());
2519
3240
  }
2520
3241
  if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
2521
3242
  if (state.areaDraft.selection) {
2522
3243
  shell.append(this.createAreaDraftOverlay(state.areaDraft));
2523
3244
  }
2524
- shell.append(this.createAreaDraftPopover(state.areaDraft));
3245
+ const areaComposer = this.createAreaDraftPopover(state.areaDraft, {
3246
+ dockComposer: shouldDockComposer
3247
+ });
3248
+ if (shouldDockComposer) {
3249
+ dockedComposer = areaComposer;
3250
+ } else {
3251
+ shell.append(areaComposer);
3252
+ }
2525
3253
  }
2526
3254
  shadow.append(shell);
3255
+ this.renderShellComposer(dockedComposer);
2527
3256
  }
2528
3257
  get state() {
2529
3258
  return this.config.getState();
2530
3259
  }
3260
+ getShellComposerHost() {
3261
+ const environment = this.config.getEnvironment();
3262
+ if (this.config.options.ui?.panel !== false) return void 0;
3263
+ return environment?.composerHost ?? void 0;
3264
+ }
3265
+ renderShellComposer(composer) {
3266
+ const host = composer ? this.getShellComposerHost() : void 0;
3267
+ if (!host || !composer) {
3268
+ this.clearShellComposer();
3269
+ return;
3270
+ }
3271
+ if (this.shellComposerHost && this.shellComposerHost !== host) {
3272
+ this.clearShellComposer();
3273
+ }
3274
+ this.shellComposerHost = host;
3275
+ host.dataset.hasDraftComposer = "true";
3276
+ if (host.parentElement) {
3277
+ host.parentElement.dataset.hasDraftComposer = "true";
3278
+ }
3279
+ const shell = document.createElement("div");
3280
+ shell.className = "dfwr-shell is-open is-shell-draft is-docked-composer";
3281
+ shell.append(composer);
3282
+ host.replaceChildren(createStyleElement(), shell);
3283
+ }
3284
+ clearShellComposer() {
3285
+ const host = this.shellComposerHost;
3286
+ host?.replaceChildren();
3287
+ if (host) {
3288
+ delete host.dataset.hasDraftComposer;
3289
+ delete host.parentElement?.dataset.hasDraftComposer;
3290
+ }
3291
+ this.shellComposerHost = void 0;
3292
+ }
2531
3293
  createDraftCancelLayer() {
2532
3294
  const layer = document.createElement("div");
2533
3295
  layer.className = "dfwr-draft-cancel-layer";
2534
3296
  layer.setAttribute("aria-hidden", "true");
2535
- const cancel = (event) => {
2536
- event.preventDefault();
2537
- event.stopPropagation();
2538
- event.stopImmediatePropagation();
2539
- this.config.actions.setModeState("idle");
2540
- this.config.actions.clearDrafts();
2541
- this.config.actions.setSelectingArea(false);
2542
- this.config.actions.render();
2543
- };
2544
3297
  layer.addEventListener("pointerdown", (event) => {
2545
3298
  if (event.button !== 0) return;
2546
- cancel(event);
3299
+ this.cancelDraft(event);
2547
3300
  });
2548
3301
  return layer;
2549
3302
  }
3303
+ cancelDraft(event) {
3304
+ event?.preventDefault();
3305
+ event?.stopPropagation();
3306
+ event?.stopImmediatePropagation();
3307
+ this.config.actions.setModeState("idle");
3308
+ this.config.actions.clearDrafts();
3309
+ this.config.actions.setSelectingArea(false);
3310
+ this.config.actions.render();
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
+ }
2550
3317
  getDraftAdjustmentMetrics(draft) {
2551
- const adjustment = draft.adjustment;
2552
- const x = adjustment?.x ?? 0;
2553
- const y = adjustment?.y ?? 0;
2554
- const scale = adjustment?.scale ?? 0;
2555
- const {
2556
- scale: viewportScale,
2557
- designWidth,
2558
- presetLabel
2559
- } = this.getDraftViewportScale(draft.viewport);
2560
- const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2561
- const scaleCssDelta = scale * viewportScale;
2562
- const scaleFactor = selection && selection.width > 0 ? Math.max(
2563
- 1 / selection.width,
2564
- (selection.width + scaleCssDelta) / selection.width
2565
- ) : 1;
2566
- return {
2567
- x,
2568
- y,
2569
- scale,
2570
- cssX: x * viewportScale,
2571
- cssY: y * viewportScale,
2572
- scaleFactor,
2573
- viewportScale,
2574
- designWidth,
2575
- presetLabel,
2576
- viewportWidth: draft.viewport.width
2577
- };
3318
+ return getDraftAdjustmentMetrics(draft, this.viewportPresets);
2578
3319
  }
2579
3320
  hasDraftAdjustment(draft) {
2580
- const metrics = this.getDraftAdjustmentMetrics(draft);
2581
- return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
3321
+ return hasDraftAdjustment(draft, this.viewportPresets);
2582
3322
  }
2583
3323
  getAdjustedDraftPoint(point, draft) {
2584
- const metrics = this.getDraftAdjustmentMetrics(draft);
2585
- return {
2586
- x: point.x + metrics.cssX,
2587
- y: point.y + metrics.cssY
2588
- };
3324
+ return getAdjustedDraftPoint(point, draft, this.viewportPresets);
2589
3325
  }
2590
3326
  getAdjustedDraftSelection(selection, draft) {
2591
- const metrics = this.getDraftAdjustmentMetrics(draft);
2592
- return {
2593
- ...selection,
2594
- left: selection.left + metrics.cssX,
2595
- top: selection.top + metrics.cssY,
2596
- width: selection.width * metrics.scaleFactor,
2597
- height: selection.height * metrics.scaleFactor
2598
- };
3327
+ return getAdjustedDraftSelection(
3328
+ selection,
3329
+ draft,
3330
+ this.viewportPresets
3331
+ );
2599
3332
  }
2600
3333
  getDraftViewportScale(viewport) {
2601
- const preset = findReviewViewportPreset(
2602
- viewport,
2603
- this.config.options.viewports?.presets
2604
- );
2605
- const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2606
- const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2607
- return { scale, designWidth, presetLabel: preset.label };
3334
+ return getDraftViewportScale(viewport, this.viewportPresets);
2608
3335
  }
2609
3336
  getDraftComposerWidth(environment) {
2610
3337
  const bounds = environment.overlayRect;
@@ -2750,6 +3477,67 @@ var WebReviewKitView = class {
2750
3477
  return trimmedComment ? `${trimmedComment}
2751
3478
  ${adjustment}` : adjustment;
2752
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
+ }
2753
3541
  getStyleableDraftElement(draft, environment) {
2754
3542
  if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2755
3543
  return draft.previewElement;
@@ -2940,11 +3728,14 @@ ${adjustment}` : adjustment;
2940
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.";
2941
3729
  return empty;
2942
3730
  }
2943
- createNotePopover(draft) {
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).
3734
+ createNotePopover(draft, options = {}) {
2944
3735
  const environment = this.config.getEnvironment();
2945
3736
  const group = document.createElement("div");
2946
3737
  group.className = "dfwr-note-draft";
2947
- if (!environment) return group;
3738
+ if (!environment) return { layer: group, composer: void 0 };
2948
3739
  const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
2949
3740
  const hostPoint = toHostPoint(
2950
3741
  isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
@@ -2968,8 +3759,14 @@ ${adjustment}` : adjustment;
2968
3759
  pin.style.top = `${hostPoint.y}px`;
2969
3760
  const popover = document.createElement("div");
2970
3761
  const position = getPopoverPosition(hostPoint, environment);
2971
- popover.className = `dfwr-note-popover${isElementDraft ? " is-composer" : ""}`;
2972
- if (isElementDraft) {
3762
+ popover.className = [
3763
+ "dfwr-note-popover",
3764
+ isElementDraft ? "is-composer" : "",
3765
+ options.dockComposer ? "is-docked-composer" : ""
3766
+ ].filter(Boolean).join(" ");
3767
+ if (options.dockComposer) {
3768
+ popover.style.width = "100%";
3769
+ } else if (isElementDraft) {
2973
3770
  const selection = draft.selection ? toHostSelection(
2974
3771
  this.getAdjustedDraftSelection(
2975
3772
  toViewportSelection(draft.selection.viewport),
@@ -2997,6 +3794,14 @@ ${adjustment}` : adjustment;
2997
3794
  meta.className = "dfwr-item-date";
2998
3795
  meta.textContent = formatNoteDraftMeta(draft);
2999
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;
3000
3805
  const textarea = document.createElement("textarea");
3001
3806
  textarea.className = "dfwr-textarea";
3002
3807
  textarea.placeholder = "Review comment";
@@ -3010,13 +3815,30 @@ ${adjustment}` : adjustment;
3010
3815
  comment: textarea.value
3011
3816
  });
3012
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
+ );
3013
3831
  const saveDraft = () => {
3014
- const comment = textarea.value.trim();
3015
3832
  const currentDraft = this.state.noteDraft ?? draft;
3833
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3834
+ const comment = fields.comment;
3016
3835
  if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
3017
3836
  void this.config.actions.createItem({
3018
3837
  kind: "note",
3838
+ title: fields.title,
3019
3839
  comment: this.withDraftAdjustmentComment(comment, currentDraft),
3840
+ assigneeId: fields.assigneeId,
3841
+ assigneeName: fields.assigneeName,
3020
3842
  viewport: currentDraft.viewport,
3021
3843
  anchor: currentDraft.anchor,
3022
3844
  marker: currentDraft.marker,
@@ -3028,18 +3850,31 @@ ${adjustment}` : adjustment;
3028
3850
  pin,
3029
3851
  popover,
3030
3852
  selectionHighlight,
3031
- textarea
3853
+ textarea,
3854
+ dockToggle: options.dockComposer
3032
3855
  }) : void 0;
3033
- const actions = this.createFormActions("Save note", saveDraft);
3856
+ const actions = this.createFormActions("Save note", saveDraft, {
3857
+ leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
3858
+ });
3859
+ const error = this.createDraftError();
3034
3860
  form.append(
3035
3861
  ...meta ? [meta] : [],
3036
3862
  ...adjustmentControls ? [adjustmentControls.panel] : [],
3863
+ ...titleInput ? [titleInput] : [],
3037
3864
  textarea,
3865
+ ...assigneeSelect ? [assigneeSelect] : [],
3866
+ ...error ? [error] : [],
3038
3867
  actions
3039
3868
  );
3040
- const dragHandle = isElementDraft ? this.createDraftDragHandle("Move DOM composer") : void 0;
3041
- popover.append(...dragHandle ? [dragHandle] : [], form);
3042
- group.append(pin, popover);
3869
+ const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
3870
+ popover.append(
3871
+ ...dragHandle ? [dragHandle] : [],
3872
+ form
3873
+ );
3874
+ group.append(pin);
3875
+ if (!options.dockComposer) {
3876
+ group.append(popover);
3877
+ }
3043
3878
  if (dragHandle) {
3044
3879
  this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3045
3880
  const noteDraft = this.state.noteDraft ?? draft;
@@ -3052,18 +3887,23 @@ ${adjustment}` : adjustment;
3052
3887
  }
3053
3888
  this.attachDraftPinDrag(
3054
3889
  pin,
3055
- isElementDraft ? void 0 : popover,
3890
+ isElementDraft || options.dockComposer ? void 0 : popover,
3056
3891
  meta,
3057
3892
  textarea
3058
3893
  );
3059
- window.setTimeout(() => {
3060
- if (draft.adjustment?.isActive) {
3061
- adjustmentControls?.focusTarget.focus();
3062
- return;
3063
- }
3064
- textarea.focus();
3065
- }, 0);
3066
- return group;
3894
+ if (!options.dockComposer) {
3895
+ window.setTimeout(() => {
3896
+ if (draft.adjustment?.isActive) {
3897
+ adjustmentControls?.focusTarget.focus();
3898
+ return;
3899
+ }
3900
+ textarea.focus();
3901
+ }, 0);
3902
+ }
3903
+ return {
3904
+ layer: group,
3905
+ composer: options.dockComposer ? popover : void 0
3906
+ };
3067
3907
  }
3068
3908
  createDraftDragHandle(label) {
3069
3909
  const handle = document.createElement("button");
@@ -3150,12 +3990,16 @@ ${adjustment}` : adjustment;
3150
3990
  handle.addEventListener("pointerup", stopDrag);
3151
3991
  handle.addEventListener("pointercancel", stopDrag);
3152
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.
3153
3996
  createAdjustmentControls({
3154
3997
  draft,
3155
3998
  pin,
3156
3999
  popover,
3157
4000
  selectionHighlight,
3158
- textarea
4001
+ textarea,
4002
+ dockToggle
3159
4003
  }) {
3160
4004
  const panel = document.createElement("div");
3161
4005
  panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
@@ -3231,12 +4075,16 @@ ${adjustment}` : adjustment;
3231
4075
  }
3232
4076
  }));
3233
4077
  });
3234
- header.append(help, adjust);
4078
+ header.append(help);
4079
+ if (!dockToggle) {
4080
+ header.append(adjust);
4081
+ }
3235
4082
  panel.append(header, xyStatus, scaleStatus);
3236
4083
  syncControls(draft);
3237
4084
  return {
3238
4085
  panel,
3239
- focusTarget: adjust
4086
+ focusTarget: adjust,
4087
+ actionButton: dockToggle ? adjust : void 0
3240
4088
  };
3241
4089
  }
3242
4090
  getAdjustmentKeyDelta(event) {
@@ -3289,6 +4137,14 @@ ${adjustment}` : adjustment;
3289
4137
  return form;
3290
4138
  }
3291
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;
3292
4148
  const textarea = document.createElement("textarea");
3293
4149
  textarea.className = "dfwr-textarea";
3294
4150
  textarea.placeholder = "Area comment";
@@ -3302,20 +4158,44 @@ ${adjustment}` : adjustment;
3302
4158
  comment: textarea.value
3303
4159
  });
3304
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
+ );
3305
4174
  const actions = this.createFormActions("Save area", () => {
3306
- const comment = textarea.value.trim();
3307
4175
  const draft = this.state.areaDraft;
4176
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
4177
+ const comment = fields.comment;
3308
4178
  if (!comment || !draft) return;
3309
4179
  void this.config.actions.createItem({
3310
4180
  kind: "area",
4181
+ title: fields.title,
3311
4182
  comment,
4183
+ assigneeId: fields.assigneeId,
4184
+ assigneeName: fields.assigneeName,
3312
4185
  viewport: draft.viewport,
3313
4186
  anchor: draft.anchor,
3314
4187
  marker: draft.marker,
3315
4188
  selection: draft.selection
3316
4189
  });
3317
4190
  });
3318
- 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
+ );
3319
4199
  return form;
3320
4200
  }
3321
4201
  createAreaMetricsPanel(draft) {
@@ -3362,11 +4242,17 @@ ${adjustment}` : adjustment;
3362
4242
  }
3363
4243
  return layer;
3364
4244
  }
3365
- createAreaDraftPopover(draft) {
4245
+ createAreaDraftPopover(draft, options = {}) {
3366
4246
  const environment = this.config.getEnvironment();
3367
4247
  const popover = document.createElement("div");
3368
- popover.className = "dfwr-area-draft is-composer";
3369
- if (environment && draft.selection) {
4248
+ popover.className = [
4249
+ "dfwr-area-draft",
4250
+ "is-composer",
4251
+ options.dockComposer ? "is-docked-composer" : ""
4252
+ ].filter(Boolean).join(" ");
4253
+ if (options.dockComposer) {
4254
+ popover.style.width = "100%";
4255
+ } else if (environment && draft.selection) {
3370
4256
  const selection = toHostSelection(
3371
4257
  toViewportSelection(draft.selection.viewport),
3372
4258
  environment
@@ -3382,40 +4268,61 @@ ${adjustment}` : adjustment;
3382
4268
  popover.style.width = `${composer.width}px`;
3383
4269
  popover.style.right = "auto";
3384
4270
  }
3385
- const dragHandle = this.createDraftDragHandle("Move area composer");
3386
- popover.append(dragHandle, this.createAreaForm());
3387
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3388
- const areaDraft = this.state.areaDraft ?? draft;
3389
- this.config.actions.setAreaDraft({
3390
- ...areaDraft,
3391
- composerPosition
4271
+ const dragHandle = options.dockComposer ? void 0 : this.createDraftDragHandle("Move area composer");
4272
+ popover.append(
4273
+ ...dragHandle ? [dragHandle] : [],
4274
+ this.createAreaForm()
4275
+ );
4276
+ if (dragHandle) {
4277
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
4278
+ const areaDraft = this.state.areaDraft ?? draft;
4279
+ this.config.actions.setAreaDraft({
4280
+ ...areaDraft,
4281
+ composerPosition
4282
+ });
3392
4283
  });
3393
- });
4284
+ }
3394
4285
  return popover;
3395
4286
  }
3396
4287
  createFormActions(saveLabel, onSave, options) {
3397
4288
  const actions = document.createElement("div");
3398
4289
  actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
4290
+ const isSaving = this.state.isCreatingItem;
3399
4291
  const save = document.createElement("button");
3400
4292
  save.className = "dfwr-button is-primary";
3401
4293
  save.type = "button";
3402
- 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
+ }
3403
4301
  save.addEventListener("click", (event) => {
3404
4302
  event.preventDefault();
3405
4303
  event.stopPropagation();
4304
+ if (this.state.isCreatingItem) return;
3406
4305
  onSave();
3407
4306
  });
3408
4307
  const cancel = document.createElement("button");
3409
4308
  cancel.className = "dfwr-button";
3410
4309
  cancel.type = "button";
4310
+ cancel.disabled = isSaving;
3411
4311
  cancel.textContent = "Cancel";
3412
4312
  cancel.addEventListener("click", (event) => {
3413
- event.preventDefault();
3414
- event.stopPropagation();
3415
- this.config.actions.setModeState("idle");
3416
- this.config.actions.clearDrafts();
3417
- this.config.actions.render();
4313
+ this.cancelDraft(event);
3418
4314
  });
4315
+ if (options?.leading?.length) {
4316
+ actions.classList.add("has-leading");
4317
+ const leading = document.createElement("div");
4318
+ leading.className = "dfwr-actions-leading";
4319
+ leading.append(...options.leading);
4320
+ const primary = document.createElement("div");
4321
+ primary.className = "dfwr-actions-primary";
4322
+ primary.append(save, cancel);
4323
+ actions.append(leading, primary);
4324
+ return actions;
4325
+ }
3419
4326
  if (options?.beforeSave?.length || options?.className) {
3420
4327
  actions.append(cancel, ...options.beforeSave ?? [], save);
3421
4328
  return actions;
@@ -3423,6 +4330,20 @@ ${adjustment}` : adjustment;
3423
4330
  actions.append(save, cancel);
3424
4331
  return actions;
3425
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
+ }
3426
4347
  createList() {
3427
4348
  const section = document.createElement("div");
3428
4349
  section.className = "dfwr-list";
@@ -3475,14 +4396,20 @@ ${adjustment}` : adjustment;
3475
4396
  kind.className = "dfwr-item-kind";
3476
4397
  kind.textContent = item.kind;
3477
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
+ }
3478
4405
  const comment = document.createElement("p");
3479
- comment.className = "dfwr-item-comment";
4406
+ comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
3480
4407
  comment.textContent = item.comment;
3481
4408
  const date = document.createElement("time");
3482
4409
  date.className = "dfwr-item-date";
3483
4410
  date.dateTime = item.createdAt;
3484
4411
  date.textContent = formatItemMeta(item);
3485
- body.append(badges, comment, date);
4412
+ body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
3486
4413
  const actions = document.createElement("div");
3487
4414
  actions.className = "dfwr-item-actions";
3488
4415
  actions.addEventListener("click", (event) => event.stopPropagation());
@@ -3671,7 +4598,14 @@ ${formatItemMeta(item)}`;
3671
4598
  event,
3672
4599
  this.config.getEnvironment()
3673
4600
  );
3674
- 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));
3675
4609
  });
3676
4610
  }
3677
4611
  createNoteLayer() {
@@ -3749,10 +4683,14 @@ ${formatItemMeta(item)}`;
3749
4683
  let startX = 0;
3750
4684
  let startY = 0;
3751
4685
  let selection;
4686
+ let activePointerId;
4687
+ let isDragging = false;
4688
+ const ownerWindow = layer.ownerDocument.defaultView ?? window;
3752
4689
  const updateBox = (event) => {
4690
+ const nextEnvironment = this.config.getEnvironment();
3753
4691
  const nextPoint = toTargetPointFromHostEvent(
3754
4692
  event,
3755
- this.config.getEnvironment()
4693
+ nextEnvironment
3756
4694
  );
3757
4695
  const left = Math.min(startX, nextPoint.x);
3758
4696
  const top = Math.min(startY, nextPoint.y);
@@ -3760,7 +4698,7 @@ ${formatItemMeta(item)}`;
3760
4698
  const height = Math.abs(nextPoint.y - startY);
3761
4699
  const hostPoint = toHostPoint(
3762
4700
  { x: left, y: top },
3763
- this.config.getEnvironment()
4701
+ nextEnvironment
3764
4702
  );
3765
4703
  selection = { left, top, width, height };
3766
4704
  box.style.left = `${hostPoint.x}px`;
@@ -3768,9 +4706,68 @@ ${formatItemMeta(item)}`;
3768
4706
  box.style.width = `${width}px`;
3769
4707
  box.style.height = `${height}px`;
3770
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
+ };
3771
4762
  layer.addEventListener("pointerdown", (event) => {
4763
+ if (event.button !== 0) return;
3772
4764
  event.preventDefault();
3773
- layer.setPointerCapture(event.pointerId);
4765
+ activePointerId = event.pointerId;
4766
+ isDragging = true;
4767
+ try {
4768
+ layer.setPointerCapture(event.pointerId);
4769
+ } catch {
4770
+ }
3774
4771
  const startPoint = toTargetPointFromHostEvent(
3775
4772
  event,
3776
4773
  this.config.getEnvironment()
@@ -3778,20 +4775,11 @@ ${formatItemMeta(item)}`;
3778
4775
  startX = startPoint.x;
3779
4776
  startY = startPoint.y;
3780
4777
  updateBox(event);
4778
+ addDragListeners();
3781
4779
  });
3782
- layer.addEventListener("pointermove", (event) => {
3783
- if (!layer.hasPointerCapture(event.pointerId)) return;
3784
- updateBox(event);
3785
- });
3786
- layer.addEventListener("pointerup", (event) => {
3787
- if (!layer.hasPointerCapture(event.pointerId)) return;
3788
- layer.releasePointerCapture(event.pointerId);
3789
- updateBox(event);
3790
- if (!selection || selection.width < 8 || selection.height < 8) return;
3791
- this.config.actions.setSelectingArea(true);
3792
- this.config.actions.render();
3793
- void this.config.actions.createAreaDraft(selection);
3794
- });
4780
+ layer.addEventListener("pointermove", handlePointerMove);
4781
+ layer.addEventListener("pointerup", handlePointerUp);
4782
+ layer.addEventListener("pointercancel", handlePointerCancel);
3795
4783
  return layer;
3796
4784
  }
3797
4785
  };
@@ -3831,6 +4819,8 @@ var WebReviewKitApp = class {
3831
4819
  this.isOpen = false;
3832
4820
  this.mode = "idle";
3833
4821
  this.items = [];
4822
+ this.draftError = "";
4823
+ this.isCreatingItem = false;
3834
4824
  this.isSelectingArea = false;
3835
4825
  this.handleKeyDown = (event) => {
3836
4826
  if (event.key === "Escape" && this.cancelMode()) {
@@ -3844,9 +4834,10 @@ var WebReviewKitApp = class {
3844
4834
  this.toggle();
3845
4835
  };
3846
4836
  this.handleViewportChange = () => {
3847
- if (!this.isOpen || this.renderFrame) return;
4837
+ if (!this.isOpen || this.renderFrame || this.isDraftComposerFocused()) return;
3848
4838
  this.renderFrame = window.requestAnimationFrame(() => {
3849
4839
  this.renderFrame = void 0;
4840
+ if (this.isDraftComposerFocused()) return;
3850
4841
  this.render();
3851
4842
  });
3852
4843
  };
@@ -3860,6 +4851,8 @@ var WebReviewKitApp = class {
3860
4851
  items: this.items,
3861
4852
  noteDraft: this.noteDraft,
3862
4853
  areaDraft: this.areaDraft,
4854
+ draftError: this.draftError,
4855
+ isCreatingItem: this.isCreatingItem,
3863
4856
  isSelectingArea: this.isSelectingArea,
3864
4857
  highlightedItemId: this.highlightedItemId
3865
4858
  }),
@@ -3874,19 +4867,22 @@ var WebReviewKitApp = class {
3874
4867
  clearDrafts: () => {
3875
4868
  this.noteDraft = void 0;
3876
4869
  this.areaDraft = void 0;
4870
+ this.draftError = "";
3877
4871
  },
3878
4872
  setNoteDraft: (draft) => {
3879
4873
  this.noteDraft = draft;
4874
+ this.draftError = "";
3880
4875
  },
3881
4876
  setAreaDraft: (draft) => {
3882
4877
  this.areaDraft = draft;
4878
+ this.draftError = "";
3883
4879
  },
3884
4880
  setSelectingArea: (isSelectingArea) => {
3885
4881
  this.isSelectingArea = isSelectingArea;
3886
4882
  },
3887
4883
  createItem: (input) => this.createItem(input),
3888
- bindNoteDraftToPoint: (point, comment) => this.bindNoteDraftToPoint(point, comment),
3889
- bindElementDraftToPoint: (point, comment) => this.bindElementDraftToPoint(point, comment),
4884
+ bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
4885
+ bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
3890
4886
  createAreaDraft: (selection) => this.createAreaDraft(selection)
3891
4887
  }
3892
4888
  });
@@ -3955,7 +4951,7 @@ var WebReviewKitApp = class {
3955
4951
  this.noteDraft = void 0;
3956
4952
  this.areaDraft = void 0;
3957
4953
  this.isSelectingArea = false;
3958
- await this.bindElementDraftToElement(element, comment);
4954
+ await this.bindElementDraftToElement(element, { comment });
3959
4955
  }
3960
4956
  getMode() {
3961
4957
  return this.mode;
@@ -4023,6 +5019,14 @@ var WebReviewKitApp = class {
4023
5019
  this.render();
4024
5020
  return true;
4025
5021
  }
5022
+ isDraftComposerFocused() {
5023
+ if (!this.noteDraft && !this.areaDraft) return false;
5024
+ const composerHost = this.getEnvironment()?.composerHost;
5025
+ const activeElement = composerHost?.ownerDocument.activeElement;
5026
+ return Boolean(
5027
+ composerHost && activeElement && composerHost.contains(activeElement)
5028
+ );
5029
+ }
4026
5030
  getEnvironment() {
4027
5031
  const target = typeof this.options.target === "function" ? this.options.target() : this.options.target;
4028
5032
  if (!target) {
@@ -4051,6 +5055,7 @@ var WebReviewKitApp = class {
4051
5055
  height: target.window.innerHeight
4052
5056
  };
4053
5057
  const overlayRect = target.getOverlayRect?.() ?? rect;
5058
+ const composerHost = target.getComposerHost?.();
4054
5059
  return {
4055
5060
  window: target.window,
4056
5061
  document: target.document,
@@ -4065,7 +5070,8 @@ var WebReviewKitApp = class {
4065
5070
  top: overlayRect.top,
4066
5071
  width: overlayRect.width,
4067
5072
  height: overlayRect.height
4068
- }
5073
+ },
5074
+ composerHost
4069
5075
  };
4070
5076
  } catch {
4071
5077
  return void 0;
@@ -4088,7 +5094,7 @@ var WebReviewKitApp = class {
4088
5094
  if (!this.shadow) return;
4089
5095
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
4090
5096
  }
4091
- async bindNoteDraftToPoint(point, comment) {
5097
+ async bindNoteDraftToPoint(point, fields = {}) {
4092
5098
  const environment = this.getEnvironment();
4093
5099
  if (!environment) return;
4094
5100
  const viewport = getViewportSize(environment);
@@ -4108,13 +5114,13 @@ var WebReviewKitApp = class {
4108
5114
  viewport,
4109
5115
  anchor,
4110
5116
  marker,
4111
- comment
5117
+ ...fields
4112
5118
  };
4113
5119
  });
4114
5120
  this.noteDraft = draft;
4115
5121
  this.render();
4116
5122
  }
4117
- async bindElementDraftToPoint(point, comment) {
5123
+ async bindElementDraftToPoint(point, fields = {}) {
4118
5124
  const environment = this.getEnvironment();
4119
5125
  if (!environment) return;
4120
5126
  const viewport = getViewportSize(environment);
@@ -4158,14 +5164,14 @@ var WebReviewKitApp = class {
4158
5164
  anchor,
4159
5165
  marker,
4160
5166
  selection: reviewSelection,
4161
- comment,
5167
+ ...fields,
4162
5168
  previewElement
4163
5169
  };
4164
5170
  });
4165
5171
  this.noteDraft = draft;
4166
5172
  this.render();
4167
5173
  }
4168
- async bindElementDraftToElement(element, comment) {
5174
+ async bindElementDraftToElement(element, fields = {}) {
4169
5175
  const environment = this.getEnvironment();
4170
5176
  if (!environment || element.ownerDocument !== environment.document) return;
4171
5177
  const viewport = getViewportSize(environment);
@@ -4198,7 +5204,7 @@ var WebReviewKitApp = class {
4198
5204
  anchor,
4199
5205
  marker,
4200
5206
  selection: reviewSelection,
4201
- comment,
5207
+ ...fields,
4202
5208
  previewElement
4203
5209
  };
4204
5210
  });
@@ -4208,26 +5214,33 @@ var WebReviewKitApp = class {
4208
5214
  }
4209
5215
  async createAreaDraft(selection) {
4210
5216
  const environment = this.getEnvironment();
4211
- if (!environment) return;
4212
- const viewport = getViewportSize(environment);
4213
- this.areaDraft = await this.withOverlayHidden(() => {
4214
- const marker = createSelectionCenterMarker(
4215
- selection,
4216
- void 0,
4217
- environment
4218
- );
4219
- const reviewSelection = {
4220
- viewport: toPublicSelection(selection)
4221
- };
4222
- return {
4223
- viewport,
4224
- marker,
4225
- selection: reviewSelection
4226
- };
4227
- });
4228
- this.isSelectingArea = false;
4229
- this.setModeState("area");
4230
- 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
+ }
4231
5244
  }
4232
5245
  async withOverlayHidden(callback) {
4233
5246
  if (!this.root) return callback();
@@ -4241,11 +5254,16 @@ var WebReviewKitApp = class {
4241
5254
  }
4242
5255
  async createItem(input) {
4243
5256
  const environment = this.getEnvironment();
4244
- if (!environment) return;
5257
+ if (!environment || this.isCreatingItem) return;
4245
5258
  const now = (/* @__PURE__ */ new Date()).toISOString();
4246
5259
  const routeKey = getRouteKey(environment);
4247
5260
  const viewport = input.viewport ?? getViewportSize(environment);
4248
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
+ );
4249
5267
  const item = {
4250
5268
  id: createId(),
4251
5269
  projectId: this.options.projectId,
@@ -4255,8 +5273,10 @@ var WebReviewKitApp = class {
4255
5273
  normalizedPath: routeKey,
4256
5274
  scope: input.scope ?? getReviewViewportScope(viewport, this.options.viewports?.presets),
4257
5275
  kind: input.kind,
4258
- title: input.comment.split("\n")[0]?.slice(0, 80),
5276
+ title: title || void 0,
4259
5277
  comment: input.comment,
5278
+ assigneeId,
5279
+ assigneeName: input.assigneeName ?? assigneeOption?.label,
4260
5280
  createdBy: createdBy || void 0,
4261
5281
  status: "todo",
4262
5282
  viewport,
@@ -4271,13 +5291,23 @@ var WebReviewKitApp = class {
4271
5291
  createdAt: now,
4272
5292
  updatedAt: now
4273
5293
  };
4274
- const createdItem = await this.adapter.create(item);
4275
- this.setModeState("idle");
4276
- this.noteDraft = void 0;
4277
- this.areaDraft = void 0;
4278
- this.highlightItem(createdItem.id);
4279
- await this.reload();
4280
- 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
+ }
4281
5311
  }
4282
5312
  async restoreItem(item) {
4283
5313
  this.setModeState("idle");
@@ -4331,16 +5361,35 @@ function createNoopController() {
4331
5361
  }
4332
5362
  // Annotate the CommonJS export names for ESM import in node:
4333
5363
  0 && (module.exports = {
5364
+ DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
5365
+ DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
5366
+ DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
4334
5367
  DEFAULT_REVIEW_VIEWPORTS,
5368
+ FIGMA_NODE_REF_SEPARATOR,
5369
+ REVIEW_FIGMA_TOKEN_MISSING_CODE,
4335
5370
  REVIEW_WORKFLOW_STATUS_OPTIONS,
5371
+ ReviewFigmaTokenError,
5372
+ collectReviewFigmaReleaseSnapshot,
5373
+ createReviewFigmaFrameUrl,
5374
+ createReviewFigmaImageStoreClient,
5375
+ createReviewFigmaImagesSnapshot,
5376
+ createReviewFigmaNodeValue,
5377
+ createReviewFigmaReleaseSnapshot,
4336
5378
  createWebReviewKit,
4337
5379
  findReviewViewportPreset,
4338
5380
  getNumberedReviewItems,
5381
+ getReviewFigmaImageMimeType,
5382
+ getReviewFigmaImageTargetKey,
4339
5383
  getReviewItemScope,
4340
5384
  getReviewItemScopeLabel,
4341
5385
  getReviewViewportScope,
5386
+ isReviewFigmaTokenError,
4342
5387
  localAdapter,
4343
5388
  normalizeReviewItemStatus,
5389
+ parseReviewFigmaNodeRef,
5390
+ readReviewFigmaToken,
5391
+ requireReviewFigmaNodeRef,
5392
+ requireReviewFigmaToken,
4344
5393
  supabaseAdapter
4345
5394
  });
4346
5395
  //# sourceMappingURL=index.cjs.map