@solhun/feedback-kit-web 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  COMMENT_TOO_LONG_MESSAGE,
8
8
  denormalizePin,
9
9
  FLOATING_BUTTON_ID as FLOATING_BUTTON_ID2,
10
+ MODAL_ACTION_HINT_TOGGLE as MODAL_ACTION_HINT_TOGGLE2,
10
11
  MODAL_ACTION_PICK as MODAL_ACTION_PICK2,
11
12
  normalizePin as normalizePin2,
12
13
  ReportModalController,
@@ -15,6 +16,7 @@ import {
15
16
  SUBMIT_PENDING_MESSAGE,
16
17
  WidgetController as WidgetController2
17
18
  } from "@solhun/feedback-kit-core";
19
+ import { HintProvider as HintProvider2, rankHints as rankHints2 } from "@solhun/feedback-kit-core";
18
20
 
19
21
  // src/react-tree.ts
20
22
  var FRAMEWORK_INTERNALS = /* @__PURE__ */ new Set([
@@ -328,6 +330,11 @@ var ElementPickingController = class {
328
330
  this.shotGeneration = 0;
329
331
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
330
332
  this.capturing = null;
333
+ /**
334
+ * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
335
+ * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
336
+ */
337
+ this.userToggledHints = false;
331
338
  this.onClick = (event) => this.handleClick(event);
332
339
  this.onMouseOver = (event) => this.handleMouseOver(event);
333
340
  this.queue = opts.queue;
@@ -340,6 +347,7 @@ var ElementPickingController = class {
340
347
  this.capture = opts.capture ?? null;
341
348
  this.reencode = opts.reencode ?? null;
342
349
  this.screenshotLimitBytes = opts.screenshotLimitBytes;
350
+ this.rankHintsFor = opts.rankHintsFor ?? null;
343
351
  this.lastPathname = this.getPathname();
344
352
  this.markers = this.store.list(this.lastPathname);
345
353
  this.unsubscribeQueue = this.queue.subscribe?.(() => {
@@ -454,14 +462,22 @@ var ElementPickingController = class {
454
462
  this.onPick(describeElement(element), point);
455
463
  return;
456
464
  }
465
+ const info = describeElement(element);
466
+ this.userToggledHints = false;
467
+ const hints = this.rankHintsFor ? this.rankHintsFor(info) : [];
457
468
  this.popup = {
458
- element: describeElement(element),
469
+ element: info,
459
470
  point,
460
471
  comment: "",
461
472
  canSave: false,
462
473
  screenshot: null,
463
474
  screenshotStatus: this.capture ? "capturing" : "none",
464
- saving: false
475
+ saving: false,
476
+ hints,
477
+ // 코멘트가 비어 있을 때만 펼친다 — 모달과 같은 규칙(계약 원문). 방금 연 팝업은 항상 빈다.
478
+ hintsExpanded: true,
479
+ hintsAvailable: hints.length > 0,
480
+ usedHintIds: []
465
481
  };
466
482
  this.emit();
467
483
  if (this.capture) this.capturing = this.runCapture();
@@ -471,10 +487,48 @@ var ElementPickingController = class {
471
487
  this.popup = {
472
488
  ...this.popup,
473
489
  comment: value,
474
- canSave: this.isSavable(value)
490
+ canSave: this.isSavable(value),
491
+ ...this.hintsPatchFor(value)
475
492
  };
476
493
  this.emit();
477
494
  }
495
+ /**
496
+ * 코멘트가 비었는지에 따라 칩 줄 상태를 갱신하는 조각. `setAnnotationComment`·`applyHint`
497
+ * 양쪽에서 쓴다 — 모달의 `hintsPatchFor` 와 같은 규칙이다.
498
+ */
499
+ hintsPatchFor(nextComment) {
500
+ if (nextComment.trim().length === 0) {
501
+ return { hintsExpanded: true, usedHintIds: [] };
502
+ }
503
+ return this.userToggledHints ? {} : { hintsExpanded: false };
504
+ }
505
+ /**
506
+ * 칩 누름. 이미 눌린 칩이면 아무 일도 하지 않는다. 코멘트가 비어 있으면 초안으로 치환,
507
+ * 아니면 줄바꿈 뒤 이어붙인다 — 모달의 `applyHint` 와 동일한 규칙이다.
508
+ */
509
+ applyHint(hintId) {
510
+ if (!this.popup || this.popup.saving) return;
511
+ if (this.popup.usedHintIds.includes(hintId)) return;
512
+ const hint = this.popup.hints.find((h) => h.id === hintId);
513
+ if (!hint) return;
514
+ const comment = this.popup.comment.length === 0 ? hint.draft : `${this.popup.comment}
515
+ ${hint.draft}`;
516
+ this.popup = {
517
+ ...this.popup,
518
+ comment,
519
+ canSave: this.isSavable(comment),
520
+ usedHintIds: [...this.popup.usedHintIds, hintId],
521
+ ...this.hintsPatchFor(comment)
522
+ };
523
+ this.emit();
524
+ }
525
+ /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
526
+ toggleHints() {
527
+ if (!this.popup) return;
528
+ this.userToggledHints = true;
529
+ this.popup = { ...this.popup, hintsExpanded: !this.popup.hintsExpanded };
530
+ this.emit();
531
+ }
478
532
  /**
479
533
  * 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
480
534
  *
@@ -581,7 +635,9 @@ var ElementPickingController = class {
581
635
  priority: "unset",
582
636
  screenshot: settled.screenshot,
583
637
  pin: settled.point,
584
- element: settled.element
638
+ element: settled.element,
639
+ // 눌린 칩 기록. 리포트 모달과 같은 필드 — 들어온 문이 달라도 채택 신호는 같은 값으로 쌓인다.
640
+ hintIds: settled.usedHintIds
585
641
  };
586
642
  let report;
587
643
  try {
@@ -741,12 +797,105 @@ var reencodeWebScreenshot = async (shot, quality) => {
741
797
  };
742
798
 
743
799
  // src/widget.ts
744
- import { WidgetController } from "@solhun/feedback-kit-core";
800
+ import {
801
+ WidgetController,
802
+ HintProvider,
803
+ rankHints
804
+ } from "@solhun/feedback-kit-core";
805
+
806
+ // src/storage.ts
807
+ function memoryStorage() {
808
+ const map = /* @__PURE__ */ new Map();
809
+ return {
810
+ getItem: (key) => map.get(key) ?? null,
811
+ setItem: (key, value) => void map.set(key, value),
812
+ removeItem: (key) => void map.delete(key)
813
+ };
814
+ }
815
+ function pickBacking() {
816
+ try {
817
+ const ls = globalThis.localStorage;
818
+ if (!ls) return memoryStorage();
819
+ const probe = "__feedback_kit_probe__";
820
+ ls.setItem(probe, "1");
821
+ ls.removeItem(probe);
822
+ return ls;
823
+ } catch {
824
+ return memoryStorage();
825
+ }
826
+ }
827
+ function createWebStorage(backing) {
828
+ const store = backing ?? pickBacking();
829
+ return {
830
+ async get(key) {
831
+ try {
832
+ return store.getItem(key);
833
+ } catch {
834
+ return null;
835
+ }
836
+ },
837
+ async set(key, value) {
838
+ store.setItem(key, value);
839
+ },
840
+ async remove(key) {
841
+ try {
842
+ store.removeItem(key);
843
+ } catch {
844
+ }
845
+ }
846
+ };
847
+ }
848
+
849
+ // src/widget.ts
850
+ function stripQueryHash(path) {
851
+ const cut = [path.indexOf("?"), path.indexOf("#")].filter((i) => i >= 0);
852
+ return cut.length > 0 ? path.slice(0, Math.min(...cut)) : path;
853
+ }
854
+ function defaultPathname2() {
855
+ const loc = globalThis.location;
856
+ return loc?.pathname ?? "/";
857
+ }
858
+ function hintSourceFrom(queue) {
859
+ const adapter = queue.adapter;
860
+ if (!adapter || typeof adapter.fetchHints !== "function") return null;
861
+ const cacheKey = adapter.hintsCacheKey ?? "default";
862
+ return { fetchHints: adapter.fetchHints.bind(adapter), cacheKey };
863
+ }
864
+ function resolveHintProvider(opts) {
865
+ if (opts.hintProvider) return opts.hintProvider;
866
+ if (opts.hints) {
867
+ return new HintProvider({ source: null, staticHints: opts.hints, cacheKey: "static" });
868
+ }
869
+ const found = hintSourceFrom(opts.queue);
870
+ if (!found) return null;
871
+ return new HintProvider({
872
+ source: { fetchHints: (etag) => found.fetchHints(etag) },
873
+ storage: createWebStorage(),
874
+ cacheKey: found.cacheKey
875
+ });
876
+ }
877
+ var POPUP_HINT_DISPLAY_MAX = 3;
878
+ function rankFor(hintProvider, opts, element, limitOverride) {
879
+ const catalog = hintProvider.catalog();
880
+ if (!catalog) return [];
881
+ const input = {
882
+ platform: "web",
883
+ path: stripQueryHash(opts.getPathname ? opts.getPathname() : defaultPathname2()),
884
+ element,
885
+ components: element ? element.attributes["react-components"] ?? null : null
886
+ };
887
+ const limit = limitOverride !== void 0 ? Math.min(limitOverride, catalog.maxDisplay) : catalog.maxDisplay;
888
+ return rankHints(catalog.hints, input, limit);
889
+ }
745
890
  function createWebWidget(opts) {
746
891
  const store = opts.store ?? new MarkerStore();
747
892
  let widgetRef = null;
748
893
  const capture = opts.capture === void 0 ? captureWebScreenshot : opts.capture;
749
894
  const reencode = opts.reencode === void 0 ? reencodeWebScreenshot : opts.reencode;
895
+ const hintProvider = resolveHintProvider(opts);
896
+ if (hintProvider) {
897
+ void hintProvider.load().then(() => hintProvider.refresh());
898
+ }
750
899
  const picking = new ElementPickingController({
751
900
  queue: opts.queue,
752
901
  createReport: opts.createReport,
@@ -762,7 +911,8 @@ function createWebWidget(opts) {
762
911
  // 모드는 어느 쪽이든 켜진 채로 둬서 한 번 켜고 여러 요소를 연달아 지목할 수 있다.
763
912
  onPick: opts.pickTarget === "modal" ? (element, point) => {
764
913
  void widgetRef?.openReport({ element, pin: point });
765
- } : void 0
914
+ } : void 0,
915
+ rankHintsFor: hintProvider ? (element) => rankFor(hintProvider, opts, element, POPUP_HINT_DISPLAY_MAX) : null
766
916
  });
767
917
  const widget = new WidgetController({
768
918
  ...opts,
@@ -777,6 +927,13 @@ function createWebWidget(opts) {
777
927
  }
778
928
  });
779
929
  widgetRef = widget;
930
+ const originalOpenReport = widget.openReport.bind(widget);
931
+ widget.openReport = async (openOpts = {}) => {
932
+ if (hintProvider) {
933
+ widget.modal.setHints(rankFor(hintProvider, opts, openOpts.element ?? null));
934
+ }
935
+ await originalOpenReport(openOpts);
936
+ };
780
937
  const originalSubmit = widget.submitReport.bind(widget);
781
938
  widget.submitReport = async () => {
782
939
  const outcome = await originalSubmit();
@@ -801,6 +958,7 @@ import {
801
958
  COMMENT_MAX_CHARS as COMMENT_MAX_CHARS2,
802
959
  FLOATING_BUTTON_ID,
803
960
  MODAL_ACTION_CANCEL,
961
+ MODAL_ACTION_HINT_TOGGLE,
804
962
  MODAL_ACTION_PICK,
805
963
  MODAL_ACTION_REMOVE_SCREENSHOT,
806
964
  MODAL_ACTION_RETRY,
@@ -815,7 +973,7 @@ import {
815
973
  } from "react";
816
974
 
817
975
  // src/version.ts
818
- var VERSION = true ? "0.4.0" : "dev";
976
+ var VERSION = true ? "0.6.0" : "dev";
819
977
 
820
978
  // src/feedback-kit.tsx
821
979
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -973,6 +1131,96 @@ var inputStyle = {
973
1131
  padding: "10px 11px",
974
1132
  font: "inherit"
975
1133
  };
1134
+ function PickToggle({
1135
+ active,
1136
+ disabled,
1137
+ variant,
1138
+ focusId,
1139
+ onToggle
1140
+ }) {
1141
+ const floating = variant === "floating";
1142
+ return /* @__PURE__ */ jsxs(
1143
+ "button",
1144
+ {
1145
+ type: "button",
1146
+ role: "switch",
1147
+ "aria-checked": active,
1148
+ "aria-label": "\uC694\uC18C \uC9C0\uBAA9 \uBAA8\uB4DC",
1149
+ "data-fk-pick-toggle": active ? "on" : "off",
1150
+ "data-fk-focus-id": focusId,
1151
+ disabled,
1152
+ onClick: onToggle,
1153
+ style: {
1154
+ ...baseButton,
1155
+ display: "flex",
1156
+ alignItems: "center",
1157
+ gap: 10,
1158
+ opacity: disabled ? 0.5 : 1,
1159
+ cursor: disabled ? "not-allowed" : "pointer",
1160
+ // 항상 값을 준다. 조건부로 빼면 리렌더 때 shorthand(border)와 충돌한다고 React 가 경고한다.
1161
+ borderColor: active ? TOKENS.accent : TOKENS.line,
1162
+ ...floating ? {
1163
+ position: "fixed",
1164
+ top: 16,
1165
+ left: "50%",
1166
+ transform: "translateX(-50%)",
1167
+ // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 토글만 되살린다.
1168
+ pointerEvents: "auto",
1169
+ background: TOKENS.surface,
1170
+ boxShadow: TOKENS.shadow
1171
+ } : {
1172
+ width: "100%",
1173
+ justifyContent: "space-between",
1174
+ marginBottom: 16,
1175
+ background: active ? "rgba(49, 93, 115, 0.06)" : TOKENS.surface
1176
+ }
1177
+ },
1178
+ children: [
1179
+ /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 7 }, children: [
1180
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: active ? TOKENS.accent : TOKENS.muted }, children: "\u25CE" }),
1181
+ "\uC694\uC18C \uC9C0\uBAA9 \uBAA8\uB4DC"
1182
+ ] }),
1183
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
1184
+ /* @__PURE__ */ jsx(
1185
+ "span",
1186
+ {
1187
+ style: { fontSize: 12, fontWeight: 600, color: active ? TOKENS.accent : TOKENS.muted },
1188
+ children: active ? "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1189
+ }
1190
+ ),
1191
+ /* @__PURE__ */ jsx(
1192
+ "span",
1193
+ {
1194
+ style: {
1195
+ position: "relative",
1196
+ display: "inline-block",
1197
+ width: 38,
1198
+ height: 22,
1199
+ borderRadius: 11,
1200
+ background: active ? TOKENS.accent : TOKENS.line
1201
+ },
1202
+ children: /* @__PURE__ */ jsx(
1203
+ "span",
1204
+ {
1205
+ style: {
1206
+ position: "absolute",
1207
+ top: 3,
1208
+ left: active ? 19 : 3,
1209
+ width: 16,
1210
+ height: 16,
1211
+ borderRadius: "50%",
1212
+ background: TOKENS.surface,
1213
+ boxShadow: "0 1px 3px rgba(23, 32, 42, 0.35)"
1214
+ }
1215
+ }
1216
+ )
1217
+ }
1218
+ )
1219
+ ] })
1220
+ ]
1221
+ }
1222
+ );
1223
+ }
976
1224
  function MarkerStatus({ status }) {
977
1225
  const presentation = status === "sending" ? { icon: "\u21BB", text: "\uC804\uC1A1 \uC911", color: TOKENS.accent } : status === "done" ? { icon: "\u2713", text: "\uC644\uB8CC", color: TOKENS.success } : { icon: "\u25F7", text: "\uB300\uAE30", color: TOKENS.warning };
978
1226
  return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
@@ -987,6 +1235,69 @@ function MarkerStatus({ status }) {
987
1235
  /* @__PURE__ */ jsx("span", { children: presentation.text })
988
1236
  ] });
989
1237
  }
1238
+ function HintChips({
1239
+ hints,
1240
+ hintsExpanded,
1241
+ usedHintIds,
1242
+ disabled,
1243
+ onApply,
1244
+ onToggle
1245
+ }) {
1246
+ return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
1247
+ hintsExpanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
1248
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 7px", color: TOKENS.muted, fontSize: 12 }, children: "\uC774\uB7F0 \uAC74\uAC00\uC694?" }),
1249
+ /* @__PURE__ */ jsx(
1250
+ "div",
1251
+ {
1252
+ role: "group",
1253
+ "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1254
+ style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1255
+ children: hints.map((hint) => {
1256
+ const used = usedHintIds.includes(hint.id);
1257
+ return /* @__PURE__ */ jsx(
1258
+ "button",
1259
+ {
1260
+ type: "button",
1261
+ "data-fk-focus-id": `hint:${hint.id}`,
1262
+ disabled: used || disabled,
1263
+ "aria-pressed": used,
1264
+ onClick: () => onApply(hint.id),
1265
+ style: {
1266
+ ...baseButton,
1267
+ minHeight: 30,
1268
+ padding: "5px 10px",
1269
+ fontSize: 12,
1270
+ fontWeight: 600,
1271
+ borderRadius: 999,
1272
+ ...used ? {
1273
+ background: TOKENS.subtle,
1274
+ color: TOKENS.muted,
1275
+ borderColor: TOKENS.line,
1276
+ cursor: "default"
1277
+ } : {}
1278
+ },
1279
+ children: hint.label
1280
+ },
1281
+ hint.id
1282
+ );
1283
+ })
1284
+ }
1285
+ )
1286
+ ] }) : null,
1287
+ /* @__PURE__ */ jsx(
1288
+ "button",
1289
+ {
1290
+ type: "button",
1291
+ "data-fk-focus-id": MODAL_ACTION_HINT_TOGGLE,
1292
+ "aria-expanded": hintsExpanded,
1293
+ disabled,
1294
+ onClick: onToggle,
1295
+ style: { ...baseButton, minHeight: 28, padding: "4px 10px", fontSize: 12 },
1296
+ children: hintsExpanded ? "\uC81C\uC548 \u25B4" : "\uC81C\uC548 \u25BE"
1297
+ }
1298
+ )
1299
+ ] });
1300
+ }
990
1301
  function FeedbackKit(props) {
991
1302
  const rootRef = useRef(null);
992
1303
  const floatingButtonRef = useRef(null);
@@ -1009,7 +1320,10 @@ function FeedbackKit(props) {
1009
1320
  store,
1010
1321
  getPathname,
1011
1322
  doc,
1012
- getViewport
1323
+ getViewport,
1324
+ pickTarget,
1325
+ hints,
1326
+ hintProvider
1013
1327
  } = props;
1014
1328
  useEffect(() => {
1015
1329
  const created = createWebWidget({
@@ -1021,7 +1335,10 @@ function FeedbackKit(props) {
1021
1335
  store,
1022
1336
  getPathname,
1023
1337
  doc,
1024
- getViewport
1338
+ getViewport,
1339
+ pickTarget,
1340
+ hints,
1341
+ hintProvider
1025
1342
  });
1026
1343
  setKit(created);
1027
1344
  const unsubscribeWidget = created.widget.subscribe(setWidgetState);
@@ -1039,6 +1356,9 @@ function FeedbackKit(props) {
1039
1356
  screenshotLimitBytes,
1040
1357
  store,
1041
1358
  getPathname,
1359
+ pickTarget,
1360
+ hints,
1361
+ hintProvider,
1042
1362
  doc,
1043
1363
  getViewport
1044
1364
  ]);
@@ -1081,8 +1401,12 @@ function FeedbackKit(props) {
1081
1401
  return /* @__PURE__ */ jsx("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
1082
1402
  }
1083
1403
  const activeKit = kit;
1084
- const { modal, screen } = widgetState;
1404
+ const { modal, screen, pickingActive } = widgetState;
1085
1405
  const draftLocked = modal.submitStatus === "sending" || modal.submitStatus === "pending";
1406
+ function togglePicking() {
1407
+ if (pickingActive) activeKit.widget.stopPicking();
1408
+ else activeKit.widget.startPicking();
1409
+ }
1086
1410
  async function withOwnUiHidden(action) {
1087
1411
  const root = rootRef.current;
1088
1412
  const previousVisibility = root?.style.visibility ?? "";
@@ -1154,8 +1478,7 @@ function FeedbackKit(props) {
1154
1478
  ref: floatingButtonRef,
1155
1479
  id: FLOATING_BUTTON_ID,
1156
1480
  type: "button",
1157
- disabled: screen === "picking",
1158
- "aria-label": screen === "picking" ? "\uC694\uC18C \uC9C0\uBAA9 \uC911" : "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1481
+ "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1159
1482
  onClick: () => void openReport(),
1160
1483
  style: {
1161
1484
  ...primaryButton,
@@ -1166,11 +1489,9 @@ function FeedbackKit(props) {
1166
1489
  minWidth: 112,
1167
1490
  minHeight: 46,
1168
1491
  borderRadius: 24,
1169
- boxShadow: TOKENS.shadow,
1170
- opacity: screen === "picking" ? 0.76 : 1,
1171
- cursor: screen === "picking" ? "default" : "pointer"
1492
+ boxShadow: TOKENS.shadow
1172
1493
  },
1173
- children: screen === "picking" ? "\uC9C0\uBAA9 \uC911" : "\uD53C\uB4DC\uBC31"
1494
+ children: "\uD53C\uB4DC\uBC31"
1174
1495
  }
1175
1496
  ) : null,
1176
1497
  announcement && !modal.open ? /* @__PURE__ */ jsx(
@@ -1254,7 +1575,17 @@ function FeedbackKit(props) {
1254
1575
  ]
1255
1576
  }
1256
1577
  ),
1257
- /* @__PURE__ */ jsx("p", { style: { margin: "0 0 18px", color: TOKENS.muted }, children: "\uD604\uC7AC \uD654\uBA74\uC758 \uBB38\uC81C\uB098 \uC758\uACAC\uC744 \uB0A8\uACA8\uC8FC\uC138\uC694." }),
1578
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 14px", color: TOKENS.muted }, children: "\uD604\uC7AC \uD654\uBA74\uC758 \uBB38\uC81C\uB098 \uC758\uACAC\uC744 \uB0A8\uACA8\uC8FC\uC138\uC694." }),
1579
+ /* @__PURE__ */ jsx(
1580
+ PickToggle,
1581
+ {
1582
+ variant: "inline",
1583
+ active: pickingActive,
1584
+ disabled: draftLocked,
1585
+ focusId: MODAL_ACTION_PICK,
1586
+ onToggle: togglePicking
1587
+ }
1588
+ ),
1258
1589
  /* @__PURE__ */ jsxs("section", { "aria-labelledby": "feedback-kit-screenshot-label", style: { marginBottom: 16 }, children: [
1259
1590
  /* @__PURE__ */ jsx("strong", { id: "feedback-kit-screenshot-label", children: "\uC2A4\uD06C\uB9B0\uC0F7" }),
1260
1591
  /* @__PURE__ */ jsx(
@@ -1326,6 +1657,17 @@ function FeedbackKit(props) {
1326
1657
  )
1327
1658
  ] })
1328
1659
  ] }),
1660
+ modal.hintsAvailable ? /* @__PURE__ */ jsx(
1661
+ HintChips,
1662
+ {
1663
+ hints: modal.hints,
1664
+ hintsExpanded: modal.hintsExpanded,
1665
+ usedHintIds: modal.usedHintIds,
1666
+ disabled: draftLocked,
1667
+ onApply: (hintId) => kit.widget.modal.applyHint(hintId),
1668
+ onToggle: () => kit.widget.modal.toggleHints()
1669
+ }
1670
+ ) : null,
1329
1671
  /* @__PURE__ */ jsxs("label", { htmlFor: "feedback-kit-comment", style: { display: "block", fontWeight: 700 }, children: [
1330
1672
  "\uCF54\uBA58\uD2B8 ",
1331
1673
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "*" })
@@ -1368,19 +1710,6 @@ function FeedbackKit(props) {
1368
1710
  ]
1369
1711
  }
1370
1712
  ),
1371
- /* @__PURE__ */ jsx(
1372
- "button",
1373
- {
1374
- type: "button",
1375
- role: "switch",
1376
- "aria-checked": "false",
1377
- disabled: draftLocked,
1378
- "data-fk-focus-id": MODAL_ACTION_PICK,
1379
- onClick: () => kit.widget.startPicking(),
1380
- style: { ...baseButton, width: "100%", marginTop: 16 },
1381
- children: "\uC694\uC18C \uC9C0\uBAA9"
1382
- }
1383
- ),
1384
1713
  modal.submitStatus !== "idle" || modal.pending > 0 ? /* @__PURE__ */ jsxs(
1385
1714
  "div",
1386
1715
  {
@@ -1503,22 +1832,7 @@ function FeedbackKit(props) {
1503
1832
  }
1504
1833
  }
1505
1834
  ) : null,
1506
- /* @__PURE__ */ jsx(
1507
- "button",
1508
- {
1509
- type: "button",
1510
- onClick: () => kit.widget.stopPicking(),
1511
- style: {
1512
- ...primaryButton,
1513
- position: "fixed",
1514
- top: 18,
1515
- right: 18,
1516
- pointerEvents: "auto",
1517
- boxShadow: TOKENS.shadow
1518
- },
1519
- children: "\uC9C0\uBAA9 \uC885\uB8CC"
1520
- }
1521
- ),
1835
+ /* @__PURE__ */ jsx(PickToggle, { variant: "floating", active: true, onToggle: togglePicking }),
1522
1836
  pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
1523
1837
  "div",
1524
1838
  {
@@ -1610,6 +1924,17 @@ function FeedbackKit(props) {
1610
1924
  ]
1611
1925
  }
1612
1926
  ),
1927
+ popup.hintsAvailable ? /* @__PURE__ */ jsx(
1928
+ HintChips,
1929
+ {
1930
+ hints: popup.hints,
1931
+ hintsExpanded: popup.hintsExpanded,
1932
+ usedHintIds: popup.usedHintIds,
1933
+ disabled: popup.saving,
1934
+ onApply: (hintId) => kit.picking.applyHint(hintId),
1935
+ onToggle: () => kit.picking.toggleHints()
1936
+ }
1937
+ ) : null,
1613
1938
  /* @__PURE__ */ jsx("label", { htmlFor: "feedback-kit-annotation", style: visuallyHidden, children: "\uC774 \uC694\uC18C\uC5D0 \uB0A8\uAE38 \uC758\uACAC" }),
1614
1939
  /* @__PURE__ */ jsx(
1615
1940
  "textarea",
@@ -1710,49 +2035,6 @@ function FeedbackKit(props) {
1710
2035
  );
1711
2036
  }
1712
2037
 
1713
- // src/storage.ts
1714
- function memoryStorage() {
1715
- const map = /* @__PURE__ */ new Map();
1716
- return {
1717
- getItem: (key) => map.get(key) ?? null,
1718
- setItem: (key, value) => void map.set(key, value),
1719
- removeItem: (key) => void map.delete(key)
1720
- };
1721
- }
1722
- function pickBacking() {
1723
- try {
1724
- const ls = globalThis.localStorage;
1725
- if (!ls) return memoryStorage();
1726
- const probe = "__feedback_kit_probe__";
1727
- ls.setItem(probe, "1");
1728
- ls.removeItem(probe);
1729
- return ls;
1730
- } catch {
1731
- return memoryStorage();
1732
- }
1733
- }
1734
- function createWebStorage(backing) {
1735
- const store = backing ?? pickBacking();
1736
- return {
1737
- async get(key) {
1738
- try {
1739
- return store.getItem(key);
1740
- } catch {
1741
- return null;
1742
- }
1743
- },
1744
- async set(key, value) {
1745
- store.setItem(key, value);
1746
- },
1747
- async remove(key) {
1748
- try {
1749
- store.removeItem(key);
1750
- } catch {
1751
- }
1752
- }
1753
- };
1754
- }
1755
-
1756
2038
  // src/providers.ts
1757
2039
  function webContextProviders() {
1758
2040
  return {
@@ -1782,8 +2064,10 @@ export {
1782
2064
  ElementPickingController,
1783
2065
  FLOATING_BUTTON_ID2 as FLOATING_BUTTON_ID,
1784
2066
  FeedbackKit,
2067
+ HintProvider2 as HintProvider,
1785
2068
  MARKER_KEY_PREFIX,
1786
2069
  MAX_MARKERS_PER_PATH,
2070
+ MODAL_ACTION_HINT_TOGGLE2 as MODAL_ACTION_HINT_TOGGLE,
1787
2071
  MODAL_ACTION_PICK2 as MODAL_ACTION_PICK,
1788
2072
  MarkerStore,
1789
2073
  OWN_UI_ATTR,
@@ -1804,6 +2088,7 @@ export {
1804
2088
  isOwnUi2 as isOwnUi,
1805
2089
  normalizePin2 as normalizePin,
1806
2090
  parseSourceAttr,
2091
+ rankHints2 as rankHints,
1807
2092
  reactComponentPath,
1808
2093
  reactComponentSummary,
1809
2094
  reencodeWebScreenshot,