@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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackHint, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, RouteWatcher, WidgetControllerOpts, HintProvider, WidgetController, FeedbackStorage, ContextProviders, DiagnosticsInstallOpts, RouteTrailStore } from '@solhun/feedback-kit-core';
1
+ import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackHint, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, RouteWatcher, WidgetControllerOpts, HintProvider, WidgetController, FeedbackAdapter, FeedbackStorage, ContextProviders, DiagnosticsInstallOpts, RouteTrailStore } from '@solhun/feedback-kit-core';
2
2
  export { COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, ElementInfo, ElementLike, FLOATING_BUTTON_ID, FeedbackAdapter, FeedbackConfig, FeedbackContext, FeedbackHint, FeedbackPin, FeedbackReport, FeedbackStorage, FeedbackUser, HintCatalog, HintElementRule, HintPlatform, HintProvider, HintRankInput, HintScope, MODAL_ACTION_HINT_TOGGLE, MODAL_ACTION_PICK, ModalSubmitStatus, ReportModalController, ReportModalState, ResolvedConfig, SCREENSHOT_FAILED_MESSAGE, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_PENDING_MESSAGE, ScreenshotStatus, SubmitResult, Visibility, VisibilityEnv, VisibilityFn, WidgetController, WidgetPosition, WidgetScreen, WidgetState, denormalizePin, normalizePin, parseSourceAttr, rankHints, resolveConfig, shouldShowWidget, sourceFromElement } from '@solhun/feedback-kit-core';
3
3
  import * as react from 'react';
4
4
 
@@ -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
  */
@@ -387,6 +391,11 @@ interface WebWidget {
387
391
  widget: WidgetController;
388
392
  picking: ElementPickingController;
389
393
  store: MarkerStore;
394
+ /**
395
+ * "이 화면의 제보가 어디로 가나" 조회. 어댑터가 지원하지 않으면 `null` 이고,
396
+ * 그러면 설정 화면이 "확인 못 했습니다"를 보여준다. 표시용이라 없어도 제보에는 영향이 없다.
397
+ */
398
+ fetchConnection: NonNullable<FeedbackAdapter["fetchConnection"]> | null;
390
399
  dispose(): void;
391
400
  }
392
401
  declare function createWebWidget(opts: WebWidgetOpts): WebWidget;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackHint, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, RouteWatcher, WidgetControllerOpts, HintProvider, WidgetController, FeedbackStorage, ContextProviders, DiagnosticsInstallOpts, RouteTrailStore } from '@solhun/feedback-kit-core';
1
+ import { ElementInfo, FeedbackPin, FeedbackScreenshot, FeedbackHint, FeedbackReport, SubmitOutcome, QueueStatus, ReportParts, ScreenshotCapture, ScreenshotReencode, RouteWatcher, WidgetControllerOpts, HintProvider, WidgetController, FeedbackAdapter, FeedbackStorage, ContextProviders, DiagnosticsInstallOpts, RouteTrailStore } from '@solhun/feedback-kit-core';
2
2
  export { COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, ElementInfo, ElementLike, FLOATING_BUTTON_ID, FeedbackAdapter, FeedbackConfig, FeedbackContext, FeedbackHint, FeedbackPin, FeedbackReport, FeedbackStorage, FeedbackUser, HintCatalog, HintElementRule, HintPlatform, HintProvider, HintRankInput, HintScope, MODAL_ACTION_HINT_TOGGLE, MODAL_ACTION_PICK, ModalSubmitStatus, ReportModalController, ReportModalState, ResolvedConfig, SCREENSHOT_FAILED_MESSAGE, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_PENDING_MESSAGE, ScreenshotStatus, SubmitResult, Visibility, VisibilityEnv, VisibilityFn, WidgetController, WidgetPosition, WidgetScreen, WidgetState, denormalizePin, normalizePin, parseSourceAttr, rankHints, resolveConfig, shouldShowWidget, sourceFromElement } from '@solhun/feedback-kit-core';
3
3
  import * as react from 'react';
4
4
 
@@ -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
  */
@@ -387,6 +391,11 @@ interface WebWidget {
387
391
  widget: WidgetController;
388
392
  picking: ElementPickingController;
389
393
  store: MarkerStore;
394
+ /**
395
+ * "이 화면의 제보가 어디로 가나" 조회. 어댑터가 지원하지 않으면 `null` 이고,
396
+ * 그러면 설정 화면이 "확인 못 했습니다"를 보여준다. 표시용이라 없어도 제보에는 영향이 없다.
397
+ */
398
+ fetchConnection: NonNullable<FeedbackAdapter["fetchConnection"]> | null;
390
399
  dispose(): void;
391
400
  }
392
401
  declare function createWebWidget(opts: WebWidgetOpts): WebWidget;
package/dist/index.js CHANGED
@@ -1062,6 +1062,11 @@ function hintSourceFrom(queue) {
1062
1062
  const cacheKey = adapter.hintsCacheKey ?? "default";
1063
1063
  return { fetchHints: adapter.fetchHints.bind(adapter), cacheKey };
1064
1064
  }
1065
+ function connectionSourceFrom(queue) {
1066
+ const adapter = queue.adapter;
1067
+ if (!adapter || typeof adapter.fetchConnection !== "function") return null;
1068
+ return adapter.fetchConnection.bind(adapter);
1069
+ }
1065
1070
  function resolveHintProvider(opts) {
1066
1071
  if (opts.hintProvider) return opts.hintProvider;
1067
1072
  if (opts.hints) {
@@ -1147,6 +1152,7 @@ function createWebWidget(opts) {
1147
1152
  widget,
1148
1153
  picking,
1149
1154
  store,
1155
+ fetchConnection: connectionSourceFrom(opts.queue),
1150
1156
  dispose() {
1151
1157
  picking.dispose();
1152
1158
  widget.dispose();
@@ -1174,7 +1180,7 @@ import {
1174
1180
  } from "react";
1175
1181
 
1176
1182
  // src/version.ts
1177
- var VERSION = true ? "0.9.0" : "dev";
1183
+ var VERSION = true ? "0.11.0" : "dev";
1178
1184
 
1179
1185
  // src/dev-clipboard.ts
1180
1186
  import { sourceFromElement } from "@solhun/feedback-kit-core";
@@ -1222,6 +1228,37 @@ async function writeToClipboard(text) {
1222
1228
  }
1223
1229
  }
1224
1230
 
1231
+ // src/dev-preference.ts
1232
+ var STORAGE_KEY = "feedback-kit:dev-mode";
1233
+ function isDevSettingsHost(hostname) {
1234
+ const host = hostname.trim().toLowerCase();
1235
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") return true;
1236
+ return host.endsWith(".localhost") || host.endsWith(".local");
1237
+ }
1238
+ function readDevPreference() {
1239
+ try {
1240
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
1241
+ if (raw === "on") return true;
1242
+ if (raw === "off") return false;
1243
+ return null;
1244
+ } catch {
1245
+ return null;
1246
+ }
1247
+ }
1248
+ function writeDevPreference(value) {
1249
+ try {
1250
+ const storage = globalThis.localStorage;
1251
+ if (!storage) return;
1252
+ if (value === null) storage.removeItem(STORAGE_KEY);
1253
+ else storage.setItem(STORAGE_KEY, value ? "on" : "off");
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ function resolveDevMode(propValue, stored) {
1258
+ if (stored !== null) return stored;
1259
+ return propValue === true;
1260
+ }
1261
+
1225
1262
  // src/feedback-kit.tsx
1226
1263
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1227
1264
  var TOKENS = {
@@ -1717,7 +1754,13 @@ function FeedbackKit(props) {
1717
1754
  const [widgetState, setWidgetState] = useState(null);
1718
1755
  const [pickingState, setPickingState] = useState(INITIAL_PICKING_STATE);
1719
1756
  const [announcement, setAnnouncement] = useState(null);
1720
- const [devMode] = useState(() => props.devMode === true);
1757
+ const [devMode, setDevMode] = useState(() => resolveDevMode(props.devMode, readDevPreference()));
1758
+ const [devSettingsAvailable] = useState(
1759
+ () => props.devMode === true || isDevSettingsHost(globalThis.location?.hostname ?? "")
1760
+ );
1761
+ const [devSettingsOpen, setDevSettingsOpen] = useState(false);
1762
+ const [connection, setConnection] = useState(null);
1763
+ const [connectionState, setConnectionState] = useState("idle");
1721
1764
  const devClipsRef = useRef([]);
1722
1765
  const [devClipCount, setDevClipCount] = useState(0);
1723
1766
  const [devFallbackText, setDevFallbackText] = useState(null);
@@ -1858,6 +1901,13 @@ function FeedbackKit(props) {
1858
1901
  const ok = await writeToClipboard(text);
1859
1902
  setDevFallbackText(ok ? null : text);
1860
1903
  }
1904
+ function commitPopup() {
1905
+ if (devMode) {
1906
+ void copyOne();
1907
+ return;
1908
+ }
1909
+ void activeKit.picking.saveAnnotation();
1910
+ }
1861
1911
  async function copyOne() {
1862
1912
  const clip = currentClip();
1863
1913
  if (!clip || clip.comment.trim() === "") return;
@@ -1870,6 +1920,30 @@ function FeedbackKit(props) {
1870
1920
  if (devClipsRef.current.length === 0) return;
1871
1921
  await putOnClipboard(renderDevClipList(devClipsRef.current));
1872
1922
  }
1923
+ function openDevSettings() {
1924
+ setDevSettingsOpen(true);
1925
+ if (connectionState !== "idle") return;
1926
+ const fetchConnection = activeKit.fetchConnection;
1927
+ if (!fetchConnection) {
1928
+ setConnectionState("fail");
1929
+ return;
1930
+ }
1931
+ setConnectionState("loading");
1932
+ void fetchConnection().then((value) => {
1933
+ setConnection(value);
1934
+ setConnectionState(value ? "done" : "fail");
1935
+ }).catch(() => setConnectionState("fail"));
1936
+ }
1937
+ function toggleDevMode(next) {
1938
+ writeDevPreference(next);
1939
+ setDevMode(next);
1940
+ devClipsRef.current = [];
1941
+ setDevClipCount(0);
1942
+ setDevFallbackText(null);
1943
+ setAnnouncement(
1944
+ 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."
1945
+ );
1946
+ }
1873
1947
  async function pressFloatingButton() {
1874
1948
  if (props.defaultMode !== "report" && !pickingActive) {
1875
1949
  setAnnouncement(null);
@@ -1927,8 +2001,26 @@ function FeedbackKit(props) {
1927
2001
  }
1928
2002
  function submitReport(event) {
1929
2003
  event.preventDefault();
2004
+ if (devMode) {
2005
+ void copyFromModal();
2006
+ return;
2007
+ }
1930
2008
  void activeKit.widget.submitReport();
1931
2009
  }
2010
+ async function copyFromModal() {
2011
+ const modalState = activeKit.widget.getState().modal;
2012
+ if (modalState.comment.trim() === "") return;
2013
+ const clip = {
2014
+ comment: modalState.comment,
2015
+ element: modalState.element ?? null,
2016
+ screen: typeof location === "undefined" ? null : location.pathname,
2017
+ priority: modalState.priority
2018
+ };
2019
+ devClipsRef.current = [...devClipsRef.current, clip];
2020
+ setDevClipCount(devClipsRef.current.length);
2021
+ activeKit.widget.confirmCloseReport();
2022
+ await putOnClipboard(renderDevClipItem(clip));
2023
+ }
1932
2024
  const floating = screen === "button" || screen === "picking";
1933
2025
  const hoverBox = pickingState.hovered?.boundingBox;
1934
2026
  const popup = pickingState.popup;
@@ -1940,27 +2032,64 @@ function FeedbackKit(props) {
1940
2032
  style: { fontFamily: FONT_STACK, color: TOKENS.ink, fontSize: 14, lineHeight: 1.45 },
1941
2033
  children: [
1942
2034
  /* @__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
- }
2035
+ floating ? (
2036
+ // 톱니와 피드백 버튼을 한 상자에 넣는다. 각각 fixed 로 두면 버튼 글자 길이가
2037
+ // 바뀔 때마다 좌표를 다시 맞춰야 하고, 그러다 겹친다.
2038
+ /* @__PURE__ */ jsxs(
2039
+ "div",
2040
+ {
2041
+ style: {
2042
+ position: "fixed",
2043
+ right: 24,
2044
+ bottom: 24,
2045
+ zIndex: 2147483600,
2046
+ display: "flex",
2047
+ alignItems: "center",
2048
+ gap: 8
2049
+ },
2050
+ children: [
2051
+ devSettingsAvailable ? /* @__PURE__ */ jsx(
2052
+ "button",
2053
+ {
2054
+ type: "button",
2055
+ "data-fk-dev-settings-toggle": "",
2056
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2057
+ "aria-expanded": devSettingsOpen,
2058
+ onClick: () => devSettingsOpen ? setDevSettingsOpen(false) : openDevSettings(),
2059
+ style: {
2060
+ ...baseButton,
2061
+ width: 46,
2062
+ minHeight: 46,
2063
+ padding: 0,
2064
+ borderRadius: 23,
2065
+ boxShadow: TOKENS.shadow,
2066
+ fontSize: 17,
2067
+ lineHeight: 1
2068
+ },
2069
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2699" })
2070
+ }
2071
+ ) : null,
2072
+ /* @__PURE__ */ jsx(
2073
+ "button",
2074
+ {
2075
+ ref: floatingButtonRef,
2076
+ id: FLOATING_BUTTON_ID,
2077
+ type: "button",
2078
+ "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
2079
+ onClick: () => void pressFloatingButton(),
2080
+ style: {
2081
+ ...primaryButton,
2082
+ minWidth: 112,
2083
+ minHeight: 46,
2084
+ borderRadius: 24,
2085
+ boxShadow: TOKENS.shadow
2086
+ },
2087
+ children: "\uD53C\uB4DC\uBC31"
2088
+ }
2089
+ )
2090
+ ]
2091
+ }
2092
+ )
1964
2093
  ) : null,
1965
2094
  floating && devMode ? (
1966
2095
  // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
@@ -2022,23 +2151,124 @@ function FeedbackKit(props) {
2022
2151
  ]
2023
2152
  }
2024
2153
  ) : null,
2025
- announcement && !modal.open ? /* @__PURE__ */ jsx(
2154
+ devSettingsOpen || announcement && !modal.open ? /* @__PURE__ */ jsxs(
2026
2155
  "div",
2027
2156
  {
2028
- role: "status",
2029
- "aria-live": "polite",
2030
2157
  style: {
2031
2158
  position: "fixed",
2032
2159
  right: 24,
2033
2160
  bottom: 82,
2034
2161
  zIndex: 2147483601,
2035
- padding: "10px 14px",
2036
- borderRadius: 10,
2037
- background: TOKENS.ink,
2038
- color: TOKENS.surface,
2039
- boxShadow: TOKENS.shadow
2162
+ display: "flex",
2163
+ flexDirection: "column",
2164
+ alignItems: "flex-end",
2165
+ gap: 8
2040
2166
  },
2041
- children: announcement
2167
+ children: [
2168
+ devSettingsOpen ? /* @__PURE__ */ jsxs(
2169
+ "div",
2170
+ {
2171
+ role: "dialog",
2172
+ "aria-label": "\uD53C\uB4DC\uBC31 \uC124\uC815",
2173
+ "data-fk-dev-settings": "",
2174
+ style: {
2175
+ width: 292,
2176
+ maxWidth: "calc(100vw - 48px)",
2177
+ padding: 14,
2178
+ borderRadius: 12,
2179
+ border: `1px solid ${TOKENS.line}`,
2180
+ background: TOKENS.surface,
2181
+ boxShadow: TOKENS.shadow
2182
+ },
2183
+ children: [
2184
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "flex-start", gap: 9, cursor: "pointer" }, children: [
2185
+ /* @__PURE__ */ jsx(
2186
+ "input",
2187
+ {
2188
+ type: "checkbox",
2189
+ checked: devMode,
2190
+ onChange: (event) => toggleDevMode(event.currentTarget.checked),
2191
+ style: { marginTop: 3 }
2192
+ }
2193
+ ),
2194
+ /* @__PURE__ */ jsxs("span", { children: [
2195
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 700 }, children: "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC" }),
2196
+ /* @__PURE__ */ jsxs("span", { style: { display: "block", marginTop: 4, color: TOKENS.muted, fontSize: 12.5 }, children: [
2197
+ "\uCF1C\uBA74 \uC81C\uBCF4\uB97C \uC218\uC9D1\uCC98\uB85C \uBCF4\uB0B4\uC9C0 \uC54A\uACE0 ",
2198
+ /* @__PURE__ */ jsx("b", { children: "\uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC" }),
2199
+ "\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."
2200
+ ] })
2201
+ ] })
2202
+ ] }),
2203
+ /* @__PURE__ */ jsxs(
2204
+ "div",
2205
+ {
2206
+ "data-fk-connection": "",
2207
+ style: {
2208
+ marginTop: 12,
2209
+ paddingTop: 12,
2210
+ borderTop: `1px solid ${TOKENS.line}`,
2211
+ fontSize: 12.5
2212
+ },
2213
+ children: [
2214
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 700, marginBottom: 6 }, children: "\uBCF4\uB0B4\uB294 \uACF3" }),
2215
+ connectionState === "loading" ? /* @__PURE__ */ jsx("div", { style: { color: TOKENS.muted }, children: "\uD655\uC778\uD558\uB294 \uC911\u2026" }) : connectionState === "fail" || !connection ? (
2216
+ // 조용히 비우면 "연결이 없다"로 읽힌다. 못 물어봤다는 것과 없다는 것은 다르다.
2217
+ /* @__PURE__ */ 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)." })
2218
+ ) : /* @__PURE__ */ jsxs("ul", { style: { margin: 0, paddingLeft: 16, color: TOKENS.ink, lineHeight: 1.7 }, children: [
2219
+ /* @__PURE__ */ jsxs("li", { children: [
2220
+ "\uC218\uC9D1\uCC98 \xB7 ",
2221
+ connection.project.name ?? "\uC774\uB984 \uC5C6\uB294 \uD504\uB85C\uC81D\uD2B8"
2222
+ ] }),
2223
+ /* @__PURE__ */ jsxs("li", { children: [
2224
+ "Linear \xB7",
2225
+ " ",
2226
+ connection.linear.connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
2227
+ connection.linear.teamName ?? "\uC5F0\uACB0\uB41C \uD300",
2228
+ connection.linear.projectName ? ` / ${connection.linear.projectName}` : "",
2229
+ " \u2014 ",
2230
+ /* @__PURE__ */ jsx("b", { children: connection.linear.autoExport ? "\uC81C\uBCF4\uAC00 \uC790\uB3D9\uC73C\uB85C \uB098\uAC11\uB2C8\uB2E4" : "\uC790\uB3D9 \uBC1C\uC1A1 \uAEBC\uC9D0" })
2231
+ ] }) : "\uC5F0\uACB0 \uC5C6\uC74C"
2232
+ ] }),
2233
+ /* @__PURE__ */ jsxs("li", { children: [
2234
+ "Symphony \xB7",
2235
+ " ",
2236
+ connection.symphonySlug ? `${connection.symphonySlug} \uAC00 \uCC98\uB9AC` : "\uC790\uB3D9 \uCC98\uB9AC \uC548 \uD568"
2237
+ ] })
2238
+ ] }),
2239
+ devMode ? (
2240
+ // 개발용 모드가 켜져 있으면 위 목록은 "원래 가던 곳"일 뿐이다. 그대로 두면
2241
+ // 지금도 거기로 가는 줄 읽는다.
2242
+ /* @__PURE__ */ 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." })
2243
+ ) : null
2244
+ ]
2245
+ }
2246
+ ),
2247
+ /* @__PURE__ */ jsxs("p", { style: { margin: "12px 0 0", color: TOKENS.muted, fontSize: 12 }, children: [
2248
+ "\uC774 \uC124\uC815\uC740 \uB85C\uCEEC(",
2249
+ /* @__PURE__ */ jsx("code", { children: "localhost" }),
2250
+ ")\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."
2251
+ ] }),
2252
+ /* @__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" }) })
2253
+ ]
2254
+ }
2255
+ ) : null,
2256
+ announcement && !modal.open ? /* @__PURE__ */ jsx(
2257
+ "div",
2258
+ {
2259
+ role: "status",
2260
+ "aria-live": "polite",
2261
+ style: {
2262
+ padding: "10px 14px",
2263
+ borderRadius: 10,
2264
+ background: TOKENS.ink,
2265
+ color: TOKENS.surface,
2266
+ boxShadow: TOKENS.shadow
2267
+ },
2268
+ children: announcement
2269
+ }
2270
+ ) : null
2271
+ ]
2042
2272
  }
2043
2273
  ) : null,
2044
2274
  screen === "modal" && modal.open ? /* @__PURE__ */ jsx(
@@ -2297,7 +2527,7 @@ function FeedbackKit(props) {
2297
2527
  opacity: modal.canSubmit ? 1 : 0.5,
2298
2528
  cursor: modal.canSubmit ? "pointer" : "not-allowed"
2299
2529
  },
2300
- children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2530
+ children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2301
2531
  }
2302
2532
  )
2303
2533
  ] })
@@ -2416,11 +2646,7 @@ function FeedbackKit(props) {
2416
2646
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2417
2647
  onSubmit: (event) => {
2418
2648
  event.preventDefault();
2419
- if (devMode) {
2420
- void copyOne();
2421
- return;
2422
- }
2423
- void kit.picking.saveAnnotation();
2649
+ commitPopup();
2424
2650
  },
2425
2651
  onPaste: (event) => {
2426
2652
  if (!firstImageOf(event.clipboardData)) return;
@@ -2502,7 +2728,7 @@ function FeedbackKit(props) {
2502
2728
  const native = event.nativeEvent;
2503
2729
  if (native.isComposing) return;
2504
2730
  event.preventDefault();
2505
- void kit.picking.saveAnnotation();
2731
+ commitPopup();
2506
2732
  },
2507
2733
  rows: 2,
2508
2734
  placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",