xgen-dex-ide 1.60.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.
Files changed (91) hide show
  1. package/README.md +42 -0
  2. package/dist/components/ActivityBar.d.ts +21 -0
  3. package/dist/components/ActivityBar.d.ts.map +1 -0
  4. package/dist/components/ActivityBar.js +14 -0
  5. package/dist/components/ActivityBar.js.map +1 -0
  6. package/dist/components/EditorArea.d.ts +5 -0
  7. package/dist/components/EditorArea.d.ts.map +1 -0
  8. package/dist/components/EditorArea.js +468 -0
  9. package/dist/components/EditorArea.js.map +1 -0
  10. package/dist/components/Explorer.d.ts +2 -0
  11. package/dist/components/Explorer.d.ts.map +1 -0
  12. package/dist/components/Explorer.js +349 -0
  13. package/dist/components/Explorer.js.map +1 -0
  14. package/dist/components/IdeView.d.ts +24 -0
  15. package/dist/components/IdeView.d.ts.map +1 -0
  16. package/dist/components/IdeView.js +359 -0
  17. package/dist/components/IdeView.js.map +1 -0
  18. package/dist/components/QuickOpen.d.ts +2 -0
  19. package/dist/components/QuickOpen.d.ts.map +1 -0
  20. package/dist/components/QuickOpen.js +204 -0
  21. package/dist/components/QuickOpen.js.map +1 -0
  22. package/dist/components/ScmView.d.ts +6 -0
  23. package/dist/components/ScmView.d.ts.map +1 -0
  24. package/dist/components/ScmView.js +283 -0
  25. package/dist/components/ScmView.js.map +1 -0
  26. package/dist/components/SearchView.d.ts +2 -0
  27. package/dist/components/SearchView.d.ts.map +1 -0
  28. package/dist/components/SearchView.js +97 -0
  29. package/dist/components/SearchView.js.map +1 -0
  30. package/dist/components/StatusBar.d.ts +2 -0
  31. package/dist/components/StatusBar.d.ts.map +1 -0
  32. package/dist/components/StatusBar.js +71 -0
  33. package/dist/components/StatusBar.js.map +1 -0
  34. package/dist/components/TerminalPanel.d.ts +6 -0
  35. package/dist/components/TerminalPanel.d.ts.map +1 -0
  36. package/dist/components/TerminalPanel.js +101 -0
  37. package/dist/components/TerminalPanel.js.map +1 -0
  38. package/dist/components/hooks.d.ts +25 -0
  39. package/dist/components/hooks.d.ts.map +1 -0
  40. package/dist/components/hooks.js +99 -0
  41. package/dist/components/hooks.js.map +1 -0
  42. package/dist/components/icons.d.ts +23 -0
  43. package/dist/components/icons.d.ts.map +1 -0
  44. package/dist/components/icons.js +123 -0
  45. package/dist/components/icons.js.map +1 -0
  46. package/dist/components/primitives.d.ts +57 -0
  47. package/dist/components/primitives.d.ts.map +1 -0
  48. package/dist/components/primitives.js +263 -0
  49. package/dist/components/primitives.js.map +1 -0
  50. package/dist/fuzzy.d.ts +17 -0
  51. package/dist/fuzzy.d.ts.map +1 -0
  52. package/dist/fuzzy.js +62 -0
  53. package/dist/fuzzy.js.map +1 -0
  54. package/dist/git-model.d.ts +104 -0
  55. package/dist/git-model.d.ts.map +1 -0
  56. package/dist/git-model.js +126 -0
  57. package/dist/git-model.js.map +1 -0
  58. package/dist/ide.css +2203 -0
  59. package/dist/index.d.ts +22 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +19 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/keys.d.ts +33 -0
  64. package/dist/keys.d.ts.map +1 -0
  65. package/dist/keys.js +98 -0
  66. package/dist/keys.js.map +1 -0
  67. package/dist/paths.d.ts +14 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +66 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/store.d.ts +406 -0
  72. package/dist/store.d.ts.map +1 -0
  73. package/dist/store.js +1464 -0
  74. package/dist/store.js.map +1 -0
  75. package/dist/terminal.d.ts +60 -0
  76. package/dist/terminal.d.ts.map +1 -0
  77. package/dist/terminal.js +293 -0
  78. package/dist/terminal.js.map +1 -0
  79. package/dist/text.d.ts +26 -0
  80. package/dist/text.d.ts.map +1 -0
  81. package/dist/text.js +76 -0
  82. package/dist/text.js.map +1 -0
  83. package/dist/tree.d.ts +31 -0
  84. package/dist/tree.d.ts.map +1 -0
  85. package/dist/tree.js +105 -0
  86. package/dist/tree.js.map +1 -0
  87. package/dist/types.d.ts +216 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +14 -0
  90. package/dist/types.js.map +1 -0
  91. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # xgen-dex-ide
2
+
3
+ ```bash
4
+ npm i xgen-dex-ide monaco-editor
5
+ ```
6
+
7
+ > 모노레포(`PlateerLab/xgen-dex-core`) 안에서는 `@dex/ide` 라는 별칭으로 쓴다. 같은 코드이고,
8
+ > 밖으로 나갈 때만 이 이름이다.
9
+
10
+ 채팅 안의 IDE 보기. 에이전트가 이미 가진 것(스토리지·샌드박스·git)을 편집기처럼 보여 준다.
11
+
12
+ | 자리 | 무엇 |
13
+ | --- | --- |
14
+ | 왼쪽 | 활동 막대와 사이드바: 탐색기(스토리지 그대로)·찾기·소스 제어. 보기는 더할 수 있다(`extraViews`) |
15
+ | 가운데 | 편집기 탭(Monaco, 최대 3묶음 나누기)·비교 화면·그림 미리보기, 아래 터미널 |
16
+ | 오른쪽 | 에이전트 채팅(호스트가 넘긴다) |
17
+
18
+ 편집기와 같은 쓰임새를 목표로 한다: 미리보기 탭, 끌어서 옮기기, 빠른 열기(`Ctrl+P`), 명령(`Ctrl+Shift+P`),
19
+ 저장 충돌 비교, 파일에서 찾기·바꾸기, 스테이지·커밋·푸시·브랜치·스태시, 연결이 끊겨도 이어지는 터미널.
20
+
21
+ ## 쓰는 법
22
+
23
+ ```tsx
24
+ import { IdeStore, IdeView, type IdeHost } from '@dex/ide'; // 밖에서는 'xgen-dex-ide'
25
+ import '@dex/ide/ide.css';
26
+
27
+ const store = new IdeStore(host); // 에이전트마다 하나. 채팅 탭을 닫을 때 store.dispose()
28
+
29
+ <IdeView store={store} chat={<Chat />} theme="dark" />;
30
+ ```
31
+
32
+ `IdeStore` 는 화면과 떨어져 산다 — 탭을 바꿔 IDE 가 다시 그려져도 고친 버퍼·되돌리기 기록·터미널
33
+ 연결이 남는다. 호스트는 `IdeHost` 를 구현한다:
34
+
35
+ - `listFiles` · `subscribeChanges` — 스토리지 목록과 변경 알림(탐색기)
36
+ - `readFile` · `saveFile(path, bytes, baseSha)` · `stat` · `readRaw` · `fs` — 샌드박스 파일(저장은 연 판의 sha 가 조건, 어긋나면 `IdeError('changed')`)
37
+ - `search` · `replace` · `git` — 샌드박스 안에서 찾기·바꾸기·git
38
+ - `terminals` · `closeTerminal` · `openTerminal(id, size, handlers)` — 터미널 소켓(같은 id 로 다시 붙으면 이어진다)
39
+ - `loadMonaco` — Monaco 와 그 worker 는 호스트의 번들러가 맡는다
40
+ - `storage` · `notify` · `openExternal` · `download` · `copyText` — 선택
41
+
42
+ 색은 `.xide-root` 의 `--xide-*` 변수로 바꾼다. xterm 은 처음 터미널을 열 때 불러온다(서버 렌더 안전).
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 활동 막대 — 사이드바 보기(탐색기·찾기·소스 제어)를 고른다. 아래쪽은 채팅·터미널.
3
+ *
4
+ * 보기는 등록부(`views`)로 받는다. 지금은 셋이지만 확장(테스트·환경 변수·미리보기 등)이
5
+ * 같은 자리에 들어올 수 있게 IdeView 가 목록을 넘긴다.
6
+ */
7
+ import type { ReactElement } from 'react';
8
+ import { type IconName } from './icons.js';
9
+ import type { IdeState } from '../store.js';
10
+ export interface SideView {
11
+ id: string;
12
+ title: string;
13
+ icon: IconName;
14
+ keybinding?: string;
15
+ badge?: (s: IdeState) => number;
16
+ render: () => ReactElement;
17
+ }
18
+ export declare function ActivityBar({ views }: {
19
+ views: SideView[];
20
+ }): import("react").JSX.Element;
21
+ //# sourceMappingURL=ActivityBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivityBar.d.ts","sourceRoot":"","sources":["../../src/components/ActivityBar.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,YAAY,CAAC;CAC5B;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,+BAmD3D"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from './icons.js';
3
+ import { useIde, useStore } from './hooks.js';
4
+ import { formatBinding } from '../keys.js';
5
+ export function ActivityBar({ views }) {
6
+ const store = useStore();
7
+ const side = useIde((s) => s.layout.sideView);
8
+ const chatOpen = useIde((s) => s.layout.chatOpen);
9
+ const panelOpen = useIde((s) => s.layout.panelOpen);
10
+ const readonly = useIde((s) => s.readonly);
11
+ const badges = useIde((s) => views.map((v) => (v.badge ? v.badge(s) : 0)));
12
+ return (_jsxs("nav", { className: "xide-activity", "aria-label": "\uBCF4\uAE30", children: [_jsx("div", { className: "xide-activity-top", children: views.map((v, i) => (_jsxs("button", { type: "button", className: `xide-activity-btn${side === v.id ? ' xide--active' : ''}`, title: v.keybinding ? `${v.title} (${formatBinding(v.keybinding)})` : v.title, "aria-label": v.title, "aria-pressed": side === v.id, onClick: () => store.toggleSideView(v.id), children: [_jsx(Icon, { name: v.icon, size: 22 }), badges[i] ? _jsx("span", { className: "xide-activity-badge", children: badges[i] > 999 ? '999+' : badges[i] }) : null] }, v.id))) }), _jsxs("div", { className: "xide-activity-bottom", children: [!readonly ? (_jsx("button", { type: "button", className: `xide-activity-btn${panelOpen ? ' xide--active' : ''}`, title: `터미널 (${formatBinding('Mod+`')})`, "aria-label": "\uD130\uBBF8\uB110", "aria-pressed": panelOpen, onClick: () => (panelOpen ? store.togglePanel() : store.showTerminal()), children: _jsx(Icon, { name: "terminal", size: 22 }) })) : null, _jsx("button", { type: "button", className: `xide-activity-btn${chatOpen ? ' xide--active' : ''}`, title: "\uC5D0\uC774\uC804\uD2B8 \uCC44\uD305", "aria-label": "\uC5D0\uC774\uC804\uD2B8 \uCC44\uD305", "aria-pressed": chatOpen, onClick: () => store.setLayout({ chatOpen: !chatOpen }), children: _jsx(Icon, { name: "chat", size: 22 }) })] })] }));
13
+ }
14
+ //# sourceMappingURL=ActivityBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivityBar.js","sourceRoot":"","sources":["../../src/components/ActivityBar.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { ThemeKind } from '../types.js';
2
+ export declare function EditorArea({ theme }: {
3
+ theme: ThemeKind;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=EditorArea.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditorArea.d.ts","sourceRoot":"","sources":["../../src/components/EditorArea.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,+BA8BzD"}
@@ -0,0 +1,468 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * 편집기 영역 — 편집기 묶음(최대 3개, 좌우로 나눔)마다 탭 줄과 편집기 하나.
4
+ *
5
+ * Monaco 편집기는 묶음마다 **하나**를 두고 탭을 바꿀 때 모델만 갈아 끼운다(편집기와 같은
6
+ * 방식). 탭마다 스크롤·커서를 기억했다가 돌아오면 되살린다. 문서(모델)는 저장소가 들고
7
+ * 있어 화면이 다시 그려져도 고친 내용과 되돌리기 기록이 남는다.
8
+ */
9
+ import { useEffect, useMemo, useRef, useState } from 'react';
10
+ import { FileIcon, Icon } from './icons.js';
11
+ import { useIde, useStore } from './hooks.js';
12
+ import { Empty, IconButton, SplitHandle, showMenu } from './primitives.js';
13
+ import { diskUri, modelUri, originalUri, stagedUri } from '../store.js';
14
+ import { basename, dirname } from '../paths.js';
15
+ import { decodeText, formatBytes, imageMime } from '../text.js';
16
+ import { formatBinding } from '../keys.js';
17
+ import { STATUS_LABEL } from '../git-model.js';
18
+ export function EditorArea({ theme }) {
19
+ const groups = useIde((s) => s.groups);
20
+ const [weights, setWeights] = useState({});
21
+ const ref = useRef(null);
22
+ const total = groups.reduce((n, g) => n + (weights[g.id] ?? 1), 0);
23
+ return (_jsx("div", { className: "xide-editor-area", ref: ref, children: groups.map((g, i) => (_jsxs("div", { className: "xide-editor-group-wrap", style: { flex: `${weights[g.id] ?? 1} 1 0` }, children: [i > 0 ? (_jsx(SplitHandle, { axis: "x", label: "\uD3B8\uC9D1\uAE30 \uB108\uBE44", value: Math.round(((weights[groups[i - 1].id] ?? 1) / total) * 1000), min: 150, max: 850, onChange: (v) => {
24
+ const a = groups[i - 1].id;
25
+ const b = g.id;
26
+ const pair = (weights[a] ?? 1) + (weights[b] ?? 1);
27
+ const left = Math.max(0.15, Math.min(pair - 0.15, (v / 1000) * total));
28
+ setWeights((w) => ({ ...w, [a]: left, [b]: pair - left }));
29
+ } })) : null, _jsx(EditorGroupView, { group: g, index: i, theme: theme })] }, g.id))) }));
30
+ }
31
+ function EditorGroupView({ group, index, theme }) {
32
+ const store = useStore();
33
+ const activeGroup = useIde((s) => s.activeGroup);
34
+ const tab = group.tabs.find((t) => t.id === group.activeId) ?? null;
35
+ return (_jsxs("section", { className: `xide-editor-group${activeGroup === group.id ? ' xide--focused' : ''}`, "aria-label": `편집기 ${index + 1}`, onMouseDownCapture: () => store.setActiveGroup(group.id), onFocusCapture: () => store.setActiveGroup(group.id), children: [_jsx(TabBar, { group: group }), tab ? _jsx(Breadcrumbs, { tab: tab }) : null, _jsx("div", { className: "xide-editor-body", children: tab ? _jsx(TabContent, { group: group, tab: tab, theme: theme }, tab.kind === 'diff' ? 'diff' : tab.kind === 'image' ? `img:${tab.path}` : 'code') : _jsx(Welcome, {}) })] }));
36
+ }
37
+ // ── 탭 줄 ─────────────────────────────────────────────────────────────
38
+ function TabBar({ group }) {
39
+ const store = useStore();
40
+ const docs = useIde((s) => s.docs);
41
+ const groupCount = useIde((s) => s.groups.length);
42
+ const layout = useIde((s) => s.layout);
43
+ const [dropAt, setDropAt] = useState(null);
44
+ const scroller = useRef(null);
45
+ const names = useMemo(() => {
46
+ const count = new Map();
47
+ for (const t of group.tabs)
48
+ count.set(basename(t.path), (count.get(basename(t.path)) ?? 0) + 1);
49
+ return count;
50
+ }, [group.tabs]);
51
+ useEffect(() => {
52
+ const el = scroller.current?.querySelector('.xide-tab.xide--active');
53
+ el?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
54
+ }, [group.activeId]);
55
+ const tabMenu = (t, e) => showMenu(e, [
56
+ { id: 'close', label: '닫기', keybinding: 'Mod+W', run: () => void store.closeTab(group.id, t.id) },
57
+ { id: 'others', label: '다른 탭 닫기', run: () => void store.closeTabs(group.id, 'others', t.id) },
58
+ { id: 'right', label: '오른쪽 탭 닫기', run: () => void store.closeTabs(group.id, 'right', t.id) },
59
+ { id: 'saved', label: '저장된 탭 닫기', run: () => void store.closeTabs(group.id, 'saved') },
60
+ { id: 'all', label: '모두 닫기', run: () => void store.closeTabs(group.id, 'all') },
61
+ 'separator',
62
+ { id: 'copy', label: '경로 복사', run: () => void copyPath(store, t.path) },
63
+ {
64
+ id: 'reveal',
65
+ label: '탐색기에서 보기',
66
+ run: () => {
67
+ store.reveal(t.path);
68
+ store.showSideView('explorer');
69
+ },
70
+ },
71
+ 'separator',
72
+ { id: 'pin', label: '고정', disabled: !t.preview, run: () => store.pinTab(group.id, t.id) },
73
+ { id: 'split', label: '오른쪽으로 나누기', keybinding: 'Mod+\\', run: () => store.splitRight({ tabId: t.id }) },
74
+ ]);
75
+ const onDragStart = (t, e) => {
76
+ e.dataTransfer.setData('application/x-xide-tab', JSON.stringify({ group: group.id, tab: t.id }));
77
+ e.dataTransfer.effectAllowed = 'move';
78
+ };
79
+ const onDragOver = (i, e) => {
80
+ if (!Array.from(e.dataTransfer.types).includes('application/x-xide-tab'))
81
+ return;
82
+ e.preventDefault();
83
+ const r = e.currentTarget.getBoundingClientRect();
84
+ setDropAt(e.clientX < r.left + r.width / 2 ? i : i + 1);
85
+ };
86
+ const onDrop = (e) => {
87
+ const raw = e.dataTransfer.getData('application/x-xide-tab');
88
+ const at = dropAt ?? group.tabs.length;
89
+ setDropAt(null);
90
+ if (!raw)
91
+ return;
92
+ e.preventDefault();
93
+ const { group: from, tab } = JSON.parse(raw);
94
+ store.moveTab(from, tab, group.id, at);
95
+ };
96
+ const active = group.tabs.find((t) => t.id === group.activeId);
97
+ const activeDoc = active ? docs[active.path] : undefined;
98
+ return (_jsxs("div", { className: "xide-tabbar", onDragLeave: () => setDropAt(null), onDrop: onDrop, onDragOver: (e) => onDragOver(group.tabs.length - 1, e), children: [_jsx("div", { className: "xide-tabs", ref: scroller, role: "tablist", onWheel: (e) => {
99
+ if (scroller.current && Math.abs(e.deltaY) > Math.abs(e.deltaX))
100
+ scroller.current.scrollLeft += e.deltaY;
101
+ }, onDoubleClick: (e) => {
102
+ if (e.target === e.currentTarget && !store.getState().readonly)
103
+ store.startCreate('', 'file');
104
+ }, children: group.tabs.map((t, i) => {
105
+ const doc = docs[t.path];
106
+ const name = basename(t.path);
107
+ const dup = (names.get(name) ?? 0) > 1;
108
+ const dirty = t.kind === 'file' && !!doc?.dirty;
109
+ const label = t.kind === 'diff'
110
+ ? t.diff?.disk
111
+ ? `${name} (디스크 ↔ 편집 중)`
112
+ : `${name} (${t.diff?.staged ? '스테이지' : '작업 트리'})`
113
+ : name;
114
+ return (_jsxs("div", { role: "tab", "aria-selected": t.id === group.activeId, className: [
115
+ 'xide-tab',
116
+ t.id === group.activeId ? 'xide--active' : '',
117
+ t.preview ? 'xide--preview' : '',
118
+ dirty ? 'xide--dirty' : '',
119
+ dropAt === i ? 'xide--drop-before' : '',
120
+ dropAt === i + 1 && i === group.tabs.length - 1 ? 'xide--drop-after' : '',
121
+ ]
122
+ .filter(Boolean)
123
+ .join(' '), title: `${t.path}${t.kind === 'diff' && t.diff?.status ? ` · ${STATUS_LABEL[t.diff.status]}` : ''}`, draggable: true, onDragStart: (e) => onDragStart(t, e), onDragOver: (e) => {
124
+ e.stopPropagation();
125
+ onDragOver(i, e);
126
+ }, onClick: () => store.activateTab(group.id, t.id), onDoubleClick: () => store.pinTab(group.id, t.id), onAuxClick: (e) => {
127
+ if (e.button === 1) {
128
+ e.preventDefault();
129
+ void store.closeTab(group.id, t.id);
130
+ }
131
+ }, onContextMenu: (e) => tabMenu(t, e), children: [t.kind === 'diff' ? _jsx(Icon, { name: "split", className: "xide-icon xide-tab-diff" }) : _jsx(FileIcon, { name: name }), _jsx("span", { className: "xide-tab-label", children: label }), dup ? _jsx("span", { className: "xide-tab-dir", children: dirname(t.path) || '/' }) : null, _jsxs("button", { type: "button", className: "xide-tab-close", "aria-label": dirty ? '저장하지 않은 변경이 있습니다. 닫기' : '닫기', title: `닫기 (${formatBinding('Mod+W')})`, onClick: (e) => {
132
+ e.stopPropagation();
133
+ void store.closeTab(group.id, t.id);
134
+ }, children: [_jsx(Icon, { name: dirty ? 'dot' : 'close', className: "xide-icon xide-tab-close-icon" }), dirty ? _jsx(Icon, { name: "close", className: "xide-icon xide-tab-close-hover" }) : null] })] }, t.id));
135
+ }) }), _jsxs("div", { className: "xide-tabbar-actions", children: [active?.kind === 'diff' && active.diff && !active.diff.disk ? (_jsx(IconButton, { icon: "go-to-file", label: "\uD30C\uC77C \uC5F4\uAE30", onClick: () => void store.openFile(active.path, { preview: false, groupId: group.id }) })) : null, activeDoc?.dirty ? (_jsx(IconButton, { icon: "check", label: "\uC800\uC7A5", keybinding: "Mod+S", onClick: () => void store.save(active.path) })) : null, _jsx(IconButton, { icon: "split", label: "\uC624\uB978\uCABD\uC73C\uB85C \uB098\uB204\uAE30", keybinding: "Mod+\\\\", disabled: !active || groupCount >= 3, onClick: () => store.splitRight() }), _jsx(IconButton, { icon: "more", label: "\uB354 \uBCF4\uAE30", onClick: (e) => showMenu(e, [
136
+ { id: 'close-all', label: '모두 닫기', run: () => void store.closeTabs(group.id, 'all') },
137
+ { id: 'close-saved', label: '저장된 탭 닫기', run: () => void store.closeTabs(group.id, 'saved') },
138
+ 'separator',
139
+ { id: 'wrap', label: layout.wordWrap ? '줄 바꿈 끄기' : '줄 바꿈 켜기', keybinding: 'Alt+Z', run: () => store.setLayout({ wordWrap: !layout.wordWrap }) },
140
+ { id: 'minimap', label: layout.minimap ? '미니맵 끄기' : '미니맵 켜기', run: () => store.setLayout({ minimap: !layout.minimap }) },
141
+ { id: 'autosave', label: layout.autoSave ? '자동 저장 끄기' : '자동 저장 켜기', run: () => store.setLayout({ autoSave: !layout.autoSave }) },
142
+ ]) })] })] }));
143
+ }
144
+ async function copyPath(store, path) {
145
+ const ok = (await store.host.copyText?.(path)) ?? (await navigator.clipboard?.writeText(path).then(() => true, () => false));
146
+ store.notify(ok ? 'success' : 'error', ok ? '경로를 복사했습니다' : '복사하지 못했습니다');
147
+ }
148
+ function Breadcrumbs({ tab }) {
149
+ const store = useStore();
150
+ const parts = tab.path.split('/');
151
+ return (_jsxs("div", { className: "xide-breadcrumbs", "aria-label": "\uACBD\uB85C", children: [parts.map((p, i) => {
152
+ const path = parts.slice(0, i + 1).join('/');
153
+ const last = i === parts.length - 1;
154
+ return (_jsxs("span", { className: "xide-crumb", children: [_jsxs("button", { type: "button", onClick: () => {
155
+ store.reveal(path);
156
+ store.showSideView('explorer');
157
+ }, children: [last ? _jsx(FileIcon, { name: p }) : null, p] }), !last ? _jsx(Icon, { name: "chevron-right" }) : null] }, path));
158
+ }), tab.kind === 'diff' && tab.diff ? (_jsx("span", { className: "xide-crumb-note", children: tab.diff.disk ? '왼쪽: 디스크의 지금 판 · 오른쪽: 편집 중인 판' : tab.diff.staged ? '왼쪽: 마지막 커밋 · 오른쪽: 스테이지' : '왼쪽: 스테이지 · 오른쪽: 작업 트리' })) : null] }));
159
+ }
160
+ function Welcome() {
161
+ const store = useStore();
162
+ const readonly = useIde((s) => s.readonly);
163
+ const rows = [
164
+ ['파일 빨리 열기', 'Mod+P', () => store.openQuickOpen('files')],
165
+ ['명령 보기', 'Mod+Shift+P', () => store.openQuickOpen('commands')],
166
+ ['찾기', 'Mod+Shift+F', () => store.showSideView('search')],
167
+ ['소스 제어', 'Mod+Shift+G', () => store.showSideView('scm')],
168
+ ];
169
+ if (!readonly)
170
+ rows.push(['터미널', 'Mod+`', () => store.showTerminal()]);
171
+ return (_jsxs("div", { className: "xide-welcome", children: [_jsx("div", { className: "xide-welcome-title", children: "\uC120\uD0DD\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" }), _jsx("div", { className: "xide-welcome-text", children: "\uD0D0\uC0C9\uAE30\uC5D0\uC11C \uD30C\uC77C\uC744 \uACE0\uB974\uAC70\uB098 \uC18C\uC2A4 \uC81C\uC5B4\uC5D0\uC11C \uBC14\uB010 \uD30C\uC77C\uC744 \uB204\uB974\uC138\uC694." }), _jsx("div", { className: "xide-welcome-keys", children: rows.map(([label, key, run]) => (_jsxs("button", { type: "button", onClick: run, children: [_jsx("span", { children: label }), _jsx("kbd", { children: formatBinding(key) })] }, label))) })] }));
172
+ }
173
+ // ── 탭 내용 ───────────────────────────────────────────────────────────
174
+ function TabContent({ group, tab, theme }) {
175
+ if (tab.kind === 'image')
176
+ return _jsx(ImagePane, { path: tab.path });
177
+ if (tab.kind === 'diff' && tab.diff)
178
+ return _jsx(DiffPane, { group: group, tab: tab, diff: tab.diff, theme: theme });
179
+ return _jsx(CodePane, { group: group, tab: tab, theme: theme });
180
+ }
181
+ function useMonaco() {
182
+ const store = useStore();
183
+ const ready = useIde((s) => s.monacoReady);
184
+ const error = useIde((s) => s.monacoError);
185
+ useEffect(() => {
186
+ if (!ready)
187
+ void store.loadMonaco().catch(() => undefined);
188
+ }, [ready, store]);
189
+ return { monaco: ready ? store.getMonaco() : null, error };
190
+ }
191
+ function editorOptions(layout, readOnly) {
192
+ return {
193
+ automaticLayout: true,
194
+ fontSize: layout.fontSize,
195
+ fontFamily: "'JetBrains Mono', 'D2Coding', 'Fira Code', Menlo, Consolas, 'Liberation Mono', monospace",
196
+ minimap: { enabled: layout.minimap },
197
+ wordWrap: layout.wordWrap ? 'on' : 'off',
198
+ readOnly,
199
+ scrollBeyondLastLine: true,
200
+ renderWhitespace: 'selection',
201
+ smoothScrolling: true,
202
+ cursorBlinking: 'smooth',
203
+ bracketPairColorization: { enabled: true },
204
+ guides: { bracketPairs: 'active', indentation: true },
205
+ stickyScroll: { enabled: true },
206
+ fixedOverflowWidgets: true,
207
+ tabSize: 2,
208
+ detectIndentation: true,
209
+ renderLineHighlight: 'all',
210
+ unicodeHighlight: { ambiguousCharacters: false },
211
+ };
212
+ }
213
+ function monacoTheme(theme) {
214
+ return theme === 'light' ? 'vs' : 'vs-dark';
215
+ }
216
+ function CodePane({ group, tab, theme }) {
217
+ const store = useStore();
218
+ const { monaco, error } = useMonaco();
219
+ const doc = useIde((s) => s.docs[tab.path]);
220
+ const layout = useIde((s) => s.layout);
221
+ const readonly = useIde((s) => s.readonly);
222
+ const host = useRef(null);
223
+ const editorRef = useRef(null);
224
+ const shownTab = useRef(null);
225
+ // 편집기를 만든다(묶음마다 하나).
226
+ useEffect(() => {
227
+ if (!monaco || !host.current)
228
+ return;
229
+ const editor = monaco.editor.create(host.current, { ...editorOptions(layout, readonly), model: null, theme: monacoTheme(theme) });
230
+ editorRef.current = editor;
231
+ store.editors.set(group.id, editor);
232
+ const subs = [
233
+ editor.onDidChangeCursorSelection(() => {
234
+ const pos = editor.getPosition();
235
+ const model = editor.getModel();
236
+ if (!pos || !model)
237
+ return;
238
+ const sel = editor.getSelections() ?? [];
239
+ const selected = sel.reduce((n, r) => n + (model.getValueInRange(r).length || 0), 0);
240
+ store.setCursor({ line: pos.lineNumber, col: pos.column, selected });
241
+ }),
242
+ editor.onDidFocusEditorWidget(() => store.setActiveGroup(group.id)),
243
+ ];
244
+ return () => {
245
+ if (shownTab.current)
246
+ store.viewStates.set(`${group.id}:${shownTab.current}`, editor.saveViewState());
247
+ subs.forEach((d) => d.dispose());
248
+ if (store.editors.get(group.id) === editor)
249
+ store.editors.delete(group.id);
250
+ editor.setModel(null);
251
+ editor.dispose();
252
+ editorRef.current = null;
253
+ shownTab.current = null;
254
+ };
255
+ // 편집기는 한 번만 만든다 — 옵션은 아래 효과가 바꾼다.
256
+ // eslint-disable-next-line react-hooks/exhaustive-deps
257
+ }, [monaco, group.id]);
258
+ useEffect(() => {
259
+ editorRef.current?.updateOptions(editorOptions(layout, readonly));
260
+ }, [layout.fontSize, layout.minimap, layout.wordWrap, readonly]); // eslint-disable-line react-hooks/exhaustive-deps
261
+ useEffect(() => {
262
+ monaco?.editor.setTheme(monacoTheme(theme));
263
+ }, [monaco, theme]);
264
+ // 탭이 바뀌거나 문서가 준비되면 모델을 갈아 끼운다.
265
+ useEffect(() => {
266
+ const editor = editorRef.current;
267
+ if (!monaco || !editor)
268
+ return;
269
+ const model = doc?.status === 'ready' ? monaco.editor.getModel(modelUri(monaco, store.host.workflowId, tab.path)) : null;
270
+ if (shownTab.current && shownTab.current !== tab.id) {
271
+ store.viewStates.set(`${group.id}:${shownTab.current}`, editor.saveViewState());
272
+ }
273
+ if (editor.getModel() !== model) {
274
+ editor.setModel(model);
275
+ if (model) {
276
+ const vs = store.viewStates.get(`${group.id}:${tab.id}`);
277
+ if (vs)
278
+ editor.restoreViewState(vs);
279
+ if (store.getState().activeGroup === group.id)
280
+ editor.focus();
281
+ }
282
+ }
283
+ shownTab.current = model ? tab.id : null;
284
+ const want = store.pendingReveal;
285
+ if (model && want && want.path === tab.path) {
286
+ store.pendingReveal = null;
287
+ const range = new monaco.Range(want.line, want.col, want.line, want.col + want.length);
288
+ editor.setSelection(range);
289
+ editor.revealRangeInCenterIfOutsideViewport(range);
290
+ editor.focus();
291
+ }
292
+ });
293
+ return (_jsxs("div", { className: "xide-code-pane", children: [doc?.diskChanged && doc.status === 'ready' ? _jsx(DiskChangedBar, { path: tab.path, deleted: !!doc.message }) : null, _jsx("div", { className: "xide-monaco", ref: host, style: { display: doc?.status === 'ready' ? undefined : 'none' } }), error ? _jsx(Empty, { icon: "error", children: error }) : null, !error && (!doc || doc.status === 'loading' || !monaco) ? _jsx("div", { className: "xide-pane-note", children: "\uC5EC\uB294 \uC911" }) : null, doc?.status === 'binary' ? (_jsxs(Empty, { icon: "file", children: [_jsx("p", { children: "\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC544\uB2C8\uAC70\uB098 UTF-8 \uC774 \uC544\uB2C8\uB77C \uD3B8\uC9D1\uAE30\uB85C \uC5F4\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4." }), _jsxs("div", { className: "xide-empty-actions", children: [_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.openFile(tab.path, { forceText: true, preview: false }), children: "\uADF8\uB798\uB3C4 \uC5F4\uAE30" }), store.host.download ? (_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.host.download?.(tab.path), children: "\uB0B4\uB824\uBC1B\uAE30" })) : null] })] })) : null, doc?.status === 'too_large' ? (_jsxs(Empty, { icon: "warning", children: [_jsxs("p", { children: ["\uD30C\uC77C\uC774 \uB108\uBB34 \uCEE4\uC11C(", formatBytes(doc.size), ") \uD3B8\uC9D1\uAE30\uB85C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."] }), store.host.download ? (_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.host.download?.(tab.path), children: "\uB0B4\uB824\uBC1B\uAE30" })) : null] })) : null, doc?.status === 'missing' || doc?.status === 'error' ? (_jsxs(Empty, { icon: "warning", children: [_jsx("p", { children: doc.message ?? '파일을 열지 못했습니다' }), _jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.ensureDoc(tab.path), children: "\uB2E4\uC2DC \uC2DC\uB3C4" })] })) : null] }));
294
+ }
295
+ function DiskChangedBar({ path, deleted }) {
296
+ const store = useStore();
297
+ return (_jsxs("div", { className: "xide-banner warning", role: "alert", children: [_jsx(Icon, { name: "warning" }), _jsx("span", { children: deleted
298
+ ? '이 파일이 디스크에서 지워졌습니다. 저장하면 다시 만들어집니다.'
299
+ : '편집하는 사이 에이전트나 터미널이 이 파일을 바꿨습니다.' }), !deleted ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.openDiskCompare(path), children: "\uBE44\uAD50" }), _jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.reloadFromDisk(path, { discard: true }), children: "\uBC14\uB010 \uD310 \uBD88\uB7EC\uC624\uAE30" })] })) : null, _jsx("button", { type: "button", className: "xide-btn xide--danger", onClick: () => void store.save(path, { force: true }), children: deleted ? '다시 저장' : '내 판으로 덮어쓰기' })] }));
300
+ }
301
+ /** git 의 어떤 판(HEAD·스테이지)의 내용 — 없으면 빈 문자열(새 파일). */
302
+ async function gitText(store, diff, ref) {
303
+ const out = await store.host.git({
304
+ op: 'show',
305
+ repo: diff.repo,
306
+ path: diff.path,
307
+ ref,
308
+ });
309
+ if (!out.exists || !out.content_b64)
310
+ return '';
311
+ if (out.too_large)
312
+ return '(파일이 너무 커서 비교하지 않습니다)';
313
+ const bin = atob(out.content_b64);
314
+ const bytes = new Uint8Array(bin.length);
315
+ for (let i = 0; i < bin.length; i += 1)
316
+ bytes[i] = bin.charCodeAt(i);
317
+ return decodeText(bytes).text;
318
+ }
319
+ function setModelText(monaco, uri, text, languageFrom) {
320
+ const existing = monaco.editor.getModel(uri);
321
+ if (existing) {
322
+ if (existing.getValue() !== text)
323
+ existing.setValue(text);
324
+ return existing;
325
+ }
326
+ const lang = monaco.editor.getModel(languageFrom)?.getLanguageId();
327
+ return monaco.editor.createModel(text, lang, uri);
328
+ }
329
+ function DiffPane({ group, tab, diff, theme }) {
330
+ const store = useStore();
331
+ const { monaco, error } = useMonaco();
332
+ const doc = useIde((s) => s.docs[tab.path]);
333
+ const statuses = useIde((s) => s.git.statuses);
334
+ const layout = useIde((s) => s.layout);
335
+ const readonly = useIde((s) => s.readonly);
336
+ const host = useRef(null);
337
+ const editorRef = useRef(null);
338
+ const [inline, setInline] = useState(false);
339
+ const [loadError, setLoadError] = useState(null);
340
+ const [ready, setReady] = useState(false);
341
+ useEffect(() => {
342
+ if (!monaco || !host.current)
343
+ return;
344
+ const editor = monaco.editor.createDiffEditor(host.current, {
345
+ ...editorOptions(layout, readonly),
346
+ renderSideBySide: !inline,
347
+ originalEditable: false,
348
+ ignoreTrimWhitespace: false,
349
+ enableSplitViewResizing: true,
350
+ renderOverviewRuler: true,
351
+ });
352
+ editorRef.current = editor;
353
+ return () => {
354
+ editor.setModel(null);
355
+ editor.dispose();
356
+ editorRef.current = null;
357
+ };
358
+ }, [monaco]); // eslint-disable-line react-hooks/exhaustive-deps
359
+ useEffect(() => {
360
+ editorRef.current?.updateOptions({ ...editorOptions(layout, readonly), renderSideBySide: !inline });
361
+ }, [inline, layout.fontSize, layout.minimap, layout.wordWrap, readonly]); // eslint-disable-line react-hooks/exhaustive-deps
362
+ useEffect(() => {
363
+ monaco?.editor.setTheme(monacoTheme(theme));
364
+ }, [monaco, theme]);
365
+ // 양쪽 모델을 준비한다. git 상태가 바뀌면(스테이지·커밋) 원래 쪽을 다시 읽는다.
366
+ useEffect(() => {
367
+ if (!monaco || !editorRef.current)
368
+ return;
369
+ let alive = true;
370
+ const wf = store.host.workflowId;
371
+ const workUri = modelUri(monaco, wf, tab.path);
372
+ (async () => {
373
+ try {
374
+ let original;
375
+ let modified;
376
+ if (diff.disk) {
377
+ original = monaco.editor.getModel(diskUri(monaco, wf, tab.path));
378
+ if (!doc || doc.status !== 'ready')
379
+ return;
380
+ modified = monaco.editor.getModel(workUri);
381
+ }
382
+ else if (diff.staged) {
383
+ const [head, index] = await Promise.all([gitText(store, diff, 'HEAD'), gitText(store, diff, 'index')]);
384
+ if (!alive)
385
+ return;
386
+ original = setModelText(monaco, originalUri(monaco, wf, diff), head, workUri);
387
+ modified = setModelText(monaco, stagedUri(monaco, wf, diff), diff.status === 'deleted' ? '' : index, workUri);
388
+ }
389
+ else {
390
+ const base = diff.status === 'untracked' ? '' : await gitText(store, diff, 'index');
391
+ if (!alive)
392
+ return;
393
+ original = setModelText(monaco, originalUri(monaco, wf, diff), base, workUri);
394
+ if (diff.status === 'deleted') {
395
+ modified = setModelText(monaco, monaco.Uri.from({ scheme: 'xide-empty', path: `/xgen/${wf}/${tab.path}` }), '', workUri);
396
+ }
397
+ else {
398
+ if (!doc || doc.status !== 'ready')
399
+ return;
400
+ modified = monaco.editor.getModel(workUri);
401
+ }
402
+ }
403
+ if (!alive || !editorRef.current || !original || !modified)
404
+ return;
405
+ const cur = editorRef.current.getModel();
406
+ if (cur?.original !== original || cur?.modified !== modified) {
407
+ editorRef.current.setModel({ original, modified });
408
+ }
409
+ editorRef.current.getModifiedEditor().updateOptions({ readOnly: readonly || diff.staged || diff.status === 'deleted' });
410
+ setReady(true);
411
+ setLoadError(null);
412
+ }
413
+ catch (err) {
414
+ if (alive)
415
+ setLoadError(err instanceof Error ? err.message : '비교할 내용을 읽지 못했습니다');
416
+ }
417
+ })();
418
+ return () => {
419
+ alive = false;
420
+ };
421
+ }, [monaco, diff, doc?.status, statuses, tab.path, store, readonly]);
422
+ useEffect(() => {
423
+ const ed = editorRef.current?.getModifiedEditor();
424
+ if (!ed)
425
+ return;
426
+ const sub = ed.onDidChangeCursorPosition((e) => store.setCursor({ line: e.position.lineNumber, col: e.position.column, selected: 0 }));
427
+ return () => sub.dispose();
428
+ }, [ready, store]);
429
+ const status = diff.repo != null ? statuses[diff.repo] : undefined;
430
+ const stillChanged = !diff.disk &&
431
+ !!status &&
432
+ (diff.staged ? status.staged : [...status.changes, ...status.untracked]).some((c) => c.path === diff.path);
433
+ return (_jsxs("div", { className: "xide-code-pane", children: [_jsxs("div", { className: "xide-diff-toolbar", children: [_jsx("span", { className: "xide-dim", children: diff.disk ? '디스크 판과 편집 중인 판' : `${STATUS_LABEL[diff.status ?? 'modified']} · ${diff.staged ? '스테이지된 변경' : '작업 트리 변경'}` }), _jsx("span", { className: "xide-spacer" }), !readonly && !diff.disk && stillChanged ? (diff.staged ? (_jsxs("button", { type: "button", className: "xide-btn", onClick: () => void store.gitRun({ op: 'unstage', repo: diff.repo, paths: [diff.path] }, '스테이지 취소'), children: [_jsx(Icon, { name: "minus" }), " \uC2A4\uD14C\uC774\uC9C0 \uCDE8\uC18C"] })) : (_jsxs("button", { type: "button", className: "xide-btn", onClick: () => void store.gitRun({ op: 'stage', repo: diff.repo, paths: [diff.path] }, '스테이지'), children: [_jsx(Icon, { name: "plus" }), " \uC2A4\uD14C\uC774\uC9C0"] }))) : null, diff.disk ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "xide-btn", onClick: () => void store.reloadFromDisk(tab.path, { discard: true }).then(() => store.closeTab(group.id, tab.id, { force: true })), children: "\uB514\uC2A4\uD06C \uD310 \uC4F0\uAE30" }), _jsx("button", { type: "button", className: "xide-btn xide--primary", onClick: () => void store.save(tab.path, { force: true }).then((ok) => ok && store.closeTab(group.id, tab.id, { force: true })), children: "\uD3B8\uC9D1 \uC911\uC778 \uD310\uC73C\uB85C \uC800\uC7A5" })] })) : null, _jsx(IconButton, { icon: "layout-sidebar", label: inline ? '나란히 보기' : '한 줄로 보기', active: inline, onClick: () => setInline((v) => !v) }), _jsx(IconButton, { icon: "arrow-up", label: "\uC774\uC804 \uBCC0\uACBD", onClick: () => editorRef.current?.goToDiff?.('previous') }), _jsx(IconButton, { icon: "arrow-down", label: "\uB2E4\uC74C \uBCC0\uACBD", onClick: () => editorRef.current?.goToDiff?.('next') })] }), _jsx("div", { className: "xide-monaco", ref: host }), error || loadError ? _jsx(Empty, { icon: "error", children: error ?? loadError }) : null, !ready && !error && !loadError ? _jsx("div", { className: "xide-pane-note", children: "\uBE44\uAD50\uD560 \uB0B4\uC6A9\uC744 \uC77D\uB294 \uC911" }) : null] }));
434
+ }
435
+ function ImagePane({ path }) {
436
+ const store = useStore();
437
+ const [url, setUrl] = useState(null);
438
+ const [size, setSize] = useState(null);
439
+ const [error, setError] = useState(null);
440
+ const [zoom, setZoom] = useState('fit');
441
+ const version = useIde((s) => s.files.find((f) => f.path === path)?.modifiedAt ?? '');
442
+ useEffect(() => {
443
+ let alive = true;
444
+ let objectUrl = null;
445
+ store.host.readRaw(path).then((bytes) => {
446
+ if (!alive)
447
+ return;
448
+ objectUrl = URL.createObjectURL(new Blob([bytes], { type: imageMime(path) }));
449
+ setUrl(objectUrl);
450
+ setSize({ bytes: bytes.length });
451
+ setError(null);
452
+ }, (err) => alive && setError(err instanceof Error ? err.message : '그림을 읽지 못했습니다'));
453
+ return () => {
454
+ alive = false;
455
+ if (objectUrl)
456
+ URL.revokeObjectURL(objectUrl);
457
+ };
458
+ }, [path, version, store]);
459
+ if (error)
460
+ return _jsx(Empty, { icon: "error", children: error });
461
+ if (!url)
462
+ return _jsx("div", { className: "xide-pane-note", children: "\uC5EC\uB294 \uC911" });
463
+ return (_jsxs("div", { className: `xide-image-pane${zoom === 'fit' ? ' xide--fit' : ''}`, children: [_jsx("div", { className: "xide-image-scroll", onClick: () => setZoom((z) => (z === 'fit' ? 1 : 'fit')), children: _jsx("img", { src: url, alt: basename(path), style: zoom === 'fit' ? undefined : { width: size?.w ? size.w * zoom : undefined }, onLoad: (e) => {
464
+ const img = e.currentTarget;
465
+ setSize((s) => ({ bytes: s?.bytes ?? 0, w: img.naturalWidth, h: img.naturalHeight }));
466
+ } }) }), _jsxs("div", { className: "xide-image-info", children: [size?.w ? `${size.w} × ${size.h}` : '', " ", size ? `· ${formatBytes(size.bytes)}` : '', " \u00B7 ", zoom === 'fit' ? '창에 맞춤' : '원래 크기'] })] }));
467
+ }
468
+ //# sourceMappingURL=EditorArea.js.map