@solhun/feedback-kit-web 0.8.0 → 0.9.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
@@ -354,6 +354,27 @@ interface WebWidgetOpts extends Omit<WidgetControllerOpts, "platform" | "initial
354
354
  };
355
355
  /** 요소를 고른 뒤 띄울 UI. 기본은 `popup`. */
356
356
  pickTarget?: PickTarget;
357
+ /**
358
+ * 플로팅 버튼을 눌렀을 때 무엇이 열리는가. **웹 기본은 `"pick"`(요소 지목)** 이다.
359
+ *
360
+ * 웹 제보는 대개 "이 요소요" 라서, 지목을 매번 켜게 하면 그 한 단계가 계속 남는다. 0.8.0
361
+ * 까지는 모달이 기본이었으므로, 그 동작을 유지하려면 `"report"` 를 준다.
362
+ *
363
+ * 앱에는 지목이 없어 이 설정이 없다(앱은 언제나 모달이다) — 없는 기능을 켤 수는 없다.
364
+ */
365
+ defaultMode?: "pick" | "report";
366
+ /**
367
+ * 개발용 모드. 제보를 **수집처로 보내지 않고 클립보드에 복사**한다.
368
+ *
369
+ * 개발자가 로컬에서 고치는 중일 때 라쏘런 왕복(보내고·트리아지하고·이슈가 되고)이 오히려
370
+ * 느리다. 정작 고칠 사람은 지금 그 화면을 보고 있는 본인이다.
371
+ *
372
+ * **기본은 꺼짐이고 마운트 시점에 고정한다** — 도는 중에 바뀌면 "지금 보낸 건 어디로 갔나"가
373
+ * 생긴다. 켜져 있는 동안은 그 사실이 화면에 계속 보여야 한다(조용한 유실을 막는 유일한 방어).
374
+ *
375
+ * 웹 전용이다. 앱에는 DOM 이 없어 선택자·컴포넌트 경로·소스 경로가 성립하지 않는다.
376
+ */
377
+ devMode?: boolean;
357
378
  /**
358
379
  * 정적 힌트 목록(설정으로 직접 넘김). 주면 서버에서 받아오지 않는다 — 도입 초기·오프라인
359
380
  * 데모·테스트용이다. 서버 목록과 합치지 않는다(둘을 섞으면 무엇이 보이는지 설명이 안 된다).
package/dist/index.d.ts CHANGED
@@ -354,6 +354,27 @@ interface WebWidgetOpts extends Omit<WidgetControllerOpts, "platform" | "initial
354
354
  };
355
355
  /** 요소를 고른 뒤 띄울 UI. 기본은 `popup`. */
356
356
  pickTarget?: PickTarget;
357
+ /**
358
+ * 플로팅 버튼을 눌렀을 때 무엇이 열리는가. **웹 기본은 `"pick"`(요소 지목)** 이다.
359
+ *
360
+ * 웹 제보는 대개 "이 요소요" 라서, 지목을 매번 켜게 하면 그 한 단계가 계속 남는다. 0.8.0
361
+ * 까지는 모달이 기본이었으므로, 그 동작을 유지하려면 `"report"` 를 준다.
362
+ *
363
+ * 앱에는 지목이 없어 이 설정이 없다(앱은 언제나 모달이다) — 없는 기능을 켤 수는 없다.
364
+ */
365
+ defaultMode?: "pick" | "report";
366
+ /**
367
+ * 개발용 모드. 제보를 **수집처로 보내지 않고 클립보드에 복사**한다.
368
+ *
369
+ * 개발자가 로컬에서 고치는 중일 때 라쏘런 왕복(보내고·트리아지하고·이슈가 되고)이 오히려
370
+ * 느리다. 정작 고칠 사람은 지금 그 화면을 보고 있는 본인이다.
371
+ *
372
+ * **기본은 꺼짐이고 마운트 시점에 고정한다** — 도는 중에 바뀌면 "지금 보낸 건 어디로 갔나"가
373
+ * 생긴다. 켜져 있는 동안은 그 사실이 화면에 계속 보여야 한다(조용한 유실을 막는 유일한 방어).
374
+ *
375
+ * 웹 전용이다. 앱에는 DOM 이 없어 선택자·컴포넌트 경로·소스 경로가 성립하지 않는다.
376
+ */
377
+ devMode?: boolean;
357
378
  /**
358
379
  * 정적 힌트 목록(설정으로 직접 넘김). 주면 서버에서 받아오지 않는다 — 도입 초기·오프라인
359
380
  * 데모·테스트용이다. 서버 목록과 합치지 않는다(둘을 섞으면 무엇이 보이는지 설명이 안 된다).
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { resolveConfig, shouldShowWidget } from "@solhun/feedback-kit-core";
3
- import { parseSourceAttr, SOURCE_ATTR, sourceFromElement } from "@solhun/feedback-kit-core";
3
+ import { parseSourceAttr, SOURCE_ATTR, sourceFromElement as sourceFromElement2 } from "@solhun/feedback-kit-core";
4
4
  import {
5
5
  COMMENT_MAX_CHARS as COMMENT_MAX_CHARS3,
6
6
  COMMENT_REQUIRED_MESSAGE,
@@ -1174,7 +1174,53 @@ import {
1174
1174
  } from "react";
1175
1175
 
1176
1176
  // src/version.ts
1177
- var VERSION = true ? "0.8.0" : "dev";
1177
+ var VERSION = true ? "0.9.0" : "dev";
1178
+
1179
+ // src/dev-clipboard.ts
1180
+ import { sourceFromElement } from "@solhun/feedback-kit-core";
1181
+ function sourceLine(item) {
1182
+ const mapping = sourceFromElement(item.element ?? void 0);
1183
+ if (!mapping.sourceFile) return null;
1184
+ return mapping.sourceLine === null ? `\uC18C\uC2A4: ${mapping.sourceFile}` : `\uC18C\uC2A4: ${mapping.sourceFile}:${mapping.sourceLine}`;
1185
+ }
1186
+ function elementLine(element) {
1187
+ if (!element) return null;
1188
+ const selector = element.selector ?? element.tag;
1189
+ const text = element.text?.trim();
1190
+ return text ? `\uC694\uC18C: ${selector} "${text}"` : `\uC694\uC18C: ${selector}`;
1191
+ }
1192
+ function componentLine(element) {
1193
+ const summary = element?.attributes?.["react-components"];
1194
+ return summary ? `\uCEF4\uD3EC\uB10C\uD2B8: ${summary}` : null;
1195
+ }
1196
+ function renderDevClipItem(item) {
1197
+ const lines = [`[\uD53C\uB4DC\uBC31] ${item.comment.trim()}`];
1198
+ const screen = item.screen?.trim();
1199
+ if (screen) lines.push(`\uD654\uBA74: ${screen}`);
1200
+ const element = elementLine(item.element);
1201
+ if (element) lines.push(element);
1202
+ const component = componentLine(item.element);
1203
+ if (component) lines.push(component);
1204
+ const source = sourceLine(item);
1205
+ if (source) lines.push(source);
1206
+ if (item.priority && item.priority !== "unset") lines.push(`\uC6B0\uC120\uC21C\uC704: ${item.priority}`);
1207
+ return lines.join("\n");
1208
+ }
1209
+ function renderDevClipList(items) {
1210
+ if (items.length === 0) return "";
1211
+ if (items.length === 1) return renderDevClipItem(items[0]);
1212
+ return items.map((item, index) => `${index + 1}. ${renderDevClipItem(item).split("\n").join("\n ")}`).join("\n\n");
1213
+ }
1214
+ async function writeToClipboard(text) {
1215
+ const clipboard = globalThis.navigator?.clipboard;
1216
+ if (!clipboard?.writeText) return false;
1217
+ try {
1218
+ await clipboard.writeText(text);
1219
+ return true;
1220
+ } catch {
1221
+ return false;
1222
+ }
1223
+ }
1178
1224
 
1179
1225
  // src/feedback-kit.tsx
1180
1226
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1443,7 +1489,10 @@ function PickBar({
1443
1489
  markerCount,
1444
1490
  onToggle,
1445
1491
  onTogglePause,
1446
- onClearMarkers
1492
+ onClearMarkers,
1493
+ onReportWholeScreen,
1494
+ devClipCount,
1495
+ onCopyAll
1447
1496
  }) {
1448
1497
  return /* @__PURE__ */ jsxs(
1449
1498
  "div",
@@ -1498,6 +1547,48 @@ function PickBar({
1498
1547
  ]
1499
1548
  }
1500
1549
  ),
1550
+ /* @__PURE__ */ jsxs(
1551
+ "button",
1552
+ {
1553
+ type: "button",
1554
+ "data-fk-pick-whole": "",
1555
+ title: "\uAC00\uB9AC\uD0A8 \uC694\uC18C \uC5C6\uC774 \uC774 \uD654\uBA74 \uC804\uCCB4\uC5D0 \uB300\uD574 \uC81C\uBCF4\uD569\uB2C8\uB2E4.",
1556
+ onClick: onReportWholeScreen,
1557
+ style: {
1558
+ ...baseButton,
1559
+ display: "inline-flex",
1560
+ alignItems: "center",
1561
+ gap: 6,
1562
+ whiteSpace: "nowrap"
1563
+ },
1564
+ children: [
1565
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25A3" }),
1566
+ "\uD654\uBA74 \uC804\uCCB4\uB85C \uBCF4\uB0B4\uAE30"
1567
+ ]
1568
+ }
1569
+ ),
1570
+ devClipCount > 0 ? /* @__PURE__ */ jsxs(
1571
+ "button",
1572
+ {
1573
+ type: "button",
1574
+ "data-fk-copy-all": devClipCount,
1575
+ title: "\uC774\uBC88 \uC138\uC158\uC5D0 \uCC0D\uC740 \uAC83\uC744 \uBC88\uD638 \uB9E4\uAE34 \uBAA9\uB85D \uD558\uB098\uB85C \uBCF5\uC0AC\uD569\uB2C8\uB2E4.",
1576
+ onClick: onCopyAll,
1577
+ style: {
1578
+ ...baseButton,
1579
+ display: "inline-flex",
1580
+ alignItems: "center",
1581
+ gap: 6,
1582
+ whiteSpace: "nowrap",
1583
+ fontWeight: 600
1584
+ },
1585
+ children: [
1586
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u29C9" }),
1587
+ "\uC804\uBD80 \uBCF5\uC0AC ",
1588
+ devClipCount
1589
+ ]
1590
+ }
1591
+ ) : null,
1501
1592
  markerCount > 0 ? /* @__PURE__ */ jsxs(
1502
1593
  "button",
1503
1594
  {
@@ -1626,6 +1717,10 @@ function FeedbackKit(props) {
1626
1717
  const [widgetState, setWidgetState] = useState(null);
1627
1718
  const [pickingState, setPickingState] = useState(INITIAL_PICKING_STATE);
1628
1719
  const [announcement, setAnnouncement] = useState(null);
1720
+ const [devMode] = useState(() => props.devMode === true);
1721
+ const devClipsRef = useRef([]);
1722
+ const [devClipCount, setDevClipCount] = useState(0);
1723
+ const [devFallbackText, setDevFallbackText] = useState(null);
1629
1724
  const {
1630
1725
  queue,
1631
1726
  createReport,
@@ -1750,6 +1845,39 @@ function FeedbackKit(props) {
1750
1845
  if (root) root.style.visibility = previousVisibility;
1751
1846
  }
1752
1847
  }
1848
+ function currentClip() {
1849
+ const state = pickingState.popup;
1850
+ if (!state) return null;
1851
+ return {
1852
+ comment: state.comment,
1853
+ element: state.element ?? null,
1854
+ screen: typeof location === "undefined" ? null : location.pathname
1855
+ };
1856
+ }
1857
+ async function putOnClipboard(text) {
1858
+ const ok = await writeToClipboard(text);
1859
+ setDevFallbackText(ok ? null : text);
1860
+ }
1861
+ async function copyOne() {
1862
+ const clip = currentClip();
1863
+ if (!clip || clip.comment.trim() === "") return;
1864
+ devClipsRef.current = [...devClipsRef.current, clip];
1865
+ setDevClipCount(devClipsRef.current.length);
1866
+ activeKit.picking.cancelAnnotation();
1867
+ await putOnClipboard(renderDevClipItem(clip));
1868
+ }
1869
+ async function copyAll() {
1870
+ if (devClipsRef.current.length === 0) return;
1871
+ await putOnClipboard(renderDevClipList(devClipsRef.current));
1872
+ }
1873
+ async function pressFloatingButton() {
1874
+ if (props.defaultMode !== "report" && !pickingActive) {
1875
+ setAnnouncement(null);
1876
+ activeKit.widget.startPicking();
1877
+ return;
1878
+ }
1879
+ await openReport();
1880
+ }
1753
1881
  async function openReport() {
1754
1882
  setAnnouncement(null);
1755
1883
  await withOwnUiHidden(() => activeKit.widget.openReport());
@@ -1819,7 +1947,7 @@ function FeedbackKit(props) {
1819
1947
  id: FLOATING_BUTTON_ID,
1820
1948
  type: "button",
1821
1949
  "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1822
- onClick: () => void openReport(),
1950
+ onClick: () => void pressFloatingButton(),
1823
1951
  style: {
1824
1952
  ...primaryButton,
1825
1953
  position: "fixed",
@@ -1834,6 +1962,66 @@ function FeedbackKit(props) {
1834
1962
  children: "\uD53C\uB4DC\uBC31"
1835
1963
  }
1836
1964
  ) : null,
1965
+ floating && devMode ? (
1966
+ // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
1967
+ // 조용한 유실이라 이 표시가 유일한 방어다. 버튼 **밖**에 두는 이유는 버튼의
1968
+ // 접근성 이름을 오염시키지 않기 위해서다.
1969
+ /* @__PURE__ */ jsx(
1970
+ "span",
1971
+ {
1972
+ "data-fk-dev-badge": "",
1973
+ "aria-label": "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC\uAC00 \uCF1C\uC838 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uC81C\uBCF4\uAC00 \uC218\uC9D1\uCC98\uB85C \uAC00\uC9C0 \uC54A\uACE0 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC\uB429\uB2C8\uB2E4",
1974
+ style: {
1975
+ position: "fixed",
1976
+ right: 24,
1977
+ bottom: 62,
1978
+ zIndex: 2147483601,
1979
+ padding: "2px 8px",
1980
+ borderRadius: 6,
1981
+ background: TOKENS.warning,
1982
+ color: "#1f2933",
1983
+ fontSize: 11,
1984
+ fontWeight: 700,
1985
+ pointerEvents: "none"
1986
+ },
1987
+ children: "\uAC1C\uBC1C\uC6A9 \u2014 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC"
1988
+ }
1989
+ )
1990
+ ) : null,
1991
+ devFallbackText !== null ? /* @__PURE__ */ jsxs(
1992
+ "div",
1993
+ {
1994
+ role: "dialog",
1995
+ "aria-label": "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBABB \uC368\uC11C \uB300\uC2E0 \uBCF4\uC5EC\uC90D\uB2C8\uB2E4",
1996
+ "data-fk-dev-fallback": "",
1997
+ style: {
1998
+ position: "fixed",
1999
+ right: 24,
2000
+ bottom: 92,
2001
+ zIndex: 2147483602,
2002
+ width: "min(420px, calc(100vw - 48px))",
2003
+ border: `1px solid ${TOKENS.line}`,
2004
+ borderRadius: 12,
2005
+ background: TOKENS.surface,
2006
+ padding: 12,
2007
+ boxShadow: TOKENS.shadow,
2008
+ pointerEvents: "auto"
2009
+ },
2010
+ children: [
2011
+ /* @__PURE__ */ jsx("div", { style: { marginBottom: 6, color: TOKENS.muted, fontSize: 12 }, children: "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uC4F0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC544\uB798\uC5D0\uC11C \uC9C1\uC811 \uBCF5\uC0AC\uD558\uC138\uC694." }),
2012
+ /* @__PURE__ */ jsx(
2013
+ "textarea",
2014
+ {
2015
+ readOnly: true,
2016
+ value: devFallbackText,
2017
+ onFocus: (event) => event.currentTarget.select(),
2018
+ style: { ...inputStyle, height: 140, fontFamily: "ui-monospace, monospace", fontSize: 12 }
2019
+ }
2020
+ ),
2021
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 }, children: /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setDevFallbackText(null), style: baseButton, children: "\uB2EB\uAE30" }) })
2022
+ ]
2023
+ }
2024
+ ) : null,
1837
2025
  announcement && !modal.open ? /* @__PURE__ */ jsx(
1838
2026
  "div",
1839
2027
  {
@@ -2188,7 +2376,10 @@ function FeedbackKit(props) {
2188
2376
  markerCount: pickingState.markers.length,
2189
2377
  onToggle: togglePicking,
2190
2378
  onTogglePause: () => activeKit.picking.togglePause(),
2191
- onClearMarkers: () => activeKit.picking.clearMarkers()
2379
+ onClearMarkers: () => activeKit.picking.clearMarkers(),
2380
+ onReportWholeScreen: () => void openReport(),
2381
+ devClipCount: devMode ? devClipCount : 0,
2382
+ onCopyAll: () => void copyAll()
2192
2383
  }
2193
2384
  ),
2194
2385
  pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
@@ -2225,6 +2416,10 @@ function FeedbackKit(props) {
2225
2416
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2226
2417
  onSubmit: (event) => {
2227
2418
  event.preventDefault();
2419
+ if (devMode) {
2420
+ void copyOne();
2421
+ return;
2422
+ }
2228
2423
  void kit.picking.saveAnnotation();
2229
2424
  },
2230
2425
  onPaste: (event) => {
@@ -2378,7 +2573,7 @@ function FeedbackKit(props) {
2378
2573
  opacity: popup.canSave && !popup.saving ? 1 : 0.5,
2379
2574
  cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
2380
2575
  },
2381
- children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2576
+ children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2382
2577
  }
2383
2578
  )
2384
2579
  ] })
@@ -2460,7 +2655,7 @@ export {
2460
2655
  reencodeWebScreenshot,
2461
2656
  resolveConfig,
2462
2657
  shouldShowWidget,
2463
- sourceFromElement,
2658
+ sourceFromElement2 as sourceFromElement,
2464
2659
  visibleText,
2465
2660
  webContextProviders,
2466
2661
  webDiagnosticsOptions,