@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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, WidgetControllerOpts, WidgetController, FeedbackStorage, ContextProviders } from '@solhun/feedback-kit-core';
2
- export { COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, ElementInfo, FLOATING_BUTTON_ID, FeedbackAdapter, FeedbackConfig, FeedbackContext, FeedbackPin, FeedbackReport, FeedbackStorage, FeedbackUser, MODAL_ACTION_PICK, ModalSubmitStatus, ReportModalController, ReportModalState, ResolvedConfig, SCREENSHOT_FAILED_MESSAGE, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_PENDING_MESSAGE, ScreenshotStatus, SubmitResult, Visibility, VisibilityEnv, VisibilityFn, WidgetController, WidgetPosition, WidgetScreen, WidgetState, denormalizePin, normalizePin, parseSourceAttr, resolveConfig, shouldShowWidget, sourceFromElement } from '@solhun/feedback-kit-core';
1
+ import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackHint, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, WidgetControllerOpts, HintProvider, WidgetController, FeedbackStorage, ContextProviders } from '@solhun/feedback-kit-core';
2
+ export { COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, ElementInfo, ElementLike, FLOATING_BUTTON_ID, FeedbackAdapter, FeedbackConfig, FeedbackContext, FeedbackHint, FeedbackPin, FeedbackReport, FeedbackStorage, FeedbackUser, HintCatalog, HintElementRule, HintPlatform, HintProvider, HintRankInput, HintScope, MODAL_ACTION_HINT_TOGGLE, MODAL_ACTION_PICK, ModalSubmitStatus, ReportModalController, ReportModalState, ResolvedConfig, SCREENSHOT_FAILED_MESSAGE, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_PENDING_MESSAGE, ScreenshotStatus, SubmitResult, Visibility, VisibilityEnv, VisibilityFn, WidgetController, WidgetPosition, WidgetScreen, WidgetState, denormalizePin, normalizePin, parseSourceAttr, rankHints, resolveConfig, shouldShowWidget, sourceFromElement } from '@solhun/feedback-kit-core';
3
3
  import * as react from 'react';
4
4
 
5
5
  /** 요소 텍스트를 담을 때의 상한. 본문 전체가 실려 페이로드가 붓는 걸 막는다. */
@@ -96,6 +96,16 @@ interface AnnotationPopupState {
96
96
  screenshotStatus: AnnotationShotStatus;
97
97
  /** 저장 중인가. 두 번 눌러 같은 주석이 두 번 가는 것을 막는다. */
98
98
  saving: boolean;
99
+ /**
100
+ * 자주 나오는 제보 제안 칩. 리포트 모달과 같은 계약(같은 `rankHints` 결과, 같은 접힘 규칙,
101
+ * 같은 `hintIds` 흐름)이다 — "들어온 문이 달라도 담기는 정보는 같다"가 이 위젯의 약속이고,
102
+ * 제안 칩도 예외가 아니다. `widget.ts` 가 만든 `HintProvider` 를 그대로 물려 쓴다
103
+ * (팝업이 자기 카탈로그를 따로 들고 있지 않는다).
104
+ */
105
+ hints: readonly FeedbackHint[];
106
+ hintsExpanded: boolean;
107
+ hintsAvailable: boolean;
108
+ usedHintIds: readonly string[];
99
109
  }
100
110
  interface PickingState {
101
111
  active: boolean;
@@ -136,6 +146,11 @@ interface ElementPickingOpts {
136
146
  * 요소 하나 고를 때마다 모드가 꺼지면 매번 다시 켜야 한다.
137
147
  */
138
148
  onPick?: (element: ElementInfo | null, point: FeedbackPin) => void;
149
+ /**
150
+ * 힌트 랭킹 계산. `widget.ts` 가 만든 `HintProvider` 의 카탈로그를 그 시점의 경로·요소로
151
+ * `rankHints` 한 결과를 돌려준다. 없으면(힌트 기능이 꺼져 있으면) 팝업에 칩이 안 뜬다.
152
+ */
153
+ rankHintsFor?: ((element: ElementInfo | null) => readonly FeedbackHint[]) | null;
139
154
  }
140
155
  declare class ElementPickingController {
141
156
  private readonly queue;
@@ -149,6 +164,7 @@ declare class ElementPickingController {
149
164
  private readonly reencode;
150
165
  private readonly screenshotLimitBytes;
151
166
  private readonly unsubscribeQueue;
167
+ private readonly rankHintsFor;
152
168
  private readonly listeners;
153
169
  private active;
154
170
  private attached;
@@ -162,6 +178,11 @@ declare class ElementPickingController {
162
178
  private shotGeneration;
163
179
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
164
180
  private capturing;
181
+ /**
182
+ * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
183
+ * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
184
+ */
185
+ private userToggledHints;
165
186
  private readonly onClick;
166
187
  private readonly onMouseOver;
167
188
  constructor(opts: ElementPickingOpts);
@@ -189,6 +210,18 @@ declare class ElementPickingController {
189
210
  y: number;
190
211
  }): void;
191
212
  setAnnotationComment(value: string): void;
213
+ /**
214
+ * 코멘트가 비었는지에 따라 칩 줄 상태를 갱신하는 조각. `setAnnotationComment`·`applyHint`
215
+ * 양쪽에서 쓴다 — 모달의 `hintsPatchFor` 와 같은 규칙이다.
216
+ */
217
+ private hintsPatchFor;
218
+ /**
219
+ * 칩 누름. 이미 눌린 칩이면 아무 일도 하지 않는다. 코멘트가 비어 있으면 초안으로 치환,
220
+ * 아니면 줄바꿈 뒤 이어붙인다 — 모달의 `applyHint` 와 동일한 규칙이다.
221
+ */
222
+ applyHint(hintId: string): void;
223
+ /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
224
+ toggleHints(): void;
192
225
  /**
193
226
  * 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
194
227
  *
@@ -268,6 +301,13 @@ interface WebWidgetOpts extends Omit<WidgetControllerOpts, "platform" | "initial
268
301
  };
269
302
  /** 요소를 고른 뒤 띄울 UI. 기본은 `popup`. */
270
303
  pickTarget?: PickTarget;
304
+ /**
305
+ * 정적 힌트 목록(설정으로 직접 넘김). 주면 서버에서 받아오지 않는다 — 도입 초기·오프라인
306
+ * 데모·테스트용이다. 서버 목록과 합치지 않는다(둘을 섞으면 무엇이 보이는지 설명이 안 된다).
307
+ */
308
+ hints?: readonly FeedbackHint[];
309
+ /** 힌트 조회 컨트롤러 주입(테스트용). 주면 큐 어댑터의 fetchHints 를 보지 않는다. */
310
+ hintProvider?: HintProvider;
271
311
  }
272
312
  interface WebWidget {
273
313
  widget: WidgetController;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, WidgetControllerOpts, WidgetController, FeedbackStorage, ContextProviders } from '@solhun/feedback-kit-core';
2
- export { COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, ElementInfo, FLOATING_BUTTON_ID, FeedbackAdapter, FeedbackConfig, FeedbackContext, FeedbackPin, FeedbackReport, FeedbackStorage, FeedbackUser, MODAL_ACTION_PICK, ModalSubmitStatus, ReportModalController, ReportModalState, ResolvedConfig, SCREENSHOT_FAILED_MESSAGE, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_PENDING_MESSAGE, ScreenshotStatus, SubmitResult, Visibility, VisibilityEnv, VisibilityFn, WidgetController, WidgetPosition, WidgetScreen, WidgetState, denormalizePin, normalizePin, parseSourceAttr, resolveConfig, shouldShowWidget, sourceFromElement } from '@solhun/feedback-kit-core';
1
+ import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackHint, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, WidgetControllerOpts, HintProvider, WidgetController, FeedbackStorage, ContextProviders } from '@solhun/feedback-kit-core';
2
+ export { COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, ElementInfo, ElementLike, FLOATING_BUTTON_ID, FeedbackAdapter, FeedbackConfig, FeedbackContext, FeedbackHint, FeedbackPin, FeedbackReport, FeedbackStorage, FeedbackUser, HintCatalog, HintElementRule, HintPlatform, HintProvider, HintRankInput, HintScope, MODAL_ACTION_HINT_TOGGLE, MODAL_ACTION_PICK, ModalSubmitStatus, ReportModalController, ReportModalState, ResolvedConfig, SCREENSHOT_FAILED_MESSAGE, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_PENDING_MESSAGE, ScreenshotStatus, SubmitResult, Visibility, VisibilityEnv, VisibilityFn, WidgetController, WidgetPosition, WidgetScreen, WidgetState, denormalizePin, normalizePin, parseSourceAttr, rankHints, resolveConfig, shouldShowWidget, sourceFromElement } from '@solhun/feedback-kit-core';
3
3
  import * as react from 'react';
4
4
 
5
5
  /** 요소 텍스트를 담을 때의 상한. 본문 전체가 실려 페이로드가 붓는 걸 막는다. */
@@ -96,6 +96,16 @@ interface AnnotationPopupState {
96
96
  screenshotStatus: AnnotationShotStatus;
97
97
  /** 저장 중인가. 두 번 눌러 같은 주석이 두 번 가는 것을 막는다. */
98
98
  saving: boolean;
99
+ /**
100
+ * 자주 나오는 제보 제안 칩. 리포트 모달과 같은 계약(같은 `rankHints` 결과, 같은 접힘 규칙,
101
+ * 같은 `hintIds` 흐름)이다 — "들어온 문이 달라도 담기는 정보는 같다"가 이 위젯의 약속이고,
102
+ * 제안 칩도 예외가 아니다. `widget.ts` 가 만든 `HintProvider` 를 그대로 물려 쓴다
103
+ * (팝업이 자기 카탈로그를 따로 들고 있지 않는다).
104
+ */
105
+ hints: readonly FeedbackHint[];
106
+ hintsExpanded: boolean;
107
+ hintsAvailable: boolean;
108
+ usedHintIds: readonly string[];
99
109
  }
100
110
  interface PickingState {
101
111
  active: boolean;
@@ -136,6 +146,11 @@ interface ElementPickingOpts {
136
146
  * 요소 하나 고를 때마다 모드가 꺼지면 매번 다시 켜야 한다.
137
147
  */
138
148
  onPick?: (element: ElementInfo | null, point: FeedbackPin) => void;
149
+ /**
150
+ * 힌트 랭킹 계산. `widget.ts` 가 만든 `HintProvider` 의 카탈로그를 그 시점의 경로·요소로
151
+ * `rankHints` 한 결과를 돌려준다. 없으면(힌트 기능이 꺼져 있으면) 팝업에 칩이 안 뜬다.
152
+ */
153
+ rankHintsFor?: ((element: ElementInfo | null) => readonly FeedbackHint[]) | null;
139
154
  }
140
155
  declare class ElementPickingController {
141
156
  private readonly queue;
@@ -149,6 +164,7 @@ declare class ElementPickingController {
149
164
  private readonly reencode;
150
165
  private readonly screenshotLimitBytes;
151
166
  private readonly unsubscribeQueue;
167
+ private readonly rankHintsFor;
152
168
  private readonly listeners;
153
169
  private active;
154
170
  private attached;
@@ -162,6 +178,11 @@ declare class ElementPickingController {
162
178
  private shotGeneration;
163
179
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
164
180
  private capturing;
181
+ /**
182
+ * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
183
+ * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
184
+ */
185
+ private userToggledHints;
165
186
  private readonly onClick;
166
187
  private readonly onMouseOver;
167
188
  constructor(opts: ElementPickingOpts);
@@ -189,6 +210,18 @@ declare class ElementPickingController {
189
210
  y: number;
190
211
  }): void;
191
212
  setAnnotationComment(value: string): void;
213
+ /**
214
+ * 코멘트가 비었는지에 따라 칩 줄 상태를 갱신하는 조각. `setAnnotationComment`·`applyHint`
215
+ * 양쪽에서 쓴다 — 모달의 `hintsPatchFor` 와 같은 규칙이다.
216
+ */
217
+ private hintsPatchFor;
218
+ /**
219
+ * 칩 누름. 이미 눌린 칩이면 아무 일도 하지 않는다. 코멘트가 비어 있으면 초안으로 치환,
220
+ * 아니면 줄바꿈 뒤 이어붙인다 — 모달의 `applyHint` 와 동일한 규칙이다.
221
+ */
222
+ applyHint(hintId: string): void;
223
+ /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
224
+ toggleHints(): void;
192
225
  /**
193
226
  * 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
194
227
  *
@@ -268,6 +301,13 @@ interface WebWidgetOpts extends Omit<WidgetControllerOpts, "platform" | "initial
268
301
  };
269
302
  /** 요소를 고른 뒤 띄울 UI. 기본은 `popup`. */
270
303
  pickTarget?: PickTarget;
304
+ /**
305
+ * 정적 힌트 목록(설정으로 직접 넘김). 주면 서버에서 받아오지 않는다 — 도입 초기·오프라인
306
+ * 데모·테스트용이다. 서버 목록과 합치지 않는다(둘을 섞으면 무엇이 보이는지 설명이 안 된다).
307
+ */
308
+ hints?: readonly FeedbackHint[];
309
+ /** 힌트 조회 컨트롤러 주입(테스트용). 주면 큐 어댑터의 fetchHints 를 보지 않는다. */
310
+ hintProvider?: HintProvider;
271
311
  }
272
312
  interface WebWidget {
273
313
  widget: WidgetController;
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.5.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";
@@ -1077,6 +1235,69 @@ function MarkerStatus({ status }) {
1077
1235
  /* @__PURE__ */ jsx("span", { children: presentation.text })
1078
1236
  ] });
1079
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
+ }
1080
1301
  function FeedbackKit(props) {
1081
1302
  const rootRef = useRef(null);
1082
1303
  const floatingButtonRef = useRef(null);
@@ -1099,7 +1320,10 @@ function FeedbackKit(props) {
1099
1320
  store,
1100
1321
  getPathname,
1101
1322
  doc,
1102
- getViewport
1323
+ getViewport,
1324
+ pickTarget,
1325
+ hints,
1326
+ hintProvider
1103
1327
  } = props;
1104
1328
  useEffect(() => {
1105
1329
  const created = createWebWidget({
@@ -1111,7 +1335,10 @@ function FeedbackKit(props) {
1111
1335
  store,
1112
1336
  getPathname,
1113
1337
  doc,
1114
- getViewport
1338
+ getViewport,
1339
+ pickTarget,
1340
+ hints,
1341
+ hintProvider
1115
1342
  });
1116
1343
  setKit(created);
1117
1344
  const unsubscribeWidget = created.widget.subscribe(setWidgetState);
@@ -1129,6 +1356,9 @@ function FeedbackKit(props) {
1129
1356
  screenshotLimitBytes,
1130
1357
  store,
1131
1358
  getPathname,
1359
+ pickTarget,
1360
+ hints,
1361
+ hintProvider,
1132
1362
  doc,
1133
1363
  getViewport
1134
1364
  ]);
@@ -1427,6 +1657,17 @@ function FeedbackKit(props) {
1427
1657
  )
1428
1658
  ] })
1429
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,
1430
1671
  /* @__PURE__ */ jsxs("label", { htmlFor: "feedback-kit-comment", style: { display: "block", fontWeight: 700 }, children: [
1431
1672
  "\uCF54\uBA58\uD2B8 ",
1432
1673
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "*" })
@@ -1683,6 +1924,17 @@ function FeedbackKit(props) {
1683
1924
  ]
1684
1925
  }
1685
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,
1686
1938
  /* @__PURE__ */ jsx("label", { htmlFor: "feedback-kit-annotation", style: visuallyHidden, children: "\uC774 \uC694\uC18C\uC5D0 \uB0A8\uAE38 \uC758\uACAC" }),
1687
1939
  /* @__PURE__ */ jsx(
1688
1940
  "textarea",
@@ -1783,49 +2035,6 @@ function FeedbackKit(props) {
1783
2035
  );
1784
2036
  }
1785
2037
 
1786
- // src/storage.ts
1787
- function memoryStorage() {
1788
- const map = /* @__PURE__ */ new Map();
1789
- return {
1790
- getItem: (key) => map.get(key) ?? null,
1791
- setItem: (key, value) => void map.set(key, value),
1792
- removeItem: (key) => void map.delete(key)
1793
- };
1794
- }
1795
- function pickBacking() {
1796
- try {
1797
- const ls = globalThis.localStorage;
1798
- if (!ls) return memoryStorage();
1799
- const probe = "__feedback_kit_probe__";
1800
- ls.setItem(probe, "1");
1801
- ls.removeItem(probe);
1802
- return ls;
1803
- } catch {
1804
- return memoryStorage();
1805
- }
1806
- }
1807
- function createWebStorage(backing) {
1808
- const store = backing ?? pickBacking();
1809
- return {
1810
- async get(key) {
1811
- try {
1812
- return store.getItem(key);
1813
- } catch {
1814
- return null;
1815
- }
1816
- },
1817
- async set(key, value) {
1818
- store.setItem(key, value);
1819
- },
1820
- async remove(key) {
1821
- try {
1822
- store.removeItem(key);
1823
- } catch {
1824
- }
1825
- }
1826
- };
1827
- }
1828
-
1829
2038
  // src/providers.ts
1830
2039
  function webContextProviders() {
1831
2040
  return {
@@ -1855,8 +2064,10 @@ export {
1855
2064
  ElementPickingController,
1856
2065
  FLOATING_BUTTON_ID2 as FLOATING_BUTTON_ID,
1857
2066
  FeedbackKit,
2067
+ HintProvider2 as HintProvider,
1858
2068
  MARKER_KEY_PREFIX,
1859
2069
  MAX_MARKERS_PER_PATH,
2070
+ MODAL_ACTION_HINT_TOGGLE2 as MODAL_ACTION_HINT_TOGGLE,
1860
2071
  MODAL_ACTION_PICK2 as MODAL_ACTION_PICK,
1861
2072
  MarkerStore,
1862
2073
  OWN_UI_ATTR,
@@ -1877,6 +2088,7 @@ export {
1877
2088
  isOwnUi2 as isOwnUi,
1878
2089
  normalizePin2 as normalizePin,
1879
2090
  parseSourceAttr,
2091
+ rankHints2 as rankHints,
1880
2092
  reactComponentPath,
1881
2093
  reactComponentSummary,
1882
2094
  reencodeWebScreenshot,