@solhun/feedback-kit-web 0.8.0 → 0.10.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,31 @@ 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
+ * 톱니는 **로컬(`localhost`)에서만** 뜬다. 배포된 화면에서 실사용자가 켜면 그 사람 제보는
375
+ * 어디에도 안 남고 본인은 보냈다고 믿는다. 이 값을 `true` 로 주면 톱니도 같이 뜬다.
376
+ *
377
+ * 켜져 있는 동안은 그 사실이 화면에 계속 보인다(조용한 유실을 막는 유일한 방어).
378
+ *
379
+ * 웹 전용이다. 앱에는 DOM 이 없어 선택자·컴포넌트 경로·소스 경로가 성립하지 않는다.
380
+ */
381
+ devMode?: boolean;
357
382
  /**
358
383
  * 정적 힌트 목록(설정으로 직접 넘김). 주면 서버에서 받아오지 않는다 — 도입 초기·오프라인
359
384
  * 데모·테스트용이다. 서버 목록과 합치지 않는다(둘을 섞으면 무엇이 보이는지 설명이 안 된다).
package/dist/index.d.ts CHANGED
@@ -354,6 +354,31 @@ 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
+ * 톱니는 **로컬(`localhost`)에서만** 뜬다. 배포된 화면에서 실사용자가 켜면 그 사람 제보는
375
+ * 어디에도 안 남고 본인은 보냈다고 믿는다. 이 값을 `true` 로 주면 톱니도 같이 뜬다.
376
+ *
377
+ * 켜져 있는 동안은 그 사실이 화면에 계속 보인다(조용한 유실을 막는 유일한 방어).
378
+ *
379
+ * 웹 전용이다. 앱에는 DOM 이 없어 선택자·컴포넌트 경로·소스 경로가 성립하지 않는다.
380
+ */
381
+ devMode?: boolean;
357
382
  /**
358
383
  * 정적 힌트 목록(설정으로 직접 넘김). 주면 서버에서 받아오지 않는다 — 도입 초기·오프라인
359
384
  * 데모·테스트용이다. 서버 목록과 합치지 않는다(둘을 섞으면 무엇이 보이는지 설명이 안 된다).
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,84 @@ 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.10.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
+ }
1224
+
1225
+ // src/dev-preference.ts
1226
+ var STORAGE_KEY = "feedback-kit:dev-mode";
1227
+ function isDevSettingsHost(hostname) {
1228
+ const host = hostname.trim().toLowerCase();
1229
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") return true;
1230
+ return host.endsWith(".localhost") || host.endsWith(".local");
1231
+ }
1232
+ function readDevPreference() {
1233
+ try {
1234
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
1235
+ if (raw === "on") return true;
1236
+ if (raw === "off") return false;
1237
+ return null;
1238
+ } catch {
1239
+ return null;
1240
+ }
1241
+ }
1242
+ function writeDevPreference(value) {
1243
+ try {
1244
+ const storage = globalThis.localStorage;
1245
+ if (!storage) return;
1246
+ if (value === null) storage.removeItem(STORAGE_KEY);
1247
+ else storage.setItem(STORAGE_KEY, value ? "on" : "off");
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ function resolveDevMode(propValue, stored) {
1252
+ if (stored !== null) return stored;
1253
+ return propValue === true;
1254
+ }
1178
1255
 
1179
1256
  // src/feedback-kit.tsx
1180
1257
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1443,7 +1520,10 @@ function PickBar({
1443
1520
  markerCount,
1444
1521
  onToggle,
1445
1522
  onTogglePause,
1446
- onClearMarkers
1523
+ onClearMarkers,
1524
+ onReportWholeScreen,
1525
+ devClipCount,
1526
+ onCopyAll
1447
1527
  }) {
1448
1528
  return /* @__PURE__ */ jsxs(
1449
1529
  "div",
@@ -1498,6 +1578,48 @@ function PickBar({
1498
1578
  ]
1499
1579
  }
1500
1580
  ),
1581
+ /* @__PURE__ */ jsxs(
1582
+ "button",
1583
+ {
1584
+ type: "button",
1585
+ "data-fk-pick-whole": "",
1586
+ title: "\uAC00\uB9AC\uD0A8 \uC694\uC18C \uC5C6\uC774 \uC774 \uD654\uBA74 \uC804\uCCB4\uC5D0 \uB300\uD574 \uC81C\uBCF4\uD569\uB2C8\uB2E4.",
1587
+ onClick: onReportWholeScreen,
1588
+ style: {
1589
+ ...baseButton,
1590
+ display: "inline-flex",
1591
+ alignItems: "center",
1592
+ gap: 6,
1593
+ whiteSpace: "nowrap"
1594
+ },
1595
+ children: [
1596
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25A3" }),
1597
+ "\uD654\uBA74 \uC804\uCCB4\uB85C \uBCF4\uB0B4\uAE30"
1598
+ ]
1599
+ }
1600
+ ),
1601
+ devClipCount > 0 ? /* @__PURE__ */ jsxs(
1602
+ "button",
1603
+ {
1604
+ type: "button",
1605
+ "data-fk-copy-all": devClipCount,
1606
+ title: "\uC774\uBC88 \uC138\uC158\uC5D0 \uCC0D\uC740 \uAC83\uC744 \uBC88\uD638 \uB9E4\uAE34 \uBAA9\uB85D \uD558\uB098\uB85C \uBCF5\uC0AC\uD569\uB2C8\uB2E4.",
1607
+ onClick: onCopyAll,
1608
+ style: {
1609
+ ...baseButton,
1610
+ display: "inline-flex",
1611
+ alignItems: "center",
1612
+ gap: 6,
1613
+ whiteSpace: "nowrap",
1614
+ fontWeight: 600
1615
+ },
1616
+ children: [
1617
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u29C9" }),
1618
+ "\uC804\uBD80 \uBCF5\uC0AC ",
1619
+ devClipCount
1620
+ ]
1621
+ }
1622
+ ) : null,
1501
1623
  markerCount > 0 ? /* @__PURE__ */ jsxs(
1502
1624
  "button",
1503
1625
  {
@@ -1626,6 +1748,14 @@ function FeedbackKit(props) {
1626
1748
  const [widgetState, setWidgetState] = useState(null);
1627
1749
  const [pickingState, setPickingState] = useState(INITIAL_PICKING_STATE);
1628
1750
  const [announcement, setAnnouncement] = useState(null);
1751
+ const [devMode, setDevMode] = useState(() => resolveDevMode(props.devMode, readDevPreference()));
1752
+ const [devSettingsAvailable] = useState(
1753
+ () => props.devMode === true || isDevSettingsHost(globalThis.location?.hostname ?? "")
1754
+ );
1755
+ const [devSettingsOpen, setDevSettingsOpen] = useState(false);
1756
+ const devClipsRef = useRef([]);
1757
+ const [devClipCount, setDevClipCount] = useState(0);
1758
+ const [devFallbackText, setDevFallbackText] = useState(null);
1629
1759
  const {
1630
1760
  queue,
1631
1761
  createReport,
@@ -1750,6 +1880,56 @@ function FeedbackKit(props) {
1750
1880
  if (root) root.style.visibility = previousVisibility;
1751
1881
  }
1752
1882
  }
1883
+ function currentClip() {
1884
+ const state = pickingState.popup;
1885
+ if (!state) return null;
1886
+ return {
1887
+ comment: state.comment,
1888
+ element: state.element ?? null,
1889
+ screen: typeof location === "undefined" ? null : location.pathname
1890
+ };
1891
+ }
1892
+ async function putOnClipboard(text) {
1893
+ const ok = await writeToClipboard(text);
1894
+ setDevFallbackText(ok ? null : text);
1895
+ }
1896
+ function commitPopup() {
1897
+ if (devMode) {
1898
+ void copyOne();
1899
+ return;
1900
+ }
1901
+ void activeKit.picking.saveAnnotation();
1902
+ }
1903
+ async function copyOne() {
1904
+ const clip = currentClip();
1905
+ if (!clip || clip.comment.trim() === "") return;
1906
+ devClipsRef.current = [...devClipsRef.current, clip];
1907
+ setDevClipCount(devClipsRef.current.length);
1908
+ activeKit.picking.cancelAnnotation();
1909
+ await putOnClipboard(renderDevClipItem(clip));
1910
+ }
1911
+ async function copyAll() {
1912
+ if (devClipsRef.current.length === 0) return;
1913
+ await putOnClipboard(renderDevClipList(devClipsRef.current));
1914
+ }
1915
+ function toggleDevMode(next) {
1916
+ writeDevPreference(next);
1917
+ setDevMode(next);
1918
+ devClipsRef.current = [];
1919
+ setDevClipCount(0);
1920
+ setDevFallbackText(null);
1921
+ setAnnouncement(
1922
+ next ? "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC\uB97C \uCF30\uC2B5\uB2C8\uB2E4. \uC774\uC81C \uC81C\uBCF4\uB294 \uC218\uC9D1\uCC98\uB85C \uAC00\uC9C0 \uC54A\uACE0 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC\uB429\uB2C8\uB2E4." : "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC\uB97C \uAED0\uC2B5\uB2C8\uB2E4. \uC774\uC81C \uC81C\uBCF4\uAC00 \uC218\uC9D1\uCC98\uB85C \uAC11\uB2C8\uB2E4."
1923
+ );
1924
+ }
1925
+ async function pressFloatingButton() {
1926
+ if (props.defaultMode !== "report" && !pickingActive) {
1927
+ setAnnouncement(null);
1928
+ activeKit.widget.startPicking();
1929
+ return;
1930
+ }
1931
+ await openReport();
1932
+ }
1753
1933
  async function openReport() {
1754
1934
  setAnnouncement(null);
1755
1935
  await withOwnUiHidden(() => activeKit.widget.openReport());
@@ -1799,8 +1979,26 @@ function FeedbackKit(props) {
1799
1979
  }
1800
1980
  function submitReport(event) {
1801
1981
  event.preventDefault();
1982
+ if (devMode) {
1983
+ void copyFromModal();
1984
+ return;
1985
+ }
1802
1986
  void activeKit.widget.submitReport();
1803
1987
  }
1988
+ async function copyFromModal() {
1989
+ const modalState = activeKit.widget.getState().modal;
1990
+ if (modalState.comment.trim() === "") return;
1991
+ const clip = {
1992
+ comment: modalState.comment,
1993
+ element: modalState.element ?? null,
1994
+ screen: typeof location === "undefined" ? null : location.pathname,
1995
+ priority: modalState.priority
1996
+ };
1997
+ devClipsRef.current = [...devClipsRef.current, clip];
1998
+ setDevClipCount(devClipsRef.current.length);
1999
+ activeKit.widget.confirmCloseReport();
2000
+ await putOnClipboard(renderDevClipItem(clip));
2001
+ }
1804
2002
  const floating = screen === "button" || screen === "picking";
1805
2003
  const hoverBox = pickingState.hovered?.boundingBox;
1806
2004
  const popup = pickingState.popup;
@@ -1812,45 +2010,199 @@ function FeedbackKit(props) {
1812
2010
  style: { fontFamily: FONT_STACK, color: TOKENS.ink, fontSize: 14, lineHeight: 1.45 },
1813
2011
  children: [
1814
2012
  /* @__PURE__ */ jsx("style", { children: `@keyframes feedback-kit-spin{to{transform:rotate(360deg)}}` }),
1815
- floating ? /* @__PURE__ */ jsx(
1816
- "button",
2013
+ floating ? (
2014
+ // 톱니와 피드백 버튼을 한 상자에 넣는다. 각각 fixed 로 두면 버튼 글자 길이가
2015
+ // 바뀔 때마다 좌표를 다시 맞춰야 하고, 그러다 겹친다.
2016
+ /* @__PURE__ */ jsxs(
2017
+ "div",
2018
+ {
2019
+ style: {
2020
+ position: "fixed",
2021
+ right: 24,
2022
+ bottom: 24,
2023
+ zIndex: 2147483600,
2024
+ display: "flex",
2025
+ alignItems: "center",
2026
+ gap: 8
2027
+ },
2028
+ children: [
2029
+ devSettingsAvailable ? /* @__PURE__ */ jsx(
2030
+ "button",
2031
+ {
2032
+ type: "button",
2033
+ "data-fk-dev-settings-toggle": "",
2034
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2035
+ "aria-expanded": devSettingsOpen,
2036
+ onClick: () => setDevSettingsOpen((open) => !open),
2037
+ style: {
2038
+ ...baseButton,
2039
+ width: 46,
2040
+ minHeight: 46,
2041
+ padding: 0,
2042
+ borderRadius: 23,
2043
+ boxShadow: TOKENS.shadow,
2044
+ fontSize: 17,
2045
+ lineHeight: 1
2046
+ },
2047
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2699" })
2048
+ }
2049
+ ) : null,
2050
+ /* @__PURE__ */ jsx(
2051
+ "button",
2052
+ {
2053
+ ref: floatingButtonRef,
2054
+ id: FLOATING_BUTTON_ID,
2055
+ type: "button",
2056
+ "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
2057
+ onClick: () => void pressFloatingButton(),
2058
+ style: {
2059
+ ...primaryButton,
2060
+ minWidth: 112,
2061
+ minHeight: 46,
2062
+ borderRadius: 24,
2063
+ boxShadow: TOKENS.shadow
2064
+ },
2065
+ children: "\uD53C\uB4DC\uBC31"
2066
+ }
2067
+ )
2068
+ ]
2069
+ }
2070
+ )
2071
+ ) : null,
2072
+ floating && devMode ? (
2073
+ // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
2074
+ // 조용한 유실이라 이 표시가 유일한 방어다. 버튼 **밖**에 두는 이유는 버튼의
2075
+ // 접근성 이름을 오염시키지 않기 위해서다.
2076
+ /* @__PURE__ */ jsx(
2077
+ "span",
2078
+ {
2079
+ "data-fk-dev-badge": "",
2080
+ "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",
2081
+ style: {
2082
+ position: "fixed",
2083
+ right: 24,
2084
+ bottom: 62,
2085
+ zIndex: 2147483601,
2086
+ padding: "2px 8px",
2087
+ borderRadius: 6,
2088
+ background: TOKENS.warning,
2089
+ color: "#1f2933",
2090
+ fontSize: 11,
2091
+ fontWeight: 700,
2092
+ pointerEvents: "none"
2093
+ },
2094
+ children: "\uAC1C\uBC1C\uC6A9 \u2014 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC"
2095
+ }
2096
+ )
2097
+ ) : null,
2098
+ devFallbackText !== null ? /* @__PURE__ */ jsxs(
2099
+ "div",
1817
2100
  {
1818
- ref: floatingButtonRef,
1819
- id: FLOATING_BUTTON_ID,
1820
- type: "button",
1821
- "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1822
- onClick: () => void openReport(),
2101
+ role: "dialog",
2102
+ "aria-label": "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBABB \uC368\uC11C \uB300\uC2E0 \uBCF4\uC5EC\uC90D\uB2C8\uB2E4",
2103
+ "data-fk-dev-fallback": "",
1823
2104
  style: {
1824
- ...primaryButton,
1825
2105
  position: "fixed",
1826
2106
  right: 24,
1827
- bottom: 24,
1828
- zIndex: 2147483600,
1829
- minWidth: 112,
1830
- minHeight: 46,
1831
- borderRadius: 24,
1832
- boxShadow: TOKENS.shadow
2107
+ bottom: 92,
2108
+ zIndex: 2147483602,
2109
+ width: "min(420px, calc(100vw - 48px))",
2110
+ border: `1px solid ${TOKENS.line}`,
2111
+ borderRadius: 12,
2112
+ background: TOKENS.surface,
2113
+ padding: 12,
2114
+ boxShadow: TOKENS.shadow,
2115
+ pointerEvents: "auto"
1833
2116
  },
1834
- children: "\uD53C\uB4DC\uBC31"
2117
+ children: [
2118
+ /* @__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." }),
2119
+ /* @__PURE__ */ jsx(
2120
+ "textarea",
2121
+ {
2122
+ readOnly: true,
2123
+ value: devFallbackText,
2124
+ onFocus: (event) => event.currentTarget.select(),
2125
+ style: { ...inputStyle, height: 140, fontFamily: "ui-monospace, monospace", fontSize: 12 }
2126
+ }
2127
+ ),
2128
+ /* @__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" }) })
2129
+ ]
1835
2130
  }
1836
2131
  ) : null,
1837
- announcement && !modal.open ? /* @__PURE__ */ jsx(
2132
+ devSettingsOpen || announcement && !modal.open ? /* @__PURE__ */ jsxs(
1838
2133
  "div",
1839
2134
  {
1840
- role: "status",
1841
- "aria-live": "polite",
1842
2135
  style: {
1843
2136
  position: "fixed",
1844
2137
  right: 24,
1845
2138
  bottom: 82,
1846
2139
  zIndex: 2147483601,
1847
- padding: "10px 14px",
1848
- borderRadius: 10,
1849
- background: TOKENS.ink,
1850
- color: TOKENS.surface,
1851
- boxShadow: TOKENS.shadow
2140
+ display: "flex",
2141
+ flexDirection: "column",
2142
+ alignItems: "flex-end",
2143
+ gap: 8
1852
2144
  },
1853
- children: announcement
2145
+ children: [
2146
+ devSettingsOpen ? /* @__PURE__ */ jsxs(
2147
+ "div",
2148
+ {
2149
+ role: "dialog",
2150
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2151
+ "data-fk-dev-settings": "",
2152
+ style: {
2153
+ width: 292,
2154
+ maxWidth: "calc(100vw - 48px)",
2155
+ padding: 14,
2156
+ borderRadius: 12,
2157
+ border: `1px solid ${TOKENS.line}`,
2158
+ background: TOKENS.surface,
2159
+ boxShadow: TOKENS.shadow
2160
+ },
2161
+ children: [
2162
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "flex-start", gap: 9, cursor: "pointer" }, children: [
2163
+ /* @__PURE__ */ jsx(
2164
+ "input",
2165
+ {
2166
+ type: "checkbox",
2167
+ checked: devMode,
2168
+ onChange: (event) => toggleDevMode(event.currentTarget.checked),
2169
+ style: { marginTop: 3 }
2170
+ }
2171
+ ),
2172
+ /* @__PURE__ */ jsxs("span", { children: [
2173
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 700 }, children: "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC" }),
2174
+ /* @__PURE__ */ jsxs("span", { style: { display: "block", marginTop: 4, color: TOKENS.muted, fontSize: 12.5 }, children: [
2175
+ "\uCF1C\uBA74 \uC81C\uBCF4\uB97C \uC218\uC9D1\uCC98\uB85C \uBCF4\uB0B4\uC9C0 \uC54A\uACE0 ",
2176
+ /* @__PURE__ */ jsx("b", { children: "\uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC" }),
2177
+ "\uD569\uB2C8\uB2E4. \uBD99\uC5EC\uB123\uC73C\uBA74 \uD654\uBA74\xB7\uC694\uC18C\xB7 \uCEF4\uD3EC\uB10C\uD2B8\xB7\uC18C\uC2A4 \uACBD\uB85C\uAC00 \uAC19\uC774 \uB098\uC635\uB2C8\uB2E4."
2178
+ ] })
2179
+ ] })
2180
+ ] }),
2181
+ /* @__PURE__ */ jsxs("p", { style: { margin: "12px 0 0", color: TOKENS.muted, fontSize: 12 }, children: [
2182
+ "\uC774 \uC124\uC815\uC740 \uB85C\uCEEC(",
2183
+ /* @__PURE__ */ jsx("code", { children: "localhost" }),
2184
+ ")\uC5D0\uC11C\uB9CC \uBCF4\uC785\uB2C8\uB2E4. \uBC30\uD3EC\uB41C \uD654\uBA74\uC5D0\uC11C\uB294 \uC2E4\uC0AC\uC6A9\uC790\uAC00 \uCF24 \uC218 \uC5C6\uC5B4\uC57C \uD574\uC11C \uC544\uC608 \uB744\uC6B0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
2185
+ ] }),
2186
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 12 }, children: /* @__PURE__ */ jsx("button", { type: "button", style: baseButton, onClick: () => setDevSettingsOpen(false), children: "\uB2EB\uAE30" }) })
2187
+ ]
2188
+ }
2189
+ ) : null,
2190
+ announcement && !modal.open ? /* @__PURE__ */ jsx(
2191
+ "div",
2192
+ {
2193
+ role: "status",
2194
+ "aria-live": "polite",
2195
+ style: {
2196
+ padding: "10px 14px",
2197
+ borderRadius: 10,
2198
+ background: TOKENS.ink,
2199
+ color: TOKENS.surface,
2200
+ boxShadow: TOKENS.shadow
2201
+ },
2202
+ children: announcement
2203
+ }
2204
+ ) : null
2205
+ ]
1854
2206
  }
1855
2207
  ) : null,
1856
2208
  screen === "modal" && modal.open ? /* @__PURE__ */ jsx(
@@ -2109,7 +2461,7 @@ function FeedbackKit(props) {
2109
2461
  opacity: modal.canSubmit ? 1 : 0.5,
2110
2462
  cursor: modal.canSubmit ? "pointer" : "not-allowed"
2111
2463
  },
2112
- children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2464
+ children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2113
2465
  }
2114
2466
  )
2115
2467
  ] })
@@ -2188,7 +2540,10 @@ function FeedbackKit(props) {
2188
2540
  markerCount: pickingState.markers.length,
2189
2541
  onToggle: togglePicking,
2190
2542
  onTogglePause: () => activeKit.picking.togglePause(),
2191
- onClearMarkers: () => activeKit.picking.clearMarkers()
2543
+ onClearMarkers: () => activeKit.picking.clearMarkers(),
2544
+ onReportWholeScreen: () => void openReport(),
2545
+ devClipCount: devMode ? devClipCount : 0,
2546
+ onCopyAll: () => void copyAll()
2192
2547
  }
2193
2548
  ),
2194
2549
  pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
@@ -2225,7 +2580,7 @@ function FeedbackKit(props) {
2225
2580
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2226
2581
  onSubmit: (event) => {
2227
2582
  event.preventDefault();
2228
- void kit.picking.saveAnnotation();
2583
+ commitPopup();
2229
2584
  },
2230
2585
  onPaste: (event) => {
2231
2586
  if (!firstImageOf(event.clipboardData)) return;
@@ -2307,7 +2662,7 @@ function FeedbackKit(props) {
2307
2662
  const native = event.nativeEvent;
2308
2663
  if (native.isComposing) return;
2309
2664
  event.preventDefault();
2310
- void kit.picking.saveAnnotation();
2665
+ commitPopup();
2311
2666
  },
2312
2667
  rows: 2,
2313
2668
  placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",
@@ -2378,7 +2733,7 @@ function FeedbackKit(props) {
2378
2733
  opacity: popup.canSave && !popup.saving ? 1 : 0.5,
2379
2734
  cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
2380
2735
  },
2381
- children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2736
+ children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2382
2737
  }
2383
2738
  )
2384
2739
  ] })
@@ -2460,7 +2815,7 @@ export {
2460
2815
  reencodeWebScreenshot,
2461
2816
  resolveConfig,
2462
2817
  shouldShowWidget,
2463
- sourceFromElement,
2818
+ sourceFromElement2 as sourceFromElement,
2464
2819
  visibleText,
2465
2820
  webContextProviders,
2466
2821
  webDiagnosticsOptions,