@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
@@ -111,6 +111,9 @@ function normalizeStoredReviewItem(value) {
111
111
  };
112
112
  }
113
113
 
114
+ // src/figma/image.types.ts
115
+ var DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT = "webp";
116
+
114
117
  // src/core/review/scope.ts
115
118
  var DEFAULT_REVIEW_VIEWPORTS = [
116
119
  { label: "Mobile", width: 390, height: 720, scope: "mobile" },
@@ -389,13 +392,13 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
389
392
  const target = environment.document.elementFromPoint(point.x, point.y);
390
393
  if (!target) return void 0;
391
394
  const candidates = createAnchorCandidates(target, configuredAttribute);
392
- const primary = candidates[0];
393
- if (!primary) return void 0;
395
+ const primaryCandidate = candidates[0];
396
+ if (!primaryCandidate) return void 0;
394
397
  return {
395
- ...primary,
398
+ ...primaryCandidate,
396
399
  candidates,
397
400
  htmlSnippet: getElementHtmlSnippet(
398
- getAnchorSourceElement(target, primary, configuredAttribute) ?? target
401
+ getAnchorSourceElement(target, primaryCandidate, configuredAttribute) ?? target
399
402
  ),
400
403
  source: getDomSourceHint(target)
401
404
  };
@@ -403,13 +406,13 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
403
406
  function getDomAnchorFromElement(target, configuredAttribute = "data-qa-id", environment) {
404
407
  if (target.ownerDocument !== environment.document) return void 0;
405
408
  const candidates = createAnchorCandidates(target, configuredAttribute);
406
- const primary = candidates[0];
407
- if (!primary) return void 0;
409
+ const primaryCandidate = candidates[0];
410
+ if (!primaryCandidate) return void 0;
408
411
  return {
409
- ...primary,
412
+ ...primaryCandidate,
410
413
  candidates,
411
414
  htmlSnippet: getElementHtmlSnippet(
412
- getAnchorSourceElement(target, primary, configuredAttribute) ?? target
415
+ getAnchorSourceElement(target, primaryCandidate, configuredAttribute) ?? target
413
416
  ),
414
417
  source: getDomSourceHint(target)
415
418
  };
@@ -580,7 +583,7 @@ function createAnchorCandidates(target, configuredAttribute) {
580
583
  function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
581
584
  for (const attributeName of attributeNames) {
582
585
  const selector = `[${attributeName}]`;
583
- const element = safeClosest(target, selector);
586
+ const element = tryClosest(target, selector);
584
587
  if (!element) continue;
585
588
  const value = getStableAttributeValue(element, attributeName);
586
589
  if (!value) continue;
@@ -686,7 +689,7 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
686
689
  return target;
687
690
  }
688
691
  }
689
- function safeClosest(element, selector) {
692
+ function tryClosest(element, selector) {
690
693
  try {
691
694
  return element.closest(selector);
692
695
  } catch {
@@ -1190,18 +1193,90 @@ function setDocumentScrollInstantly(environment, position) {
1190
1193
  );
1191
1194
  }
1192
1195
 
1196
+ // src/core/draft.metrics.ts
1197
+ function getDraftViewportScale(viewport, presets) {
1198
+ const preset = findReviewViewportPreset(viewport, presets);
1199
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
1200
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
1201
+ return { scale, designWidth, presetLabel: preset.label };
1202
+ }
1203
+ function getDraftAdjustmentMetrics(draft, presets) {
1204
+ const adjustment = draft.adjustment;
1205
+ const x = adjustment?.x ?? 0;
1206
+ const y = adjustment?.y ?? 0;
1207
+ const scale = adjustment?.scale ?? 0;
1208
+ const {
1209
+ scale: viewportScale,
1210
+ designWidth,
1211
+ presetLabel
1212
+ } = getDraftViewportScale(draft.viewport, presets);
1213
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
1214
+ const scaleCssDelta = scale * viewportScale;
1215
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
1216
+ 1 / selection.width,
1217
+ (selection.width + scaleCssDelta) / selection.width
1218
+ ) : 1;
1219
+ return {
1220
+ x,
1221
+ y,
1222
+ scale,
1223
+ cssX: x * viewportScale,
1224
+ cssY: y * viewportScale,
1225
+ scaleFactor,
1226
+ viewportScale,
1227
+ designWidth,
1228
+ presetLabel,
1229
+ viewportWidth: draft.viewport.width
1230
+ };
1231
+ }
1232
+ function hasDraftAdjustment(draft, presets) {
1233
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
1234
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
1235
+ }
1236
+ function getAdjustedDraftPoint(point, draft, presets) {
1237
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
1238
+ return {
1239
+ x: point.x + metrics.cssX,
1240
+ y: point.y + metrics.cssY
1241
+ };
1242
+ }
1243
+ function getAdjustedDraftSelection(selection, draft, presets) {
1244
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
1245
+ return {
1246
+ ...selection,
1247
+ left: selection.left + metrics.cssX,
1248
+ top: selection.top + metrics.cssY,
1249
+ width: selection.width * metrics.scaleFactor,
1250
+ height: selection.height * metrics.scaleFactor
1251
+ };
1252
+ }
1253
+
1254
+ // src/core/typography.tokens.ts
1255
+ var reviewTypographyTokens = `
1256
+ --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1257
+ --df-review-font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
1258
+ --df-review-font-size-3xs: 9px;
1259
+ --df-review-font-size-2xs: 10px;
1260
+ --df-review-font-size-xs: 11px;
1261
+ --df-review-font-size-sm: 12px;
1262
+ --df-review-font-size-md: 13px;
1263
+ --df-review-font-size-lg: 14px;
1264
+ --df-review-font-size-xl: 15px;
1265
+ --df-review-font-size-2xl: 18px;
1266
+ --df-review-font-weight-normal: 400;
1267
+ --df-review-font-weight-emphasis: 500;
1268
+ --df-review-line-height-tight: 1.25;
1269
+ --df-review-line-height-base: 1.42;
1270
+ --df-review-line-height-relaxed: 1.55;
1271
+ `;
1272
+
1193
1273
  // src/core/overlay.style.ts
1194
1274
  function createStyleElement() {
1195
1275
  const style = document.createElement("style");
1196
1276
  style.textContent = `
1197
1277
  :host {
1198
1278
  color-scheme: dark;
1199
- --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1200
- --df-review-font-size-2xs: 10px;
1201
- --df-review-font-size-xs: 11px;
1202
- --df-review-font-size-sm: 12px;
1203
- --df-review-font-size-md: 13px;
1204
- --df-review-font-size-xl: 15px;
1279
+ ${reviewTypographyTokens}
1205
1280
  --df-review-space-1: 4px;
1206
1281
  --df-review-space-1-5: 6px;
1207
1282
  --df-review-space-2: 8px;
@@ -1300,8 +1375,8 @@ function createStyleElement() {
1300
1375
  }
1301
1376
 
1302
1377
  .dfwr-title {
1303
- font-size: 15px;
1304
- font-weight: 700;
1378
+ font-size: var(--df-review-font-size-xl);
1379
+ font-weight: var(--df-review-font-weight-emphasis);
1305
1380
  line-height: 1.25;
1306
1381
  }
1307
1382
 
@@ -1345,11 +1420,15 @@ function createStyleElement() {
1345
1420
  }
1346
1421
 
1347
1422
  .dfwr-button {
1423
+ display: inline-flex;
1424
+ align-items: center;
1425
+ justify-content: center;
1426
+ gap: 6px;
1348
1427
  min-height: var(--df-review-control-height-md);
1349
1428
  padding: 0 12px;
1350
1429
  border-radius: var(--df-review-radius-sm);
1351
1430
  font-size: var(--df-review-font-size-sm);
1352
- font-weight: 650;
1431
+ font-weight: var(--df-review-font-weight-emphasis);
1353
1432
  }
1354
1433
 
1355
1434
  .dfwr-button:hover,
@@ -1365,6 +1444,21 @@ function createStyleElement() {
1365
1444
  color: var(--df-review-color-accent-contrast);
1366
1445
  }
1367
1446
 
1447
+ .dfwr-button:disabled {
1448
+ cursor: default;
1449
+ opacity: 0.62;
1450
+ }
1451
+
1452
+ .dfwr-spinner {
1453
+ display: inline-block;
1454
+ width: 12px;
1455
+ height: 12px;
1456
+ border: 2px solid currentColor;
1457
+ border-right-color: transparent;
1458
+ border-radius: 999px;
1459
+ animation: dfwr-spin 720ms linear infinite;
1460
+ }
1461
+
1368
1462
  .dfwr-icon-button {
1369
1463
  display: inline-flex;
1370
1464
  align-items: center;
@@ -1374,7 +1468,7 @@ function createStyleElement() {
1374
1468
  padding: 0 8px;
1375
1469
  border-radius: var(--df-review-radius-sm);
1376
1470
  font-size: var(--df-review-font-size-xs);
1377
- font-weight: 700;
1471
+ font-weight: var(--df-review-font-weight-emphasis);
1378
1472
  line-height: 1;
1379
1473
  text-transform: uppercase;
1380
1474
  }
@@ -1529,7 +1623,7 @@ function createStyleElement() {
1529
1623
  0 8px 18px rgba(0, 0, 0, 0.28);
1530
1624
  color: #111820;
1531
1625
  font-size: var(--df-review-font-size-2xs);
1532
- font-weight: 900;
1626
+ font-weight: var(--df-review-font-weight-emphasis);
1533
1627
  line-height: 1;
1534
1628
  pointer-events: none;
1535
1629
  }
@@ -1555,7 +1649,7 @@ function createStyleElement() {
1555
1649
  box-shadow: 0 0 0 4px rgba(var(--dfwr-scope-rgb), 0.18);
1556
1650
  color: var(--dfwr-scope);
1557
1651
  font-size: var(--df-review-font-size-2xs);
1558
- font-weight: 800;
1652
+ font-weight: var(--df-review-font-weight-emphasis);
1559
1653
  }
1560
1654
 
1561
1655
  .dfwr-bound-marker.is-highlighted {
@@ -1897,10 +1991,18 @@ function createStyleElement() {
1897
1991
  gap: 10px;
1898
1992
  }
1899
1993
 
1994
+ .dfwr-form-error {
1995
+ margin: 0;
1996
+ color: #ff8f61;
1997
+ font-size: var(--df-review-font-size-sm);
1998
+ line-height: 1.4;
1999
+ overflow-wrap: anywhere;
2000
+ }
2001
+
2002
+ .dfwr-input,
2003
+ .dfwr-select,
1900
2004
  .dfwr-textarea {
1901
2005
  width: 100%;
1902
- min-height: 92px;
1903
- resize: vertical;
1904
2006
  border: 1px solid rgba(255, 255, 255, 0.16);
1905
2007
  border-radius: var(--df-review-radius-sm);
1906
2008
  padding: 10px;
@@ -1911,14 +2013,39 @@ function createStyleElement() {
1911
2013
  line-height: 1.45;
1912
2014
  }
1913
2015
 
2016
+ .dfwr-input,
2017
+ .dfwr-select {
2018
+ min-height: 38px;
2019
+ }
2020
+
2021
+ .dfwr-select {
2022
+ appearance: none;
2023
+ cursor: pointer;
2024
+ }
2025
+
2026
+ .dfwr-textarea {
2027
+ min-height: 92px;
2028
+ resize: vertical;
2029
+ }
2030
+
2031
+ .dfwr-input:focus,
2032
+ .dfwr-select:focus,
1914
2033
  .dfwr-textarea:focus {
1915
2034
  outline: 2px solid var(--df-review-color-accent-ring);
1916
2035
  outline-offset: 1px;
1917
2036
  }
1918
2037
 
1919
2038
  @media (hover: none) and (pointer: coarse) {
2039
+ .dfwr-input,
2040
+ .dfwr-select,
1920
2041
  .dfwr-textarea {
1921
- font-size: 16px;
2042
+ font-size: var(--df-review-font-size-xl);
2043
+ }
2044
+ }
2045
+
2046
+ @keyframes dfwr-spin {
2047
+ to {
2048
+ transform: rotate(360deg);
1922
2049
  }
1923
2050
  }
1924
2051
 
@@ -1976,8 +2103,8 @@ function createStyleElement() {
1976
2103
  color: var(--df-review-color-text);
1977
2104
  cursor: pointer;
1978
2105
  font: inherit;
1979
- font-size: 14px;
1980
- font-weight: 800;
2106
+ font-size: var(--df-review-font-size-lg);
2107
+ font-weight: var(--df-review-font-weight-emphasis);
1981
2108
  line-height: 1;
1982
2109
  }
1983
2110
 
@@ -2030,7 +2157,7 @@ function createStyleElement() {
2030
2157
  margin-bottom: 10px;
2031
2158
  color: rgba(247, 247, 242, 0.74);
2032
2159
  font-size: var(--df-review-font-size-sm);
2033
- font-weight: 700;
2160
+ font-weight: var(--df-review-font-weight-emphasis);
2034
2161
  }
2035
2162
 
2036
2163
  .dfwr-item {
@@ -2052,6 +2179,8 @@ function createStyleElement() {
2052
2179
  }
2053
2180
 
2054
2181
  .dfwr-item-body {
2182
+ display: grid;
2183
+ gap: 4px;
2055
2184
  min-width: 0;
2056
2185
  flex: 1;
2057
2186
  }
@@ -2071,7 +2200,7 @@ function createStyleElement() {
2071
2200
  border-radius: var(--df-review-radius-pill);
2072
2201
  padding: 0 7px;
2073
2202
  font-size: var(--df-review-font-size-2xs);
2074
- font-weight: 800;
2203
+ font-weight: var(--df-review-font-weight-emphasis);
2075
2204
  line-height: 1;
2076
2205
  letter-spacing: 0;
2077
2206
  text-transform: uppercase;
@@ -2089,13 +2218,29 @@ function createStyleElement() {
2089
2218
  color: rgba(247, 247, 242, 0.64);
2090
2219
  }
2091
2220
 
2221
+ .dfwr-item-title {
2222
+ margin: 4px 0 0;
2223
+ color: var(--df-review-color-text);
2224
+ font-size: var(--df-review-font-size-md);
2225
+ font-weight: var(--df-review-font-weight-normal);
2226
+ line-height: 1.35;
2227
+ overflow-wrap: anywhere;
2228
+ }
2229
+
2092
2230
  .dfwr-item-comment {
2231
+ margin: 0;
2232
+ color: var(--df-review-color-text-muted);
2233
+ font-size: var(--df-review-font-size-sm);
2234
+ line-height: 1.45;
2235
+ overflow-wrap: anywhere;
2236
+ white-space: pre-wrap;
2237
+ }
2238
+
2239
+ .dfwr-item-comment.is-primary {
2093
2240
  margin: 4px 0;
2094
2241
  color: var(--df-review-color-text);
2095
2242
  font-size: var(--df-review-font-size-md);
2096
2243
  line-height: 1.42;
2097
- overflow-wrap: anywhere;
2098
- white-space: pre-wrap;
2099
2244
  }
2100
2245
 
2101
2246
  .dfwr-item-date {
@@ -2339,7 +2484,7 @@ var WebReviewKitView = class {
2339
2484
  );
2340
2485
  }
2341
2486
  }
2342
- if (state.isOpen && state.mode === "area" && !state.areaDraft) {
2487
+ if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
2343
2488
  shell.append(this.createAreaLayer());
2344
2489
  }
2345
2490
  if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
@@ -2413,64 +2558,29 @@ var WebReviewKitView = class {
2413
2558
  this.config.actions.setSelectingArea(false);
2414
2559
  this.config.actions.render();
2415
2560
  }
2561
+ // Draft adjustment geometry lives in draft.metrics.ts; these thin wrappers
2562
+ // supply the configured viewport presets so call sites stay unchanged.
2563
+ get viewportPresets() {
2564
+ return this.config.options.viewports?.presets;
2565
+ }
2416
2566
  getDraftAdjustmentMetrics(draft) {
2417
- const adjustment = draft.adjustment;
2418
- const x = adjustment?.x ?? 0;
2419
- const y = adjustment?.y ?? 0;
2420
- const scale = adjustment?.scale ?? 0;
2421
- const {
2422
- scale: viewportScale,
2423
- designWidth,
2424
- presetLabel
2425
- } = this.getDraftViewportScale(draft.viewport);
2426
- const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2427
- const scaleCssDelta = scale * viewportScale;
2428
- const scaleFactor = selection && selection.width > 0 ? Math.max(
2429
- 1 / selection.width,
2430
- (selection.width + scaleCssDelta) / selection.width
2431
- ) : 1;
2432
- return {
2433
- x,
2434
- y,
2435
- scale,
2436
- cssX: x * viewportScale,
2437
- cssY: y * viewportScale,
2438
- scaleFactor,
2439
- viewportScale,
2440
- designWidth,
2441
- presetLabel,
2442
- viewportWidth: draft.viewport.width
2443
- };
2567
+ return getDraftAdjustmentMetrics(draft, this.viewportPresets);
2444
2568
  }
2445
2569
  hasDraftAdjustment(draft) {
2446
- const metrics = this.getDraftAdjustmentMetrics(draft);
2447
- return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
2570
+ return hasDraftAdjustment(draft, this.viewportPresets);
2448
2571
  }
2449
2572
  getAdjustedDraftPoint(point, draft) {
2450
- const metrics = this.getDraftAdjustmentMetrics(draft);
2451
- return {
2452
- x: point.x + metrics.cssX,
2453
- y: point.y + metrics.cssY
2454
- };
2573
+ return getAdjustedDraftPoint(point, draft, this.viewportPresets);
2455
2574
  }
2456
2575
  getAdjustedDraftSelection(selection, draft) {
2457
- const metrics = this.getDraftAdjustmentMetrics(draft);
2458
- return {
2459
- ...selection,
2460
- left: selection.left + metrics.cssX,
2461
- top: selection.top + metrics.cssY,
2462
- width: selection.width * metrics.scaleFactor,
2463
- height: selection.height * metrics.scaleFactor
2464
- };
2576
+ return getAdjustedDraftSelection(
2577
+ selection,
2578
+ draft,
2579
+ this.viewportPresets
2580
+ );
2465
2581
  }
2466
2582
  getDraftViewportScale(viewport) {
2467
- const preset = findReviewViewportPreset(
2468
- viewport,
2469
- this.config.options.viewports?.presets
2470
- );
2471
- const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2472
- const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2473
- return { scale, designWidth, presetLabel: preset.label };
2583
+ return getDraftViewportScale(viewport, this.viewportPresets);
2474
2584
  }
2475
2585
  getDraftComposerWidth(environment) {
2476
2586
  const bounds = environment.overlayRect;
@@ -2616,6 +2726,67 @@ var WebReviewKitView = class {
2616
2726
  return trimmedComment ? `${trimmedComment}
2617
2727
  ${adjustment}` : adjustment;
2618
2728
  }
2729
+ getAssigneeOption(assigneeId) {
2730
+ if (!assigneeId) return void 0;
2731
+ return this.config.options.assigneeOptions?.find(
2732
+ (option) => option.value === assigneeId
2733
+ );
2734
+ }
2735
+ getAssigneeName(assigneeId) {
2736
+ return this.getAssigneeOption(assigneeId)?.label;
2737
+ }
2738
+ createDraftTitleInput(value, onInput) {
2739
+ const input = document.createElement("input");
2740
+ input.className = "dfwr-input";
2741
+ input.placeholder = "Title";
2742
+ input.type = "text";
2743
+ input.value = value ?? "";
2744
+ input.addEventListener("input", () => onInput(input.value));
2745
+ return input;
2746
+ }
2747
+ isTitleFieldEnabled() {
2748
+ return this.config.options.fields?.title === true;
2749
+ }
2750
+ createDraftAssigneeSelect(value, fallbackLabel, onChange) {
2751
+ const assigneeOptions = this.config.options.assigneeOptions ?? [];
2752
+ if (assigneeOptions.length === 0) return void 0;
2753
+ const assigneeTitle = this.config.options.assigneeTitle?.trim() || "Assignee";
2754
+ const select = document.createElement("select");
2755
+ select.className = "dfwr-select";
2756
+ const emptyOption = document.createElement("option");
2757
+ emptyOption.value = "";
2758
+ emptyOption.textContent = assigneeTitle;
2759
+ select.append(emptyOption);
2760
+ const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
2761
+ if (hasUnknownAssignee && value) {
2762
+ const option = document.createElement("option");
2763
+ option.value = value;
2764
+ option.textContent = fallbackLabel ?? value;
2765
+ select.append(option);
2766
+ }
2767
+ assigneeOptions.forEach((assigneeOption) => {
2768
+ const option = document.createElement("option");
2769
+ option.value = assigneeOption.value;
2770
+ option.textContent = assigneeOption.label;
2771
+ select.append(option);
2772
+ });
2773
+ select.value = value ?? "";
2774
+ select.addEventListener("change", () => {
2775
+ onChange(select.value || null, this.getAssigneeName(select.value));
2776
+ });
2777
+ return select;
2778
+ }
2779
+ getDraftFields(titleInput, textarea, assigneeSelect) {
2780
+ const title = titleInput?.value.trim();
2781
+ const comment = textarea.value.trim();
2782
+ const assigneeId = assigneeSelect?.value.trim() || void 0;
2783
+ return {
2784
+ title: title || void 0,
2785
+ comment,
2786
+ assigneeId,
2787
+ assigneeName: this.getAssigneeName(assigneeId)
2788
+ };
2789
+ }
2619
2790
  getStyleableDraftElement(draft, environment) {
2620
2791
  if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2621
2792
  return draft.previewElement;
@@ -2806,6 +2977,9 @@ ${adjustment}` : adjustment;
2806
2977
  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.";
2807
2978
  return empty;
2808
2979
  }
2980
+ // Builds the note draft layer: the on-page marker/highlight plus its composer
2981
+ // popover. When dockComposer is set the composer renders into the side panel
2982
+ // instead of floating next to the marker (used for the docked review mode).
2809
2983
  createNotePopover(draft, options = {}) {
2810
2984
  const environment = this.config.getEnvironment();
2811
2985
  const group = document.createElement("div");
@@ -2869,6 +3043,14 @@ ${adjustment}` : adjustment;
2869
3043
  meta.className = "dfwr-item-date";
2870
3044
  meta.textContent = formatNoteDraftMeta(draft);
2871
3045
  }
3046
+ const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(draft.title, (title) => {
3047
+ const noteDraft = this.state.noteDraft;
3048
+ if (!noteDraft) return;
3049
+ this.config.actions.setNoteDraft({
3050
+ ...noteDraft,
3051
+ title
3052
+ });
3053
+ }) : void 0;
2872
3054
  const textarea = document.createElement("textarea");
2873
3055
  textarea.className = "dfwr-textarea";
2874
3056
  textarea.placeholder = "Review comment";
@@ -2882,13 +3064,30 @@ ${adjustment}` : adjustment;
2882
3064
  comment: textarea.value
2883
3065
  });
2884
3066
  });
3067
+ const assigneeSelect = this.createDraftAssigneeSelect(
3068
+ draft.assigneeId,
3069
+ draft.assigneeName,
3070
+ (assigneeId, assigneeName) => {
3071
+ const noteDraft = this.state.noteDraft;
3072
+ if (!noteDraft) return;
3073
+ this.config.actions.setNoteDraft({
3074
+ ...noteDraft,
3075
+ assigneeId,
3076
+ assigneeName
3077
+ });
3078
+ }
3079
+ );
2885
3080
  const saveDraft = () => {
2886
- const comment = textarea.value.trim();
2887
3081
  const currentDraft = this.state.noteDraft ?? draft;
3082
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3083
+ const comment = fields.comment;
2888
3084
  if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
2889
3085
  void this.config.actions.createItem({
2890
3086
  kind: "note",
3087
+ title: fields.title,
2891
3088
  comment: this.withDraftAdjustmentComment(comment, currentDraft),
3089
+ assigneeId: fields.assigneeId,
3090
+ assigneeName: fields.assigneeName,
2892
3091
  viewport: currentDraft.viewport,
2893
3092
  anchor: currentDraft.anchor,
2894
3093
  marker: currentDraft.marker,
@@ -2906,10 +3105,14 @@ ${adjustment}` : adjustment;
2906
3105
  const actions = this.createFormActions("Save note", saveDraft, {
2907
3106
  leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
2908
3107
  });
3108
+ const error = this.createDraftError();
2909
3109
  form.append(
2910
3110
  ...meta ? [meta] : [],
2911
3111
  ...adjustmentControls ? [adjustmentControls.panel] : [],
3112
+ ...titleInput ? [titleInput] : [],
2912
3113
  textarea,
3114
+ ...assigneeSelect ? [assigneeSelect] : [],
3115
+ ...error ? [error] : [],
2913
3116
  actions
2914
3117
  );
2915
3118
  const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
@@ -3036,6 +3239,9 @@ ${adjustment}` : adjustment;
3036
3239
  handle.addEventListener("pointerup", stopDrag);
3037
3240
  handle.addEventListener("pointercancel", stopDrag);
3038
3241
  }
3242
+ // Builds the element-adjustment controls (nudge the previewed element via
3243
+ // arrow keys / buttons). Wires keyboard deltas to the draft transform and
3244
+ // keeps the pin, popover, highlight and textarea in sync as the value changes.
3039
3245
  createAdjustmentControls({
3040
3246
  draft,
3041
3247
  pin,
@@ -3180,6 +3386,14 @@ ${adjustment}` : adjustment;
3180
3386
  return form;
3181
3387
  }
3182
3388
  form.append(this.createAreaMetricsPanel(areaDraft));
3389
+ const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(areaDraft.title, (title) => {
3390
+ const draft = this.state.areaDraft;
3391
+ if (!draft) return;
3392
+ this.config.actions.setAreaDraft({
3393
+ ...draft,
3394
+ title
3395
+ });
3396
+ }) : void 0;
3183
3397
  const textarea = document.createElement("textarea");
3184
3398
  textarea.className = "dfwr-textarea";
3185
3399
  textarea.placeholder = "Area comment";
@@ -3193,20 +3407,44 @@ ${adjustment}` : adjustment;
3193
3407
  comment: textarea.value
3194
3408
  });
3195
3409
  });
3410
+ const assigneeSelect = this.createDraftAssigneeSelect(
3411
+ areaDraft.assigneeId,
3412
+ areaDraft.assigneeName,
3413
+ (assigneeId, assigneeName) => {
3414
+ const draft = this.state.areaDraft;
3415
+ if (!draft) return;
3416
+ this.config.actions.setAreaDraft({
3417
+ ...draft,
3418
+ assigneeId,
3419
+ assigneeName
3420
+ });
3421
+ }
3422
+ );
3196
3423
  const actions = this.createFormActions("Save area", () => {
3197
- const comment = textarea.value.trim();
3198
3424
  const draft = this.state.areaDraft;
3425
+ const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3426
+ const comment = fields.comment;
3199
3427
  if (!comment || !draft) return;
3200
3428
  void this.config.actions.createItem({
3201
3429
  kind: "area",
3430
+ title: fields.title,
3202
3431
  comment,
3432
+ assigneeId: fields.assigneeId,
3433
+ assigneeName: fields.assigneeName,
3203
3434
  viewport: draft.viewport,
3204
3435
  anchor: draft.anchor,
3205
3436
  marker: draft.marker,
3206
3437
  selection: draft.selection
3207
3438
  });
3208
3439
  });
3209
- form.append(textarea, actions);
3440
+ const error = this.createDraftError();
3441
+ form.append(
3442
+ ...titleInput ? [titleInput] : [],
3443
+ textarea,
3444
+ ...assigneeSelect ? [assigneeSelect] : [],
3445
+ ...error ? [error] : [],
3446
+ actions
3447
+ );
3210
3448
  return form;
3211
3449
  }
3212
3450
  createAreaMetricsPanel(draft) {
@@ -3298,18 +3536,27 @@ ${adjustment}` : adjustment;
3298
3536
  createFormActions(saveLabel, onSave, options) {
3299
3537
  const actions = document.createElement("div");
3300
3538
  actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
3539
+ const isSaving = this.state.isCreatingItem;
3301
3540
  const save = document.createElement("button");
3302
3541
  save.className = "dfwr-button is-primary";
3303
3542
  save.type = "button";
3304
- save.textContent = saveLabel;
3543
+ save.disabled = isSaving;
3544
+ save.setAttribute("aria-busy", isSaving ? "true" : "false");
3545
+ if (isSaving) {
3546
+ save.append(this.createSpinner("dfwr-spinner"), "Saving...");
3547
+ } else {
3548
+ save.textContent = saveLabel;
3549
+ }
3305
3550
  save.addEventListener("click", (event) => {
3306
3551
  event.preventDefault();
3307
3552
  event.stopPropagation();
3553
+ if (this.state.isCreatingItem) return;
3308
3554
  onSave();
3309
3555
  });
3310
3556
  const cancel = document.createElement("button");
3311
3557
  cancel.className = "dfwr-button";
3312
3558
  cancel.type = "button";
3559
+ cancel.disabled = isSaving;
3313
3560
  cancel.textContent = "Cancel";
3314
3561
  cancel.addEventListener("click", (event) => {
3315
3562
  this.cancelDraft(event);
@@ -3332,6 +3579,20 @@ ${adjustment}` : adjustment;
3332
3579
  actions.append(save, cancel);
3333
3580
  return actions;
3334
3581
  }
3582
+ createSpinner(className) {
3583
+ const spinner = document.createElement("span");
3584
+ spinner.className = className;
3585
+ spinner.setAttribute("aria-hidden", "true");
3586
+ return spinner;
3587
+ }
3588
+ createDraftError() {
3589
+ if (!this.state.draftError) return void 0;
3590
+ const error = document.createElement("p");
3591
+ error.className = "dfwr-form-error";
3592
+ error.setAttribute("role", "alert");
3593
+ error.textContent = this.state.draftError;
3594
+ return error;
3595
+ }
3335
3596
  createList() {
3336
3597
  const section = document.createElement("div");
3337
3598
  section.className = "dfwr-list";
@@ -3384,14 +3645,20 @@ ${adjustment}` : adjustment;
3384
3645
  kind.className = "dfwr-item-kind";
3385
3646
  kind.textContent = item.kind;
3386
3647
  badges.append(scope, kind);
3648
+ const title = this.isTitleFieldEnabled() ? item.title?.trim() : "";
3649
+ const titleElement = title ? document.createElement("strong") : void 0;
3650
+ if (title && titleElement) {
3651
+ titleElement.className = "dfwr-item-title";
3652
+ titleElement.textContent = title;
3653
+ }
3387
3654
  const comment = document.createElement("p");
3388
- comment.className = "dfwr-item-comment";
3655
+ comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
3389
3656
  comment.textContent = item.comment;
3390
3657
  const date = document.createElement("time");
3391
3658
  date.className = "dfwr-item-date";
3392
3659
  date.dateTime = item.createdAt;
3393
3660
  date.textContent = formatItemMeta(item);
3394
- body.append(badges, comment, date);
3661
+ body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
3395
3662
  const actions = document.createElement("div");
3396
3663
  actions.className = "dfwr-item-actions";
3397
3664
  actions.addEventListener("click", (event) => event.stopPropagation());
@@ -3580,7 +3847,14 @@ ${formatItemMeta(item)}`;
3580
3847
  event,
3581
3848
  this.config.getEnvironment()
3582
3849
  );
3583
- void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, textarea.value) : this.config.actions.bindNoteDraftToPoint(nextPoint, textarea.value));
3850
+ const currentDraft = this.state.noteDraft;
3851
+ const fields = {
3852
+ title: currentDraft?.title,
3853
+ comment: textarea.value,
3854
+ assigneeId: currentDraft?.assigneeId,
3855
+ assigneeName: currentDraft?.assigneeName
3856
+ };
3857
+ void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, fields) : this.config.actions.bindNoteDraftToPoint(nextPoint, fields));
3584
3858
  });
3585
3859
  }
3586
3860
  createNoteLayer() {
@@ -3658,10 +3932,14 @@ ${formatItemMeta(item)}`;
3658
3932
  let startX = 0;
3659
3933
  let startY = 0;
3660
3934
  let selection;
3935
+ let activePointerId;
3936
+ let isDragging = false;
3937
+ const ownerWindow = layer.ownerDocument.defaultView ?? window;
3661
3938
  const updateBox = (event) => {
3939
+ const nextEnvironment = this.config.getEnvironment();
3662
3940
  const nextPoint = toTargetPointFromHostEvent(
3663
3941
  event,
3664
- this.config.getEnvironment()
3942
+ nextEnvironment
3665
3943
  );
3666
3944
  const left = Math.min(startX, nextPoint.x);
3667
3945
  const top = Math.min(startY, nextPoint.y);
@@ -3669,7 +3947,7 @@ ${formatItemMeta(item)}`;
3669
3947
  const height = Math.abs(nextPoint.y - startY);
3670
3948
  const hostPoint = toHostPoint(
3671
3949
  { x: left, y: top },
3672
- this.config.getEnvironment()
3950
+ nextEnvironment
3673
3951
  );
3674
3952
  selection = { left, top, width, height };
3675
3953
  box.style.left = `${hostPoint.x}px`;
@@ -3677,9 +3955,68 @@ ${formatItemMeta(item)}`;
3677
3955
  box.style.width = `${width}px`;
3678
3956
  box.style.height = `${height}px`;
3679
3957
  };
3958
+ const addDragListeners = () => {
3959
+ ownerWindow.addEventListener("pointermove", handlePointerMove, true);
3960
+ ownerWindow.addEventListener("pointerup", handlePointerUp, true);
3961
+ ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
3962
+ };
3963
+ const removeDragListeners = () => {
3964
+ ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
3965
+ ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
3966
+ ownerWindow.removeEventListener(
3967
+ "pointercancel",
3968
+ handlePointerCancel,
3969
+ true
3970
+ );
3971
+ };
3972
+ const releasePointerCapture = (event) => {
3973
+ try {
3974
+ if (layer.hasPointerCapture(event.pointerId)) {
3975
+ layer.releasePointerCapture(event.pointerId);
3976
+ }
3977
+ } catch {
3978
+ }
3979
+ };
3980
+ function isActivePointer(event) {
3981
+ return isDragging && event.pointerId === activePointerId;
3982
+ }
3983
+ const finishAreaSelection = (event) => {
3984
+ if (!isActivePointer(event)) return;
3985
+ event.preventDefault();
3986
+ updateBox(event);
3987
+ releasePointerCapture(event);
3988
+ removeDragListeners();
3989
+ isDragging = false;
3990
+ activePointerId = void 0;
3991
+ if (!selection || selection.width < 8 || selection.height < 8) return;
3992
+ this.config.actions.setSelectingArea(true);
3993
+ this.config.actions.render();
3994
+ void this.config.actions.createAreaDraft(selection);
3995
+ };
3996
+ function handlePointerMove(event) {
3997
+ if (!isActivePointer(event)) return;
3998
+ event.preventDefault();
3999
+ updateBox(event);
4000
+ }
4001
+ const handlePointerUp = (event) => {
4002
+ finishAreaSelection(event);
4003
+ };
4004
+ const handlePointerCancel = (event) => {
4005
+ if (!isActivePointer(event)) return;
4006
+ releasePointerCapture(event);
4007
+ removeDragListeners();
4008
+ isDragging = false;
4009
+ activePointerId = void 0;
4010
+ };
3680
4011
  layer.addEventListener("pointerdown", (event) => {
4012
+ if (event.button !== 0) return;
3681
4013
  event.preventDefault();
3682
- layer.setPointerCapture(event.pointerId);
4014
+ activePointerId = event.pointerId;
4015
+ isDragging = true;
4016
+ try {
4017
+ layer.setPointerCapture(event.pointerId);
4018
+ } catch {
4019
+ }
3683
4020
  const startPoint = toTargetPointFromHostEvent(
3684
4021
  event,
3685
4022
  this.config.getEnvironment()
@@ -3687,20 +4024,11 @@ ${formatItemMeta(item)}`;
3687
4024
  startX = startPoint.x;
3688
4025
  startY = startPoint.y;
3689
4026
  updateBox(event);
4027
+ addDragListeners();
3690
4028
  });
3691
- layer.addEventListener("pointermove", (event) => {
3692
- if (!layer.hasPointerCapture(event.pointerId)) return;
3693
- updateBox(event);
3694
- });
3695
- layer.addEventListener("pointerup", (event) => {
3696
- if (!layer.hasPointerCapture(event.pointerId)) return;
3697
- layer.releasePointerCapture(event.pointerId);
3698
- updateBox(event);
3699
- if (!selection || selection.width < 8 || selection.height < 8) return;
3700
- this.config.actions.setSelectingArea(true);
3701
- this.config.actions.render();
3702
- void this.config.actions.createAreaDraft(selection);
3703
- });
4029
+ layer.addEventListener("pointermove", handlePointerMove);
4030
+ layer.addEventListener("pointerup", handlePointerUp);
4031
+ layer.addEventListener("pointercancel", handlePointerCancel);
3704
4032
  return layer;
3705
4033
  }
3706
4034
  };
@@ -3740,6 +4068,8 @@ var WebReviewKitApp = class {
3740
4068
  this.isOpen = false;
3741
4069
  this.mode = "idle";
3742
4070
  this.items = [];
4071
+ this.draftError = "";
4072
+ this.isCreatingItem = false;
3743
4073
  this.isSelectingArea = false;
3744
4074
  this.handleKeyDown = (event) => {
3745
4075
  if (event.key === "Escape" && this.cancelMode()) {
@@ -3770,6 +4100,8 @@ var WebReviewKitApp = class {
3770
4100
  items: this.items,
3771
4101
  noteDraft: this.noteDraft,
3772
4102
  areaDraft: this.areaDraft,
4103
+ draftError: this.draftError,
4104
+ isCreatingItem: this.isCreatingItem,
3773
4105
  isSelectingArea: this.isSelectingArea,
3774
4106
  highlightedItemId: this.highlightedItemId
3775
4107
  }),
@@ -3784,19 +4116,22 @@ var WebReviewKitApp = class {
3784
4116
  clearDrafts: () => {
3785
4117
  this.noteDraft = void 0;
3786
4118
  this.areaDraft = void 0;
4119
+ this.draftError = "";
3787
4120
  },
3788
4121
  setNoteDraft: (draft) => {
3789
4122
  this.noteDraft = draft;
4123
+ this.draftError = "";
3790
4124
  },
3791
4125
  setAreaDraft: (draft) => {
3792
4126
  this.areaDraft = draft;
4127
+ this.draftError = "";
3793
4128
  },
3794
4129
  setSelectingArea: (isSelectingArea) => {
3795
4130
  this.isSelectingArea = isSelectingArea;
3796
4131
  },
3797
4132
  createItem: (input) => this.createItem(input),
3798
- bindNoteDraftToPoint: (point, comment) => this.bindNoteDraftToPoint(point, comment),
3799
- bindElementDraftToPoint: (point, comment) => this.bindElementDraftToPoint(point, comment),
4133
+ bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
4134
+ bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
3800
4135
  createAreaDraft: (selection) => this.createAreaDraft(selection)
3801
4136
  }
3802
4137
  });
@@ -3865,7 +4200,7 @@ var WebReviewKitApp = class {
3865
4200
  this.noteDraft = void 0;
3866
4201
  this.areaDraft = void 0;
3867
4202
  this.isSelectingArea = false;
3868
- await this.bindElementDraftToElement(element, comment);
4203
+ await this.bindElementDraftToElement(element, { comment });
3869
4204
  }
3870
4205
  getMode() {
3871
4206
  return this.mode;
@@ -4008,7 +4343,7 @@ var WebReviewKitApp = class {
4008
4343
  if (!this.shadow) return;
4009
4344
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
4010
4345
  }
4011
- async bindNoteDraftToPoint(point, comment) {
4346
+ async bindNoteDraftToPoint(point, fields = {}) {
4012
4347
  const environment = this.getEnvironment();
4013
4348
  if (!environment) return;
4014
4349
  const viewport = getViewportSize(environment);
@@ -4028,13 +4363,13 @@ var WebReviewKitApp = class {
4028
4363
  viewport,
4029
4364
  anchor,
4030
4365
  marker,
4031
- comment
4366
+ ...fields
4032
4367
  };
4033
4368
  });
4034
4369
  this.noteDraft = draft;
4035
4370
  this.render();
4036
4371
  }
4037
- async bindElementDraftToPoint(point, comment) {
4372
+ async bindElementDraftToPoint(point, fields = {}) {
4038
4373
  const environment = this.getEnvironment();
4039
4374
  if (!environment) return;
4040
4375
  const viewport = getViewportSize(environment);
@@ -4078,14 +4413,14 @@ var WebReviewKitApp = class {
4078
4413
  anchor,
4079
4414
  marker,
4080
4415
  selection: reviewSelection,
4081
- comment,
4416
+ ...fields,
4082
4417
  previewElement
4083
4418
  };
4084
4419
  });
4085
4420
  this.noteDraft = draft;
4086
4421
  this.render();
4087
4422
  }
4088
- async bindElementDraftToElement(element, comment) {
4423
+ async bindElementDraftToElement(element, fields = {}) {
4089
4424
  const environment = this.getEnvironment();
4090
4425
  if (!environment || element.ownerDocument !== environment.document) return;
4091
4426
  const viewport = getViewportSize(environment);
@@ -4118,7 +4453,7 @@ var WebReviewKitApp = class {
4118
4453
  anchor,
4119
4454
  marker,
4120
4455
  selection: reviewSelection,
4121
- comment,
4456
+ ...fields,
4122
4457
  previewElement
4123
4458
  };
4124
4459
  });
@@ -4128,26 +4463,33 @@ var WebReviewKitApp = class {
4128
4463
  }
4129
4464
  async createAreaDraft(selection) {
4130
4465
  const environment = this.getEnvironment();
4131
- if (!environment) return;
4132
- const viewport = getViewportSize(environment);
4133
- this.areaDraft = await this.withOverlayHidden(() => {
4134
- const marker = createSelectionCenterMarker(
4135
- selection,
4136
- void 0,
4137
- environment
4138
- );
4139
- const reviewSelection = {
4140
- viewport: toPublicSelection(selection)
4141
- };
4142
- return {
4143
- viewport,
4144
- marker,
4145
- selection: reviewSelection
4146
- };
4147
- });
4148
- this.isSelectingArea = false;
4149
- this.setModeState("area");
4150
- this.render();
4466
+ if (!environment) {
4467
+ this.isSelectingArea = false;
4468
+ this.render();
4469
+ return;
4470
+ }
4471
+ try {
4472
+ const viewport = getViewportSize(environment);
4473
+ this.areaDraft = await this.withOverlayHidden(() => {
4474
+ const marker = createSelectionCenterMarker(
4475
+ selection,
4476
+ void 0,
4477
+ environment
4478
+ );
4479
+ const reviewSelection = {
4480
+ viewport: toPublicSelection(selection)
4481
+ };
4482
+ return {
4483
+ viewport,
4484
+ marker,
4485
+ selection: reviewSelection
4486
+ };
4487
+ });
4488
+ this.setModeState("area");
4489
+ } finally {
4490
+ this.isSelectingArea = false;
4491
+ this.render();
4492
+ }
4151
4493
  }
4152
4494
  async withOverlayHidden(callback) {
4153
4495
  if (!this.root) return callback();
@@ -4161,11 +4503,16 @@ var WebReviewKitApp = class {
4161
4503
  }
4162
4504
  async createItem(input) {
4163
4505
  const environment = this.getEnvironment();
4164
- if (!environment) return;
4506
+ if (!environment || this.isCreatingItem) return;
4165
4507
  const now = (/* @__PURE__ */ new Date()).toISOString();
4166
4508
  const routeKey = getRouteKey(environment);
4167
4509
  const viewport = input.viewport ?? getViewportSize(environment);
4168
4510
  const createdBy = this.options.userId?.trim();
4511
+ const title = input.title?.trim();
4512
+ const assigneeId = input.assigneeId?.trim() || void 0;
4513
+ const assigneeOption = this.options.assigneeOptions?.find(
4514
+ (option) => option.value === assigneeId
4515
+ );
4169
4516
  const item = {
4170
4517
  id: createId(),
4171
4518
  projectId: this.options.projectId,
@@ -4175,8 +4522,10 @@ var WebReviewKitApp = class {
4175
4522
  normalizedPath: routeKey,
4176
4523
  scope: input.scope ?? getReviewViewportScope(viewport, this.options.viewports?.presets),
4177
4524
  kind: input.kind,
4178
- title: input.comment.split("\n")[0]?.slice(0, 80),
4525
+ title: title || void 0,
4179
4526
  comment: input.comment,
4527
+ assigneeId,
4528
+ assigneeName: input.assigneeName ?? assigneeOption?.label,
4180
4529
  createdBy: createdBy || void 0,
4181
4530
  status: "todo",
4182
4531
  viewport,
@@ -4191,13 +4540,23 @@ var WebReviewKitApp = class {
4191
4540
  createdAt: now,
4192
4541
  updatedAt: now
4193
4542
  };
4194
- const createdItem = await this.adapter.create(item);
4195
- this.setModeState("idle");
4196
- this.noteDraft = void 0;
4197
- this.areaDraft = void 0;
4198
- this.highlightItem(createdItem.id);
4199
- await this.reload();
4200
- await this.options.onCreateItem?.(createdItem);
4543
+ this.draftError = "";
4544
+ this.isCreatingItem = true;
4545
+ this.render();
4546
+ try {
4547
+ const createdItem = await this.adapter.create(item);
4548
+ this.setModeState("idle");
4549
+ this.noteDraft = void 0;
4550
+ this.areaDraft = void 0;
4551
+ this.highlightItem(createdItem.id);
4552
+ await this.reload();
4553
+ await this.options.onCreateItem?.(createdItem);
4554
+ } catch (error) {
4555
+ this.draftError = error instanceof Error ? error.message : "Failed to save QA.";
4556
+ } finally {
4557
+ this.isCreatingItem = false;
4558
+ this.render();
4559
+ }
4201
4560
  }
4202
4561
  async restoreItem(item) {
4203
4562
  this.setModeState("idle");
@@ -4254,6 +4613,7 @@ export {
4254
4613
  REVIEW_WORKFLOW_STATUS_OPTIONS,
4255
4614
  normalizeReviewItemStatus,
4256
4615
  localAdapter,
4616
+ DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
4257
4617
  clamp,
4258
4618
  DEFAULT_REVIEW_VIEWPORTS,
4259
4619
  findReviewViewportPreset,
@@ -4262,6 +4622,7 @@ export {
4262
4622
  getReviewItemScopeLabel,
4263
4623
  getNumberedReviewItems,
4264
4624
  runWithAutoScrollBehavior,
4625
+ reviewTypographyTokens,
4265
4626
  createWebReviewKit
4266
4627
  };
4267
- //# sourceMappingURL=chunk-IN36JHEU.js.map
4628
+ //# sourceMappingURL=chunk-RPVLRULC.js.map