@solhun/feedback-kit-web 0.9.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
@@ -369,8 +369,12 @@ interface WebWidgetOpts extends Omit<WidgetControllerOpts, "platform" | "initial
369
369
  * 개발자가 로컬에서 고치는 중일 때 라쏘런 왕복(보내고·트리아지하고·이슈가 되고)이 오히려
370
370
  * 느리다. 정작 고칠 사람은 지금 그 화면을 보고 있는 본인이다.
371
371
  *
372
- * **기본은 꺼짐이고 마운트 시점에 고정한다** 도는 중에 바뀌면 "지금 보낸 건 어디로 갔나"가
373
- * 생긴다. 켜져 있는 동안은 사실이 화면에 계속 보여야 한다(조용한 유실을 막는 유일한 방어).
372
+ * **기본은 꺼짐**이다. 이건 초기값일 뿐이고, 사람이 화면의 톱니(설정)에서 켜거나 끄면
373
+ * 선택이 값을 이긴다 그러면 개발 빌드에서 끄려고 코드를 고쳐야 한다.
374
+ * 톱니는 **로컬(`localhost`)에서만** 뜬다. 배포된 화면에서 실사용자가 켜면 그 사람 제보는
375
+ * 어디에도 안 남고 본인은 보냈다고 믿는다. 이 값을 `true` 로 주면 톱니도 같이 뜬다.
376
+ *
377
+ * 켜져 있는 동안은 그 사실이 화면에 계속 보인다(조용한 유실을 막는 유일한 방어).
374
378
  *
375
379
  * 웹 전용이다. 앱에는 DOM 이 없어 선택자·컴포넌트 경로·소스 경로가 성립하지 않는다.
376
380
  */
package/dist/index.d.ts CHANGED
@@ -369,8 +369,12 @@ interface WebWidgetOpts extends Omit<WidgetControllerOpts, "platform" | "initial
369
369
  * 개발자가 로컬에서 고치는 중일 때 라쏘런 왕복(보내고·트리아지하고·이슈가 되고)이 오히려
370
370
  * 느리다. 정작 고칠 사람은 지금 그 화면을 보고 있는 본인이다.
371
371
  *
372
- * **기본은 꺼짐이고 마운트 시점에 고정한다** 도는 중에 바뀌면 "지금 보낸 건 어디로 갔나"가
373
- * 생긴다. 켜져 있는 동안은 사실이 화면에 계속 보여야 한다(조용한 유실을 막는 유일한 방어).
372
+ * **기본은 꺼짐**이다. 이건 초기값일 뿐이고, 사람이 화면의 톱니(설정)에서 켜거나 끄면
373
+ * 선택이 값을 이긴다 그러면 개발 빌드에서 끄려고 코드를 고쳐야 한다.
374
+ * 톱니는 **로컬(`localhost`)에서만** 뜬다. 배포된 화면에서 실사용자가 켜면 그 사람 제보는
375
+ * 어디에도 안 남고 본인은 보냈다고 믿는다. 이 값을 `true` 로 주면 톱니도 같이 뜬다.
376
+ *
377
+ * 켜져 있는 동안은 그 사실이 화면에 계속 보인다(조용한 유실을 막는 유일한 방어).
374
378
  *
375
379
  * 웹 전용이다. 앱에는 DOM 이 없어 선택자·컴포넌트 경로·소스 경로가 성립하지 않는다.
376
380
  */
package/dist/index.js CHANGED
@@ -1174,7 +1174,7 @@ import {
1174
1174
  } from "react";
1175
1175
 
1176
1176
  // src/version.ts
1177
- var VERSION = true ? "0.9.0" : "dev";
1177
+ var VERSION = true ? "0.10.0" : "dev";
1178
1178
 
1179
1179
  // src/dev-clipboard.ts
1180
1180
  import { sourceFromElement } from "@solhun/feedback-kit-core";
@@ -1222,6 +1222,37 @@ async function writeToClipboard(text) {
1222
1222
  }
1223
1223
  }
1224
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
+ }
1255
+
1225
1256
  // src/feedback-kit.tsx
1226
1257
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1227
1258
  var TOKENS = {
@@ -1717,7 +1748,11 @@ function FeedbackKit(props) {
1717
1748
  const [widgetState, setWidgetState] = useState(null);
1718
1749
  const [pickingState, setPickingState] = useState(INITIAL_PICKING_STATE);
1719
1750
  const [announcement, setAnnouncement] = useState(null);
1720
- const [devMode] = useState(() => props.devMode === true);
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);
1721
1756
  const devClipsRef = useRef([]);
1722
1757
  const [devClipCount, setDevClipCount] = useState(0);
1723
1758
  const [devFallbackText, setDevFallbackText] = useState(null);
@@ -1858,6 +1893,13 @@ function FeedbackKit(props) {
1858
1893
  const ok = await writeToClipboard(text);
1859
1894
  setDevFallbackText(ok ? null : text);
1860
1895
  }
1896
+ function commitPopup() {
1897
+ if (devMode) {
1898
+ void copyOne();
1899
+ return;
1900
+ }
1901
+ void activeKit.picking.saveAnnotation();
1902
+ }
1861
1903
  async function copyOne() {
1862
1904
  const clip = currentClip();
1863
1905
  if (!clip || clip.comment.trim() === "") return;
@@ -1870,6 +1912,16 @@ function FeedbackKit(props) {
1870
1912
  if (devClipsRef.current.length === 0) return;
1871
1913
  await putOnClipboard(renderDevClipList(devClipsRef.current));
1872
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
+ }
1873
1925
  async function pressFloatingButton() {
1874
1926
  if (props.defaultMode !== "report" && !pickingActive) {
1875
1927
  setAnnouncement(null);
@@ -1927,8 +1979,26 @@ function FeedbackKit(props) {
1927
1979
  }
1928
1980
  function submitReport(event) {
1929
1981
  event.preventDefault();
1982
+ if (devMode) {
1983
+ void copyFromModal();
1984
+ return;
1985
+ }
1930
1986
  void activeKit.widget.submitReport();
1931
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
+ }
1932
2002
  const floating = screen === "button" || screen === "picking";
1933
2003
  const hoverBox = pickingState.hovered?.boundingBox;
1934
2004
  const popup = pickingState.popup;
@@ -1940,27 +2010,64 @@ function FeedbackKit(props) {
1940
2010
  style: { fontFamily: FONT_STACK, color: TOKENS.ink, fontSize: 14, lineHeight: 1.45 },
1941
2011
  children: [
1942
2012
  /* @__PURE__ */ jsx("style", { children: `@keyframes feedback-kit-spin{to{transform:rotate(360deg)}}` }),
1943
- floating ? /* @__PURE__ */ jsx(
1944
- "button",
1945
- {
1946
- ref: floatingButtonRef,
1947
- id: FLOATING_BUTTON_ID,
1948
- type: "button",
1949
- "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1950
- onClick: () => void pressFloatingButton(),
1951
- style: {
1952
- ...primaryButton,
1953
- position: "fixed",
1954
- right: 24,
1955
- bottom: 24,
1956
- zIndex: 2147483600,
1957
- minWidth: 112,
1958
- minHeight: 46,
1959
- borderRadius: 24,
1960
- boxShadow: TOKENS.shadow
1961
- },
1962
- children: "\uD53C\uB4DC\uBC31"
1963
- }
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
+ )
1964
2071
  ) : null,
1965
2072
  floating && devMode ? (
1966
2073
  // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
@@ -2022,23 +2129,80 @@ function FeedbackKit(props) {
2022
2129
  ]
2023
2130
  }
2024
2131
  ) : null,
2025
- announcement && !modal.open ? /* @__PURE__ */ jsx(
2132
+ devSettingsOpen || announcement && !modal.open ? /* @__PURE__ */ jsxs(
2026
2133
  "div",
2027
2134
  {
2028
- role: "status",
2029
- "aria-live": "polite",
2030
2135
  style: {
2031
2136
  position: "fixed",
2032
2137
  right: 24,
2033
2138
  bottom: 82,
2034
2139
  zIndex: 2147483601,
2035
- padding: "10px 14px",
2036
- borderRadius: 10,
2037
- background: TOKENS.ink,
2038
- color: TOKENS.surface,
2039
- boxShadow: TOKENS.shadow
2140
+ display: "flex",
2141
+ flexDirection: "column",
2142
+ alignItems: "flex-end",
2143
+ gap: 8
2040
2144
  },
2041
- 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
+ ]
2042
2206
  }
2043
2207
  ) : null,
2044
2208
  screen === "modal" && modal.open ? /* @__PURE__ */ jsx(
@@ -2297,7 +2461,7 @@ function FeedbackKit(props) {
2297
2461
  opacity: modal.canSubmit ? 1 : 0.5,
2298
2462
  cursor: modal.canSubmit ? "pointer" : "not-allowed"
2299
2463
  },
2300
- 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"
2301
2465
  }
2302
2466
  )
2303
2467
  ] })
@@ -2416,11 +2580,7 @@ function FeedbackKit(props) {
2416
2580
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2417
2581
  onSubmit: (event) => {
2418
2582
  event.preventDefault();
2419
- if (devMode) {
2420
- void copyOne();
2421
- return;
2422
- }
2423
- void kit.picking.saveAnnotation();
2583
+ commitPopup();
2424
2584
  },
2425
2585
  onPaste: (event) => {
2426
2586
  if (!firstImageOf(event.clipboardData)) return;
@@ -2502,7 +2662,7 @@ function FeedbackKit(props) {
2502
2662
  const native = event.nativeEvent;
2503
2663
  if (native.isComposing) return;
2504
2664
  event.preventDefault();
2505
- void kit.picking.saveAnnotation();
2665
+ commitPopup();
2506
2666
  },
2507
2667
  rows: 2,
2508
2668
  placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",