@jbpark/live-editor 4.6.1 → 6.0.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.ko.md +37 -35
  2. package/README.md +37 -35
  3. package/dist/{boundary-BLB80tyw.js → boundary-CySnDUs2.js} +5 -4
  4. package/dist/boundary-CySnDUs2.js.map +1 -0
  5. package/dist/{compile-Dxas-D05.js → compile-my0AjsCw.js} +8 -7
  6. package/dist/compile-my0AjsCw.js.map +1 -0
  7. package/dist/constants-st9gnJ5g.js +55 -0
  8. package/dist/constants-st9gnJ5g.js.map +1 -0
  9. package/dist/{context-B8MzP7_p.js → context-Bjq0CvBE.js} +3 -3
  10. package/dist/{context-B8MzP7_p.js.map → context-Bjq0CvBE.js.map} +1 -1
  11. package/dist/{core-Bgfek364.js → core-BNVuH8-l.js} +51 -14
  12. package/dist/core-BNVuH8-l.js.map +1 -0
  13. package/dist/{detect-typescript-Dr_oLniM.js → detect-typescript-CMF0FVKD.js} +2 -2
  14. package/dist/{detect-typescript-Dr_oLniM.js.map → detect-typescript-CMF0FVKD.js.map} +1 -1
  15. package/dist/dnd/index.d.ts +3 -2
  16. package/dist/dnd/index.js +3 -2
  17. package/dist/dnd-BvjEDo-x.js +4197 -0
  18. package/dist/dnd-BvjEDo-x.js.map +1 -0
  19. package/dist/document-BQhb7YR8.d.ts +17 -0
  20. package/dist/{document-CHO215iP.js → document-uHqw0mvr.js} +194 -109
  21. package/dist/document-uHqw0mvr.js.map +1 -0
  22. package/dist/edit-options-C_r9aCz4.d.ts +100 -0
  23. package/dist/editor/index.d.ts +1 -1
  24. package/dist/editor/index.js +2 -2
  25. package/dist/{editor-CMElfHer.js → editor-CvnzrM9J.js} +2 -2
  26. package/dist/{editor-CMElfHer.js.map → editor-CvnzrM9J.js.map} +1 -1
  27. package/dist/error/index.d.ts +1 -1
  28. package/dist/error/index.js +2 -2
  29. package/dist/{error-CN_GCnXB.js → error-CFzvnLCf.js} +18 -32
  30. package/dist/error-CFzvnLCf.js.map +1 -0
  31. package/dist/{index-DpDU1IAw.d.ts → index-92L7F440.d.ts} +2 -2
  32. package/dist/{index-DrWtSKXu.d.ts → index-BZpZwrxT.d.ts} +2 -2
  33. package/dist/index-C8h8TcIl.d.ts +15 -0
  34. package/dist/{index-13BDSrQc.d.ts → index-C93npmTU.d.ts} +4 -6
  35. package/dist/{index-DQkL37eL.d.ts → index-CqsS207F.d.ts} +6 -12
  36. package/dist/{index-CiWB0zFb.d.ts → index-DQcdHg_V.d.ts} +18 -136
  37. package/dist/{index-CRwEhJ7w.d.ts → index-DfIvkht8.d.ts} +3 -3
  38. package/dist/index.d.ts +12 -11
  39. package/dist/index.js +6 -6
  40. package/dist/index.js.map +1 -1
  41. package/dist/{messages-IBPs4TkZ.d.ts → messages-BpN3QIhj.d.ts} +8 -2
  42. package/dist/{messages-CSrMI4Rm.js → messages-DrINcRdG.js} +8 -2
  43. package/dist/messages-DrINcRdG.js.map +1 -0
  44. package/dist/preview/index.d.ts +2 -2
  45. package/dist/preview/index.js +2 -3
  46. package/dist/{preview-CtyG5UbH.js → preview-B05MkVIQ.js} +16 -13
  47. package/dist/preview-B05MkVIQ.js.map +1 -0
  48. package/dist/provider/index.d.ts +2 -2
  49. package/dist/provider/index.js +2 -2
  50. package/dist/{sections-jYuqS4KB.js → sections-DhH_aVf5.js} +4 -7
  51. package/dist/sections-DhH_aVf5.js.map +1 -0
  52. package/dist/style.css +809 -4950
  53. package/dist/{validate-DB59Z_Nx.d.ts → types-Czps1Txy.d.ts} +12 -28
  54. package/dist/ui-kit/index.d.ts +22 -0
  55. package/dist/{constants-DU17Tdwo.js → ui-kit/index.js} +147 -54
  56. package/dist/ui-kit/index.js.map +1 -0
  57. package/dist/ui-kit/style.css +5039 -0
  58. package/dist/update-2PJbwoS8.d.ts +68 -0
  59. package/dist/{items-BXXtMUNI.js → update-BuYQJqjq.js} +143 -392
  60. package/dist/update-BuYQJqjq.js.map +1 -0
  61. package/dist/{use-dynamic-tailwind-BP1toJkW.js → use-dynamic-tailwind-B3l1qZRW.js} +318 -287
  62. package/dist/use-dynamic-tailwind-B3l1qZRW.js.map +1 -0
  63. package/dist/use-field-state-iQGW0vc6.js +131 -0
  64. package/dist/use-field-state-iQGW0vc6.js.map +1 -0
  65. package/dist/utils/ast/index.d.ts +47 -4
  66. package/dist/utils/ast/index.js +4 -64
  67. package/dist/utils/ast/index.js.map +1 -1
  68. package/dist/utils/index.d.ts +13 -32
  69. package/dist/utils/index.js +4 -26
  70. package/dist/validate-CZz30Pnb.js +122 -0
  71. package/dist/validate-CZz30Pnb.js.map +1 -0
  72. package/dist/validate-CiA1LF6S.d.ts +33 -0
  73. package/package.json +20 -10
  74. package/dist/boundary-BLB80tyw.js.map +0 -1
  75. package/dist/compile-Dxas-D05.js.map +0 -1
  76. package/dist/constants-DU17Tdwo.js.map +0 -1
  77. package/dist/core-Bgfek364.js.map +0 -1
  78. package/dist/dnd-V8NAxd_R.js +0 -2755
  79. package/dist/dnd-V8NAxd_R.js.map +0 -1
  80. package/dist/document-CHO215iP.js.map +0 -1
  81. package/dist/document-DqRSCtsx.d.ts +0 -1614
  82. package/dist/error-CN_GCnXB.js.map +0 -1
  83. package/dist/index-C_PfR4X9.d.ts +0 -734
  84. package/dist/items-BXXtMUNI.js.map +0 -1
  85. package/dist/messages-CSrMI4Rm.js.map +0 -1
  86. package/dist/preview-CtyG5UbH.js.map +0 -1
  87. package/dist/sections-jYuqS4KB.js.map +0 -1
  88. package/dist/use-dynamic-tailwind-BP1toJkW.js.map +0 -1
  89. package/dist/utils/index.js.map +0 -1
  90. package/dist/validate-iLo8IONY.js +0 -64
  91. package/dist/validate-iLo8IONY.js.map +0 -1
package/README.ko.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [English](./README.md) | [한국어](./README.ko.md)
4
4
 
5
- 실시간 프리뷰와 드래그 앤 드롭을 지원하는 인터랙티브 UI 에디터입니다. 캔버스에서의 편집 내용은 AST 변환을 통해 실제 소스 코드에 정확히 반영되며, 결과는 DOM/CSS 격리를 위해 iframe 안에서 렌더링됩니다. React 19와 TypeScript로 구현되었습니다. iframe은 보안 샌드박스가 아닙니다 — [보안 참고사항](#-보안-참고사항)을 확인하세요.
5
+ 실시간 프리뷰와 드래그 앤 드롭을 지원하는 인터랙티브 UI 에디터입니다. 캔버스에서의 편집 내용은 AST 변환을 통해 실제 소스 코드에 정확히 반영되며, 결과는 DOM/CSS 격리를 위해 iframe 안에서 렌더링됩니다. React 19와 TypeScript로 구현되었습니다. iframe은 보안 샌드박스가 아닙니다 — [보안 참고사항](#보안-참고사항)을 확인하세요.
6
6
 
7
- 📖 **문서 & 라이브 데모:** https://live-editor-lab.vercel.app
7
+ **문서 & 라이브 데모:** https://live-editor-lab.vercel.app
8
8
 
9
- ## 📁 프로젝트 구조
9
+ ## 프로젝트 구조
10
10
 
11
11
  ```text
12
12
  live-editor/
@@ -36,16 +36,16 @@ live-editor/
36
36
 
37
37
  각 부분이 어떻게 맞물리는지(읽기·쓰기 경로, commit 단계, 용어집)는 [ARCHITECTURE.ko.md](./ARCHITECTURE.ko.md)에 있습니다.
38
38
 
39
- ## 🎯 주요 기능
39
+ ## 주요 기능
40
40
 
41
41
  - **AST 기반 실시간 코드 반영**: 캔버스 상호작용(추가/이동/삭제, 속성 변경)을 안전하게 소스 코드에 적용합니다.
42
42
  - **인터랙티브 속성 패널**: 숫자, 문자열, 불리언, 배열, 객체를 패널에서 바로 편집합니다.
43
43
  - **고급 JSX 바인딩 시스템**: 타입 기반 감지를 통해 모든 JSX 요소 속성(children, label, icon 등)을 자동으로 인식하고 편집 가능하게 합니다.
44
44
  - **스마트 Items 에디터**: 배열 아이템을 추가/이동/삭제하고, 일반 속성과 중첩된 JSX 컴포넌트를 편집합니다. 순서 변경 시에도 안정적인 컴포넌트 ID를 유지합니다.
45
- - **프리뷰 런타임**: 컴파일된 결과를 DOM/CSS 격리를 위해 iframe 안에서 렌더링합니다 (보안 샌드박스는 아닙니다 — [보안 참고사항](#-보안-참고사항) 참고).
45
+ - **프리뷰 런타임**: 컴파일된 결과를 DOM/CSS 격리를 위해 iframe 안에서 렌더링합니다 (보안 샌드박스는 아닙니다 — [보안 참고사항](#보안-참고사항) 참고).
46
46
  - **강력한 드래그 앤 드롭**: `@dnd-kit` 기반으로 부드러운 정렬과 배치를 지원합니다.
47
47
 
48
- ## 📦 설치
48
+ ## 설치
49
49
 
50
50
  ```bash
51
51
  pnpm add @jbpark/live-editor react react-dom
@@ -55,7 +55,7 @@ pnpm add @jbpark/live-editor react react-dom
55
55
  peer로, 에디터의 저장 시 포맷(format-on-save)을 쓰려면 함께 설치하세요. 없어도
56
56
  에디터는 정상 동작하며 포맷만 건너뜁니다.
57
57
 
58
- ## 🧑‍💻 사용법
58
+ ## 사용법
59
59
 
60
60
  스타일시트를 앱 루트 근처에서 한 번 import하세요 — **필수**이며 JS 엔트리가
61
61
  자동으로 불러오지 않습니다:
@@ -69,12 +69,12 @@ import '@jbpark/live-editor/style.css';
69
69
  // 필수 — JS 엔트리가 import하지 않음
70
70
 
71
71
  const SAMPLE = `
72
- import * as ui from 'ui-kit';
73
-
74
72
  const App = () => (
75
73
  <div className="flex flex-col items-start gap-2 p-6">
76
- <ui.Typography.Title level={3}>Hello</ui.Typography.Title>
77
- <ui.Button type="primary">Edit me</ui.Button>
74
+ <h3 className="text-xl font-semibold">Hello</h3>
75
+ <button type="button" className="rounded bg-indigo-500 px-3 py-1 text-white">
76
+ Edit me
77
+ </button>
78
78
  </div>
79
79
  );
80
80
 
@@ -96,32 +96,34 @@ export default function Example() {
96
96
  전체 가이드(Tailwind가 프리뷰에 반영되는 방식, 커스텀 패널 등)는
97
97
  [문서 사이트](https://live-editor-lab.vercel.app)를 참고하세요.
98
98
 
99
- ## 🧩 엔트리 포인트
99
+ ## 엔트리 포인트
100
100
 
101
101
  패키지는 서브패스 export를 제공해, 한 기능 영역만 필요한 소비자가 나머지를
102
102
  번들에 포함하지 않도록 합니다(예: 프리뷰 전용 빌드에는 CodeMirror 미포함) —
103
103
  [#194](https://github.com/pjb0811/live-editor/issues/194) 참고:
104
104
 
105
- | Import | 내용 |
106
- | ------------------------------------ | -------------------------------------- |
107
- | `@jbpark/live-editor` | 전체 (`Live` 프로바이더 + 모든 서피스) |
108
- | `@jbpark/live-editor/provider` | 공유 편집 컨텍스트 프로바이더만 |
109
- | `@jbpark/live-editor/dnd` | 드래그 앤 드롭 캔버스 + 속성 패널 |
110
- | `@jbpark/live-editor/editor` | CodeMirror 코드 에디터 |
111
- | `@jbpark/live-editor/preview` | 격리된 프리뷰 런타임 |
112
- | `@jbpark/live-editor/error` | 에러 바운더리 |
113
- | `@jbpark/live-editor/utils` | 컴파일/섹션 헬퍼 |
114
- | `@jbpark/live-editor/utils/ast` | AST 조작 및 코드 생성 |
115
- | `@jbpark/live-editor/utils/tailwind` | Tailwind 테마 헬퍼 |
116
- | `@jbpark/live-editor/style.css` | 컴파일된 스타일시트 (필수) |
117
-
118
- ## 🔒 보안 참고사항
119
-
120
- - 프리뷰 `<iframe>`(`src/components/frame/iframe.tsx`)의 `sandbox` prop은 **실제 보안 경계가 아닙니다**. 컴파일된 프리뷰 코드는 host 페이지 자신의 JS realm에서 `new Function(...)`으로 실행되며(`src/utils/index.ts`의 `compileModule`), iframe에는 그 결과로 생성된 React 엘리먼트만 `contentDocument`에 포탈되어 DOM/CSS 렌더링 용도로만 쓰입니다. iframe 자체는 사용자 코드를 실행하지 않습니다.
105
+ | Import | 내용 |
106
+ | -------------------------------------- | --------------------------------------------------------------------- |
107
+ | `@jbpark/live-editor` | 전체 (`Live` 프로바이더 + 모든 서피스) |
108
+ | `@jbpark/live-editor/provider` | 공유 편집 컨텍스트 프로바이더만 |
109
+ | `@jbpark/live-editor/dnd` | 드래그 앤 드롭 캔버스 + 속성 패널 |
110
+ | `@jbpark/live-editor/editor` | CodeMirror 코드 에디터 |
111
+ | `@jbpark/live-editor/preview` | 격리된 프리뷰 런타임 |
112
+ | `@jbpark/live-editor/error` | 에러 바운더리 |
113
+ | `@jbpark/live-editor/utils` | 컴파일/섹션 헬퍼 |
114
+ | `@jbpark/live-editor/utils/ast` | AST 조작 및 코드 생성 |
115
+ | `@jbpark/live-editor/utils/tailwind` | Tailwind 테마 헬퍼 |
116
+ | `@jbpark/live-editor/ui-kit` | 선택형 `@jbpark/ui-kit` 필드·모듈·팔레트 (`@jbpark/ui-kit` 설치 필요) |
117
+ | `@jbpark/live-editor/ui-kit/style.css` | ui-kit 진입점용 ui-kit 스타일시트 |
118
+ | `@jbpark/live-editor/style.css` | 컴파일된 스타일시트 (필수) |
119
+
120
+ ## 보안 참고사항
121
+
122
+ - 프리뷰 `<iframe>`(`src/components/frame/iframe/iframe.tsx`)의 `sandbox` prop은 **실제 보안 경계가 아닙니다**. 컴파일된 프리뷰 코드는 host 페이지 자신의 JS realm에서 `new Function(...)`으로 실행되며(`src/utils/compile.ts`의 `compileModule`), iframe에는 그 결과로 생성된 React 엘리먼트만 `contentDocument`에 포탈되어 DOM/CSS 렌더링 용도로만 쓰입니다. iframe 자체는 사용자 코드를 실행하지 않습니다.
121
123
  - 실질적 의미: 미리보기되는 코드는 호스트 애플리케이션과 동일한 수준의 JS 접근 권한(쿠키, DOM, 메모리상의 상태 등)을 가지며, iframe 경계가 이를 막아주지 않습니다.
122
124
  - 신뢰할 수 있는 프로젝트만 열어서 편집하세요. 직접 별도의 격리 조치(예: iframe 자체 `contentWindow` realm에서 컴파일을 실행하고 결과를 `postMessage`로 주고받는 방식)를 추가하지 않은 채로, 신뢰할 수 없는 제3자 프로젝트 파일을 미리보기하는 용도로 사용하지 마세요 — 그런 격리는 현재 구현되어 있지 않습니다.
123
125
 
124
- ## 🧰 기술 스택
126
+ ## 기술 스택
125
127
 
126
128
  - **Core**: React 19, TypeScript 6, Vite 8
127
129
  - **DnD**: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/modifiers`
@@ -129,16 +131,16 @@ export default function Example() {
129
131
  - **UI/스타일**: `@jbpark/ui-kit`, `lucide-react`, Tailwind CSS 4
130
132
  - **변환**: Babel(standalone) 기반 브라우저 내 변환
131
133
 
132
- ## ⚙️ 요구 사항
134
+ ## 요구 사항
133
135
 
134
136
  - Peer dependencies: `react >=19`, `react-dom >=19`
135
- - 패키지 메타데이터는 Node.js >=20을 선언합니다. 저장소 개발·빌드와 CI는 `.nvmrc`에 고정된 버전(현재 24.18.0)을 사용합니다.
137
+ - 패키지 메타데이터는 Babel 8이 지원하는 범위인 Node.js `^22.18.0 || >=24.11.0`을 선언합니다. CI는 Node용 진입점(`utils`, `utils/ast`, `utils/tailwind`, `provider`, `error`)이 Node 22.18에서 로드되는지 확인합니다. 저장소 개발·빌드와 나머지 CI는 `.nvmrc`에 고정된 버전(현재 24.18.0)을 사용합니다.
136
138
  - **pnpm**: 10.x 이상 ([Corepack](https://nodejs.org/api/corepack.html)으로 관리)
137
139
 
138
- ## 🚀 개발 (이 저장소)
140
+ ## 개발 (이 저장소)
139
141
 
140
142
  > Live Editor 자체를 개발할 때 참고하세요. 패키지를 앱에서 _사용_ 하려면
141
- > 위의 [설치](#-설치)를 보세요. 코드 구조는
143
+ > 위의 [설치](#설치)를 보세요. 코드 구조는
142
144
  > [ARCHITECTURE.ko.md](./ARCHITECTURE.ko.md)부터 읽기를 권합니다.
143
145
 
144
146
  ### pnpm 설정 (권장)
@@ -191,7 +193,7 @@ pnpm run build:demo
191
193
  pnpm exec vite preview --outDir dist-demo
192
194
  ```
193
195
 
194
- ## 📦 버전 관리 & 배포
196
+ ## 버전 관리 & 배포
195
197
 
196
198
  배포는 [changesets](https://github.com/changesets/changesets)로 완전히 자동화되어 있습니다:
197
199
 
@@ -207,6 +209,6 @@ CI 워크플로우:
207
209
  - `release.yml`: 기존 태그에 대한 GitHub Release를 수동(`workflow_dispatch`)으로 재생성하는 백업 유틸리티
208
210
  - Docusaurus 문서 사이트는 `website/`에서 빌드되며 Vercel로 배포됩니다(`vercel.json` 참고).
209
211
 
210
- ## 📄 라이선스
212
+ ## 라이선스
211
213
 
212
214
  [MIT License](./LICENSE) — Copyright (c) 2026 jbpark
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [English](./README.md) | [한국어](./README.ko.md)
4
4
 
5
- An interactive editor for building UIs with real-time preview and drag‑and‑drop. Canvas edits are synced back to source code via AST transforms, and the result renders inside an iframe for DOM/CSS isolation. Built with React 19 and TypeScript. See [Security Notes](#-security-notes) — the iframe is not a security sandbox.
5
+ An interactive editor for building UIs with real-time preview and drag‑and‑drop. Canvas edits are synced back to source code via AST transforms, and the result renders inside an iframe for DOM/CSS isolation. Built with React 19 and TypeScript. See [Security Notes](#security-notes) — the iframe is not a security sandbox.
6
6
 
7
- 📖 **Documentation & live demos:** https://live-editor-lab.vercel.app
7
+ **Documentation & live demos:** https://live-editor-lab.vercel.app
8
8
 
9
- ## 📁 Project Structure
9
+ ## Project Structure
10
10
 
11
11
  ```text
12
12
  live-editor/
@@ -37,16 +37,16 @@ live-editor/
37
37
  How the pieces fit together — the read and write paths, the commit levels and
38
38
  a glossary — is in [ARCHITECTURE.md](./ARCHITECTURE.md).
39
39
 
40
- ## 🎯 Highlights
40
+ ## Highlights
41
41
 
42
42
  - **Real-time code updates**: Canvas interactions (add/move/remove, property edits) are propagated back to source code safely.
43
43
  - **Interactive property panel**: Edit numbers, strings, booleans, arrays, and objects from the side panel.
44
44
  - **Advanced JSX binding system**: Automatically detects and enables editing for all JSX element properties (children, label, icon, etc.) through type-based detection.
45
45
  - **Smart Items editor**: Manage array items with add/move/delete operations, edit properties and nested JSX components with stable component identity across reorders.
46
- - **Preview runtime**: Renders compiled output inside an iframe for DOM/CSS isolation (not a security sandbox — see [Security Notes](#-security-notes)).
46
+ - **Preview runtime**: Renders compiled output inside an iframe for DOM/CSS isolation (not a security sandbox — see [Security Notes](#security-notes)).
47
47
  - **Robust drag-and-drop**: Powered by `@dnd-kit` for smooth sorting and positioning.
48
48
 
49
- ## 📦 Installation
49
+ ## Installation
50
50
 
51
51
  ```bash
52
52
  pnpm add @jbpark/live-editor react react-dom
@@ -56,7 +56,7 @@ pnpm add @jbpark/live-editor react react-dom
56
56
  optional peer — install it too if you want the editor's format-on-save; without
57
57
  it the editor still works and simply skips formatting.
58
58
 
59
- ## 🧑‍💻 Usage
59
+ ## Usage
60
60
 
61
61
  Import the stylesheet once near your app root — it is **required** and is not
62
62
  imported by the JS entry:
@@ -70,12 +70,12 @@ import '@jbpark/live-editor/style.css';
70
70
  // required — not imported by the JS entry
71
71
 
72
72
  const SAMPLE = `
73
- import * as ui from 'ui-kit';
74
-
75
73
  const App = () => (
76
74
  <div className="flex flex-col items-start gap-2 p-6">
77
- <ui.Typography.Title level={3}>Hello</ui.Typography.Title>
78
- <ui.Button type="primary">Edit me</ui.Button>
75
+ <h3 className="text-xl font-semibold">Hello</h3>
76
+ <button type="button" className="rounded bg-indigo-500 px-3 py-1 text-white">
77
+ Edit me
78
+ </button>
79
79
  </div>
80
80
  );
81
81
 
@@ -97,32 +97,34 @@ export default function Example() {
97
97
  See the [documentation site](https://live-editor-lab.vercel.app) for the full
98
98
  guide (how Tailwind reaches the preview, custom panels, and more).
99
99
 
100
- ## 🧩 Entry points
100
+ ## Entry points
101
101
 
102
102
  The package ships subpath exports so a consumer who needs only one feature area
103
103
  can avoid pulling in the rest (e.g. no CodeMirror in a preview-only build) — see
104
104
  [#194](https://github.com/pjb0811/live-editor/issues/194):
105
105
 
106
- | Import | Contains |
107
- | ------------------------------------ | ------------------------------------------- |
108
- | `@jbpark/live-editor` | Everything (`Live` provider + all surfaces) |
109
- | `@jbpark/live-editor/provider` | The shared editing context provider only |
110
- | `@jbpark/live-editor/dnd` | Drag-and-drop canvas + property panel |
111
- | `@jbpark/live-editor/editor` | CodeMirror code editor |
112
- | `@jbpark/live-editor/preview` | Isolated preview runtime |
113
- | `@jbpark/live-editor/error` | Error boundary |
114
- | `@jbpark/live-editor/utils` | Compile/section helpers |
115
- | `@jbpark/live-editor/utils/ast` | AST manipulation & code generation |
116
- | `@jbpark/live-editor/utils/tailwind` | Tailwind theme helpers |
117
- | `@jbpark/live-editor/style.css` | Compiled stylesheet (required) |
118
-
119
- ## 🔒 Security Notes
120
-
121
- - The `sandbox` prop on the preview `<iframe>` (`src/components/frame/iframe.tsx`) is **not a security boundary**. Compiled preview code is executed via `new Function(...)` in the host page's own JS realm (`compileModule` in `src/utils/index.ts`); only the resulting React elements are portaled into the iframe's `contentDocument` for DOM/CSS rendering. The iframe itself never evaluates user code.
106
+ | Import | Contains |
107
+ | -------------------------------------- | ------------------------------------------------------------------------------ |
108
+ | `@jbpark/live-editor` | Everything (`Live` provider + all surfaces) |
109
+ | `@jbpark/live-editor/provider` | The shared editing context provider only |
110
+ | `@jbpark/live-editor/dnd` | Drag-and-drop canvas + property panel |
111
+ | `@jbpark/live-editor/editor` | CodeMirror code editor |
112
+ | `@jbpark/live-editor/preview` | Isolated preview runtime |
113
+ | `@jbpark/live-editor/error` | Error boundary |
114
+ | `@jbpark/live-editor/utils` | Compile/section helpers |
115
+ | `@jbpark/live-editor/utils/ast` | AST manipulation & code generation |
116
+ | `@jbpark/live-editor/utils/tailwind` | Tailwind theme helpers |
117
+ | `@jbpark/live-editor/ui-kit` | Opt-in `@jbpark/ui-kit` fields, modules and palette (install `@jbpark/ui-kit`) |
118
+ | `@jbpark/live-editor/ui-kit/style.css` | ui-kit's stylesheet, for the ui-kit entry |
119
+ | `@jbpark/live-editor/style.css` | Compiled stylesheet (required) |
120
+
121
+ ## Security Notes
122
+
123
+ - The `sandbox` prop on the preview `<iframe>` (`src/components/frame/iframe/iframe.tsx`) is **not a security boundary**. Compiled preview code is executed via `new Function(...)` in the host page's own JS realm (`compileModule` in `src/utils/compile.ts`); only the resulting React elements are portaled into the iframe's `contentDocument` for DOM/CSS rendering. The iframe itself never evaluates user code.
122
124
  - Practical implication: previewed code runs with the same JS-level access as the host application (cookies, DOM, in-memory state) — the iframe boundary does not contain it.
123
125
  - Only open/edit projects you trust. Don't use this editor to preview arbitrary third-party project files without adding real isolation yourself (e.g. running compilation inside the iframe's own `contentWindow` realm and communicating results back via `postMessage`) — that isolation is not implemented here today.
124
126
 
125
- ## 🧰 Tech Stack
127
+ ## Tech Stack
126
128
 
127
129
  - **Core**: React 19, TypeScript 6, Vite 8
128
130
  - **DnD**: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/modifiers`
@@ -130,16 +132,16 @@ can avoid pulling in the rest (e.g. no CodeMirror in a preview-only build) — s
130
132
  - **UI/Styling**: `@jbpark/ui-kit`, `lucide-react`, Tailwind CSS 4
131
133
  - **Transform**: Babel (standalone) for in-browser transforms
132
134
 
133
- ## ⚙️ Requirements
135
+ ## Requirements
134
136
 
135
137
  - Peer deps: `react >=19`, `react-dom >=19`
136
- - Package metadata declares Node.js >=20. CI checks that the entries meant for Node (`utils`, `utils/ast`, `utils/tailwind`, `provider`, `error`) load on Node 20. Repository development, builds and the rest of CI use the version pinned in `.nvmrc` (currently 24.18.0).
138
+ - Package metadata declares Node.js `^22.18.0 || >=24.11.0`, the range Babel 8 supports. CI checks that the entries meant for Node (`utils`, `utils/ast`, `utils/tailwind`, `provider`, `error`) load on Node 22.18. Repository development, builds and the rest of CI use the version pinned in `.nvmrc` (currently 24.18.0).
137
139
  - **pnpm** (developing this repo only): the version in `packageManager` (currently 12.8.1), managed via [Corepack](https://nodejs.org/api/corepack.html). Installing the package works with any package manager.
138
140
 
139
- ## 🚀 Development (this repo)
141
+ ## Development (this repo)
140
142
 
141
143
  > Working on Live Editor itself. To _use_ the package in your app, see
142
- > [Installation](#-installation) above. Start with
144
+ > [Installation](#installation) above. Start with
143
145
  > [ARCHITECTURE.md](./ARCHITECTURE.md) for how the code is organized.
144
146
 
145
147
  ### pnpm Setup (Recommended)
@@ -193,7 +195,7 @@ pnpm run build:demo
193
195
  pnpm exec vite preview --outDir dist-demo
194
196
  ```
195
197
 
196
- ## 📦 Versioning & Release
198
+ ## Versioning & Release
197
199
 
198
200
  Releases are fully automated via [changesets](https://github.com/changesets/changesets):
199
201
 
@@ -210,6 +212,6 @@ CI workflows:
210
212
  - The Docusaurus documentation site is built from `website/` and deployed via
211
213
  Vercel (see `vercel.json`).
212
214
 
213
- ## 📄 License
215
+ ## License
214
216
 
215
217
  [MIT License](./LICENSE) — Copyright (c) 2026 jbpark
@@ -1,9 +1,9 @@
1
- import { i as useLiveMessages, t as MessagesContext } from "./messages-CSrMI4Rm.js";
1
+ import { i as useLiveMessages, t as MessagesContext } from "./messages-DrINcRdG.js";
2
2
  import { t as cn } from "./cn-CAnad2vL.js";
3
3
  import React from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/error/error.tsx
6
- const Error = ({ message, title, className, onReset }) => {
6
+ const Error = ({ message, title, className, style, onReset }) => {
7
7
  const messages = useLiveMessages();
8
8
  if (!message) return null;
9
9
  return /* @__PURE__ */ jsxs("div", {
@@ -13,7 +13,8 @@ const Error = ({ message, title, className, onReset }) => {
13
13
  backgroundColor: "#fee2e2",
14
14
  border: "1px solid #fca5a5",
15
15
  borderRadius: "6px",
16
- color: "#7f1d1d"
16
+ color: "#7f1d1d",
17
+ ...style
17
18
  },
18
19
  children: [
19
20
  /* @__PURE__ */ jsx("h3", {
@@ -96,4 +97,4 @@ var ErrorBoundary = class extends React.Component {
96
97
  //#endregion
97
98
  export { Error as n, ErrorBoundary as t };
98
99
 
99
- //# sourceMappingURL=boundary-BLB80tyw.js.map
100
+ //# sourceMappingURL=boundary-CySnDUs2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundary-CySnDUs2.js","names":[],"sources":["../src/components/error/error.tsx","../src/components/error/boundary.tsx"],"sourcesContent":["import { useLiveMessages } from '~/components/context/messages';\nimport { cn } from '~/utils/cn';\n\nexport interface Props extends React.ComponentPropsWithRef<'div'> {\n message?: string | null;\n title?: string;\n onReset?: () => void;\n}\n\nconst Error = ({ message, title, className, style, onReset }: Props) => {\n const messages = useLiveMessages();\n\n if (!message) {\n return null;\n }\n\n return (\n <div\n className={cn(className)}\n style={{\n padding: '16px',\n backgroundColor: '#fee2e2',\n border: '1px solid #fca5a5',\n borderRadius: '6px',\n color: '#7f1d1d',\n ...style,\n }}\n >\n <h3 style={{ fontSize: '14px', fontWeight: 'bold', margin: 0 }}>\n {title ?? messages.errors.default}\n </h3>\n <pre\n style={{\n marginTop: '8px',\n fontSize: '12px',\n whiteSpace: 'pre-wrap',\n margin: '8px 0 0 0',\n }}\n >\n {message}\n </pre>\n {onReset && (\n <button\n style={{\n marginTop: '8px',\n padding: '4px 12px',\n fontSize: '12px',\n color: 'white',\n backgroundColor: '#dc2626',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n }}\n onClick={onReset}\n >\n {messages.errors.tryAgain}\n </button>\n )}\n </div>\n );\n};\n\nexport default Error;\n","import React from 'react';\n\nimport {\n type LiveMessages,\n MessagesContext,\n} from '~/components/context/messages';\n\nimport Error from './error';\n\ninterface Props {\n children: React.ReactNode;\n // `reset` clears the caught error and renders `children` again, the same\n // thing the built-in error's reset button does.\n fallback?: (message?: string, reset?: () => void) => React.ReactNode;\n onError?: (e: Error, info: React.ErrorInfo) => void;\n // When any of these values changes (compared one by one, by identity), a\n // caught error resets and `children` render again, such as when the code\n // that failed is edited.\n resetKeys?: readonly unknown[];\n}\n\ninterface State {\n hasError: boolean;\n error?: Error;\n // The resetKeys the last render saw, so getDerivedStateFromProps can tell\n // when they change.\n resetKeys?: readonly unknown[];\n}\n\nconst keysChanged = (\n prev: readonly unknown[] = [],\n next: readonly unknown[] = [],\n) => prev.length !== next.length || next.some((key, i) => key !== prev[i]);\n\nclass ErrorBoundary extends React.Component<Props, State> {\n static contextType = MessagesContext;\n declare context: LiveMessages;\n\n constructor(props: Props) {\n super(props);\n this.state = { hasError: false, resetKeys: props.resetKeys };\n }\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n console.error('🚨 [Boundary] Rendering error:', error, errorInfo);\n this.props.onError?.(error, errorInfo);\n }\n\n // Clears a caught error in the same render that sees new resetKeys, so\n // `children` get their next chance right away and the fallback doesn't\n // render (and run its effects) once more first (#442). If `children` throw\n // again with the new keys, getDerivedStateFromError catches that in this\n // same render pass.\n static getDerivedStateFromProps(\n props: Props,\n state: State,\n ): Partial<State> | null {\n if (!keysChanged(state.resetKeys, props.resetKeys)) {\n return null;\n }\n\n return state.hasError\n ? { hasError: false, error: undefined, resetKeys: props.resetKeys }\n : { resetKeys: props.resetKeys };\n }\n\n reset = () => this.setState({ hasError: false, error: undefined });\n\n render() {\n if (this.state.hasError) {\n return this.props.fallback ? (\n <>{this.props.fallback(this.state.error?.message, this.reset)}</>\n ) : (\n <Error\n message={this.state.error?.message}\n onReset={this.reset}\n title={this.context.errors.rendering}\n />\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default ErrorBoundary;\n"],"mappings":";;;;;AASA,MAAM,SAAS,EAAE,SAAS,OAAO,WAAW,OAAO,cAAqB;CACtE,MAAM,WAAW,gBAAgB;CAEjC,IAAI,CAAC,SACH,OAAO;CAGT,OACE,qBAAC,OAAD;EACE,WAAW,GAAG,SAAS;EACvB,OAAO;GACL,SAAS;GACT,iBAAiB;GACjB,QAAQ;GACR,cAAc;GACd,OAAO;GACP,GAAG;EACL;EATF,UAAA;GAWE,oBAAC,MAAD;IAAI,OAAO;KAAE,UAAU;KAAQ,YAAY;KAAQ,QAAQ;IAAE;IAC1D,UAAA,SAAS,SAAS,OAAO;GACxB,CAAA;GACJ,oBAAC,OAAD;IACE,OAAO;KACL,WAAW;KACX,UAAU;KACV,YAAY;KACZ,QAAQ;IACV;IAEC,UAAA;GACE,CAAA;GACJ,WACC,oBAAC,UAAD;IACE,OAAO;KACL,WAAW;KACX,SAAS;KACT,UAAU;KACV,OAAO;KACP,iBAAiB;KACjB,QAAQ;KACR,cAAc;KACd,QAAQ;IACV;IACA,SAAS;IAER,UAAA,SAAS,OAAO;GACX,CAAA;EAEP;;AAET;;;AC/BA,MAAM,eACJ,OAA2B,CAAC,GAC5B,OAA2B,CAAC,MACzB,KAAK,WAAW,KAAK,UAAU,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,EAAE;AAEzE,IAAM,gBAAN,cAA4B,MAAM,UAAwB;CACxD,OAAO,cAAc;CAGrB,YAAY,OAAc;EACxB,MAAM,KAAK;EACX,KAAK,QAAQ;GAAE,UAAU;GAAO,WAAW,MAAM;EAAU;CAC7D;CAEA,OAAO,yBAAyB,OAA8B;EAC5D,OAAO;GAAE,UAAU;GAAM;EAAM;CACjC;CAEA,kBAAkB,OAAc,WAA4B;EAC1D,QAAQ,MAAM,kCAAkC,OAAO,SAAS;EAChE,KAAK,MAAM,UAAU,OAAO,SAAS;CACvC;CAOA,OAAO,yBACL,OACA,OACuB;EACvB,IAAI,CAAC,YAAY,MAAM,WAAW,MAAM,SAAS,GAC/C,OAAO;EAGT,OAAO,MAAM,WACT;GAAE,UAAU;GAAO,OAAO,KAAA;GAAW,WAAW,MAAM;EAAU,IAChE,EAAE,WAAW,MAAM,UAAU;CACnC;CAEA,cAAc,KAAK,SAAS;EAAE,UAAU;EAAO,OAAO,KAAA;CAAU,CAAC;CAEjE,SAAS;EACP,IAAI,KAAK,MAAM,UACb,OAAO,KAAK,MAAM,WAChB,oBAAA,UAAA,EAAA,UAAG,KAAK,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK,KAAK,EAAI,CAAA,IAEhE,oBAAC,OAAD;GACE,SAAS,KAAK,MAAM,OAAO;GAC3B,SAAS,KAAK;GACd,OAAO,KAAK,QAAQ,OAAO;EAC5B,CAAA;EAIL,OAAO,KAAK,MAAM;CACpB;AACF"}
@@ -1,7 +1,7 @@
1
- import { n as CONFIG } from "./constants-DU17Tdwo.js";
1
+ import { n as CONFIG } from "./constants-st9gnJ5g.js";
2
2
  import { n as registerEditorCache } from "./editor-caches-ClMvdcQF.js";
3
3
  import { t as createBoundedCache } from "./cache-B0-XcIxJ.js";
4
- import { t as detectTypeScript } from "./detect-typescript-Dr_oLniM.js";
4
+ import { t as detectTypeScript } from "./detect-typescript-CMF0FVKD.js";
5
5
  import React from "react";
6
6
  import * as Babel from "@babel/standalone";
7
7
  //#region src/utils/scripts.ts
@@ -84,6 +84,7 @@ const compile = (code, modules) => {
84
84
  const clearCompilationCache = () => {
85
85
  compilationCache.clear();
86
86
  };
87
+ const REACT_PRESET = ["react", { runtime: "classic" }];
87
88
  const transformCode = (code, isTypeScript = false) => {
88
89
  try {
89
90
  return Babel.transform(code, {
@@ -91,11 +92,11 @@ const transformCode = (code, isTypeScript = false) => {
91
92
  presets: isTypeScript ? [
92
93
  "typescript",
93
94
  "env",
94
- "react"
95
- ] : ["env", "react"],
95
+ REACT_PRESET
96
+ ] : ["env", REACT_PRESET],
96
97
  sourceType: "module",
97
98
  plugins: [Babel.availablePlugins["transform-modules-commonjs"]]
98
- }).code || "";
99
+ })?.code || "";
99
100
  } catch (e) {
100
101
  console.error("❌ Babel transformation error:", e);
101
102
  return code;
@@ -130,6 +131,6 @@ const compileModule = (code, modules) => {
130
131
  };
131
132
  registerEditorCache(clearCompilationCache);
132
133
  //#endregion
133
- export { getCachedScriptBlob as a, clearScriptCache as i, compile as n, preloadScripts as o, transformCode as r, withScriptBlobs as s, clearCompilationCache as t };
134
+ export { withScriptBlobs as i, compile as n, preloadScripts as r, clearCompilationCache as t };
134
135
 
135
- //# sourceMappingURL=compile-Dxas-D05.js.map
136
+ //# sourceMappingURL=compile-my0AjsCw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-my0AjsCw.js","names":[],"sources":["../src/utils/scripts.ts","../src/utils/compile.ts"],"sourcesContent":["import { CONFIG } from '../constants';\nimport { createBoundedCache } from './cache';\nimport { registerEditorCache } from './editor-caches';\n\n// How many callers have requested each script and not yet injected it. A\n// blob URL evicted in that time isn't revoked until the last of them is done\n// (`deferredRevocations`), or its `<script>` would fail to load without an\n// error (#443).\nconst pins = new Map<string, number>();\nconst deferredRevocations = new Map<string, string[]>();\n\nconst revoke = (src: string, blobUrl: string) => {\n if (!pins.has(src)) {\n URL.revokeObjectURL(blobUrl);\n return;\n }\n\n deferredRevocations.set(src, [\n ...(deferredRevocations.get(src) ?? []),\n blobUrl,\n ]);\n};\n\nconst pin = (src: string) => {\n pins.set(src, (pins.get(src) ?? 0) + 1);\n};\n\nconst unpin = (src: string) => {\n const count = (pins.get(src) ?? 0) - 1;\n\n if (count > 0) {\n pins.set(src, count);\n return;\n }\n\n pins.delete(src);\n deferredRevocations.get(src)?.forEach(blobUrl => {\n URL.revokeObjectURL(blobUrl);\n });\n deferredRevocations.delete(src);\n};\n\nconst scriptCache = createBoundedCache<string, string>(\n CONFIG.SCRIPT_CACHE_LIMIT,\n revoke,\n);\nconst loadingScriptCache = new Map<string, Promise<string>>();\n\nexport const getCachedScriptBlob = async (src: string): Promise<string> => {\n const cached = scriptCache.get(src);\n\n if (cached) {\n return cached;\n }\n\n const existing = loadingScriptCache.get(src);\n\n if (existing) {\n return existing;\n }\n\n const promise = fetch(src)\n .then(res => res.text())\n .then(text => {\n const blob = new Blob([text], { type: 'application/javascript' });\n const blobUrl = URL.createObjectURL(blob);\n\n scriptCache.set(src, blobUrl);\n\n return blobUrl;\n })\n // Clean up after a failure too: a rejected promise left here would be\n // reused by every later caller, so the script could never load again.\n .finally(() => {\n loadingScriptCache.delete(src);\n });\n\n loadingScriptCache.set(src, promise);\n\n return promise;\n};\n\n// Resolves every script to a blob URL and hands them to `inject` while none\n// of them can be revoked. `inject` must use them at once, as appending\n// `<script>` elements does: the browser resolves a blob URL on insertion.\n// Several scripts, or several frames, can push more scripts through the cache\n// than it holds before the last one arrives (#443).\nexport const withScriptBlobs = async (\n scripts: string[],\n inject: (blobUrls: string[]) => void,\n): Promise<void> => {\n scripts.forEach(pin);\n\n try {\n inject(await Promise.all(scripts.map(getCachedScriptBlob)));\n } finally {\n scripts.forEach(unpin);\n }\n};\n\nexport const preloadScripts = (scripts: string[]): void => {\n scripts.forEach(src => {\n // Not awaited, so the rejection is handled here. The real load reports a\n // failure to whoever awaits it.\n getCachedScriptBlob(src).catch(() => {});\n });\n};\n\n// Revokes every blob URL, through the same `onEvict` the LRU uses, and drops\n// the pending loads, so the next session doesn't get a URL created before\n// the revocation.\nexport const clearScriptCache = () => {\n scriptCache.clear();\n loadingScriptCache.clear();\n};\n\nregisterEditorCache(clearScriptCache);\n","import React from 'react';\n\nimport * as Babel from '@babel/standalone';\n\nimport type { Module } from '~/types';\n\nimport { CONFIG } from '../constants';\nimport { createBoundedCache } from './cache';\nimport { detectTypeScript } from './detect-typescript';\nimport { registerEditorCache } from './editor-caches';\n\ninterface CompilationKey {\n code: string;\n modules: [string, unknown][];\n}\n\n// The keys still in the LRU. Comparing at most `CACHE_LIMIT` of them avoids\n// keeping every module object ever seen, or serializing modules, which can be\n// cyclic or closures.\nconst compilationKeys = new Set<CompilationKey>();\nconst compilationCache = createBoundedCache<CompilationKey, Module>(\n CONFIG.CACHE_LIMIT,\n key => compilationKeys.delete(key),\n);\n\nconst createCacheKey = (\n code: string,\n modules: Record<string, unknown>,\n): CompilationKey => ({\n code,\n modules: Object.keys(modules)\n .sort()\n .map(name => [name, modules[name]]),\n});\n\n// Objects/functions compare by reference; primitives compare by value.\n// Replace a module object when its implementation changes. In-place edits\n// inside an existing module are intentionally not observed by this cache.\nexport const compile = (\n code: string,\n modules: Record<string, unknown>,\n): Module => {\n const candidate = createCacheKey(code, modules);\n\n for (const key of compilationKeys) {\n if (\n key.code === candidate.code &&\n key.modules.length === candidate.modules.length &&\n key.modules.every(\n ([name, value], index) =>\n name === candidate.modules[index]![0] &&\n Object.is(value, candidate.modules[index]![1]),\n )\n ) {\n return compilationCache.get(key)!;\n }\n }\n\n const result = compileModule(code, Object.fromEntries(candidate.modules));\n\n compilationCache.set(candidate, result);\n compilationKeys.add(candidate);\n\n return result;\n};\n\n// `compilationKeys` needs no separate pass: it is drained by the `onEvict`\n// the cache was built with, which `clear()` runs for every entry.\nexport const clearCompilationCache = () => {\n compilationCache.clear();\n};\n\n// JSX compiles to `React.createElement` (the classic runtime). The module runs\n// with `React` in scope and has no `react/jsx-runtime` to import.\nconst REACT_PRESET: [string, object] = ['react', { runtime: 'classic' }];\n\n// Transforms JSX and TypeScript with `@babel/standalone`'s presets, without\n// the `typescript` package (#192). Babel works per file without types, so a\n// `const enum` becomes a real enum and legacy decorators aren't supported.\n// On an error, the input comes back unchanged.\nconst transformCode = (code: string, isTypeScript = false): string => {\n try {\n const result = Babel.transform(code, {\n filename: isTypeScript ? 'preview.tsx' : 'preview.jsx',\n presets: isTypeScript\n ? ['typescript', 'env', REACT_PRESET]\n : ['env', REACT_PRESET],\n sourceType: 'module',\n plugins: [Babel.availablePlugins['transform-modules-commonjs']],\n })?.code;\n\n return result || '';\n } catch (e) {\n console.error('❌ Babel transformation error:', e);\n return code;\n }\n};\n\nconst compileModule = (\n code: string,\n modules: Record<string, unknown>,\n): Module => {\n const isTypeScript = detectTypeScript(code);\n\n type RenderFunction = (\n exports: Module['exports'],\n require: (name: string) => unknown,\n module: Module,\n React: typeof import('react'),\n ) => void;\n\n let render: RenderFunction;\n\n // Runs in this window's own JS realm, not inside the preview iframe — the iframe only\n // renders the resulting React elements via portal. See README \"Security Notes\".\n try {\n render = new Function(\n 'exports',\n 'require',\n 'module',\n 'React',\n transformCode(code, isTypeScript),\n ) as RenderFunction;\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Syntax error';\n return { exports: {}, error: errorMessage };\n }\n\n const module: Module = { exports: {} };\n\n const customRequire = (name: string) => {\n if (name === 'react') {\n return React;\n }\n\n // Check presence, not truthiness: a module can be a falsy value (`0`, `''`,\n // `false`, `null`).\n if (name in modules) {\n return modules[name];\n }\n\n throw new Error(`Module not found: ${name}`);\n };\n\n try {\n render(module.exports, customRequire, module, React);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Runtime error';\n return { exports: {}, error: errorMessage };\n }\n\n return module;\n};\n\nregisterEditorCache(clearCompilationCache);\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ //#region src/constants/index.ts
2
+ const CONFIG = {
3
+ CACHE_LIMIT: 50,
4
+ DOCUMENT_CACHE_LIMIT: 4,
5
+ SCRIPT_CACHE_LIMIT: 10
6
+ };
7
+ const DATA_ATTR = {
8
+ ID: "data-id",
9
+ BINDING: "data-binding",
10
+ BINDING_KEY: "data-binding-key",
11
+ ITEM: "data-item"
12
+ };
13
+ const RESERVED_BINDING_PROPERTIES = [
14
+ DATA_ATTR.ID,
15
+ "data-name",
16
+ DATA_ATTR.BINDING,
17
+ DATA_ATTR.BINDING_KEY
18
+ ];
19
+ const REGEX = {
20
+ NUMBER: /^\d+(\.\d+)?$/,
21
+ BOOLEAN_OR_NULL: /^(true|false|null|undefined)$/,
22
+ MODULE_IMPORT_EXPORT_LINE: /^\s*(?:import|export)\b[^\n]*\bfrom\s+['"][^'"]*['"];?\s*$/gm
23
+ };
24
+ const TS_PATTERNS = [
25
+ /interface\s+\w+/,
26
+ /type\s+\w+\s*=/,
27
+ /:\s*\w+(\[\])?(\s*\||\s*&|\s*=|\s*;|\s*,|\s*\))/,
28
+ /as\s+\w+/,
29
+ /<[A-Z]\w*>/,
30
+ /enum\s+\w+/,
31
+ /public\s+|private\s+|protected\s+/,
32
+ /readonly\s+/,
33
+ /\?\s*:/
34
+ ];
35
+ const BINDING_PROP = {
36
+ CHILDREN: "children",
37
+ INNER_TEXT: "innerText",
38
+ INNER_HTML: "innerHTML",
39
+ ITEMS: "items"
40
+ };
41
+ const DEFAULT_CONTAINER_ID = "app-container";
42
+ const documentTemplate = (containerId) => `
43
+ const App = () => {
44
+ return (
45
+ <main id="${containerId}"></main>
46
+ )
47
+ }
48
+
49
+ export default App;
50
+ `;
51
+ const DEFAULT_TEMPLATE = documentTemplate(DEFAULT_CONTAINER_ID);
52
+ //#endregion
53
+ export { DEFAULT_TEMPLATE as a, TS_PATTERNS as c, DEFAULT_CONTAINER_ID as i, documentTemplate as l, CONFIG as n, REGEX as o, DATA_ATTR as r, RESERVED_BINDING_PROPERTIES as s, BINDING_PROP as t };
54
+
55
+ //# sourceMappingURL=constants-st9gnJ5g.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants-st9gnJ5g.js","names":[],"sources":["../src/constants/index.ts"],"sourcesContent":["export const STORAGE_KEY = 'live-editor-code';\n\nexport const CONFIG = {\n CACHE_LIMIT: 50,\n // The document parse cache. Its entries are whole Babel ASTs (about\n // 0.66 MB per document), and only the current and previous version get hits,\n // so it keeps few (#106).\n DOCUMENT_CACHE_LIMIT: 4,\n // The script cache (`utils/scripts.ts`). Its entries are live blob URLs,\n // revoked on eviction, and a preview loads only a few scripts (#104).\n SCRIPT_CACHE_LIMIT: 10,\n} as const;\n\nexport const DATA_ATTR = {\n ID: 'data-id',\n BINDING: 'data-binding',\n // Names an entry of `Live.Dnd`'s `bindingKeys` instead of declaring the\n // bindings inline (#513).\n BINDING_KEY: 'data-binding-key',\n ITEM: 'data-item',\n} as const;\n\n// Attributes the editor owns. `data-id` links the canvas to the source,\n// `data-name` is a section's name, and `data-binding` and `data-binding-key`\n// declare what each edit is checked against. A binding can't target them,\n// since rewriting one would break that link (#429).\nexport const RESERVED_BINDING_PROPERTIES: readonly string[] = [\n DATA_ATTR.ID,\n 'data-name',\n DATA_ATTR.BINDING,\n DATA_ATTR.BINDING_KEY,\n];\n\nexport const REGEX = {\n NUMBER: /^\\d+(\\.\\d+)?$/,\n BOOLEAN_OR_NULL: /^(true|false|null|undefined)$/,\n MODULE_IMPORT_EXPORT_LINE:\n /^\\s*(?:import|export)\\b[^\\n]*\\bfrom\\s+['\"][^'\"]*['\"];?\\s*$/gm,\n} as const;\n\nexport const TS_PATTERNS = [\n /interface\\s+\\w+/,\n /type\\s+\\w+\\s*=/,\n /:\\s*\\w+(\\[\\])?(\\s*\\||\\s*&|\\s*=|\\s*;|\\s*,|\\s*\\))/,\n /as\\s+\\w+/,\n /<[A-Z]\\w*>/,\n /enum\\s+\\w+/,\n /public\\s+|private\\s+|protected\\s+/,\n /readonly\\s+/,\n /\\?\\s*:/,\n] as const;\n\nexport const BINDING_PROP = {\n CHILDREN: 'children',\n INNER_TEXT: 'innerText',\n INNER_HTML: 'innerHTML',\n ITEMS: 'items',\n} as const;\n\n// The element whose `<section>` children are a document's sections, unless\n// `Live.Dnd`'s `containerId` says otherwise (#449).\nexport const DEFAULT_CONTAINER_ID = 'app-container';\n\n// A new, empty document built around `containerId`. `createDocument()` in\n// `@jbpark/live-editor/utils` is the public way in; this is the template\n// behind it and behind `DEFAULT_TEMPLATE`.\nexport const documentTemplate = (containerId: string) => `\nconst App = () => {\n return (\n <main id=\"${containerId}\"></main>\n )\n}\n\nexport default App;\n`;\n\nexport const DEFAULT_TEMPLATE = documentTemplate(DEFAULT_CONTAINER_ID);\n\nexport { PALETTE_SECTIONS } from './palette-sections';\n"],"mappings":";AAEA,MAAa,SAAS;CACpB,aAAa;CAIb,sBAAsB;CAGtB,oBAAoB;AACtB;AAEA,MAAa,YAAY;CACvB,IAAI;CACJ,SAAS;CAGT,aAAa;CACb,MAAM;AACR;AAMA,MAAa,8BAAiD;CAC5D,UAAU;CACV;CACA,UAAU;CACV,UAAU;AACZ;AAEA,MAAa,QAAQ;CACnB,QAAQ;CACR,iBAAiB;CACjB,2BACE;AACJ;AAEA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAa,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,YAAY;CACZ,OAAO;AACT;AAIA,MAAa,uBAAuB;AAKpC,MAAa,oBAAoB,gBAAwB;;;gBAGzC,YAAY;;;;;;AAO5B,MAAa,mBAAmB,iBAAiB,oBAAoB"}
@@ -1,6 +1,6 @@
1
- import { a as DEFAULT_TEMPLATE } from "./constants-DU17Tdwo.js";
1
+ import { a as DEFAULT_TEMPLATE } from "./constants-st9gnJ5g.js";
2
2
  import { r as registerEditorSession } from "./editor-caches-ClMvdcQF.js";
3
- import { r as mergeMessages, t as MessagesContext } from "./messages-CSrMI4Rm.js";
3
+ import { r as mergeMessages, t as MessagesContext } from "./messages-DrINcRdG.js";
4
4
  import { n as PreviewContext, t as ErrorContext } from "./states-Ci1AvoQ9.js";
5
5
  import { useCallback, useContext, useEffect, useMemo, useState } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
@@ -37,4 +37,4 @@ const ContextProvider = ({ children, messages }) => {
37
37
  //#endregion
38
38
  export { ContextProvider as t };
39
39
 
40
- //# sourceMappingURL=context-B8MzP7_p.js.map
40
+ //# sourceMappingURL=context-Bjq0CvBE.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-B8MzP7_p.js","names":[],"sources":["../src/components/context/context.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\n\nimport { DEFAULT_TEMPLATE } from '~/constants';\nimport { registerEditorSession } from '~/utils/editor-caches';\n\nimport {\n type LiveMessagesInput,\n MessagesContext,\n mergeMessages,\n} from './messages';\nimport type { ErrorContextType, PreviewContextType } from './states';\nimport { ErrorContext, PreviewContext } from './states';\n\nexport interface Props {\n children?: React.ReactNode;\n // Replaces any of the editor's text, group by group; the rest stays\n // English, or the enclosing `Live`'s. Define it outside render or memoize\n // it: a new object re-renders everything that shows text (#524).\n messages?: LiveMessagesInput;\n}\n\nconst ContextProvider = ({ children, messages }: Props) => {\n const [code, setCodeState] = useState(DEFAULT_TEMPLATE);\n const [error, setError] = useState<ErrorContextType['error']>(null);\n\n // Clears the last error in the same update that sets the code. An effect\n // would run after the new code's own error was reported, and wipe it.\n const setCode = useCallback<PreviewContextType['setCode']>(next => {\n setError(null);\n setCodeState(next);\n }, []);\n\n // Counts this editor, so the page-wide caches are released when the last\n // one unmounts (`utils/editor-caches.ts`).\n useEffect(() => registerEditorSession(), []);\n\n // Memoized, so a parent re-render doesn't re-render every `usePreview()`\n // and `useError()` consumer.\n const previewValue = useMemo<PreviewContextType>(\n () => ({ code, setCode }),\n [code, setCode],\n );\n\n const errorValue = useMemo<ErrorContextType>(\n () => ({ error, setError }),\n [error, setError],\n );\n\n // A provider inside another one starts from the outer one's messages, so\n // a page can set them once around several editors.\n const inherited = useContext(MessagesContext);\n const messagesValue = useMemo(\n () => mergeMessages(messages, inherited),\n [messages, inherited],\n );\n\n return (\n <PreviewContext.Provider value={previewValue}>\n <ErrorContext.Provider value={errorValue}>\n <MessagesContext.Provider value={messagesValue}>\n {children}\n </MessagesContext.Provider>\n </ErrorContext.Provider>\n </PreviewContext.Provider>\n );\n};\n\nexport default ContextProvider;\n"],"mappings":";;;;;;;AA6BA,MAAM,mBAAmB,EAAE,UAAU,eAAsB;CACzD,MAAM,CAAC,MAAM,gBAAgB,SAAS,gBAAgB;CACtD,MAAM,CAAC,OAAO,YAAY,SAAoC,IAAI;CAIlE,MAAM,UAAU,aAA2C,SAAQ;EACjE,SAAS,IAAI;EACb,aAAa,IAAI;CACnB,GAAG,CAAC,CAAC;CAIL,gBAAgB,sBAAsB,GAAG,CAAC,CAAC;CAI3C,MAAM,eAAe,eACZ;EAAE;EAAM;CAAQ,IACvB,CAAC,MAAM,OAAO,CAChB;CAEA,MAAM,aAAa,eACV;EAAE;EAAO;CAAS,IACzB,CAAC,OAAO,QAAQ,CAClB;CAIA,MAAM,YAAY,WAAW,eAAe;CAC5C,MAAM,gBAAgB,cACd,cAAc,UAAU,SAAS,GACvC,CAAC,UAAU,SAAS,CACtB;CAEA,OACE,oBAAC,eAAe,UAAhB;EAAyB,OAAO;EAC9B,UAAA,oBAAC,aAAa,UAAd;GAAuB,OAAO;GAC5B,UAAA,oBAAC,gBAAgB,UAAjB;IAA0B,OAAO;IAC9B;GACuB,CAAA;EACL,CAAA;CACA,CAAA;AAE7B"}
1
+ {"version":3,"file":"context-Bjq0CvBE.js","names":[],"sources":["../src/components/context/context.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\n\nimport { DEFAULT_TEMPLATE } from '~/constants';\nimport { registerEditorSession } from '~/utils/editor-caches';\n\nimport {\n type LiveMessagesInput,\n MessagesContext,\n mergeMessages,\n} from './messages';\nimport type { ErrorContextType, PreviewContextType } from './states';\nimport { ErrorContext, PreviewContext } from './states';\n\nexport interface Props {\n children?: React.ReactNode;\n // Replaces any of the editor's text, group by group; the rest stays\n // English, or the enclosing `Live`'s. Define it outside render or memoize\n // it: a new object re-renders everything that shows text (#524).\n messages?: LiveMessagesInput;\n}\n\nconst ContextProvider = ({ children, messages }: Props) => {\n const [code, setCodeState] = useState(DEFAULT_TEMPLATE);\n const [error, setError] = useState<ErrorContextType['error']>(null);\n\n // Clears the last error in the same update that sets the code. An effect\n // would run after the new code's own error was reported, and wipe it.\n const setCode = useCallback<PreviewContextType['setCode']>(next => {\n setError(null);\n setCodeState(next);\n }, []);\n\n // Counts this editor, so the page-wide caches are released when the last\n // one unmounts (`utils/editor-caches.ts`).\n useEffect(() => registerEditorSession(), []);\n\n // Memoized, so a parent re-render doesn't re-render every `usePreview()`\n // and `useError()` consumer.\n const previewValue = useMemo<PreviewContextType>(\n () => ({ code, setCode }),\n [code, setCode],\n );\n\n const errorValue = useMemo<ErrorContextType>(\n () => ({ error, setError }),\n [error, setError],\n );\n\n // A provider inside another one starts from the outer one's messages, so\n // a page can set them once around several editors.\n const inherited = useContext(MessagesContext);\n const messagesValue = useMemo(\n () => mergeMessages(messages, inherited),\n [messages, inherited],\n );\n\n return (\n <PreviewContext.Provider value={previewValue}>\n <ErrorContext.Provider value={errorValue}>\n <MessagesContext.Provider value={messagesValue}>\n {children}\n </MessagesContext.Provider>\n </ErrorContext.Provider>\n </PreviewContext.Provider>\n );\n};\n\nexport default ContextProvider;\n"],"mappings":";;;;;;;AA6BA,MAAM,mBAAmB,EAAE,UAAU,eAAsB;CACzD,MAAM,CAAC,MAAM,gBAAgB,SAAS,gBAAgB;CACtD,MAAM,CAAC,OAAO,YAAY,SAAoC,IAAI;CAIlE,MAAM,UAAU,aAA2C,SAAQ;EACjE,SAAS,IAAI;EACb,aAAa,IAAI;CACnB,GAAG,CAAC,CAAC;CAIL,gBAAgB,sBAAsB,GAAG,CAAC,CAAC;CAI3C,MAAM,eAAe,eACZ;EAAE;EAAM;CAAQ,IACvB,CAAC,MAAM,OAAO,CAChB;CAEA,MAAM,aAAa,eACV;EAAE;EAAO;CAAS,IACzB,CAAC,OAAO,QAAQ,CAClB;CAIA,MAAM,YAAY,WAAW,eAAe;CAC5C,MAAM,gBAAgB,cACd,cAAc,UAAU,SAAS,GACvC,CAAC,UAAU,SAAS,CACtB;CAEA,OACE,oBAAC,eAAe,UAAhB;EAAyB,OAAO;EAC9B,UAAA,oBAAC,aAAa,UAAd;GAAuB,OAAO;GAC5B,UAAA,oBAAC,gBAAgB,UAAjB;IAA0B,OAAO;IAC9B;GACuB,CAAA;EACL,CAAA;CACA,CAAA;AAE7B"}
@@ -1,10 +1,10 @@
1
1
  import { t as cn } from "./cn-CAnad2vL.js";
2
- import { t as detectTypeScript } from "./detect-typescript-Dr_oLniM.js";
3
- import { useCallback, useMemo } from "react";
2
+ import { t as detectTypeScript } from "./detect-typescript-CMF0FVKD.js";
3
+ import { useCallback, useMemo, useRef } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
- import CodeEditor from "@jbpark/ui-kit/CodeEditor";
5
+ import { javascript } from "@codemirror/lang-javascript";
6
6
  import { vscodeLight } from "@uiw/codemirror-theme-vscode";
7
- import "@uiw/react-codemirror";
7
+ import CodeMirror, { EditorView } from "@uiw/react-codemirror";
8
8
  //#region src/components/editor/use-format-code.ts
9
9
  const loadPrettier = async () => {
10
10
  const [prettier, babel, estree, typescript] = await Promise.all([
@@ -55,24 +55,61 @@ const useFormatCode = ({ fragment, prettierOptions } = {}) => {
55
55
  };
56
56
  //#endregion
57
57
  //#region src/components/editor/core.tsx
58
+ const EXTENSIONS = [javascript({
59
+ jsx: true,
60
+ typescript: true
61
+ }), EditorView.lineWrapping];
58
62
  const Core = ({ value, theme, height, className, prettierOptions, fragment, raw, onChange, onSave, onError, ...props }) => {
59
63
  const formatCode = useFormatCode({
60
64
  fragment,
61
65
  prettierOptions
62
66
  });
63
- return /* @__PURE__ */ jsx(CodeEditor, {
64
- value,
65
- theme: theme ?? vscodeLight,
66
- height: height || "100%",
67
+ const editorRef = useRef(null);
68
+ const save = async () => {
69
+ const view = editorRef.current?.view;
70
+ if (!view) return;
71
+ const current = view.state.doc.toString();
72
+ try {
73
+ const next = raw ? current : await formatCode(current);
74
+ if (view.state.doc.toString() !== current) {
75
+ onError?.(null);
76
+ return;
77
+ }
78
+ if (next !== current) view.dispatch({
79
+ changes: {
80
+ from: 0,
81
+ to: current.length,
82
+ insert: next
83
+ },
84
+ selection: { anchor: Math.min(view.state.selection.main.head, next.length) }
85
+ });
86
+ onChange?.(next);
87
+ onSave?.(next);
88
+ onError?.(null);
89
+ } catch (e) {
90
+ onError?.(e instanceof Error ? e.message : String(e));
91
+ }
92
+ };
93
+ return /* @__PURE__ */ jsx("div", {
67
94
  className: cn(className),
68
- formatCode: raw ? void 0 : formatCode,
69
- onChange,
70
- onSave,
71
- onFormatError: onError,
72
- ...props
95
+ onKeyDown: (e) => {
96
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
97
+ e.preventDefault();
98
+ save();
99
+ }
100
+ },
101
+ children: /* @__PURE__ */ jsx(CodeMirror, {
102
+ ref: editorRef,
103
+ value,
104
+ theme: theme ?? vscodeLight,
105
+ height: height || "100%",
106
+ extensions: EXTENSIONS,
107
+ onChange,
108
+ ...props
109
+ })
73
110
  });
74
111
  };
75
112
  //#endregion
76
113
  export { useFormatCode as n, Core as t };
77
114
 
78
- //# sourceMappingURL=core-Bgfek364.js.map
115
+ //# sourceMappingURL=core-BNVuH8-l.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-BNVuH8-l.js","names":[],"sources":["../src/components/editor/use-format-code.ts","../src/components/editor/core.tsx"],"sourcesContent":["import { useCallback, useMemo } from 'react';\n\nimport { detectTypeScript } from '~/utils/detect-typescript';\n\n// Prettier is an optional peer dependency (#282), loaded on first use.\n// Without it, code comes back unformatted.\nconst loadPrettier = async () => {\n const [prettier, babel, estree, typescript] = await Promise.all([\n import('prettier'),\n import('prettier/plugins/babel'),\n import('prettier/plugins/estree'),\n import('prettier/plugins/typescript'),\n ]);\n\n return {\n format: prettier.format,\n plugins: [babel.default, estree.default, typescript.default],\n };\n};\n\nconst DEFAULT_PRETTIER_OPTIONS: Record<string, unknown> = {\n tabWidth: 2,\n singleQuote: true,\n trailingComma: 'all',\n htmlWhitespaceSensitivity: 'ignore',\n arrowParens: 'avoid',\n printWidth: 60,\n};\n\ninterface UseFormatCodeOptions {\n fragment?: boolean;\n prettierOptions?: Record<string, unknown>;\n}\n\n// The formatter the built-in editor uses, shared with a custom\n// `renderEditor`.\nexport const useFormatCode = ({\n fragment,\n prettierOptions,\n}: UseFormatCodeOptions = {}) => {\n const prettierConfig = useMemo(\n () => ({\n ...DEFAULT_PRETTIER_OPTIONS,\n ...prettierOptions,\n }),\n [prettierOptions],\n );\n\n return useCallback(\n async (code: string) => {\n const isTypeScript = detectTypeScript(code);\n const source = fragment ? `<>${code}</>` : code;\n\n // Only a missing prettier is swallowed here; a genuine format error\n // (prettier present, but the code is unparseable) still propagates so\n // the editor's Cmd+S path can surface it via onError.\n let prettier: Awaited<ReturnType<typeof loadPrettier>>;\n try {\n prettier = await loadPrettier();\n } catch {\n return code;\n }\n\n const formatted = await prettier.format(source, {\n parser: isTypeScript ? 'typescript' : 'babel',\n plugins: prettier.plugins,\n ...prettierConfig,\n });\n\n if (fragment) {\n return formatted\n .replace(/^<>\\n?/, '')\n .replace(/\\n?<\\/>;?\\s*$/, '')\n .replace(/^ {2}/gm, '')\n .trim();\n }\n\n return formatted;\n },\n [prettierConfig, fragment],\n );\n};\n","import { useRef } from 'react';\n\nimport { javascript } from '@codemirror/lang-javascript';\nimport { vscodeLight } from '@uiw/codemirror-theme-vscode';\nimport CodeMirror, {\n EditorView,\n type Extension,\n type ReactCodeMirrorProps,\n type ReactCodeMirrorRef,\n} from '@uiw/react-codemirror';\n\nimport { cn } from '~/utils/cn';\n\nimport { useFormatCode } from './use-format-code';\n\nexport interface Props {\n value: string;\n height?: string;\n theme?: Extension | 'light' | 'dark' | 'none';\n prettierOptions?: Record<string, unknown>;\n fragment?: boolean;\n raw?: boolean;\n className?: string;\n onChange?: (value: string) => void;\n onSave?: (value: string) => void;\n onError?: (error: string | null) => void;\n onCreateEditor?: ReactCodeMirrorProps['onCreateEditor'];\n onUpdate?: ReactCodeMirrorProps['onUpdate'];\n}\n\n// JSX and TypeScript highlighting, with long lines wrapped.\nconst EXTENSIONS = [\n javascript({ jsx: true, typescript: true }),\n EditorView.lineWrapping,\n];\n\n// CodeMirror with this library's options (#346, #309). Cmd/Ctrl+S formats\n// the code with Prettier, shaped by `raw`, `fragment` and\n// `prettierOptions`, then calls `onChange` and `onSave`; a formatter error\n// goes to `onError`.\nconst Core = ({\n value,\n theme,\n height,\n className,\n prettierOptions,\n fragment,\n raw,\n onChange,\n onSave,\n onError,\n ...props\n}: Props) => {\n const formatCode = useFormatCode({ fragment, prettierOptions });\n\n const editorRef = useRef<ReactCodeMirrorRef>(null);\n\n const save = async () => {\n const view = editorRef.current?.view;\n\n if (!view) {\n return;\n }\n\n const current = view.state.doc.toString();\n\n try {\n // `raw` (e.g. innerHTML) skips Prettier.\n const next = raw ? current : await formatCode(current);\n\n // An edit landed while Prettier ran: keep it rather than the result.\n if (view.state.doc.toString() !== current) {\n onError?.(null);\n\n return;\n }\n\n if (next !== current) {\n view.dispatch({\n changes: { from: 0, to: current.length, insert: next },\n selection: {\n anchor: Math.min(view.state.selection.main.head, next.length),\n },\n });\n }\n\n onChange?.(next);\n onSave?.(next);\n onError?.(null);\n } catch (e) {\n onError?.(e instanceof Error ? e.message : String(e));\n }\n };\n\n return (\n <div\n className={cn(className)}\n onKeyDown={e => {\n if ((e.metaKey || e.ctrlKey) && e.key === 's') {\n e.preventDefault();\n void save();\n }\n }}\n >\n <CodeMirror\n ref={editorRef}\n value={value}\n theme={theme ?? vscodeLight}\n height={height || '100%'}\n extensions={EXTENSIONS}\n onChange={onChange}\n {...props}\n />\n </div>\n );\n};\n\nexport default Core;\n"],"mappings":";;;;;;;;AAMA,MAAM,eAAe,YAAY;CAC/B,MAAM,CAAC,UAAU,OAAO,QAAQ,cAAc,MAAM,QAAQ,IAAI;EAC9D,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;CACT,CAAC;CAED,OAAO;EACL,QAAQ,SAAS;EACjB,SAAS;GAAC,MAAM;GAAS,OAAO;GAAS,WAAW;EAAO;CAC7D;AACF;AAEA,MAAM,2BAAoD;CACxD,UAAU;CACV,aAAa;CACb,eAAe;CACf,2BAA2B;CAC3B,aAAa;CACb,YAAY;AACd;AASA,MAAa,iBAAiB,EAC5B,UACA,oBACwB,CAAC,MAAM;CAC/B,MAAM,iBAAiB,eACd;EACL,GAAG;EACH,GAAG;CACL,IACA,CAAC,eAAe,CAClB;CAEA,OAAO,YACL,OAAO,SAAiB;EACtB,MAAM,eAAe,iBAAiB,IAAI;EAC1C,MAAM,SAAS,WAAW,KAAK,KAAK,OAAO;EAK3C,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,aAAa;EAChC,QAAQ;GACN,OAAO;EACT;EAEA,MAAM,YAAY,MAAM,SAAS,OAAO,QAAQ;GAC9C,QAAQ,eAAe,eAAe;GACtC,SAAS,SAAS;GAClB,GAAG;EACL,CAAC;EAED,IAAI,UACF,OAAO,UACJ,QAAQ,UAAU,EAAE,CAAC,CACrB,QAAQ,iBAAiB,EAAE,CAAC,CAC5B,QAAQ,WAAW,EAAE,CAAC,CACtB,KAAK;EAGV,OAAO;CACT,GACA,CAAC,gBAAgB,QAAQ,CAC3B;AACF;;;AClDA,MAAM,aAAa,CACjB,WAAW;CAAE,KAAK;CAAM,YAAY;AAAK,CAAC,GAC1C,WAAW,YACb;AAMA,MAAM,QAAQ,EACZ,OACA,OACA,QACA,WACA,iBACA,UACA,KACA,UACA,QACA,SACA,GAAG,YACQ;CACX,MAAM,aAAa,cAAc;EAAE;EAAU;CAAgB,CAAC;CAE9D,MAAM,YAAY,OAA2B,IAAI;CAEjD,MAAM,OAAO,YAAY;EACvB,MAAM,OAAO,UAAU,SAAS;EAEhC,IAAI,CAAC,MACH;EAGF,MAAM,UAAU,KAAK,MAAM,IAAI,SAAS;EAExC,IAAI;GAEF,MAAM,OAAO,MAAM,UAAU,MAAM,WAAW,OAAO;GAGrD,IAAI,KAAK,MAAM,IAAI,SAAS,MAAM,SAAS;IACzC,UAAU,IAAI;IAEd;GACF;GAEA,IAAI,SAAS,SACX,KAAK,SAAS;IACZ,SAAS;KAAE,MAAM;KAAG,IAAI,QAAQ;KAAQ,QAAQ;IAAK;IACrD,WAAW,EACT,QAAQ,KAAK,IAAI,KAAK,MAAM,UAAU,KAAK,MAAM,KAAK,MAAM,EAC9D;GACF,CAAC;GAGH,WAAW,IAAI;GACf,SAAS,IAAI;GACb,UAAU,IAAI;EAChB,SAAS,GAAG;GACV,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;EACtD;CACF;CAEA,OACE,oBAAC,OAAD;EACE,WAAW,GAAG,SAAS;EACvB,YAAW,MAAK;GACd,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,KAAK;IAC7C,EAAE,eAAe;IACjB,KAAU;GACZ;EACF;EAEA,UAAA,oBAAC,YAAD;GACE,KAAK;GACE;GACP,OAAO,SAAS;GAChB,QAAQ,UAAU;GAClB,YAAY;GACF;GACV,GAAI;EACL,CAAA;CACE,CAAA;AAET"}