@solhun/feedback-kit-web 0.9.0 → 0.11.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
@@ -1117,6 +1117,11 @@ function hintSourceFrom(queue) {
1117
1117
  const cacheKey = adapter.hintsCacheKey ?? "default";
1118
1118
  return { fetchHints: adapter.fetchHints.bind(adapter), cacheKey };
1119
1119
  }
1120
+ function connectionSourceFrom(queue) {
1121
+ const adapter = queue.adapter;
1122
+ if (!adapter || typeof adapter.fetchConnection !== "function") return null;
1123
+ return adapter.fetchConnection.bind(adapter);
1124
+ }
1120
1125
  function resolveHintProvider(opts) {
1121
1126
  if (opts.hintProvider) return opts.hintProvider;
1122
1127
  if (opts.hints) {
@@ -1202,6 +1207,7 @@ function createWebWidget(opts) {
1202
1207
  widget,
1203
1208
  picking,
1204
1209
  store,
1210
+ fetchConnection: connectionSourceFrom(opts.queue),
1205
1211
  dispose() {
1206
1212
  picking.dispose();
1207
1213
  widget.dispose();
@@ -1214,7 +1220,7 @@ var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
1214
1220
  var import_react = require("react");
1215
1221
 
1216
1222
  // src/version.ts
1217
- var VERSION = true ? "0.9.0" : "dev";
1223
+ var VERSION = true ? "0.11.0" : "dev";
1218
1224
 
1219
1225
  // src/dev-clipboard.ts
1220
1226
  var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
@@ -1262,6 +1268,37 @@ async function writeToClipboard(text) {
1262
1268
  }
1263
1269
  }
1264
1270
 
1271
+ // src/dev-preference.ts
1272
+ var STORAGE_KEY = "feedback-kit:dev-mode";
1273
+ function isDevSettingsHost(hostname) {
1274
+ const host = hostname.trim().toLowerCase();
1275
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") return true;
1276
+ return host.endsWith(".localhost") || host.endsWith(".local");
1277
+ }
1278
+ function readDevPreference() {
1279
+ try {
1280
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
1281
+ if (raw === "on") return true;
1282
+ if (raw === "off") return false;
1283
+ return null;
1284
+ } catch {
1285
+ return null;
1286
+ }
1287
+ }
1288
+ function writeDevPreference(value) {
1289
+ try {
1290
+ const storage = globalThis.localStorage;
1291
+ if (!storage) return;
1292
+ if (value === null) storage.removeItem(STORAGE_KEY);
1293
+ else storage.setItem(STORAGE_KEY, value ? "on" : "off");
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ function resolveDevMode(propValue, stored) {
1298
+ if (stored !== null) return stored;
1299
+ return propValue === true;
1300
+ }
1301
+
1265
1302
  // src/feedback-kit.tsx
1266
1303
  var import_jsx_runtime = require("react/jsx-runtime");
1267
1304
  var TOKENS = {
@@ -1757,7 +1794,13 @@ function FeedbackKit(props) {
1757
1794
  const [widgetState, setWidgetState] = (0, import_react.useState)(null);
1758
1795
  const [pickingState, setPickingState] = (0, import_react.useState)(INITIAL_PICKING_STATE);
1759
1796
  const [announcement, setAnnouncement] = (0, import_react.useState)(null);
1760
- const [devMode] = (0, import_react.useState)(() => props.devMode === true);
1797
+ const [devMode, setDevMode] = (0, import_react.useState)(() => resolveDevMode(props.devMode, readDevPreference()));
1798
+ const [devSettingsAvailable] = (0, import_react.useState)(
1799
+ () => props.devMode === true || isDevSettingsHost(globalThis.location?.hostname ?? "")
1800
+ );
1801
+ const [devSettingsOpen, setDevSettingsOpen] = (0, import_react.useState)(false);
1802
+ const [connection, setConnection] = (0, import_react.useState)(null);
1803
+ const [connectionState, setConnectionState] = (0, import_react.useState)("idle");
1761
1804
  const devClipsRef = (0, import_react.useRef)([]);
1762
1805
  const [devClipCount, setDevClipCount] = (0, import_react.useState)(0);
1763
1806
  const [devFallbackText, setDevFallbackText] = (0, import_react.useState)(null);
@@ -1898,6 +1941,13 @@ function FeedbackKit(props) {
1898
1941
  const ok = await writeToClipboard(text);
1899
1942
  setDevFallbackText(ok ? null : text);
1900
1943
  }
1944
+ function commitPopup() {
1945
+ if (devMode) {
1946
+ void copyOne();
1947
+ return;
1948
+ }
1949
+ void activeKit.picking.saveAnnotation();
1950
+ }
1901
1951
  async function copyOne() {
1902
1952
  const clip = currentClip();
1903
1953
  if (!clip || clip.comment.trim() === "") return;
@@ -1910,6 +1960,30 @@ function FeedbackKit(props) {
1910
1960
  if (devClipsRef.current.length === 0) return;
1911
1961
  await putOnClipboard(renderDevClipList(devClipsRef.current));
1912
1962
  }
1963
+ function openDevSettings() {
1964
+ setDevSettingsOpen(true);
1965
+ if (connectionState !== "idle") return;
1966
+ const fetchConnection = activeKit.fetchConnection;
1967
+ if (!fetchConnection) {
1968
+ setConnectionState("fail");
1969
+ return;
1970
+ }
1971
+ setConnectionState("loading");
1972
+ void fetchConnection().then((value) => {
1973
+ setConnection(value);
1974
+ setConnectionState(value ? "done" : "fail");
1975
+ }).catch(() => setConnectionState("fail"));
1976
+ }
1977
+ function toggleDevMode(next) {
1978
+ writeDevPreference(next);
1979
+ setDevMode(next);
1980
+ devClipsRef.current = [];
1981
+ setDevClipCount(0);
1982
+ setDevFallbackText(null);
1983
+ setAnnouncement(
1984
+ 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."
1985
+ );
1986
+ }
1913
1987
  async function pressFloatingButton() {
1914
1988
  if (props.defaultMode !== "report" && !pickingActive) {
1915
1989
  setAnnouncement(null);
@@ -1967,8 +2041,26 @@ function FeedbackKit(props) {
1967
2041
  }
1968
2042
  function submitReport(event) {
1969
2043
  event.preventDefault();
2044
+ if (devMode) {
2045
+ void copyFromModal();
2046
+ return;
2047
+ }
1970
2048
  void activeKit.widget.submitReport();
1971
2049
  }
2050
+ async function copyFromModal() {
2051
+ const modalState = activeKit.widget.getState().modal;
2052
+ if (modalState.comment.trim() === "") return;
2053
+ const clip = {
2054
+ comment: modalState.comment,
2055
+ element: modalState.element ?? null,
2056
+ screen: typeof location === "undefined" ? null : location.pathname,
2057
+ priority: modalState.priority
2058
+ };
2059
+ devClipsRef.current = [...devClipsRef.current, clip];
2060
+ setDevClipCount(devClipsRef.current.length);
2061
+ activeKit.widget.confirmCloseReport();
2062
+ await putOnClipboard(renderDevClipItem(clip));
2063
+ }
1972
2064
  const floating = screen === "button" || screen === "picking";
1973
2065
  const hoverBox = pickingState.hovered?.boundingBox;
1974
2066
  const popup = pickingState.popup;
@@ -1980,27 +2072,64 @@ function FeedbackKit(props) {
1980
2072
  style: { fontFamily: FONT_STACK, color: TOKENS.ink, fontSize: 14, lineHeight: 1.45 },
1981
2073
  children: [
1982
2074
  /* @__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
- }
2075
+ floating ? (
2076
+ // 톱니와 피드백 버튼을 한 상자에 넣는다. 각각 fixed 로 두면 버튼 글자 길이가
2077
+ // 바뀔 때마다 좌표를 다시 맞춰야 하고, 그러다 겹친다.
2078
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2079
+ "div",
2080
+ {
2081
+ style: {
2082
+ position: "fixed",
2083
+ right: 24,
2084
+ bottom: 24,
2085
+ zIndex: 2147483600,
2086
+ display: "flex",
2087
+ alignItems: "center",
2088
+ gap: 8
2089
+ },
2090
+ children: [
2091
+ devSettingsAvailable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2092
+ "button",
2093
+ {
2094
+ type: "button",
2095
+ "data-fk-dev-settings-toggle": "",
2096
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2097
+ "aria-expanded": devSettingsOpen,
2098
+ onClick: () => devSettingsOpen ? setDevSettingsOpen(false) : openDevSettings(),
2099
+ style: {
2100
+ ...baseButton,
2101
+ width: 46,
2102
+ minHeight: 46,
2103
+ padding: 0,
2104
+ borderRadius: 23,
2105
+ boxShadow: TOKENS.shadow,
2106
+ fontSize: 17,
2107
+ lineHeight: 1
2108
+ },
2109
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2699" })
2110
+ }
2111
+ ) : null,
2112
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2113
+ "button",
2114
+ {
2115
+ ref: floatingButtonRef,
2116
+ id: import_feedback_kit_core4.FLOATING_BUTTON_ID,
2117
+ type: "button",
2118
+ "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
2119
+ onClick: () => void pressFloatingButton(),
2120
+ style: {
2121
+ ...primaryButton,
2122
+ minWidth: 112,
2123
+ minHeight: 46,
2124
+ borderRadius: 24,
2125
+ boxShadow: TOKENS.shadow
2126
+ },
2127
+ children: "\uD53C\uB4DC\uBC31"
2128
+ }
2129
+ )
2130
+ ]
2131
+ }
2132
+ )
2004
2133
  ) : null,
2005
2134
  floating && devMode ? (
2006
2135
  // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
@@ -2062,23 +2191,124 @@ function FeedbackKit(props) {
2062
2191
  ]
2063
2192
  }
2064
2193
  ) : null,
2065
- announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2194
+ devSettingsOpen || announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2066
2195
  "div",
2067
2196
  {
2068
- role: "status",
2069
- "aria-live": "polite",
2070
2197
  style: {
2071
2198
  position: "fixed",
2072
2199
  right: 24,
2073
2200
  bottom: 82,
2074
2201
  zIndex: 2147483601,
2075
- padding: "10px 14px",
2076
- borderRadius: 10,
2077
- background: TOKENS.ink,
2078
- color: TOKENS.surface,
2079
- boxShadow: TOKENS.shadow
2202
+ display: "flex",
2203
+ flexDirection: "column",
2204
+ alignItems: "flex-end",
2205
+ gap: 8
2080
2206
  },
2081
- children: announcement
2207
+ children: [
2208
+ devSettingsOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2209
+ "div",
2210
+ {
2211
+ role: "dialog",
2212
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2213
+ "data-fk-dev-settings": "",
2214
+ style: {
2215
+ width: 292,
2216
+ maxWidth: "calc(100vw - 48px)",
2217
+ padding: 14,
2218
+ borderRadius: 12,
2219
+ border: `1px solid ${TOKENS.line}`,
2220
+ background: TOKENS.surface,
2221
+ boxShadow: TOKENS.shadow
2222
+ },
2223
+ children: [
2224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", alignItems: "flex-start", gap: 9, cursor: "pointer" }, children: [
2225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2226
+ "input",
2227
+ {
2228
+ type: "checkbox",
2229
+ checked: devMode,
2230
+ onChange: (event) => toggleDevMode(event.currentTarget.checked),
2231
+ style: { marginTop: 3 }
2232
+ }
2233
+ ),
2234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
2235
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: 700 }, children: "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC" }),
2236
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "block", marginTop: 4, color: TOKENS.muted, fontSize: 12.5 }, children: [
2237
+ "\uCF1C\uBA74 \uC81C\uBCF4\uB97C \uC218\uC9D1\uCC98\uB85C \uBCF4\uB0B4\uC9C0 \uC54A\uACE0 ",
2238
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: "\uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC" }),
2239
+ "\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."
2240
+ ] })
2241
+ ] })
2242
+ ] }),
2243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2244
+ "div",
2245
+ {
2246
+ "data-fk-connection": "",
2247
+ style: {
2248
+ marginTop: 12,
2249
+ paddingTop: 12,
2250
+ borderTop: `1px solid ${TOKENS.line}`,
2251
+ fontSize: 12.5
2252
+ },
2253
+ children: [
2254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 700, marginBottom: 6 }, children: "\uBCF4\uB0B4\uB294 \uACF3" }),
2255
+ connectionState === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: TOKENS.muted }, children: "\uD655\uC778\uD558\uB294 \uC911\u2026" }) : connectionState === "fail" || !connection ? (
2256
+ // 조용히 비우면 "연결이 없다"로 읽힌다. 못 물어봤다는 것과 없다는 것은 다르다.
2257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: TOKENS.muted }, children: "\uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(\uC218\uC9D1\uCC98\uAC00 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC74C)." })
2258
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { margin: 0, paddingLeft: 16, color: TOKENS.ink, lineHeight: 1.7 }, children: [
2259
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
2260
+ "\uC218\uC9D1\uCC98 \xB7 ",
2261
+ connection.project.name ?? "\uC774\uB984 \uC5C6\uB294 \uD504\uB85C\uC81D\uD2B8"
2262
+ ] }),
2263
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
2264
+ "Linear \xB7",
2265
+ " ",
2266
+ connection.linear.connected ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2267
+ connection.linear.teamName ?? "\uC5F0\uACB0\uB41C \uD300",
2268
+ connection.linear.projectName ? ` / ${connection.linear.projectName}` : "",
2269
+ " \u2014 ",
2270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: connection.linear.autoExport ? "\uC81C\uBCF4\uAC00 \uC790\uB3D9\uC73C\uB85C \uB098\uAC11\uB2C8\uB2E4" : "\uC790\uB3D9 \uBC1C\uC1A1 \uAEBC\uC9D0" })
2271
+ ] }) : "\uC5F0\uACB0 \uC5C6\uC74C"
2272
+ ] }),
2273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
2274
+ "Symphony \xB7",
2275
+ " ",
2276
+ connection.symphonySlug ? `${connection.symphonySlug} \uAC00 \uCC98\uB9AC` : "\uC790\uB3D9 \uCC98\uB9AC \uC548 \uD568"
2277
+ ] })
2278
+ ] }),
2279
+ devMode ? (
2280
+ // 개발용 모드가 켜져 있으면 위 목록은 "원래 가던 곳"일 뿐이다. 그대로 두면
2281
+ // 지금도 거기로 가는 줄 읽는다.
2282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 8, color: TOKENS.warning, fontWeight: 700 }, children: "\uC9C0\uAE08\uC740 \uC5B4\uB514\uC5D0\uB3C4 \uC548 \uBCF4\uB0C5\uB2C8\uB2E4 \u2014 \uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC\uAC00 \uCF1C\uC838 \uC788\uC2B5\uB2C8\uB2E4." })
2283
+ ) : null
2284
+ ]
2285
+ }
2286
+ ),
2287
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { margin: "12px 0 0", color: TOKENS.muted, fontSize: 12 }, children: [
2288
+ "\uC774 \uC124\uC815\uC740 \uB85C\uCEEC(",
2289
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "localhost" }),
2290
+ ")\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."
2291
+ ] }),
2292
+ /* @__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" }) })
2293
+ ]
2294
+ }
2295
+ ) : null,
2296
+ announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2297
+ "div",
2298
+ {
2299
+ role: "status",
2300
+ "aria-live": "polite",
2301
+ style: {
2302
+ padding: "10px 14px",
2303
+ borderRadius: 10,
2304
+ background: TOKENS.ink,
2305
+ color: TOKENS.surface,
2306
+ boxShadow: TOKENS.shadow
2307
+ },
2308
+ children: announcement
2309
+ }
2310
+ ) : null
2311
+ ]
2082
2312
  }
2083
2313
  ) : null,
2084
2314
  screen === "modal" && modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -2337,7 +2567,7 @@ function FeedbackKit(props) {
2337
2567
  opacity: modal.canSubmit ? 1 : 0.5,
2338
2568
  cursor: modal.canSubmit ? "pointer" : "not-allowed"
2339
2569
  },
2340
- children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2570
+ children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2341
2571
  }
2342
2572
  )
2343
2573
  ] })
@@ -2456,11 +2686,7 @@ function FeedbackKit(props) {
2456
2686
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2457
2687
  onSubmit: (event) => {
2458
2688
  event.preventDefault();
2459
- if (devMode) {
2460
- void copyOne();
2461
- return;
2462
- }
2463
- void kit.picking.saveAnnotation();
2689
+ commitPopup();
2464
2690
  },
2465
2691
  onPaste: (event) => {
2466
2692
  if (!firstImageOf(event.clipboardData)) return;
@@ -2542,7 +2768,7 @@ function FeedbackKit(props) {
2542
2768
  const native = event.nativeEvent;
2543
2769
  if (native.isComposing) return;
2544
2770
  event.preventDefault();
2545
- void kit.picking.saveAnnotation();
2771
+ commitPopup();
2546
2772
  },
2547
2773
  rows: 2,
2548
2774
  placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",