@solhun/feedback-kit-core 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.cjs +623 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +332 -4
- package/dist/index.d.ts +332 -4
- package/dist/index.js +603 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -216,7 +216,9 @@ async function buildReport(parts, opts) {
|
|
|
216
216
|
pin: parts.pin,
|
|
217
217
|
element: parts.element,
|
|
218
218
|
context,
|
|
219
|
-
createdAt: iso(now())
|
|
219
|
+
createdAt: iso(now()),
|
|
220
|
+
// 생략과 "정말 아무 것도 안 눌렀음"을 굳이 구분하지 않는다 — 둘 다 빈 배열로 싣는다.
|
|
221
|
+
hintIds: parts.hintIds ?? []
|
|
220
222
|
};
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -1339,6 +1341,7 @@ var COMMENT_TOO_LONG_MESSAGE = "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD57
|
|
|
1339
1341
|
var SUBMIT_DONE_MESSAGE = "\uBCF4\uB0C8\uC2B5\uB2C8\uB2E4";
|
|
1340
1342
|
var SUBMIT_PENDING_MESSAGE = "\uB300\uAE30 \uC911";
|
|
1341
1343
|
var SUBMIT_FAILED_MESSAGE = "\uBCF4\uB0B4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4";
|
|
1344
|
+
var MODAL_ACTION_HINT_TOGGLE = "hint-toggle";
|
|
1342
1345
|
var MODAL_FIELD_COMMENT = "comment";
|
|
1343
1346
|
var MODAL_FIELD_PRIORITY = "priority";
|
|
1344
1347
|
var MODAL_ACTION_ATTACH = "attach";
|
|
@@ -1360,6 +1363,12 @@ var ReportModalController = class {
|
|
|
1360
1363
|
this.captureGeneration = 0;
|
|
1361
1364
|
/** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
|
|
1362
1365
|
this.pinAnchor = "picture";
|
|
1366
|
+
/**
|
|
1367
|
+
* 사용자가 [제안 ▾] 를 직접 건드렸는가(방향 무관). 한 번이라도 건드리면 이후
|
|
1368
|
+
* 타이핑에 의한 자동 접힘/펼침이 멈춘다 — "사람이 편 상태는 모달이 닫힐 때까지 유지"를
|
|
1369
|
+
* 이렇게 구현한다. `open()` 에서 매번 리셋된다.
|
|
1370
|
+
*/
|
|
1371
|
+
this.userToggledHints = false;
|
|
1363
1372
|
this.state = {
|
|
1364
1373
|
open: false,
|
|
1365
1374
|
element: null,
|
|
@@ -1380,7 +1389,11 @@ var ReportModalController = class {
|
|
|
1380
1389
|
actions: [],
|
|
1381
1390
|
focusOrder: [],
|
|
1382
1391
|
focused: null,
|
|
1383
|
-
restoreFocusTo: null
|
|
1392
|
+
restoreFocusTo: null,
|
|
1393
|
+
hints: [],
|
|
1394
|
+
hintsExpanded: false,
|
|
1395
|
+
hintsAvailable: false,
|
|
1396
|
+
usedHintIds: []
|
|
1384
1397
|
};
|
|
1385
1398
|
this.queue = opts.queue;
|
|
1386
1399
|
this.createReport = opts.createReport;
|
|
@@ -1440,9 +1453,11 @@ var ReportModalController = class {
|
|
|
1440
1453
|
this.touched = false;
|
|
1441
1454
|
this.submitAttempted = false;
|
|
1442
1455
|
this.pinAnchor = opts.pin && !pendingReport ? "viewport" : "picture";
|
|
1456
|
+
this.userToggledHints = false;
|
|
1457
|
+
const resolvedComment = pendingReport?.comment ?? "";
|
|
1443
1458
|
this.patch({
|
|
1444
1459
|
open: true,
|
|
1445
|
-
comment:
|
|
1460
|
+
comment: resolvedComment,
|
|
1446
1461
|
// 요소를 지목해서 연 경우엔 그 요소를 물고 간다. 대기 항목 복원이 우선이다.
|
|
1447
1462
|
element: pendingReport?.element ?? opts.element ?? null,
|
|
1448
1463
|
priority: pendingReport?.priority ?? "unset",
|
|
@@ -1453,7 +1468,10 @@ var ReportModalController = class {
|
|
|
1453
1468
|
submitStatus: wasPending ? "pending" : "idle",
|
|
1454
1469
|
submitMessage: wasPending ? SUBMIT_PENDING_MESSAGE : null,
|
|
1455
1470
|
closeConfirmVisible: false,
|
|
1456
|
-
restoreFocusTo: null
|
|
1471
|
+
restoreFocusTo: null,
|
|
1472
|
+
// 코멘트가 비어 있을 때만 펼쳐진 채로 연다. 대기 항목을 복원해도 규칙은 같다(계약 원문).
|
|
1473
|
+
hintsExpanded: resolvedComment === "",
|
|
1474
|
+
usedHintIds: []
|
|
1457
1475
|
});
|
|
1458
1476
|
this.ring.focus(MODAL_FIELD_COMMENT);
|
|
1459
1477
|
this.patch({ focused: this.ring.current });
|
|
@@ -1512,7 +1530,22 @@ var ReportModalController = class {
|
|
|
1512
1530
|
setComment(value) {
|
|
1513
1531
|
if (this.isDraftLocked()) return;
|
|
1514
1532
|
this.touched = true;
|
|
1515
|
-
this.patch({ comment: value });
|
|
1533
|
+
this.patch({ comment: value, ...this.hintsPatchFor(value) });
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* 코멘트가 비었는지에 따라 칩 줄 상태를 갱신하는 조각을 만든다. `setComment`·`applyHint`
|
|
1537
|
+
* 양쪽에서 쓴다.
|
|
1538
|
+
*
|
|
1539
|
+
* - 완전히 비워지면(trim 기준) 눌린 칩 기록을 버리고 다시 펼친다 — "지우고 처음부터 다시
|
|
1540
|
+
* 쓴 글"에 이전 제안을 귀속시키지 않기 위해서다. 사용자가 직접 편 상태여도 예외 없다.
|
|
1541
|
+
* - 비어 있지 않게 되면, 사용자가 직접 편 게 아닌 이상 접는다(좁은 화면에서 입력란을
|
|
1542
|
+
* 밀어내지 않으려고). 직접 편 상태(`userToggledHints`)면 손대지 않는다.
|
|
1543
|
+
*/
|
|
1544
|
+
hintsPatchFor(nextComment) {
|
|
1545
|
+
if (nextComment.trim().length === 0) {
|
|
1546
|
+
return { hintsExpanded: true, usedHintIds: [] };
|
|
1547
|
+
}
|
|
1548
|
+
return this.userToggledHints ? {} : { hintsExpanded: false };
|
|
1516
1549
|
}
|
|
1517
1550
|
setPriority(priority) {
|
|
1518
1551
|
if (this.isDraftLocked()) return;
|
|
@@ -1585,6 +1618,41 @@ var ReportModalController = class {
|
|
|
1585
1618
|
...dropPin
|
|
1586
1619
|
});
|
|
1587
1620
|
}
|
|
1621
|
+
// ── 제안 힌트 ─────────────────────────────────────────────────────────────
|
|
1622
|
+
/**
|
|
1623
|
+
* 컨트롤러가 랭킹 결과를 밀어넣는다. **모달이 열려 있는 동안에는 무시한다** — 지목 대상을
|
|
1624
|
+
* 바꿔 새로 열기 직전에 호출되는 흐름이 정상이고, 열려 있는 모달의 칩을 도중에 바꾸면
|
|
1625
|
+
* 누르려던 칩이 손가락 밑에서 바뀐다(캐시 백그라운드 갱신도 이 경로로 들어올 수 있다).
|
|
1626
|
+
*/
|
|
1627
|
+
setHints(hints) {
|
|
1628
|
+
if (this.state.open) return;
|
|
1629
|
+
this.patch({ hints });
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* 칩 누름. 이미 눌린 칩이면 아무 일도 하지 않는다(같은 문구가 두 번 이어붙는 걸 막는다).
|
|
1633
|
+
* 코멘트가 비어 있으면 초안으로 치환, 아니면 줄바꿈 뒤 이어붙인다 — 어느 쪽도 사용자가
|
|
1634
|
+
* 쓴 글을 지우지 않는다.
|
|
1635
|
+
*/
|
|
1636
|
+
applyHint(hintId) {
|
|
1637
|
+
if (this.isDraftLocked()) return;
|
|
1638
|
+
if (this.state.usedHintIds.includes(hintId)) return;
|
|
1639
|
+
const hint = this.state.hints.find((h) => h.id === hintId);
|
|
1640
|
+
if (!hint) return;
|
|
1641
|
+
this.touched = true;
|
|
1642
|
+
const comment = this.state.comment.length === 0 ? hint.draft : `${this.state.comment}
|
|
1643
|
+
${hint.draft}`;
|
|
1644
|
+
this.patch({
|
|
1645
|
+
comment,
|
|
1646
|
+
usedHintIds: [...this.state.usedHintIds, hintId],
|
|
1647
|
+
// 방금 채워 넣었으니 비어 있지 않다 — setComment 와 같은 접힘 규칙을 그대로 쓴다.
|
|
1648
|
+
...this.hintsPatchFor(comment)
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
/** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다(`userToggledHints`). */
|
|
1652
|
+
toggleHints() {
|
|
1653
|
+
this.userToggledHints = true;
|
|
1654
|
+
this.patch({ hintsExpanded: !this.state.hintsExpanded });
|
|
1655
|
+
}
|
|
1588
1656
|
// ── 포커스 ────────────────────────────────────────────────────────────────
|
|
1589
1657
|
tabNext() {
|
|
1590
1658
|
const next = this.ring.next();
|
|
@@ -1622,7 +1690,9 @@ var ReportModalController = class {
|
|
|
1622
1690
|
priority: this.state.priority,
|
|
1623
1691
|
screenshot: this.state.screenshot,
|
|
1624
1692
|
pin: this.state.pin,
|
|
1625
|
-
element: this.state.element
|
|
1693
|
+
element: this.state.element,
|
|
1694
|
+
// 눌린 칩 기록. 문구를 고쳐 보내도 "이 제안을 눌렀다"는 사실은 그대로 남는다.
|
|
1695
|
+
hintIds: this.state.usedHintIds
|
|
1626
1696
|
};
|
|
1627
1697
|
let outcome;
|
|
1628
1698
|
try {
|
|
@@ -1688,8 +1758,15 @@ var ReportModalController = class {
|
|
|
1688
1758
|
actionsFor() {
|
|
1689
1759
|
return this.platform === "web" ? [MODAL_ACTION_PICK] : [MODAL_ACTION_PIN];
|
|
1690
1760
|
}
|
|
1691
|
-
focusOrderFor() {
|
|
1692
|
-
const order = [
|
|
1761
|
+
focusOrderFor(hintsAvailable) {
|
|
1762
|
+
const order = [];
|
|
1763
|
+
if (hintsAvailable) {
|
|
1764
|
+
if (this.state.hintsExpanded) {
|
|
1765
|
+
for (const hint of this.state.hints) order.push(`hint:${hint.id}`);
|
|
1766
|
+
}
|
|
1767
|
+
order.push(MODAL_ACTION_HINT_TOGGLE);
|
|
1768
|
+
}
|
|
1769
|
+
order.push(MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY);
|
|
1693
1770
|
if (this.state.screenshot) order.push(MODAL_ACTION_REMOVE_SCREENSHOT);
|
|
1694
1771
|
else order.push(MODAL_ACTION_ATTACH);
|
|
1695
1772
|
order.push(...this.actionsFor());
|
|
@@ -1702,7 +1779,8 @@ var ReportModalController = class {
|
|
|
1702
1779
|
const canSubmit = this.isCommentValid() && this.state.screenshotStatus !== "capturing" && (this.state.submitStatus === "idle" || this.state.submitStatus === "failed");
|
|
1703
1780
|
const canPin = this.platform === "native" && this.state.screenshot !== null && this.state.screenshotStatus !== "capturing" && !this.isDraftLocked() && this.state.open;
|
|
1704
1781
|
const actions = this.actionsFor();
|
|
1705
|
-
const
|
|
1782
|
+
const hintsAvailable = this.state.hints.length > 0;
|
|
1783
|
+
const focusOrder = this.state.open ? this.focusOrderFor(hintsAvailable) : [];
|
|
1706
1784
|
this.ring.setItems(focusOrder);
|
|
1707
1785
|
this.state = {
|
|
1708
1786
|
...this.state,
|
|
@@ -1711,6 +1789,7 @@ var ReportModalController = class {
|
|
|
1711
1789
|
canPin,
|
|
1712
1790
|
showRetry: this.state.submitStatus === "pending" || this.state.pending > 0,
|
|
1713
1791
|
actions,
|
|
1792
|
+
hintsAvailable,
|
|
1714
1793
|
focusOrder,
|
|
1715
1794
|
focused: this.state.open ? this.ring.current : null
|
|
1716
1795
|
};
|
|
@@ -2101,6 +2180,13 @@ function absoluteEndpoint(endpoint) {
|
|
|
2101
2180
|
if (typeof origin !== "string" || origin.length === 0) return null;
|
|
2102
2181
|
return endpoint.startsWith("/") ? `${origin}${endpoint}` : `${origin}/${endpoint}`;
|
|
2103
2182
|
}
|
|
2183
|
+
function hintsCacheKeyFrom(endpoint) {
|
|
2184
|
+
const path = endpoint.split(/[?#]/)[0] ?? "";
|
|
2185
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
2186
|
+
const last = segments.at(-1);
|
|
2187
|
+
if (!last || last === "verify") return void 0;
|
|
2188
|
+
return last;
|
|
2189
|
+
}
|
|
2104
2190
|
function parseRetryAfterMs(res) {
|
|
2105
2191
|
const raw = res.headers?.get("Retry-After") ?? null;
|
|
2106
2192
|
if (raw === null) return null;
|
|
@@ -2165,6 +2251,8 @@ function buildLassoContext(report) {
|
|
|
2165
2251
|
} : null,
|
|
2166
2252
|
// 속성이 하나도 없으면 빈 객체를 보내지 않는다 — 화면에 빈 항목이 늘어난다.
|
|
2167
2253
|
element: el && (el.id !== null || Object.keys(el.attributes).length > 0) ? { id: el.id, attributes: el.attributes } : null,
|
|
2254
|
+
// 생략과 "정말 하나도 안 눌렀음"을 구분하지 않는다 — 둘 다 빈 배열.
|
|
2255
|
+
hintIds: [...report.hintIds ?? []],
|
|
2168
2256
|
native: c.native ? {
|
|
2169
2257
|
screenPath: c.native.screenPath,
|
|
2170
2258
|
navPath: c.native.navPath,
|
|
@@ -2224,6 +2312,7 @@ function createLassoAdapter(opts = {}) {
|
|
|
2224
2312
|
const token = opts.token ?? null;
|
|
2225
2313
|
const endpoint = opts.endpoint && opts.endpoint.trim().length > 0 ? opts.endpoint.trim() : LASSO_DEFAULT_ENDPOINT;
|
|
2226
2314
|
const warn = opts.warn ?? null;
|
|
2315
|
+
const hintsCacheKey = hintsCacheKeyFrom(endpoint);
|
|
2227
2316
|
const emit = (message) => {
|
|
2228
2317
|
if (!warn) return;
|
|
2229
2318
|
try {
|
|
@@ -2232,6 +2321,7 @@ function createLassoAdapter(opts = {}) {
|
|
|
2232
2321
|
}
|
|
2233
2322
|
};
|
|
2234
2323
|
return {
|
|
2324
|
+
hintsCacheKey,
|
|
2235
2325
|
async submit(report) {
|
|
2236
2326
|
const url = absoluteEndpoint(endpoint);
|
|
2237
2327
|
if (url === null) {
|
|
@@ -2302,7 +2392,491 @@ function createLassoAdapter(opts = {}) {
|
|
|
2302
2392
|
return fail(false);
|
|
2303
2393
|
}
|
|
2304
2394
|
return { ok: true, id: idFrom(parsed), retryable: false, retryAfterMs: null };
|
|
2395
|
+
},
|
|
2396
|
+
/**
|
|
2397
|
+
* 힌트 카탈로그 조회. `submit` 과 같은 절대 규칙이 그대로 적용된다 — **던지지 않는다**,
|
|
2398
|
+
* **본문·헤더를 로그에 남기지 않는다.** 다만 여기는 `warn` 을 아예 부르지 않는다.
|
|
2399
|
+
* "실패하면 조용히 포기"·"개발 빌드에서만 1회 경고"의 판단(쿨다운·환경 분기)은
|
|
2400
|
+
* `HintProvider`(hints/ 소유자) 몫이라, 어댑터가 매 실패마다 경고를 찍으면 그 판단과
|
|
2401
|
+
* 어긋난다.
|
|
2402
|
+
*/
|
|
2403
|
+
async fetchHints(etag) {
|
|
2404
|
+
const url = absoluteEndpoint(`${endpoint.replace(/\/+$/, "")}/hints`);
|
|
2405
|
+
if (url === null) return { kind: "fail" };
|
|
2406
|
+
const sender = resolveFetch(opts.fetch);
|
|
2407
|
+
if (sender === null) return { kind: "fail" };
|
|
2408
|
+
const headers = {};
|
|
2409
|
+
if (token !== null) headers.Authorization = `Bearer ${token}`;
|
|
2410
|
+
if (etag !== null) headers["If-None-Match"] = etag;
|
|
2411
|
+
let res;
|
|
2412
|
+
try {
|
|
2413
|
+
res = await sender(url, { method: "GET", headers });
|
|
2414
|
+
} catch {
|
|
2415
|
+
return { kind: "fail" };
|
|
2416
|
+
}
|
|
2417
|
+
if (res.status === 304) return { kind: "not-modified" };
|
|
2418
|
+
if (res.status === 401 || res.status === 403) {
|
|
2419
|
+
return { kind: "fail", permanent: true };
|
|
2420
|
+
}
|
|
2421
|
+
if (res.status < 200 || res.status >= 300) return { kind: "fail" };
|
|
2422
|
+
let text;
|
|
2423
|
+
try {
|
|
2424
|
+
text = await res.text();
|
|
2425
|
+
} catch {
|
|
2426
|
+
return { kind: "fail" };
|
|
2427
|
+
}
|
|
2428
|
+
try {
|
|
2429
|
+
const raw = JSON.parse(text);
|
|
2430
|
+
return { kind: "ok", raw, etag: res.headers?.get("ETag") ?? null };
|
|
2431
|
+
} catch {
|
|
2432
|
+
return { kind: "fail" };
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/hints/constants.ts
|
|
2439
|
+
var HINT_LABEL_MAX_CHARS = 12;
|
|
2440
|
+
var HINT_DRAFT_MAX_CHARS = 200;
|
|
2441
|
+
var HINT_DISPLAY_DEFAULT = 4;
|
|
2442
|
+
var HINT_DISPLAY_MIN = 3;
|
|
2443
|
+
var HINT_DISPLAY_MAX = 6;
|
|
2444
|
+
var HINT_MAX_ITEMS = 300;
|
|
2445
|
+
var HINT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2446
|
+
var HINT_TIMEOUT_MS = 5e3;
|
|
2447
|
+
var HINT_FAIL_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
2448
|
+
var SCORE_PATH_EXACT = 40;
|
|
2449
|
+
var SCORE_PATH_PATTERN = 25;
|
|
2450
|
+
var SCORE_ELEMENT = 30;
|
|
2451
|
+
|
|
2452
|
+
// src/hints/chars.ts
|
|
2453
|
+
function hintCharCount(value) {
|
|
2454
|
+
return [...value.normalize("NFC")].length;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/hints/path.ts
|
|
2458
|
+
function appPathFromNav(navPath) {
|
|
2459
|
+
if (navPath === null || navPath === void 0 || navPath.length === 0) return null;
|
|
2460
|
+
return "/" + navPath.join("/");
|
|
2461
|
+
}
|
|
2462
|
+
function matchPath(pattern, path) {
|
|
2463
|
+
const patternSegs = splitSegments(pattern);
|
|
2464
|
+
const pathSegs = splitSegments(path);
|
|
2465
|
+
if (patternSegs === null || pathSegs === null) return null;
|
|
2466
|
+
const hasWildcard = patternSegs.some((seg) => seg === "*" || seg === "**");
|
|
2467
|
+
if (!hasWildcard) {
|
|
2468
|
+
if (patternSegs.length !== pathSegs.length) return null;
|
|
2469
|
+
for (let i = 0; i < patternSegs.length; i++) {
|
|
2470
|
+
if (patternSegs[i] !== pathSegs[i]) return null;
|
|
2471
|
+
}
|
|
2472
|
+
return "exact";
|
|
2473
|
+
}
|
|
2474
|
+
return matchesFrom(patternSegs, 0, pathSegs, 0) ? "pattern" : null;
|
|
2475
|
+
}
|
|
2476
|
+
function splitSegments(value) {
|
|
2477
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
2478
|
+
if (!value.startsWith("/")) return null;
|
|
2479
|
+
if (value === "/") return [];
|
|
2480
|
+
const trimmed = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2481
|
+
const segments = trimmed.split("/").slice(1);
|
|
2482
|
+
if (segments.some((seg) => seg.length === 0)) return null;
|
|
2483
|
+
return segments;
|
|
2484
|
+
}
|
|
2485
|
+
function matchesFrom(pattern, patternIndex, path, pathIndex) {
|
|
2486
|
+
if (patternIndex === pattern.length) return pathIndex === path.length;
|
|
2487
|
+
const seg = pattern[patternIndex];
|
|
2488
|
+
if (seg === "**") {
|
|
2489
|
+
for (let consume = 0; pathIndex + consume <= path.length; consume++) {
|
|
2490
|
+
if (matchesFrom(pattern, patternIndex + 1, path, pathIndex + consume)) return true;
|
|
2491
|
+
}
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
if (pathIndex >= path.length) return false;
|
|
2495
|
+
if (seg === "*") return matchesFrom(pattern, patternIndex + 1, path, pathIndex + 1);
|
|
2496
|
+
return path[pathIndex] === seg && matchesFrom(pattern, patternIndex + 1, path, pathIndex + 1);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/hints/element.ts
|
|
2500
|
+
function matchElement(rule, element, components) {
|
|
2501
|
+
if (rule === null) return true;
|
|
2502
|
+
try {
|
|
2503
|
+
const axes = [rule.tags, rule.roles, rule.classIncludes, rule.componentIncludes, rule.testIds];
|
|
2504
|
+
const hasAnyAxis = axes.some((axis) => Array.isArray(axis) && axis.length > 0);
|
|
2505
|
+
if (!hasAnyAxis) return true;
|
|
2506
|
+
if (element === null) return false;
|
|
2507
|
+
return matchesTags(rule.tags, element) || matchesRoles(rule.roles, element) || matchesClassIncludes(rule.classIncludes, element) || matchesComponentIncludes(rule.componentIncludes, components) || matchesTestIds(rule.testIds, element);
|
|
2508
|
+
} catch {
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
function matchesTags(tags, element) {
|
|
2513
|
+
if (!Array.isArray(tags)) return false;
|
|
2514
|
+
const tag = safeLower(element.tag);
|
|
2515
|
+
if (tag === null) return false;
|
|
2516
|
+
return tags.some((t) => typeof t === "string" && t.toLowerCase() === tag);
|
|
2517
|
+
}
|
|
2518
|
+
function matchesRoles(roles, element) {
|
|
2519
|
+
if (!Array.isArray(roles)) return false;
|
|
2520
|
+
const role = safeLower(element.attributes?.role ?? null);
|
|
2521
|
+
const type = safeLower(element.attributes?.type ?? null);
|
|
2522
|
+
return roles.some((r) => {
|
|
2523
|
+
if (typeof r !== "string") return false;
|
|
2524
|
+
const lowered = r.toLowerCase();
|
|
2525
|
+
return lowered === role || lowered === type;
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
function matchesClassIncludes(classIncludes, element) {
|
|
2529
|
+
if (!Array.isArray(classIncludes)) return false;
|
|
2530
|
+
const className = safeLower(element.className);
|
|
2531
|
+
if (className === null) return false;
|
|
2532
|
+
return classIncludes.some((c) => typeof c === "string" && className.includes(c.toLowerCase()));
|
|
2533
|
+
}
|
|
2534
|
+
function matchesComponentIncludes(componentIncludes, components) {
|
|
2535
|
+
if (!Array.isArray(componentIncludes)) return false;
|
|
2536
|
+
if (components === null) return false;
|
|
2537
|
+
return componentIncludes.some((c) => typeof c === "string" && components.includes(c));
|
|
2538
|
+
}
|
|
2539
|
+
function matchesTestIds(testIds, element) {
|
|
2540
|
+
if (!Array.isArray(testIds)) return false;
|
|
2541
|
+
const testId = element.attributes?.["data-testid"];
|
|
2542
|
+
if (typeof testId !== "string") return false;
|
|
2543
|
+
return testIds.some((t) => t === testId);
|
|
2544
|
+
}
|
|
2545
|
+
function safeLower(value) {
|
|
2546
|
+
return typeof value === "string" ? value.toLowerCase() : null;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// src/hints/parse.ts
|
|
2550
|
+
var SCOPES = ["screen", "element", "both"];
|
|
2551
|
+
var PLATFORMS = ["web", "app"];
|
|
2552
|
+
function parseHintCatalog(raw) {
|
|
2553
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
2554
|
+
const obj = raw;
|
|
2555
|
+
if (obj.ok !== true) return null;
|
|
2556
|
+
if (!Array.isArray(obj.hints)) return null;
|
|
2557
|
+
const hints = [];
|
|
2558
|
+
for (const item of obj.hints) {
|
|
2559
|
+
if (hints.length >= HINT_MAX_ITEMS) break;
|
|
2560
|
+
const parsed = parseHintItem(item);
|
|
2561
|
+
if (parsed !== null) hints.push(parsed);
|
|
2562
|
+
}
|
|
2563
|
+
return {
|
|
2564
|
+
version: typeof obj.version === "number" && Number.isInteger(obj.version) ? obj.version : null,
|
|
2565
|
+
maxDisplay: parseMaxDisplay(obj.maxDisplay),
|
|
2566
|
+
hints
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
function parseMaxDisplay(value) {
|
|
2570
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= HINT_DISPLAY_MIN && value <= HINT_DISPLAY_MAX) {
|
|
2571
|
+
return value;
|
|
2572
|
+
}
|
|
2573
|
+
return HINT_DISPLAY_DEFAULT;
|
|
2574
|
+
}
|
|
2575
|
+
function parseHintItem(item) {
|
|
2576
|
+
if (typeof item !== "object" || item === null) return null;
|
|
2577
|
+
const obj = item;
|
|
2578
|
+
const id = nonEmptyString(obj.id);
|
|
2579
|
+
const label = nonEmptyString(obj.label);
|
|
2580
|
+
const draft = nonEmptyString(obj.draft);
|
|
2581
|
+
if (id === null || label === null || draft === null) return null;
|
|
2582
|
+
if (typeof obj.scope !== "string" || !SCOPES.includes(obj.scope)) return null;
|
|
2583
|
+
const scope = obj.scope;
|
|
2584
|
+
const platforms = parsePlatforms(obj.platforms);
|
|
2585
|
+
if (platforms === null) return null;
|
|
2586
|
+
return {
|
|
2587
|
+
id,
|
|
2588
|
+
label,
|
|
2589
|
+
draft,
|
|
2590
|
+
scope,
|
|
2591
|
+
platforms,
|
|
2592
|
+
paths: parsePaths(obj.paths),
|
|
2593
|
+
element: parseElementRule(obj.element),
|
|
2594
|
+
active: parseActive(obj.active)
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
function parseActive(value) {
|
|
2598
|
+
return value !== false;
|
|
2599
|
+
}
|
|
2600
|
+
function parsePlatforms(value) {
|
|
2601
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
2602
|
+
const result = [];
|
|
2603
|
+
for (const p of value) {
|
|
2604
|
+
if (typeof p !== "string" || !PLATFORMS.includes(p)) return null;
|
|
2605
|
+
result.push(p);
|
|
2606
|
+
}
|
|
2607
|
+
return result;
|
|
2608
|
+
}
|
|
2609
|
+
function parsePaths(value) {
|
|
2610
|
+
if (!Array.isArray(value)) return [];
|
|
2611
|
+
return value.filter((p) => typeof p === "string");
|
|
2612
|
+
}
|
|
2613
|
+
function parseElementRule(value) {
|
|
2614
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
2615
|
+
const obj = value;
|
|
2616
|
+
const rule = {};
|
|
2617
|
+
if (Array.isArray(obj.tags)) rule.tags = obj.tags.filter((v) => typeof v === "string");
|
|
2618
|
+
if (Array.isArray(obj.roles)) rule.roles = obj.roles.filter((v) => typeof v === "string");
|
|
2619
|
+
if (Array.isArray(obj.classIncludes)) {
|
|
2620
|
+
rule.classIncludes = obj.classIncludes.filter((v) => typeof v === "string");
|
|
2621
|
+
}
|
|
2622
|
+
if (Array.isArray(obj.componentIncludes)) {
|
|
2623
|
+
rule.componentIncludes = obj.componentIncludes.filter((v) => typeof v === "string");
|
|
2624
|
+
}
|
|
2625
|
+
if (Array.isArray(obj.testIds)) rule.testIds = obj.testIds.filter((v) => typeof v === "string");
|
|
2626
|
+
return rule;
|
|
2627
|
+
}
|
|
2628
|
+
function nonEmptyString(value) {
|
|
2629
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/hints/rank.ts
|
|
2633
|
+
function rankHints(hints, input, limit) {
|
|
2634
|
+
const scored = [];
|
|
2635
|
+
for (let index = 0; index < hints.length; index++) {
|
|
2636
|
+
const hint = hints[index];
|
|
2637
|
+
if (!passesHardFilter(hint, input)) continue;
|
|
2638
|
+
scored.push({ hint, score: scoreHint(hint, input), index });
|
|
2639
|
+
}
|
|
2640
|
+
const ordered = orderByScoreThenRotatedGroup(scored, input.path);
|
|
2641
|
+
return ordered.slice(0, Math.max(0, limit)).map((s) => s.hint);
|
|
2642
|
+
}
|
|
2643
|
+
function orderByScoreThenRotatedGroup(scored, path) {
|
|
2644
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2645
|
+
for (const s of scored) {
|
|
2646
|
+
const group = groups.get(s.score);
|
|
2647
|
+
if (group) group.push(s);
|
|
2648
|
+
else groups.set(s.score, [s]);
|
|
2649
|
+
}
|
|
2650
|
+
const scoresDesc = [...groups.keys()].sort((a, b) => b - a);
|
|
2651
|
+
const result = [];
|
|
2652
|
+
for (const score of scoresDesc) {
|
|
2653
|
+
result.push(...rotateGroup(groups.get(score), path));
|
|
2654
|
+
}
|
|
2655
|
+
return result;
|
|
2656
|
+
}
|
|
2657
|
+
function rotateGroup(group, path) {
|
|
2658
|
+
if (group.length <= 1 || path === null) return group;
|
|
2659
|
+
const offset = fnv1a32(path) % group.length;
|
|
2660
|
+
if (offset === 0) return group;
|
|
2661
|
+
return [...group.slice(offset), ...group.slice(0, offset)];
|
|
2662
|
+
}
|
|
2663
|
+
function fnv1a32(value) {
|
|
2664
|
+
let hash = 2166136261;
|
|
2665
|
+
for (let i = 0; i < value.length; i++) {
|
|
2666
|
+
hash ^= value.charCodeAt(i);
|
|
2667
|
+
hash = Math.imul(hash, 16777619);
|
|
2668
|
+
}
|
|
2669
|
+
return hash >>> 0;
|
|
2670
|
+
}
|
|
2671
|
+
function passesHardFilter(hint, input) {
|
|
2672
|
+
if (!hint.platforms.includes(input.platform)) return false;
|
|
2673
|
+
const wantsElementScope = input.element !== null;
|
|
2674
|
+
if (wantsElementScope) {
|
|
2675
|
+
if (hint.scope !== "element" && hint.scope !== "both") return false;
|
|
2676
|
+
} else if (hint.scope !== "screen" && hint.scope !== "both") {
|
|
2677
|
+
return false;
|
|
2678
|
+
}
|
|
2679
|
+
if (hint.paths.length > 0 && !matchesAnyPath(hint.paths, input.path)) return false;
|
|
2680
|
+
if (hint.active === false) return false;
|
|
2681
|
+
if (hintCharCount(hint.label) > HINT_LABEL_MAX_CHARS) return false;
|
|
2682
|
+
if (hintCharCount(hint.draft) > HINT_DRAFT_MAX_CHARS) return false;
|
|
2683
|
+
return true;
|
|
2684
|
+
}
|
|
2685
|
+
function matchesAnyPath(paths, path) {
|
|
2686
|
+
if (path === null) return false;
|
|
2687
|
+
return paths.some((pattern) => matchPath(pattern, path) !== null);
|
|
2688
|
+
}
|
|
2689
|
+
function scoreHint(hint, input) {
|
|
2690
|
+
let score = bestPathScore(hint.paths, input.path);
|
|
2691
|
+
if (input.element !== null && matchElement(hint.element, input.element, input.components)) {
|
|
2692
|
+
score += SCORE_ELEMENT;
|
|
2693
|
+
}
|
|
2694
|
+
return score;
|
|
2695
|
+
}
|
|
2696
|
+
function bestPathScore(paths, path) {
|
|
2697
|
+
if (paths.length === 0 || path === null) return 0;
|
|
2698
|
+
let best = 0;
|
|
2699
|
+
for (const pattern of paths) {
|
|
2700
|
+
const result = matchPath(pattern, path);
|
|
2701
|
+
if (result === "exact") return SCORE_PATH_EXACT;
|
|
2702
|
+
if (result === "pattern") best = SCORE_PATH_PATTERN;
|
|
2703
|
+
}
|
|
2704
|
+
return best;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// src/hints/provider.ts
|
|
2708
|
+
var CACHE_KEY_PREFIX = "feedback-kit:hints:";
|
|
2709
|
+
var HintProvider = class {
|
|
2710
|
+
constructor(opts) {
|
|
2711
|
+
this.entry = null;
|
|
2712
|
+
/** refresh() 동시 호출을 한 번의 요청으로 합친다. */
|
|
2713
|
+
this.inflight = null;
|
|
2714
|
+
/**
|
|
2715
|
+
* 401/403 등 설정 문제로 영구 실패했는가 — 이 인스턴스가 살아 있는 동안 재요청하지
|
|
2716
|
+
* 않는다. **저장소에는 남기지 않는다**(메모리만): 토큰을 고치고 재배포하면 그건 새
|
|
2717
|
+
* 페이지 로드 = 새 인스턴스이므로 자연히 풀린다. 캐시에 박아두면 사용자가 캐시를
|
|
2718
|
+
* 지우기 전까지 죽은 채로 남는다.
|
|
2719
|
+
*/
|
|
2720
|
+
this.permanentlyFailed = false;
|
|
2721
|
+
/**
|
|
2722
|
+
* 실패(네트워크·타임아웃·5xx·401/403 전부 포함) 경고를 이미 냈는가 — 인스턴스당 1회로
|
|
2723
|
+
* 제한한다. 5분 쿨다운마다 매번 경고를 찍으면 콘솔이 "실패했다"만 반복하는 노이즈가
|
|
2724
|
+
* 된다("개발 빌드에서만 경고를 1회 남긴다" — 계약 원문).
|
|
2725
|
+
*/
|
|
2726
|
+
this.warnedFailure = false;
|
|
2727
|
+
this.source = opts.source;
|
|
2728
|
+
this.storage = opts.storage ?? null;
|
|
2729
|
+
this.storageKey = CACHE_KEY_PREFIX + opts.cacheKey;
|
|
2730
|
+
this.now = opts.now ?? (() => Date.now());
|
|
2731
|
+
this.warn = opts.warn ?? (() => void 0);
|
|
2732
|
+
this.staticCatalog = opts.staticHints ? { version: null, maxDisplay: HINT_DISPLAY_DEFAULT, hints: opts.staticHints } : null;
|
|
2733
|
+
}
|
|
2734
|
+
/** 캐시 복원. 마운트 직후 1회. 던지지 않는다. */
|
|
2735
|
+
async load() {
|
|
2736
|
+
if (this.staticCatalog !== null) return;
|
|
2737
|
+
if (this.storage === null) return;
|
|
2738
|
+
try {
|
|
2739
|
+
const raw = await this.storage.get(this.storageKey);
|
|
2740
|
+
if (raw === null) return;
|
|
2741
|
+
const parsed = parseCacheEntry(JSON.parse(raw));
|
|
2742
|
+
if (parsed !== null) this.entry = parsed;
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
this.warn(`feedback-kit: \uD78C\uD2B8 \uCE90\uC2DC\uB97C \uC77D\uC9C0 \uBABB\uD588\uB2E4(${String(err)})`);
|
|
2305
2745
|
}
|
|
2746
|
+
}
|
|
2747
|
+
/** 필요하면 네트워크. TTL 안이거나 쿨다운 중이면 아무것도 안 한다. 던지지 않는다. */
|
|
2748
|
+
refresh() {
|
|
2749
|
+
if (this.staticCatalog !== null) return Promise.resolve();
|
|
2750
|
+
if (this.source === null) return Promise.resolve();
|
|
2751
|
+
if (this.inflight !== null) return this.inflight;
|
|
2752
|
+
const p = this.doRefresh().finally(() => {
|
|
2753
|
+
this.inflight = null;
|
|
2754
|
+
});
|
|
2755
|
+
this.inflight = p;
|
|
2756
|
+
return p;
|
|
2757
|
+
}
|
|
2758
|
+
/** 지금 쓸 수 있는 카탈로그. 없으면 null. 동기 — 요청을 기다리지 않는다. */
|
|
2759
|
+
catalog() {
|
|
2760
|
+
if (this.staticCatalog !== null) return this.staticCatalog;
|
|
2761
|
+
if (this.entry === null || this.entry.hints.length === 0) return null;
|
|
2762
|
+
return { version: this.entry.version, maxDisplay: this.entry.maxDisplay, hints: this.entry.hints };
|
|
2763
|
+
}
|
|
2764
|
+
async doRefresh() {
|
|
2765
|
+
if (this.permanentlyFailed) return;
|
|
2766
|
+
const now = this.now();
|
|
2767
|
+
if (this.entry !== null) {
|
|
2768
|
+
if (now - this.entry.fetchedAt < HINT_TTL_MS) return;
|
|
2769
|
+
if (this.entry.failedAt !== null && now - this.entry.failedAt < HINT_FAIL_COOLDOWN_MS) return;
|
|
2770
|
+
}
|
|
2771
|
+
const etag = this.entry?.etag ?? null;
|
|
2772
|
+
const result = await this.fetchWithTimeout(etag);
|
|
2773
|
+
if (result.kind === "ok") {
|
|
2774
|
+
const parsed = parseHintCatalog(result.raw);
|
|
2775
|
+
if (parsed === null) {
|
|
2776
|
+
this.warn("feedback-kit: \uD78C\uD2B8 \uC751\uB2F5\uC774 \uC2A4\uD0A4\uB9C8\uB97C \uBC97\uC5B4\uB098 \uAE30\uC874 \uCE90\uC2DC\uB97C \uC720\uC9C0\uD55C\uB2E4");
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
await this.setEntry({
|
|
2780
|
+
version: parsed.version,
|
|
2781
|
+
etag: result.etag,
|
|
2782
|
+
fetchedAt: now,
|
|
2783
|
+
maxDisplay: parsed.maxDisplay,
|
|
2784
|
+
hints: parsed.hints,
|
|
2785
|
+
failedAt: null
|
|
2786
|
+
});
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (result.kind === "not-modified") {
|
|
2790
|
+
if (this.entry !== null) {
|
|
2791
|
+
await this.setEntry({ ...this.entry, fetchedAt: now, failedAt: null });
|
|
2792
|
+
}
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (result.permanent === true) {
|
|
2796
|
+
this.permanentlyFailed = true;
|
|
2797
|
+
}
|
|
2798
|
+
if (!this.warnedFailure && detectDevBuild()) {
|
|
2799
|
+
this.warnedFailure = true;
|
|
2800
|
+
this.warn(
|
|
2801
|
+
result.permanent === true ? "feedback-kit: \uD78C\uD2B8 \uC694\uCCAD\uC774 \uAC70\uBD80\uB410\uB2E4(401/403) \u2014 \uC774 \uC138\uC158\uC5D0\uC11C\uB294 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4" : "feedback-kit: \uD78C\uD2B8 \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uB2E4 \u2014 \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4"
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
const base = this.entry ?? {
|
|
2805
|
+
version: null,
|
|
2806
|
+
etag: null,
|
|
2807
|
+
fetchedAt: 0,
|
|
2808
|
+
maxDisplay: HINT_DISPLAY_DEFAULT,
|
|
2809
|
+
hints: [],
|
|
2810
|
+
failedAt: null
|
|
2811
|
+
};
|
|
2812
|
+
await this.setEntry({ ...base, failedAt: now });
|
|
2813
|
+
}
|
|
2814
|
+
async fetchWithTimeout(etag) {
|
|
2815
|
+
if (this.source === null) return { kind: "fail" };
|
|
2816
|
+
const g = globalThis;
|
|
2817
|
+
const controller = typeof g.AbortController === "function" ? new g.AbortController() : null;
|
|
2818
|
+
try {
|
|
2819
|
+
return await withTimeout(
|
|
2820
|
+
this.source.fetchHints(etag, controller?.signal),
|
|
2821
|
+
HINT_TIMEOUT_MS,
|
|
2822
|
+
() => controller?.abort()
|
|
2823
|
+
);
|
|
2824
|
+
} catch {
|
|
2825
|
+
return { kind: "fail" };
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
async setEntry(entry) {
|
|
2829
|
+
this.entry = entry;
|
|
2830
|
+
if (this.storage === null) return;
|
|
2831
|
+
try {
|
|
2832
|
+
await this.storage.set(this.storageKey, JSON.stringify(entry));
|
|
2833
|
+
} catch (err) {
|
|
2834
|
+
this.warn(`feedback-kit: \uD78C\uD2B8 \uCE90\uC2DC\uB97C \uC800\uC7A5\uD558\uC9C0 \uBABB\uD588\uB2E4(${String(err)})`);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
function withTimeout(promise, ms, onTimeout) {
|
|
2839
|
+
return new Promise((resolve, reject) => {
|
|
2840
|
+
const g = globalThis;
|
|
2841
|
+
let settled = false;
|
|
2842
|
+
const timer = typeof g.setTimeout === "function" ? g.setTimeout(() => {
|
|
2843
|
+
if (settled) return;
|
|
2844
|
+
settled = true;
|
|
2845
|
+
onTimeout?.();
|
|
2846
|
+
reject(new Error("feedback-kit: hint fetch timed out"));
|
|
2847
|
+
}, ms) : null;
|
|
2848
|
+
const clear = () => {
|
|
2849
|
+
if (timer !== null && typeof g.clearTimeout === "function") g.clearTimeout(timer);
|
|
2850
|
+
};
|
|
2851
|
+
promise.then(
|
|
2852
|
+
(value) => {
|
|
2853
|
+
if (settled) return;
|
|
2854
|
+
settled = true;
|
|
2855
|
+
clear();
|
|
2856
|
+
resolve(value);
|
|
2857
|
+
},
|
|
2858
|
+
(err) => {
|
|
2859
|
+
if (settled) return;
|
|
2860
|
+
settled = true;
|
|
2861
|
+
clear();
|
|
2862
|
+
reject(err);
|
|
2863
|
+
}
|
|
2864
|
+
);
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
function parseCacheEntry(value) {
|
|
2868
|
+
if (typeof value !== "object" || value === null) return null;
|
|
2869
|
+
const obj = value;
|
|
2870
|
+
if (!Array.isArray(obj.hints)) return null;
|
|
2871
|
+
if (typeof obj.fetchedAt !== "number") return null;
|
|
2872
|
+
if (typeof obj.maxDisplay !== "number") return null;
|
|
2873
|
+
return {
|
|
2874
|
+
version: typeof obj.version === "number" ? obj.version : null,
|
|
2875
|
+
etag: typeof obj.etag === "string" ? obj.etag : null,
|
|
2876
|
+
fetchedAt: obj.fetchedAt,
|
|
2877
|
+
maxDisplay: obj.maxDisplay,
|
|
2878
|
+
hints: obj.hints,
|
|
2879
|
+
failedAt: typeof obj.failedAt === "number" ? obj.failedAt : null
|
|
2306
2880
|
};
|
|
2307
2881
|
}
|
|
2308
2882
|
export {
|
|
@@ -2319,6 +2893,16 @@ export {
|
|
|
2319
2893
|
FLOATING_BUTTON_ID,
|
|
2320
2894
|
FeedbackQueue,
|
|
2321
2895
|
FocusRing,
|
|
2896
|
+
HINT_DISPLAY_DEFAULT,
|
|
2897
|
+
HINT_DISPLAY_MAX,
|
|
2898
|
+
HINT_DISPLAY_MIN,
|
|
2899
|
+
HINT_DRAFT_MAX_CHARS,
|
|
2900
|
+
HINT_FAIL_COOLDOWN_MS,
|
|
2901
|
+
HINT_LABEL_MAX_CHARS,
|
|
2902
|
+
HINT_MAX_ITEMS,
|
|
2903
|
+
HINT_TIMEOUT_MS,
|
|
2904
|
+
HINT_TTL_MS,
|
|
2905
|
+
HintProvider,
|
|
2322
2906
|
KEEPALIVE_BODY_LIMIT_BYTES,
|
|
2323
2907
|
LASSO_DEFAULT_ENDPOINT,
|
|
2324
2908
|
LOG_BUFFER_LIMIT,
|
|
@@ -2327,6 +2911,7 @@ export {
|
|
|
2327
2911
|
MAX_QUEUE_ITEMS,
|
|
2328
2912
|
MODAL_ACTION_ATTACH,
|
|
2329
2913
|
MODAL_ACTION_CANCEL,
|
|
2914
|
+
MODAL_ACTION_HINT_TOGGLE,
|
|
2330
2915
|
MODAL_ACTION_PICK,
|
|
2331
2916
|
MODAL_ACTION_PIN,
|
|
2332
2917
|
MODAL_ACTION_REMOVE_SCREENSHOT,
|
|
@@ -2340,6 +2925,9 @@ export {
|
|
|
2340
2925
|
REDACTED,
|
|
2341
2926
|
ReportModalController,
|
|
2342
2927
|
RingBuffer,
|
|
2928
|
+
SCORE_ELEMENT,
|
|
2929
|
+
SCORE_PATH_EXACT,
|
|
2930
|
+
SCORE_PATH_PATTERN,
|
|
2343
2931
|
SCREENSHOT_FAILED_MESSAGE,
|
|
2344
2932
|
SCREENSHOT_MAX_BASE64_BYTES,
|
|
2345
2933
|
SCREENSHOT_QUALITY_STEPS,
|
|
@@ -2349,6 +2937,7 @@ export {
|
|
|
2349
2937
|
SUBMIT_PENDING_MESSAGE,
|
|
2350
2938
|
TITLE_MAX_CHARS,
|
|
2351
2939
|
WidgetController,
|
|
2940
|
+
appPathFromNav,
|
|
2352
2941
|
buildContext,
|
|
2353
2942
|
buildLassoContext,
|
|
2354
2943
|
buildLassoEnvelope,
|
|
@@ -2366,10 +2955,15 @@ export {
|
|
|
2366
2955
|
diagnosticsProviderFor,
|
|
2367
2956
|
getOrCreateGuestId,
|
|
2368
2957
|
guestUser,
|
|
2958
|
+
hintCharCount,
|
|
2369
2959
|
isInternalUser,
|
|
2960
|
+
matchElement,
|
|
2961
|
+
matchPath,
|
|
2370
2962
|
normalizePin,
|
|
2371
2963
|
normalizeUser,
|
|
2964
|
+
parseHintCatalog,
|
|
2372
2965
|
parseSourceAttr,
|
|
2966
|
+
rankHints,
|
|
2373
2967
|
renderReportBody,
|
|
2374
2968
|
resolveConfig,
|
|
2375
2969
|
resolveContextUser,
|