@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.cjs CHANGED
@@ -1214,7 +1214,7 @@ var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
1214
1214
  var import_react = require("react");
1215
1215
 
1216
1216
  // src/version.ts
1217
- var VERSION = true ? "0.9.0" : "dev";
1217
+ var VERSION = true ? "0.10.0" : "dev";
1218
1218
 
1219
1219
  // src/dev-clipboard.ts
1220
1220
  var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
@@ -1262,6 +1262,37 @@ async function writeToClipboard(text) {
1262
1262
  }
1263
1263
  }
1264
1264
 
1265
+ // src/dev-preference.ts
1266
+ var STORAGE_KEY = "feedback-kit:dev-mode";
1267
+ function isDevSettingsHost(hostname) {
1268
+ const host = hostname.trim().toLowerCase();
1269
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") return true;
1270
+ return host.endsWith(".localhost") || host.endsWith(".local");
1271
+ }
1272
+ function readDevPreference() {
1273
+ try {
1274
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
1275
+ if (raw === "on") return true;
1276
+ if (raw === "off") return false;
1277
+ return null;
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
1282
+ function writeDevPreference(value) {
1283
+ try {
1284
+ const storage = globalThis.localStorage;
1285
+ if (!storage) return;
1286
+ if (value === null) storage.removeItem(STORAGE_KEY);
1287
+ else storage.setItem(STORAGE_KEY, value ? "on" : "off");
1288
+ } catch {
1289
+ }
1290
+ }
1291
+ function resolveDevMode(propValue, stored) {
1292
+ if (stored !== null) return stored;
1293
+ return propValue === true;
1294
+ }
1295
+
1265
1296
  // src/feedback-kit.tsx
1266
1297
  var import_jsx_runtime = require("react/jsx-runtime");
1267
1298
  var TOKENS = {
@@ -1757,7 +1788,11 @@ function FeedbackKit(props) {
1757
1788
  const [widgetState, setWidgetState] = (0, import_react.useState)(null);
1758
1789
  const [pickingState, setPickingState] = (0, import_react.useState)(INITIAL_PICKING_STATE);
1759
1790
  const [announcement, setAnnouncement] = (0, import_react.useState)(null);
1760
- const [devMode] = (0, import_react.useState)(() => props.devMode === true);
1791
+ const [devMode, setDevMode] = (0, import_react.useState)(() => resolveDevMode(props.devMode, readDevPreference()));
1792
+ const [devSettingsAvailable] = (0, import_react.useState)(
1793
+ () => props.devMode === true || isDevSettingsHost(globalThis.location?.hostname ?? "")
1794
+ );
1795
+ const [devSettingsOpen, setDevSettingsOpen] = (0, import_react.useState)(false);
1761
1796
  const devClipsRef = (0, import_react.useRef)([]);
1762
1797
  const [devClipCount, setDevClipCount] = (0, import_react.useState)(0);
1763
1798
  const [devFallbackText, setDevFallbackText] = (0, import_react.useState)(null);
@@ -1898,6 +1933,13 @@ function FeedbackKit(props) {
1898
1933
  const ok = await writeToClipboard(text);
1899
1934
  setDevFallbackText(ok ? null : text);
1900
1935
  }
1936
+ function commitPopup() {
1937
+ if (devMode) {
1938
+ void copyOne();
1939
+ return;
1940
+ }
1941
+ void activeKit.picking.saveAnnotation();
1942
+ }
1901
1943
  async function copyOne() {
1902
1944
  const clip = currentClip();
1903
1945
  if (!clip || clip.comment.trim() === "") return;
@@ -1910,6 +1952,16 @@ function FeedbackKit(props) {
1910
1952
  if (devClipsRef.current.length === 0) return;
1911
1953
  await putOnClipboard(renderDevClipList(devClipsRef.current));
1912
1954
  }
1955
+ function toggleDevMode(next) {
1956
+ writeDevPreference(next);
1957
+ setDevMode(next);
1958
+ devClipsRef.current = [];
1959
+ setDevClipCount(0);
1960
+ setDevFallbackText(null);
1961
+ setAnnouncement(
1962
+ 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."
1963
+ );
1964
+ }
1913
1965
  async function pressFloatingButton() {
1914
1966
  if (props.defaultMode !== "report" && !pickingActive) {
1915
1967
  setAnnouncement(null);
@@ -1967,8 +2019,26 @@ function FeedbackKit(props) {
1967
2019
  }
1968
2020
  function submitReport(event) {
1969
2021
  event.preventDefault();
2022
+ if (devMode) {
2023
+ void copyFromModal();
2024
+ return;
2025
+ }
1970
2026
  void activeKit.widget.submitReport();
1971
2027
  }
2028
+ async function copyFromModal() {
2029
+ const modalState = activeKit.widget.getState().modal;
2030
+ if (modalState.comment.trim() === "") return;
2031
+ const clip = {
2032
+ comment: modalState.comment,
2033
+ element: modalState.element ?? null,
2034
+ screen: typeof location === "undefined" ? null : location.pathname,
2035
+ priority: modalState.priority
2036
+ };
2037
+ devClipsRef.current = [...devClipsRef.current, clip];
2038
+ setDevClipCount(devClipsRef.current.length);
2039
+ activeKit.widget.confirmCloseReport();
2040
+ await putOnClipboard(renderDevClipItem(clip));
2041
+ }
1972
2042
  const floating = screen === "button" || screen === "picking";
1973
2043
  const hoverBox = pickingState.hovered?.boundingBox;
1974
2044
  const popup = pickingState.popup;
@@ -1980,27 +2050,64 @@ function FeedbackKit(props) {
1980
2050
  style: { fontFamily: FONT_STACK, color: TOKENS.ink, fontSize: 14, lineHeight: 1.45 },
1981
2051
  children: [
1982
2052
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes feedback-kit-spin{to{transform:rotate(360deg)}}` }),
1983
- floating ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1984
- "button",
1985
- {
1986
- ref: floatingButtonRef,
1987
- id: import_feedback_kit_core4.FLOATING_BUTTON_ID,
1988
- type: "button",
1989
- "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1990
- onClick: () => void pressFloatingButton(),
1991
- style: {
1992
- ...primaryButton,
1993
- position: "fixed",
1994
- right: 24,
1995
- bottom: 24,
1996
- zIndex: 2147483600,
1997
- minWidth: 112,
1998
- minHeight: 46,
1999
- borderRadius: 24,
2000
- boxShadow: TOKENS.shadow
2001
- },
2002
- children: "\uD53C\uB4DC\uBC31"
2003
- }
2053
+ floating ? (
2054
+ // 톱니와 피드백 버튼을 한 상자에 넣는다. 각각 fixed 로 두면 버튼 글자 길이가
2055
+ // 바뀔 때마다 좌표를 다시 맞춰야 하고, 그러다 겹친다.
2056
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2057
+ "div",
2058
+ {
2059
+ style: {
2060
+ position: "fixed",
2061
+ right: 24,
2062
+ bottom: 24,
2063
+ zIndex: 2147483600,
2064
+ display: "flex",
2065
+ alignItems: "center",
2066
+ gap: 8
2067
+ },
2068
+ children: [
2069
+ devSettingsAvailable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2070
+ "button",
2071
+ {
2072
+ type: "button",
2073
+ "data-fk-dev-settings-toggle": "",
2074
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2075
+ "aria-expanded": devSettingsOpen,
2076
+ onClick: () => setDevSettingsOpen((open) => !open),
2077
+ style: {
2078
+ ...baseButton,
2079
+ width: 46,
2080
+ minHeight: 46,
2081
+ padding: 0,
2082
+ borderRadius: 23,
2083
+ boxShadow: TOKENS.shadow,
2084
+ fontSize: 17,
2085
+ lineHeight: 1
2086
+ },
2087
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2699" })
2088
+ }
2089
+ ) : null,
2090
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2091
+ "button",
2092
+ {
2093
+ ref: floatingButtonRef,
2094
+ id: import_feedback_kit_core4.FLOATING_BUTTON_ID,
2095
+ type: "button",
2096
+ "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
2097
+ onClick: () => void pressFloatingButton(),
2098
+ style: {
2099
+ ...primaryButton,
2100
+ minWidth: 112,
2101
+ minHeight: 46,
2102
+ borderRadius: 24,
2103
+ boxShadow: TOKENS.shadow
2104
+ },
2105
+ children: "\uD53C\uB4DC\uBC31"
2106
+ }
2107
+ )
2108
+ ]
2109
+ }
2110
+ )
2004
2111
  ) : null,
2005
2112
  floating && devMode ? (
2006
2113
  // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
@@ -2062,23 +2169,80 @@ function FeedbackKit(props) {
2062
2169
  ]
2063
2170
  }
2064
2171
  ) : null,
2065
- announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2172
+ devSettingsOpen || announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2066
2173
  "div",
2067
2174
  {
2068
- role: "status",
2069
- "aria-live": "polite",
2070
2175
  style: {
2071
2176
  position: "fixed",
2072
2177
  right: 24,
2073
2178
  bottom: 82,
2074
2179
  zIndex: 2147483601,
2075
- padding: "10px 14px",
2076
- borderRadius: 10,
2077
- background: TOKENS.ink,
2078
- color: TOKENS.surface,
2079
- boxShadow: TOKENS.shadow
2180
+ display: "flex",
2181
+ flexDirection: "column",
2182
+ alignItems: "flex-end",
2183
+ gap: 8
2080
2184
  },
2081
- children: announcement
2185
+ children: [
2186
+ devSettingsOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2187
+ "div",
2188
+ {
2189
+ role: "dialog",
2190
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2191
+ "data-fk-dev-settings": "",
2192
+ style: {
2193
+ width: 292,
2194
+ maxWidth: "calc(100vw - 48px)",
2195
+ padding: 14,
2196
+ borderRadius: 12,
2197
+ border: `1px solid ${TOKENS.line}`,
2198
+ background: TOKENS.surface,
2199
+ boxShadow: TOKENS.shadow
2200
+ },
2201
+ children: [
2202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", alignItems: "flex-start", gap: 9, cursor: "pointer" }, children: [
2203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2204
+ "input",
2205
+ {
2206
+ type: "checkbox",
2207
+ checked: devMode,
2208
+ onChange: (event) => toggleDevMode(event.currentTarget.checked),
2209
+ style: { marginTop: 3 }
2210
+ }
2211
+ ),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
2213
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: 700 }, children: "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC" }),
2214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "block", marginTop: 4, color: TOKENS.muted, fontSize: 12.5 }, children: [
2215
+ "\uCF1C\uBA74 \uC81C\uBCF4\uB97C \uC218\uC9D1\uCC98\uB85C \uBCF4\uB0B4\uC9C0 \uC54A\uACE0 ",
2216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: "\uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC" }),
2217
+ "\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."
2218
+ ] })
2219
+ ] })
2220
+ ] }),
2221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { margin: "12px 0 0", color: TOKENS.muted, fontSize: 12 }, children: [
2222
+ "\uC774 \uC124\uC815\uC740 \uB85C\uCEEC(",
2223
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "localhost" }),
2224
+ ")\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."
2225
+ ] }),
2226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 12 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: baseButton, onClick: () => setDevSettingsOpen(false), children: "\uB2EB\uAE30" }) })
2227
+ ]
2228
+ }
2229
+ ) : null,
2230
+ announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2231
+ "div",
2232
+ {
2233
+ role: "status",
2234
+ "aria-live": "polite",
2235
+ style: {
2236
+ padding: "10px 14px",
2237
+ borderRadius: 10,
2238
+ background: TOKENS.ink,
2239
+ color: TOKENS.surface,
2240
+ boxShadow: TOKENS.shadow
2241
+ },
2242
+ children: announcement
2243
+ }
2244
+ ) : null
2245
+ ]
2082
2246
  }
2083
2247
  ) : null,
2084
2248
  screen === "modal" && modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -2337,7 +2501,7 @@ function FeedbackKit(props) {
2337
2501
  opacity: modal.canSubmit ? 1 : 0.5,
2338
2502
  cursor: modal.canSubmit ? "pointer" : "not-allowed"
2339
2503
  },
2340
- children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2504
+ children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2341
2505
  }
2342
2506
  )
2343
2507
  ] })
@@ -2456,11 +2620,7 @@ function FeedbackKit(props) {
2456
2620
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2457
2621
  onSubmit: (event) => {
2458
2622
  event.preventDefault();
2459
- if (devMode) {
2460
- void copyOne();
2461
- return;
2462
- }
2463
- void kit.picking.saveAnnotation();
2623
+ commitPopup();
2464
2624
  },
2465
2625
  onPaste: (event) => {
2466
2626
  if (!firstImageOf(event.clipboardData)) return;
@@ -2542,7 +2702,7 @@ function FeedbackKit(props) {
2542
2702
  const native = event.nativeEvent;
2543
2703
  if (native.isComposing) return;
2544
2704
  event.preventDefault();
2545
- void kit.picking.saveAnnotation();
2705
+ commitPopup();
2546
2706
  },
2547
2707
  rows: 2,
2548
2708
  placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",