@solhun/feedback-kit-web 0.5.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.cjs CHANGED
@@ -37,8 +37,10 @@ __export(index_exports, {
37
37
  ElementPickingController: () => ElementPickingController,
38
38
  FLOATING_BUTTON_ID: () => import_feedback_kit_core6.FLOATING_BUTTON_ID,
39
39
  FeedbackKit: () => FeedbackKit,
40
+ HintProvider: () => import_feedback_kit_core7.HintProvider,
40
41
  MARKER_KEY_PREFIX: () => MARKER_KEY_PREFIX,
41
42
  MAX_MARKERS_PER_PATH: () => MAX_MARKERS_PER_PATH,
43
+ MODAL_ACTION_HINT_TOGGLE: () => import_feedback_kit_core6.MODAL_ACTION_HINT_TOGGLE,
42
44
  MODAL_ACTION_PICK: () => import_feedback_kit_core6.MODAL_ACTION_PICK,
43
45
  MarkerStore: () => MarkerStore,
44
46
  OWN_UI_ATTR: () => OWN_UI_ATTR,
@@ -59,6 +61,7 @@ __export(index_exports, {
59
61
  isOwnUi: () => isOwnUi2,
60
62
  normalizePin: () => import_feedback_kit_core6.normalizePin,
61
63
  parseSourceAttr: () => import_feedback_kit_core5.parseSourceAttr,
64
+ rankHints: () => import_feedback_kit_core7.rankHints,
62
65
  reactComponentPath: () => reactComponentPath,
63
66
  reactComponentSummary: () => reactComponentSummary,
64
67
  reencodeWebScreenshot: () => reencodeWebScreenshot,
@@ -72,6 +75,7 @@ module.exports = __toCommonJS(index_exports);
72
75
  var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
73
76
  var import_feedback_kit_core5 = require("@solhun/feedback-kit-core");
74
77
  var import_feedback_kit_core6 = require("@solhun/feedback-kit-core");
78
+ var import_feedback_kit_core7 = require("@solhun/feedback-kit-core");
75
79
 
76
80
  // src/react-tree.ts
77
81
  var FRAMEWORK_INTERNALS = /* @__PURE__ */ new Set([
@@ -381,6 +385,11 @@ var ElementPickingController = class {
381
385
  this.shotGeneration = 0;
382
386
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
383
387
  this.capturing = null;
388
+ /**
389
+ * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
390
+ * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
391
+ */
392
+ this.userToggledHints = false;
384
393
  this.onClick = (event) => this.handleClick(event);
385
394
  this.onMouseOver = (event) => this.handleMouseOver(event);
386
395
  this.queue = opts.queue;
@@ -393,6 +402,7 @@ var ElementPickingController = class {
393
402
  this.capture = opts.capture ?? null;
394
403
  this.reencode = opts.reencode ?? null;
395
404
  this.screenshotLimitBytes = opts.screenshotLimitBytes;
405
+ this.rankHintsFor = opts.rankHintsFor ?? null;
396
406
  this.lastPathname = this.getPathname();
397
407
  this.markers = this.store.list(this.lastPathname);
398
408
  this.unsubscribeQueue = this.queue.subscribe?.(() => {
@@ -507,14 +517,22 @@ var ElementPickingController = class {
507
517
  this.onPick(describeElement(element), point);
508
518
  return;
509
519
  }
520
+ const info = describeElement(element);
521
+ this.userToggledHints = false;
522
+ const hints = this.rankHintsFor ? this.rankHintsFor(info) : [];
510
523
  this.popup = {
511
- element: describeElement(element),
524
+ element: info,
512
525
  point,
513
526
  comment: "",
514
527
  canSave: false,
515
528
  screenshot: null,
516
529
  screenshotStatus: this.capture ? "capturing" : "none",
517
- saving: false
530
+ saving: false,
531
+ hints,
532
+ // 코멘트가 비어 있을 때만 펼친다 — 모달과 같은 규칙(계약 원문). 방금 연 팝업은 항상 빈다.
533
+ hintsExpanded: true,
534
+ hintsAvailable: hints.length > 0,
535
+ usedHintIds: []
518
536
  };
519
537
  this.emit();
520
538
  if (this.capture) this.capturing = this.runCapture();
@@ -524,10 +542,48 @@ var ElementPickingController = class {
524
542
  this.popup = {
525
543
  ...this.popup,
526
544
  comment: value,
527
- canSave: this.isSavable(value)
545
+ canSave: this.isSavable(value),
546
+ ...this.hintsPatchFor(value)
528
547
  };
529
548
  this.emit();
530
549
  }
550
+ /**
551
+ * 코멘트가 비었는지에 따라 칩 줄 상태를 갱신하는 조각. `setAnnotationComment`·`applyHint`
552
+ * 양쪽에서 쓴다 — 모달의 `hintsPatchFor` 와 같은 규칙이다.
553
+ */
554
+ hintsPatchFor(nextComment) {
555
+ if (nextComment.trim().length === 0) {
556
+ return { hintsExpanded: true, usedHintIds: [] };
557
+ }
558
+ return this.userToggledHints ? {} : { hintsExpanded: false };
559
+ }
560
+ /**
561
+ * 칩 누름. 이미 눌린 칩이면 아무 일도 하지 않는다. 코멘트가 비어 있으면 초안으로 치환,
562
+ * 아니면 줄바꿈 뒤 이어붙인다 — 모달의 `applyHint` 와 동일한 규칙이다.
563
+ */
564
+ applyHint(hintId) {
565
+ if (!this.popup || this.popup.saving) return;
566
+ if (this.popup.usedHintIds.includes(hintId)) return;
567
+ const hint = this.popup.hints.find((h) => h.id === hintId);
568
+ if (!hint) return;
569
+ const comment = this.popup.comment.length === 0 ? hint.draft : `${this.popup.comment}
570
+ ${hint.draft}`;
571
+ this.popup = {
572
+ ...this.popup,
573
+ comment,
574
+ canSave: this.isSavable(comment),
575
+ usedHintIds: [...this.popup.usedHintIds, hintId],
576
+ ...this.hintsPatchFor(comment)
577
+ };
578
+ this.emit();
579
+ }
580
+ /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
581
+ toggleHints() {
582
+ if (!this.popup) return;
583
+ this.userToggledHints = true;
584
+ this.popup = { ...this.popup, hintsExpanded: !this.popup.hintsExpanded };
585
+ this.emit();
586
+ }
531
587
  /**
532
588
  * 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
533
589
  *
@@ -634,7 +690,9 @@ var ElementPickingController = class {
634
690
  priority: "unset",
635
691
  screenshot: settled.screenshot,
636
692
  pin: settled.point,
637
- element: settled.element
693
+ element: settled.element,
694
+ // 눌린 칩 기록. 리포트 모달과 같은 필드 — 들어온 문이 달라도 채택 신호는 같은 값으로 쌓인다.
695
+ hintIds: settled.usedHintIds
638
696
  };
639
697
  let report;
640
698
  try {
@@ -795,11 +853,100 @@ var reencodeWebScreenshot = async (shot, quality) => {
795
853
 
796
854
  // src/widget.ts
797
855
  var import_feedback_kit_core2 = require("@solhun/feedback-kit-core");
856
+
857
+ // src/storage.ts
858
+ function memoryStorage() {
859
+ const map = /* @__PURE__ */ new Map();
860
+ return {
861
+ getItem: (key) => map.get(key) ?? null,
862
+ setItem: (key, value) => void map.set(key, value),
863
+ removeItem: (key) => void map.delete(key)
864
+ };
865
+ }
866
+ function pickBacking() {
867
+ try {
868
+ const ls = globalThis.localStorage;
869
+ if (!ls) return memoryStorage();
870
+ const probe = "__feedback_kit_probe__";
871
+ ls.setItem(probe, "1");
872
+ ls.removeItem(probe);
873
+ return ls;
874
+ } catch {
875
+ return memoryStorage();
876
+ }
877
+ }
878
+ function createWebStorage(backing) {
879
+ const store = backing ?? pickBacking();
880
+ return {
881
+ async get(key) {
882
+ try {
883
+ return store.getItem(key);
884
+ } catch {
885
+ return null;
886
+ }
887
+ },
888
+ async set(key, value) {
889
+ store.setItem(key, value);
890
+ },
891
+ async remove(key) {
892
+ try {
893
+ store.removeItem(key);
894
+ } catch {
895
+ }
896
+ }
897
+ };
898
+ }
899
+
900
+ // src/widget.ts
901
+ function stripQueryHash(path) {
902
+ const cut = [path.indexOf("?"), path.indexOf("#")].filter((i) => i >= 0);
903
+ return cut.length > 0 ? path.slice(0, Math.min(...cut)) : path;
904
+ }
905
+ function defaultPathname2() {
906
+ const loc = globalThis.location;
907
+ return loc?.pathname ?? "/";
908
+ }
909
+ function hintSourceFrom(queue) {
910
+ const adapter = queue.adapter;
911
+ if (!adapter || typeof adapter.fetchHints !== "function") return null;
912
+ const cacheKey = adapter.hintsCacheKey ?? "default";
913
+ return { fetchHints: adapter.fetchHints.bind(adapter), cacheKey };
914
+ }
915
+ function resolveHintProvider(opts) {
916
+ if (opts.hintProvider) return opts.hintProvider;
917
+ if (opts.hints) {
918
+ return new import_feedback_kit_core2.HintProvider({ source: null, staticHints: opts.hints, cacheKey: "static" });
919
+ }
920
+ const found = hintSourceFrom(opts.queue);
921
+ if (!found) return null;
922
+ return new import_feedback_kit_core2.HintProvider({
923
+ source: { fetchHints: (etag) => found.fetchHints(etag) },
924
+ storage: createWebStorage(),
925
+ cacheKey: found.cacheKey
926
+ });
927
+ }
928
+ var POPUP_HINT_DISPLAY_MAX = 3;
929
+ function rankFor(hintProvider, opts, element, limitOverride) {
930
+ const catalog = hintProvider.catalog();
931
+ if (!catalog) return [];
932
+ const input = {
933
+ platform: "web",
934
+ path: stripQueryHash(opts.getPathname ? opts.getPathname() : defaultPathname2()),
935
+ element,
936
+ components: element ? element.attributes["react-components"] ?? null : null
937
+ };
938
+ const limit = limitOverride !== void 0 ? Math.min(limitOverride, catalog.maxDisplay) : catalog.maxDisplay;
939
+ return (0, import_feedback_kit_core2.rankHints)(catalog.hints, input, limit);
940
+ }
798
941
  function createWebWidget(opts) {
799
942
  const store = opts.store ?? new MarkerStore();
800
943
  let widgetRef = null;
801
944
  const capture = opts.capture === void 0 ? captureWebScreenshot : opts.capture;
802
945
  const reencode = opts.reencode === void 0 ? reencodeWebScreenshot : opts.reencode;
946
+ const hintProvider = resolveHintProvider(opts);
947
+ if (hintProvider) {
948
+ void hintProvider.load().then(() => hintProvider.refresh());
949
+ }
803
950
  const picking = new ElementPickingController({
804
951
  queue: opts.queue,
805
952
  createReport: opts.createReport,
@@ -815,7 +962,8 @@ function createWebWidget(opts) {
815
962
  // 모드는 어느 쪽이든 켜진 채로 둬서 한 번 켜고 여러 요소를 연달아 지목할 수 있다.
816
963
  onPick: opts.pickTarget === "modal" ? (element, point) => {
817
964
  void widgetRef?.openReport({ element, pin: point });
818
- } : void 0
965
+ } : void 0,
966
+ rankHintsFor: hintProvider ? (element) => rankFor(hintProvider, opts, element, POPUP_HINT_DISPLAY_MAX) : null
819
967
  });
820
968
  const widget = new import_feedback_kit_core2.WidgetController({
821
969
  ...opts,
@@ -830,6 +978,13 @@ function createWebWidget(opts) {
830
978
  }
831
979
  });
832
980
  widgetRef = widget;
981
+ const originalOpenReport = widget.openReport.bind(widget);
982
+ widget.openReport = async (openOpts = {}) => {
983
+ if (hintProvider) {
984
+ widget.modal.setHints(rankFor(hintProvider, opts, openOpts.element ?? null));
985
+ }
986
+ await originalOpenReport(openOpts);
987
+ };
833
988
  const originalSubmit = widget.submitReport.bind(widget);
834
989
  widget.submitReport = async () => {
835
990
  const outcome = await originalSubmit();
@@ -854,7 +1009,7 @@ var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
854
1009
  var import_react = require("react");
855
1010
 
856
1011
  // src/version.ts
857
- var VERSION = true ? "0.5.0" : "dev";
1012
+ var VERSION = true ? "0.6.0" : "dev";
858
1013
 
859
1014
  // src/feedback-kit.tsx
860
1015
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -1116,6 +1271,69 @@ function MarkerStatus({ status }) {
1116
1271
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: presentation.text })
1117
1272
  ] });
1118
1273
  }
1274
+ function HintChips({
1275
+ hints,
1276
+ hintsExpanded,
1277
+ usedHintIds,
1278
+ disabled,
1279
+ onApply,
1280
+ onToggle
1281
+ }) {
1282
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 16 }, children: [
1283
+ hintsExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "0 0 7px", color: TOKENS.muted, fontSize: 12 }, children: "\uC774\uB7F0 \uAC74\uAC00\uC694?" }),
1285
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1286
+ "div",
1287
+ {
1288
+ role: "group",
1289
+ "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1290
+ style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1291
+ children: hints.map((hint) => {
1292
+ const used = usedHintIds.includes(hint.id);
1293
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1294
+ "button",
1295
+ {
1296
+ type: "button",
1297
+ "data-fk-focus-id": `hint:${hint.id}`,
1298
+ disabled: used || disabled,
1299
+ "aria-pressed": used,
1300
+ onClick: () => onApply(hint.id),
1301
+ style: {
1302
+ ...baseButton,
1303
+ minHeight: 30,
1304
+ padding: "5px 10px",
1305
+ fontSize: 12,
1306
+ fontWeight: 600,
1307
+ borderRadius: 999,
1308
+ ...used ? {
1309
+ background: TOKENS.subtle,
1310
+ color: TOKENS.muted,
1311
+ borderColor: TOKENS.line,
1312
+ cursor: "default"
1313
+ } : {}
1314
+ },
1315
+ children: hint.label
1316
+ },
1317
+ hint.id
1318
+ );
1319
+ })
1320
+ }
1321
+ )
1322
+ ] }) : null,
1323
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1324
+ "button",
1325
+ {
1326
+ type: "button",
1327
+ "data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_HINT_TOGGLE,
1328
+ "aria-expanded": hintsExpanded,
1329
+ disabled,
1330
+ onClick: onToggle,
1331
+ style: { ...baseButton, minHeight: 28, padding: "4px 10px", fontSize: 12 },
1332
+ children: hintsExpanded ? "\uC81C\uC548 \u25B4" : "\uC81C\uC548 \u25BE"
1333
+ }
1334
+ )
1335
+ ] });
1336
+ }
1119
1337
  function FeedbackKit(props) {
1120
1338
  const rootRef = (0, import_react.useRef)(null);
1121
1339
  const floatingButtonRef = (0, import_react.useRef)(null);
@@ -1138,7 +1356,10 @@ function FeedbackKit(props) {
1138
1356
  store,
1139
1357
  getPathname,
1140
1358
  doc,
1141
- getViewport
1359
+ getViewport,
1360
+ pickTarget,
1361
+ hints,
1362
+ hintProvider
1142
1363
  } = props;
1143
1364
  (0, import_react.useEffect)(() => {
1144
1365
  const created = createWebWidget({
@@ -1150,7 +1371,10 @@ function FeedbackKit(props) {
1150
1371
  store,
1151
1372
  getPathname,
1152
1373
  doc,
1153
- getViewport
1374
+ getViewport,
1375
+ pickTarget,
1376
+ hints,
1377
+ hintProvider
1154
1378
  });
1155
1379
  setKit(created);
1156
1380
  const unsubscribeWidget = created.widget.subscribe(setWidgetState);
@@ -1168,6 +1392,9 @@ function FeedbackKit(props) {
1168
1392
  screenshotLimitBytes,
1169
1393
  store,
1170
1394
  getPathname,
1395
+ pickTarget,
1396
+ hints,
1397
+ hintProvider,
1171
1398
  doc,
1172
1399
  getViewport
1173
1400
  ]);
@@ -1466,6 +1693,17 @@ function FeedbackKit(props) {
1466
1693
  )
1467
1694
  ] })
1468
1695
  ] }),
1696
+ modal.hintsAvailable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1697
+ HintChips,
1698
+ {
1699
+ hints: modal.hints,
1700
+ hintsExpanded: modal.hintsExpanded,
1701
+ usedHintIds: modal.usedHintIds,
1702
+ disabled: draftLocked,
1703
+ onApply: (hintId) => kit.widget.modal.applyHint(hintId),
1704
+ onToggle: () => kit.widget.modal.toggleHints()
1705
+ }
1706
+ ) : null,
1469
1707
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: "feedback-kit-comment", style: { display: "block", fontWeight: 700 }, children: [
1470
1708
  "\uCF54\uBA58\uD2B8 ",
1471
1709
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "*" })
@@ -1722,6 +1960,17 @@ function FeedbackKit(props) {
1722
1960
  ]
1723
1961
  }
1724
1962
  ),
1963
+ popup.hintsAvailable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1964
+ HintChips,
1965
+ {
1966
+ hints: popup.hints,
1967
+ hintsExpanded: popup.hintsExpanded,
1968
+ usedHintIds: popup.usedHintIds,
1969
+ disabled: popup.saving,
1970
+ onApply: (hintId) => kit.picking.applyHint(hintId),
1971
+ onToggle: () => kit.picking.toggleHints()
1972
+ }
1973
+ ) : null,
1725
1974
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "feedback-kit-annotation", style: visuallyHidden, children: "\uC774 \uC694\uC18C\uC5D0 \uB0A8\uAE38 \uC758\uACAC" }),
1726
1975
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1727
1976
  "textarea",
@@ -1822,49 +2071,6 @@ function FeedbackKit(props) {
1822
2071
  );
1823
2072
  }
1824
2073
 
1825
- // src/storage.ts
1826
- function memoryStorage() {
1827
- const map = /* @__PURE__ */ new Map();
1828
- return {
1829
- getItem: (key) => map.get(key) ?? null,
1830
- setItem: (key, value) => void map.set(key, value),
1831
- removeItem: (key) => void map.delete(key)
1832
- };
1833
- }
1834
- function pickBacking() {
1835
- try {
1836
- const ls = globalThis.localStorage;
1837
- if (!ls) return memoryStorage();
1838
- const probe = "__feedback_kit_probe__";
1839
- ls.setItem(probe, "1");
1840
- ls.removeItem(probe);
1841
- return ls;
1842
- } catch {
1843
- return memoryStorage();
1844
- }
1845
- }
1846
- function createWebStorage(backing) {
1847
- const store = backing ?? pickBacking();
1848
- return {
1849
- async get(key) {
1850
- try {
1851
- return store.getItem(key);
1852
- } catch {
1853
- return null;
1854
- }
1855
- },
1856
- async set(key, value) {
1857
- store.setItem(key, value);
1858
- },
1859
- async remove(key) {
1860
- try {
1861
- store.removeItem(key);
1862
- } catch {
1863
- }
1864
- }
1865
- };
1866
- }
1867
-
1868
2074
  // src/providers.ts
1869
2075
  function webContextProviders() {
1870
2076
  return {
@@ -1895,8 +2101,10 @@ function webContextProviders() {
1895
2101
  ElementPickingController,
1896
2102
  FLOATING_BUTTON_ID,
1897
2103
  FeedbackKit,
2104
+ HintProvider,
1898
2105
  MARKER_KEY_PREFIX,
1899
2106
  MAX_MARKERS_PER_PATH,
2107
+ MODAL_ACTION_HINT_TOGGLE,
1900
2108
  MODAL_ACTION_PICK,
1901
2109
  MarkerStore,
1902
2110
  OWN_UI_ATTR,
@@ -1917,6 +2125,7 @@ function webContextProviders() {
1917
2125
  isOwnUi,
1918
2126
  normalizePin,
1919
2127
  parseSourceAttr,
2128
+ rankHints,
1920
2129
  reactComponentPath,
1921
2130
  reactComponentSummary,
1922
2131
  reencodeWebScreenshot,