@mappoh/nova 0.3.0 → 0.5.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 (252) hide show
  1. package/README.md +180 -30
  2. package/dist/alert/alert.d.ts +32 -0
  3. package/dist/alert/alert.d.ts.map +1 -0
  4. package/dist/alert/alert.js +307 -0
  5. package/dist/alert/alert.js.map +1 -0
  6. package/dist/alert/index.d.ts +3 -0
  7. package/dist/alert/index.d.ts.map +1 -0
  8. package/dist/alert/index.js +2 -0
  9. package/dist/alert/index.js.map +1 -0
  10. package/dist/animation/flip.d.ts.map +1 -1
  11. package/dist/animation/flip.js +19 -12
  12. package/dist/animation/flip.js.map +1 -1
  13. package/dist/animation/stagger.d.ts +43 -0
  14. package/dist/animation/stagger.d.ts.map +1 -0
  15. package/dist/animation/stagger.js +150 -0
  16. package/dist/animation/stagger.js.map +1 -0
  17. package/dist/avatar/avatar.d.ts +27 -0
  18. package/dist/avatar/avatar.d.ts.map +1 -0
  19. package/dist/avatar/avatar.js +132 -0
  20. package/dist/avatar/avatar.js.map +1 -0
  21. package/dist/avatar/index.d.ts +3 -0
  22. package/dist/avatar/index.d.ts.map +1 -0
  23. package/dist/avatar/index.js +2 -0
  24. package/dist/avatar/index.js.map +1 -0
  25. package/dist/badge/badge.d.ts +27 -0
  26. package/dist/badge/badge.d.ts.map +1 -0
  27. package/dist/badge/badge.js +118 -0
  28. package/dist/badge/badge.js.map +1 -0
  29. package/dist/badge/index.d.ts +3 -0
  30. package/dist/badge/index.d.ts.map +1 -0
  31. package/dist/badge/index.js +2 -0
  32. package/dist/badge/index.js.map +1 -0
  33. package/dist/cache/cache.d.ts +28 -0
  34. package/dist/cache/cache.d.ts.map +1 -0
  35. package/dist/cache/cache.js +67 -0
  36. package/dist/cache/cache.js.map +1 -0
  37. package/dist/cache/index.d.ts +3 -0
  38. package/dist/cache/index.d.ts.map +1 -0
  39. package/dist/cache/index.js +2 -0
  40. package/dist/cache/index.js.map +1 -0
  41. package/dist/chart/chart.d.ts +4 -0
  42. package/dist/chart/chart.d.ts.map +1 -1
  43. package/dist/chart/chart.js +89 -6
  44. package/dist/chart/chart.js.map +1 -1
  45. package/dist/command-palette/command-palette.d.ts +31 -0
  46. package/dist/command-palette/command-palette.d.ts.map +1 -0
  47. package/dist/command-palette/command-palette.js +590 -0
  48. package/dist/command-palette/command-palette.js.map +1 -0
  49. package/dist/command-palette/index.d.ts +3 -0
  50. package/dist/command-palette/index.d.ts.map +1 -0
  51. package/dist/command-palette/index.js +2 -0
  52. package/dist/command-palette/index.js.map +1 -0
  53. package/dist/component/component.d.ts +20 -2
  54. package/dist/component/component.d.ts.map +1 -1
  55. package/dist/component/component.js +115 -5
  56. package/dist/component/component.js.map +1 -1
  57. package/dist/component/connect.d.ts +50 -0
  58. package/dist/component/connect.d.ts.map +1 -1
  59. package/dist/component/connect.js +135 -0
  60. package/dist/component/connect.js.map +1 -1
  61. package/dist/component/directives.d.ts +20 -0
  62. package/dist/component/directives.d.ts.map +1 -0
  63. package/dist/component/directives.js +42 -0
  64. package/dist/component/directives.js.map +1 -0
  65. package/dist/component/html.d.ts +8 -0
  66. package/dist/component/html.d.ts.map +1 -1
  67. package/dist/component/html.js +11 -0
  68. package/dist/component/html.js.map +1 -1
  69. package/dist/component/index.d.ts +9 -3
  70. package/dist/component/index.d.ts.map +1 -1
  71. package/dist/component/index.js +6 -2
  72. package/dist/component/index.js.map +1 -1
  73. package/dist/component/portal.d.ts +32 -0
  74. package/dist/component/portal.d.ts.map +1 -0
  75. package/dist/component/portal.js +59 -0
  76. package/dist/component/portal.js.map +1 -0
  77. package/dist/component/ref.d.ts +18 -0
  78. package/dist/component/ref.d.ts.map +1 -0
  79. package/dist/component/ref.js +17 -0
  80. package/dist/component/ref.js.map +1 -0
  81. package/dist/component/slot-styles.d.ts +18 -0
  82. package/dist/component/slot-styles.d.ts.map +1 -0
  83. package/dist/component/slot-styles.js +47 -0
  84. package/dist/component/slot-styles.js.map +1 -0
  85. package/dist/component/template.d.ts +2 -0
  86. package/dist/component/template.d.ts.map +1 -1
  87. package/dist/component/template.js +122 -4
  88. package/dist/component/template.js.map +1 -1
  89. package/dist/context/context.d.ts +39 -0
  90. package/dist/context/context.d.ts.map +1 -0
  91. package/dist/context/context.js +111 -0
  92. package/dist/context/context.js.map +1 -0
  93. package/dist/context/index.d.ts +3 -0
  94. package/dist/context/index.d.ts.map +1 -0
  95. package/dist/context/index.js +2 -0
  96. package/dist/context/index.js.map +1 -0
  97. package/dist/devtools/devtools.d.ts +54 -0
  98. package/dist/devtools/devtools.d.ts.map +1 -1
  99. package/dist/devtools/devtools.js +86 -0
  100. package/dist/devtools/devtools.js.map +1 -1
  101. package/dist/devtools/index.d.ts +2 -2
  102. package/dist/devtools/index.d.ts.map +1 -1
  103. package/dist/devtools/index.js +1 -1
  104. package/dist/devtools/index.js.map +1 -1
  105. package/dist/editor/editor.d.ts +40 -0
  106. package/dist/editor/editor.d.ts.map +1 -0
  107. package/dist/editor/editor.js +955 -0
  108. package/dist/editor/editor.js.map +1 -0
  109. package/dist/editor/index.d.ts +3 -0
  110. package/dist/editor/index.d.ts.map +1 -0
  111. package/dist/editor/index.js +2 -0
  112. package/dist/editor/index.js.map +1 -0
  113. package/dist/event-bus/event-bus.d.ts +20 -0
  114. package/dist/event-bus/event-bus.d.ts.map +1 -0
  115. package/dist/event-bus/event-bus.js +55 -0
  116. package/dist/event-bus/event-bus.js.map +1 -0
  117. package/dist/event-bus/index.d.ts +3 -0
  118. package/dist/event-bus/index.d.ts.map +1 -0
  119. package/dist/event-bus/index.js +2 -0
  120. package/dist/event-bus/index.js.map +1 -0
  121. package/dist/forms/form-engine.d.ts +1 -1
  122. package/dist/forms/form-engine.d.ts.map +1 -1
  123. package/dist/forms/wasm-validators.d.ts +19 -11
  124. package/dist/forms/wasm-validators.d.ts.map +1 -1
  125. package/dist/forms/wasm-validators.js +191 -31
  126. package/dist/forms/wasm-validators.js.map +1 -1
  127. package/dist/gesture/gesture.d.ts +2 -0
  128. package/dist/gesture/gesture.d.ts.map +1 -1
  129. package/dist/gesture/gesture.js +81 -0
  130. package/dist/gesture/gesture.js.map +1 -1
  131. package/dist/http/http.d.ts +8 -0
  132. package/dist/http/http.d.ts.map +1 -1
  133. package/dist/http/http.js +18 -4
  134. package/dist/http/http.js.map +1 -1
  135. package/dist/i18n/i18n.d.ts +6 -0
  136. package/dist/i18n/i18n.d.ts.map +1 -1
  137. package/dist/i18n/i18n.js +71 -9
  138. package/dist/i18n/i18n.js.map +1 -1
  139. package/dist/machine/index.d.ts +3 -0
  140. package/dist/machine/index.d.ts.map +1 -0
  141. package/dist/machine/index.js +2 -0
  142. package/dist/machine/index.js.map +1 -0
  143. package/dist/machine/machine.d.ts +26 -0
  144. package/dist/machine/machine.d.ts.map +1 -0
  145. package/dist/machine/machine.js +79 -0
  146. package/dist/machine/machine.js.map +1 -0
  147. package/dist/modal/modal.d.ts.map +1 -1
  148. package/dist/modal/modal.js +13 -29
  149. package/dist/modal/modal.js.map +1 -1
  150. package/dist/notification-center/index.d.ts +3 -0
  151. package/dist/notification-center/index.d.ts.map +1 -0
  152. package/dist/notification-center/index.js +2 -0
  153. package/dist/notification-center/index.js.map +1 -0
  154. package/dist/notification-center/notification-center.d.ts +55 -0
  155. package/dist/notification-center/notification-center.d.ts.map +1 -0
  156. package/dist/notification-center/notification-center.js +941 -0
  157. package/dist/notification-center/notification-center.js.map +1 -0
  158. package/dist/pagination/index.d.ts +3 -0
  159. package/dist/pagination/index.d.ts.map +1 -0
  160. package/dist/pagination/index.js +2 -0
  161. package/dist/pagination/index.js.map +1 -0
  162. package/dist/pagination/pagination.d.ts +31 -0
  163. package/dist/pagination/pagination.d.ts.map +1 -0
  164. package/dist/pagination/pagination.js +213 -0
  165. package/dist/pagination/pagination.js.map +1 -0
  166. package/dist/progress/progress.d.ts.map +1 -1
  167. package/dist/progress/progress.js +5 -7
  168. package/dist/progress/progress.js.map +1 -1
  169. package/dist/query/index.d.ts +3 -0
  170. package/dist/query/index.d.ts.map +1 -0
  171. package/dist/query/index.js +2 -0
  172. package/dist/query/index.js.map +1 -0
  173. package/dist/query/query.d.ts +31 -0
  174. package/dist/query/query.d.ts.map +1 -0
  175. package/dist/query/query.js +150 -0
  176. package/dist/query/query.js.map +1 -0
  177. package/dist/radio-group/index.d.ts +3 -0
  178. package/dist/radio-group/index.d.ts.map +1 -0
  179. package/dist/radio-group/index.js +2 -0
  180. package/dist/radio-group/index.js.map +1 -0
  181. package/dist/radio-group/radio-group.d.ts +37 -0
  182. package/dist/radio-group/radio-group.d.ts.map +1 -0
  183. package/dist/radio-group/radio-group.js +251 -0
  184. package/dist/radio-group/radio-group.js.map +1 -0
  185. package/dist/rating/index.d.ts +3 -0
  186. package/dist/rating/index.d.ts.map +1 -0
  187. package/dist/rating/index.js +2 -0
  188. package/dist/rating/index.js.map +1 -0
  189. package/dist/rating/rating.d.ts +31 -0
  190. package/dist/rating/rating.d.ts.map +1 -0
  191. package/dist/rating/rating.js +187 -0
  192. package/dist/rating/rating.js.map +1 -0
  193. package/dist/router/router.d.ts +16 -1
  194. package/dist/router/router.d.ts.map +1 -1
  195. package/dist/router/router.js +88 -11
  196. package/dist/router/router.js.map +1 -1
  197. package/dist/skeleton/index.d.ts +3 -0
  198. package/dist/skeleton/index.d.ts.map +1 -0
  199. package/dist/skeleton/index.js +2 -0
  200. package/dist/skeleton/index.js.map +1 -0
  201. package/dist/skeleton/skeleton.d.ts +24 -0
  202. package/dist/skeleton/skeleton.d.ts.map +1 -0
  203. package/dist/skeleton/skeleton.js +91 -0
  204. package/dist/skeleton/skeleton.js.map +1 -0
  205. package/dist/slider/index.d.ts +3 -0
  206. package/dist/slider/index.d.ts.map +1 -0
  207. package/dist/slider/index.js +2 -0
  208. package/dist/slider/index.js.map +1 -0
  209. package/dist/slider/slider.d.ts +33 -0
  210. package/dist/slider/slider.d.ts.map +1 -0
  211. package/dist/slider/slider.js +248 -0
  212. package/dist/slider/slider.js.map +1 -0
  213. package/dist/spinner/index.d.ts +3 -0
  214. package/dist/spinner/index.d.ts.map +1 -0
  215. package/dist/spinner/index.js +2 -0
  216. package/dist/spinner/index.js.map +1 -0
  217. package/dist/spinner/spinner.d.ts +23 -0
  218. package/dist/spinner/spinner.d.ts.map +1 -0
  219. package/dist/spinner/spinner.js +82 -0
  220. package/dist/spinner/spinner.js.map +1 -0
  221. package/dist/sw/sw.d.ts.map +1 -1
  222. package/dist/sw/sw.js +39 -7
  223. package/dist/sw/sw.js.map +1 -1
  224. package/dist/switch/index.d.ts +3 -0
  225. package/dist/switch/index.d.ts.map +1 -0
  226. package/dist/switch/index.js +2 -0
  227. package/dist/switch/index.js.map +1 -0
  228. package/dist/switch/switch.d.ts +27 -0
  229. package/dist/switch/switch.d.ts.map +1 -0
  230. package/dist/switch/switch.js +163 -0
  231. package/dist/switch/switch.js.map +1 -0
  232. package/dist/theme/index.d.ts +2 -0
  233. package/dist/theme/index.d.ts.map +1 -1
  234. package/dist/theme/index.js +1 -0
  235. package/dist/theme/index.js.map +1 -1
  236. package/dist/theme/scale.d.ts +40 -0
  237. package/dist/theme/scale.d.ts.map +1 -0
  238. package/dist/theme/scale.js +62 -0
  239. package/dist/theme/scale.js.map +1 -0
  240. package/dist/utils/index.d.ts +29 -0
  241. package/dist/utils/index.d.ts.map +1 -0
  242. package/dist/utils/index.js +114 -0
  243. package/dist/utils/index.js.map +1 -0
  244. package/dist/websocket/index.d.ts +3 -0
  245. package/dist/websocket/index.d.ts.map +1 -0
  246. package/dist/websocket/index.js +2 -0
  247. package/dist/websocket/index.js.map +1 -0
  248. package/dist/websocket/websocket.d.ts +31 -0
  249. package/dist/websocket/websocket.d.ts.map +1 -0
  250. package/dist/websocket/websocket.js +164 -0
  251. package/dist/websocket/websocket.js.map +1 -0
  252. package/package.json +85 -1
@@ -0,0 +1,955 @@
1
+ /**
2
+ * Nova Engine — Markdown Editor
3
+ *
4
+ * Interactive split-pane markdown editor with live preview,
5
+ * toolbar, keyboard shortcuts, and auto-list continuation.
6
+ * Uses Nova's internal markdown parser for rendering.
7
+ */
8
+ import { parse } from '../markdown/markdown';
9
+ // ── Style Injection ─────────────────────────────────────────────────
10
+ const STYLE_ID = 'nova-editor-styles';
11
+ function injectStyles() {
12
+ if (document.getElementById(STYLE_ID))
13
+ return;
14
+ const s = document.createElement('style');
15
+ s.id = STYLE_ID;
16
+ s.textContent = `
17
+ .nova-editor {
18
+ display: flex;
19
+ flex-direction: column;
20
+ border: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
21
+ border-radius: 8px;
22
+ background: var(--color-bg-surface, rgba(18, 18, 18, 0.95));
23
+ color: var(--color-text-primary, #e5e5e5);
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
25
+ overflow: hidden;
26
+ }
27
+
28
+ /* ── Toolbar ── */
29
+ .nova-editor__toolbar {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 2px;
33
+ padding: 6px 8px;
34
+ border-bottom: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
35
+ background: var(--color-bg-elevated, rgba(22, 22, 22, 0.98));
36
+ flex-wrap: wrap;
37
+ }
38
+ .nova-editor__toolbar-btn {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ width: 32px;
43
+ height: 32px;
44
+ border: none;
45
+ border-radius: 6px;
46
+ background: transparent;
47
+ color: var(--color-text-secondary, rgba(255, 255, 255, 0.55));
48
+ cursor: pointer;
49
+ padding: 0;
50
+ transition: background 0.15s ease, color 0.15s ease;
51
+ }
52
+ .nova-editor__toolbar-btn:hover {
53
+ background: var(--color-bg-hover, rgba(255, 255, 255, 0.06));
54
+ color: var(--color-text-primary, #e5e5e5);
55
+ }
56
+ .nova-editor__toolbar-btn:focus-visible {
57
+ outline: 2px solid var(--color-focus-ring, rgba(99, 102, 241, 0.6));
58
+ outline-offset: -2px;
59
+ }
60
+ .nova-editor__toolbar-btn[aria-pressed="true"] {
61
+ background: var(--color-bg-active, rgba(255, 255, 255, 0.1));
62
+ color: var(--color-text-primary, #e5e5e5);
63
+ }
64
+ .nova-editor__toolbar-sep {
65
+ width: 1px;
66
+ height: 20px;
67
+ background: var(--color-border, rgba(255, 255, 255, 0.08));
68
+ margin: 0 4px;
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ /* ── Body ── */
73
+ .nova-editor__body {
74
+ display: flex;
75
+ flex: 1;
76
+ min-height: 0;
77
+ }
78
+ .nova-editor__body--split {
79
+ flex-direction: row;
80
+ }
81
+ .nova-editor__body--write .nova-editor__pane-write {
82
+ flex: 1;
83
+ }
84
+ .nova-editor__body--write .nova-editor__pane-preview {
85
+ display: none;
86
+ }
87
+ .nova-editor__body--preview .nova-editor__pane-write {
88
+ display: none;
89
+ }
90
+ .nova-editor__body--preview .nova-editor__pane-preview {
91
+ flex: 1;
92
+ }
93
+
94
+ /* ── Write Pane ── */
95
+ .nova-editor__pane-write {
96
+ position: relative;
97
+ display: flex;
98
+ flex: 1;
99
+ min-width: 0;
100
+ }
101
+ .nova-editor__line-numbers {
102
+ padding: 12px 0;
103
+ width: 44px;
104
+ flex-shrink: 0;
105
+ text-align: right;
106
+ font: 13px/1.6 "SF Mono", "Fira Code", "Cascadia Code", Menlo, Monaco, Consolas, monospace;
107
+ color: var(--color-text-tertiary, rgba(255, 255, 255, 0.25));
108
+ background: var(--color-bg-inset, rgba(0, 0, 0, 0.15));
109
+ border-right: 1px solid var(--color-border, rgba(255, 255, 255, 0.06));
110
+ user-select: none;
111
+ overflow: hidden;
112
+ }
113
+ .nova-editor__line-numbers span {
114
+ display: block;
115
+ padding-right: 8px;
116
+ }
117
+ .nova-editor__textarea {
118
+ flex: 1;
119
+ resize: none;
120
+ border: none;
121
+ outline: none;
122
+ background: transparent;
123
+ color: var(--color-text-primary, #e5e5e5);
124
+ font: 14px/1.6 "SF Mono", "Fira Code", "Cascadia Code", Menlo, Monaco, Consolas, monospace;
125
+ padding: 12px 16px;
126
+ tab-size: 2;
127
+ white-space: pre-wrap;
128
+ word-wrap: break-word;
129
+ overflow-y: auto;
130
+ }
131
+ .nova-editor__textarea::placeholder {
132
+ color: var(--color-text-tertiary, rgba(255, 255, 255, 0.25));
133
+ }
134
+ .nova-editor__textarea:focus {
135
+ box-shadow: inset 0 0 0 1px var(--color-focus-ring, rgba(99, 102, 241, 0.4));
136
+ }
137
+
138
+ /* ── Divider ── */
139
+ .nova-editor__divider {
140
+ width: 1px;
141
+ background: var(--color-border, rgba(255, 255, 255, 0.08));
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ /* ── Preview Pane ── */
146
+ .nova-editor__pane-preview {
147
+ flex: 1;
148
+ overflow-y: auto;
149
+ padding: 12px 20px;
150
+ min-width: 0;
151
+ }
152
+
153
+ /* ── Preview Typography ── */
154
+ .nova-editor__preview h1,
155
+ .nova-editor__preview h2,
156
+ .nova-editor__preview h3,
157
+ .nova-editor__preview h4,
158
+ .nova-editor__preview h5,
159
+ .nova-editor__preview h6 {
160
+ color: var(--color-text-primary, #e5e5e5);
161
+ font-weight: 600;
162
+ line-height: 1.3;
163
+ margin-top: 1.4em;
164
+ margin-bottom: 0.6em;
165
+ }
166
+ .nova-editor__preview h1:first-child,
167
+ .nova-editor__preview h2:first-child,
168
+ .nova-editor__preview h3:first-child {
169
+ margin-top: 0;
170
+ }
171
+ .nova-editor__preview h1 { font-size: 1.75em; }
172
+ .nova-editor__preview h2 { font-size: 1.4em; }
173
+ .nova-editor__preview h3 { font-size: 1.15em; }
174
+ .nova-editor__preview h4 { font-size: 1em; }
175
+ .nova-editor__preview p {
176
+ margin: 0 0 0.8em 0;
177
+ line-height: 1.65;
178
+ }
179
+ .nova-editor__preview a {
180
+ color: var(--color-accent, #6366f1);
181
+ text-decoration: underline;
182
+ text-decoration-color: var(--color-accent-muted, rgba(99, 102, 241, 0.4));
183
+ text-underline-offset: 2px;
184
+ }
185
+ .nova-editor__preview a:hover {
186
+ text-decoration-color: var(--color-accent, #6366f1);
187
+ }
188
+ .nova-editor__preview strong { color: var(--color-text-primary, #e5e5e5); }
189
+ .nova-editor__preview code {
190
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", Menlo, Monaco, Consolas, monospace;
191
+ font-size: 0.875em;
192
+ background: var(--color-bg-inset, rgba(255, 255, 255, 0.06));
193
+ padding: 0.15em 0.4em;
194
+ border-radius: 4px;
195
+ border: 1px solid var(--color-border, rgba(255, 255, 255, 0.06));
196
+ }
197
+ .nova-editor__preview pre {
198
+ background: var(--color-bg-inset, rgba(0, 0, 0, 0.25));
199
+ border: 1px solid var(--color-border, rgba(255, 255, 255, 0.06));
200
+ border-radius: 6px;
201
+ padding: 14px 16px;
202
+ overflow-x: auto;
203
+ margin: 0 0 1em 0;
204
+ }
205
+ .nova-editor__preview pre code {
206
+ background: none;
207
+ border: none;
208
+ padding: 0;
209
+ font-size: 0.85em;
210
+ line-height: 1.6;
211
+ }
212
+ .nova-editor__preview blockquote {
213
+ border-left: 3px solid var(--color-accent-muted, rgba(99, 102, 241, 0.4));
214
+ margin: 0 0 1em 0;
215
+ padding: 0.4em 0 0.4em 16px;
216
+ color: var(--color-text-secondary, rgba(255, 255, 255, 0.55));
217
+ }
218
+ .nova-editor__preview ul,
219
+ .nova-editor__preview ol {
220
+ margin: 0 0 1em 0;
221
+ padding-left: 1.6em;
222
+ }
223
+ .nova-editor__preview ul { list-style: disc; }
224
+ .nova-editor__preview ol { list-style: decimal; }
225
+ .nova-editor__preview li {
226
+ margin-bottom: 0.3em;
227
+ line-height: 1.6;
228
+ }
229
+ .nova-editor__preview hr {
230
+ border: none;
231
+ border-top: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
232
+ margin: 1.5em 0;
233
+ }
234
+ .nova-editor__preview img {
235
+ max-width: 100%;
236
+ border-radius: 6px;
237
+ }
238
+ .nova-editor__preview del {
239
+ color: var(--color-text-tertiary, rgba(255, 255, 255, 0.35));
240
+ }
241
+
242
+ /* ── Responsive ── */
243
+ @media (max-width: 768px) {
244
+ .nova-editor__body--split {
245
+ flex-direction: column;
246
+ }
247
+ .nova-editor__body--split .nova-editor__divider {
248
+ width: auto;
249
+ height: 1px;
250
+ }
251
+ }
252
+
253
+ /* ── Reduced Motion ── */
254
+ @media (prefers-reduced-motion: reduce) {
255
+ .nova-editor__toolbar-btn { transition: none; }
256
+ }`;
257
+ document.head.appendChild(s);
258
+ }
259
+ // ── SVG Icon Helpers ────────────────────────────────────────────────
260
+ const SVG_NS = 'http://www.w3.org/2000/svg';
261
+ function svgIcon(paths, viewBox = '0 0 24 24', size = 16) {
262
+ const svg = document.createElementNS(SVG_NS, 'svg');
263
+ svg.setAttribute('width', String(size));
264
+ svg.setAttribute('height', String(size));
265
+ svg.setAttribute('viewBox', viewBox);
266
+ svg.setAttribute('fill', 'none');
267
+ svg.setAttribute('stroke', 'currentColor');
268
+ svg.setAttribute('stroke-width', '2');
269
+ svg.setAttribute('stroke-linecap', 'round');
270
+ svg.setAttribute('stroke-linejoin', 'round');
271
+ for (const d of paths) {
272
+ const path = document.createElementNS(SVG_NS, 'path');
273
+ path.setAttribute('d', d);
274
+ svg.appendChild(path);
275
+ }
276
+ return svg;
277
+ }
278
+ function svgIconRaw(children, viewBox = '0 0 24 24', size = 16) {
279
+ const svg = document.createElementNS(SVG_NS, 'svg');
280
+ svg.setAttribute('width', String(size));
281
+ svg.setAttribute('height', String(size));
282
+ svg.setAttribute('viewBox', viewBox);
283
+ svg.setAttribute('fill', 'none');
284
+ svg.setAttribute('stroke', 'currentColor');
285
+ svg.setAttribute('stroke-width', '2');
286
+ svg.setAttribute('stroke-linecap', 'round');
287
+ svg.setAttribute('stroke-linejoin', 'round');
288
+ children(svg);
289
+ return svg;
290
+ }
291
+ // ── Toolbar Icons ───────────────────────────────────────────────────
292
+ function iconBold() {
293
+ return svgIconRaw((svg) => {
294
+ const p1 = document.createElementNS(SVG_NS, 'path');
295
+ p1.setAttribute('d', 'M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z');
296
+ const p2 = document.createElementNS(SVG_NS, 'path');
297
+ p2.setAttribute('d', 'M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z');
298
+ svg.appendChild(p1);
299
+ svg.appendChild(p2);
300
+ });
301
+ }
302
+ function iconItalic() {
303
+ return svgIconRaw((svg) => {
304
+ const l1 = document.createElementNS(SVG_NS, 'line');
305
+ l1.setAttribute('x1', '19');
306
+ l1.setAttribute('y1', '4');
307
+ l1.setAttribute('x2', '10');
308
+ l1.setAttribute('y2', '4');
309
+ const l2 = document.createElementNS(SVG_NS, 'line');
310
+ l2.setAttribute('x1', '14');
311
+ l2.setAttribute('y1', '20');
312
+ l2.setAttribute('x2', '5');
313
+ l2.setAttribute('y2', '20');
314
+ const l3 = document.createElementNS(SVG_NS, 'line');
315
+ l3.setAttribute('x1', '15');
316
+ l3.setAttribute('y1', '4');
317
+ l3.setAttribute('x2', '9');
318
+ l3.setAttribute('y2', '20');
319
+ svg.appendChild(l1);
320
+ svg.appendChild(l2);
321
+ svg.appendChild(l3);
322
+ });
323
+ }
324
+ function iconStrikethrough() {
325
+ return svgIconRaw((svg) => {
326
+ const p = document.createElementNS(SVG_NS, 'path');
327
+ p.setAttribute('d', 'M16 4H9a3 3 0 0 0-3 3v0a3 3 0 0 0 3 3h6a3 3 0 0 1 3 3v0a3 3 0 0 1-3 3H8');
328
+ const l = document.createElementNS(SVG_NS, 'line');
329
+ l.setAttribute('x1', '4');
330
+ l.setAttribute('y1', '12');
331
+ l.setAttribute('x2', '20');
332
+ l.setAttribute('y2', '12');
333
+ svg.appendChild(p);
334
+ svg.appendChild(l);
335
+ });
336
+ }
337
+ function iconHeading() {
338
+ return svgIconRaw((svg) => {
339
+ const l1 = document.createElementNS(SVG_NS, 'line');
340
+ l1.setAttribute('x1', '4');
341
+ l1.setAttribute('y1', '4');
342
+ l1.setAttribute('x2', '4');
343
+ l1.setAttribute('y2', '20');
344
+ const l2 = document.createElementNS(SVG_NS, 'line');
345
+ l2.setAttribute('x1', '18');
346
+ l2.setAttribute('y1', '4');
347
+ l2.setAttribute('x2', '18');
348
+ l2.setAttribute('y2', '20');
349
+ const l3 = document.createElementNS(SVG_NS, 'line');
350
+ l3.setAttribute('x1', '4');
351
+ l3.setAttribute('y1', '12');
352
+ l3.setAttribute('x2', '18');
353
+ l3.setAttribute('y2', '12');
354
+ svg.appendChild(l1);
355
+ svg.appendChild(l2);
356
+ svg.appendChild(l3);
357
+ });
358
+ }
359
+ function iconLink() {
360
+ return svgIcon([
361
+ 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',
362
+ 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
363
+ ]);
364
+ }
365
+ function iconImage() {
366
+ return svgIconRaw((svg) => {
367
+ const rect = document.createElementNS(SVG_NS, 'rect');
368
+ rect.setAttribute('x', '3');
369
+ rect.setAttribute('y', '3');
370
+ rect.setAttribute('width', '18');
371
+ rect.setAttribute('height', '18');
372
+ rect.setAttribute('rx', '2');
373
+ rect.setAttribute('ry', '2');
374
+ const circle = document.createElementNS(SVG_NS, 'circle');
375
+ circle.setAttribute('cx', '8.5');
376
+ circle.setAttribute('cy', '8.5');
377
+ circle.setAttribute('r', '1.5');
378
+ circle.setAttribute('fill', 'currentColor');
379
+ circle.setAttribute('stroke', 'none');
380
+ const poly = document.createElementNS(SVG_NS, 'polyline');
381
+ poly.setAttribute('points', '21 15 16 10 5 21');
382
+ svg.appendChild(rect);
383
+ svg.appendChild(circle);
384
+ svg.appendChild(poly);
385
+ });
386
+ }
387
+ function iconCode() {
388
+ return svgIconRaw((svg) => {
389
+ const p1 = document.createElementNS(SVG_NS, 'polyline');
390
+ p1.setAttribute('points', '16 18 22 12 16 6');
391
+ const p2 = document.createElementNS(SVG_NS, 'polyline');
392
+ p2.setAttribute('points', '8 6 2 12 8 18');
393
+ svg.appendChild(p1);
394
+ svg.appendChild(p2);
395
+ });
396
+ }
397
+ function iconQuote() {
398
+ return svgIconRaw((svg) => {
399
+ const l1 = document.createElementNS(SVG_NS, 'line');
400
+ l1.setAttribute('x1', '3');
401
+ l1.setAttribute('y1', '6');
402
+ l1.setAttribute('x2', '3');
403
+ l1.setAttribute('y2', '18');
404
+ l1.setAttribute('stroke-width', '3');
405
+ const l2 = document.createElementNS(SVG_NS, 'line');
406
+ l2.setAttribute('x1', '9');
407
+ l2.setAttribute('y1', '8');
408
+ l2.setAttribute('x2', '21');
409
+ l2.setAttribute('y2', '8');
410
+ const l3 = document.createElementNS(SVG_NS, 'line');
411
+ l3.setAttribute('x1', '9');
412
+ l3.setAttribute('y1', '12');
413
+ l3.setAttribute('x2', '18');
414
+ l3.setAttribute('y2', '12');
415
+ const l4 = document.createElementNS(SVG_NS, 'line');
416
+ l4.setAttribute('x1', '9');
417
+ l4.setAttribute('y1', '16');
418
+ l4.setAttribute('x2', '15');
419
+ l4.setAttribute('y2', '16');
420
+ svg.appendChild(l1);
421
+ svg.appendChild(l2);
422
+ svg.appendChild(l3);
423
+ svg.appendChild(l4);
424
+ });
425
+ }
426
+ function iconUnorderedList() {
427
+ return svgIconRaw((svg) => {
428
+ const items = [
429
+ ['10', '6', '21', '6'],
430
+ ['10', '12', '21', '12'],
431
+ ['10', '18', '21', '18'],
432
+ ];
433
+ for (const [x1, y1, x2, y2] of items) {
434
+ const l = document.createElementNS(SVG_NS, 'line');
435
+ l.setAttribute('x1', x1);
436
+ l.setAttribute('y1', y1);
437
+ l.setAttribute('x2', x2);
438
+ l.setAttribute('y2', y2);
439
+ svg.appendChild(l);
440
+ }
441
+ const dots = [['4', '6'], ['4', '12'], ['4', '18']];
442
+ for (const [cx, cy] of dots) {
443
+ const c = document.createElementNS(SVG_NS, 'circle');
444
+ c.setAttribute('cx', cx);
445
+ c.setAttribute('cy', cy);
446
+ c.setAttribute('r', '1');
447
+ c.setAttribute('fill', 'currentColor');
448
+ c.setAttribute('stroke', 'none');
449
+ svg.appendChild(c);
450
+ }
451
+ });
452
+ }
453
+ function iconOrderedList() {
454
+ return svgIconRaw((svg) => {
455
+ const items = [
456
+ ['12', '6', '21', '6'],
457
+ ['12', '12', '21', '12'],
458
+ ['12', '18', '21', '18'],
459
+ ];
460
+ for (const [x1, y1, x2, y2] of items) {
461
+ const l = document.createElementNS(SVG_NS, 'line');
462
+ l.setAttribute('x1', x1);
463
+ l.setAttribute('y1', y1);
464
+ l.setAttribute('x2', x2);
465
+ l.setAttribute('y2', y2);
466
+ svg.appendChild(l);
467
+ }
468
+ const text = document.createElementNS(SVG_NS, 'text');
469
+ text.setAttribute('x', '4');
470
+ text.setAttribute('y', '8');
471
+ text.setAttribute('font-size', '7');
472
+ text.setAttribute('fill', 'currentColor');
473
+ text.setAttribute('stroke', 'none');
474
+ text.setAttribute('font-family', 'sans-serif');
475
+ text.textContent = '1';
476
+ const text2 = document.createElementNS(SVG_NS, 'text');
477
+ text2.setAttribute('x', '4');
478
+ text2.setAttribute('y', '14');
479
+ text2.setAttribute('font-size', '7');
480
+ text2.setAttribute('fill', 'currentColor');
481
+ text2.setAttribute('stroke', 'none');
482
+ text2.setAttribute('font-family', 'sans-serif');
483
+ text2.textContent = '2';
484
+ const text3 = document.createElementNS(SVG_NS, 'text');
485
+ text3.setAttribute('x', '4');
486
+ text3.setAttribute('y', '20');
487
+ text3.setAttribute('font-size', '7');
488
+ text3.setAttribute('fill', 'currentColor');
489
+ text3.setAttribute('stroke', 'none');
490
+ text3.setAttribute('font-family', 'sans-serif');
491
+ text3.textContent = '3';
492
+ svg.appendChild(text);
493
+ svg.appendChild(text2);
494
+ svg.appendChild(text3);
495
+ });
496
+ }
497
+ function iconWrite() {
498
+ return svgIcon([
499
+ 'M12 20h9',
500
+ 'M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z',
501
+ ]);
502
+ }
503
+ function iconSplit() {
504
+ return svgIconRaw((svg) => {
505
+ const rect = document.createElementNS(SVG_NS, 'rect');
506
+ rect.setAttribute('x', '3');
507
+ rect.setAttribute('y', '3');
508
+ rect.setAttribute('width', '18');
509
+ rect.setAttribute('height', '18');
510
+ rect.setAttribute('rx', '2');
511
+ const line = document.createElementNS(SVG_NS, 'line');
512
+ line.setAttribute('x1', '12');
513
+ line.setAttribute('y1', '3');
514
+ line.setAttribute('x2', '12');
515
+ line.setAttribute('y2', '21');
516
+ svg.appendChild(rect);
517
+ svg.appendChild(line);
518
+ });
519
+ }
520
+ function iconPreview() {
521
+ return svgIconRaw((svg) => {
522
+ const p1 = document.createElementNS(SVG_NS, 'path');
523
+ p1.setAttribute('d', 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z');
524
+ const c = document.createElementNS(SVG_NS, 'circle');
525
+ c.setAttribute('cx', '12');
526
+ c.setAttribute('cy', '12');
527
+ c.setAttribute('r', '3');
528
+ svg.appendChild(p1);
529
+ svg.appendChild(c);
530
+ });
531
+ }
532
+ // ── Factory ─────────────────────────────────────────────────────────
533
+ /** Create an interactive markdown editor. */
534
+ export function createEditor(container, options = {}) {
535
+ const { value: initialValue = '', mode: initialMode = 'split', placeholder = 'Write markdown...', toolbar: showToolbar = true, lineNumbers = false, minHeight = '300px', maxHeight = 'none', onChange, onSave, } = options;
536
+ injectStyles();
537
+ let destroyed = false;
538
+ let currentMode = initialMode;
539
+ let debounceTimer;
540
+ // Listener bookkeeping for cleanup
541
+ const listeners = [];
542
+ function on(target, evt, fn, capture) {
543
+ target.addEventListener(evt, fn, capture);
544
+ listeners.push([target, evt, fn, capture]);
545
+ }
546
+ // ── Root Element ──────────────────────────────────────────────────
547
+ const root = document.createElement('div');
548
+ root.className = 'nova-editor';
549
+ root.setAttribute('role', 'group');
550
+ root.setAttribute('aria-label', 'Markdown editor');
551
+ root.style.minHeight = minHeight;
552
+ if (maxHeight !== 'none')
553
+ root.style.maxHeight = maxHeight;
554
+ // ── Toolbar ───────────────────────────────────────────────────────
555
+ let toolbarEl = null;
556
+ const modeButtons = new Map();
557
+ function createToolbarButton(label, icon, action, shortcut) {
558
+ const btn = document.createElement('button');
559
+ btn.type = 'button';
560
+ btn.className = 'nova-editor__toolbar-btn';
561
+ const title = shortcut ? `${label} (${shortcut})` : label;
562
+ btn.setAttribute('aria-label', title);
563
+ btn.setAttribute('title', title);
564
+ btn.appendChild(icon);
565
+ on(btn, 'click', (e) => {
566
+ e.preventDefault();
567
+ action();
568
+ });
569
+ return btn;
570
+ }
571
+ function createSeparator() {
572
+ const sep = document.createElement('span');
573
+ sep.className = 'nova-editor__toolbar-sep';
574
+ sep.setAttribute('role', 'separator');
575
+ return sep;
576
+ }
577
+ function createModeButton(label, icon, mode) {
578
+ const btn = createToolbarButton(label, icon, () => setMode(mode));
579
+ btn.setAttribute('aria-pressed', String(currentMode === mode));
580
+ modeButtons.set(mode, btn);
581
+ return btn;
582
+ }
583
+ if (showToolbar) {
584
+ toolbarEl = document.createElement('div');
585
+ toolbarEl.className = 'nova-editor__toolbar';
586
+ toolbarEl.setAttribute('role', 'toolbar');
587
+ toolbarEl.setAttribute('aria-label', 'Formatting');
588
+ // Formatting buttons
589
+ toolbarEl.appendChild(createToolbarButton('Bold', iconBold(), () => wrapSelection('**', '**'), 'Ctrl+B'));
590
+ toolbarEl.appendChild(createToolbarButton('Italic', iconItalic(), () => wrapSelection('*', '*'), 'Ctrl+I'));
591
+ toolbarEl.appendChild(createToolbarButton('Strikethrough', iconStrikethrough(), () => wrapSelection('~~', '~~')));
592
+ toolbarEl.appendChild(createSeparator());
593
+ toolbarEl.appendChild(createToolbarButton('Heading', iconHeading(), () => insertAtLineStart('## ')));
594
+ toolbarEl.appendChild(createToolbarButton('Link', iconLink(), () => insertLink(), 'Ctrl+K'));
595
+ toolbarEl.appendChild(createToolbarButton('Image', iconImage(), () => insertImage()));
596
+ toolbarEl.appendChild(createToolbarButton('Code', iconCode(), () => insertCode()));
597
+ toolbarEl.appendChild(createToolbarButton('Quote', iconQuote(), () => insertAtLineStart('> ')));
598
+ toolbarEl.appendChild(createToolbarButton('Unordered list', iconUnorderedList(), () => insertAtLineStart('- ')));
599
+ toolbarEl.appendChild(createToolbarButton('Ordered list', iconOrderedList(), () => insertAtLineStart('1. ')));
600
+ toolbarEl.appendChild(createSeparator());
601
+ // Mode buttons
602
+ toolbarEl.appendChild(createModeButton('Write', iconWrite(), 'write'));
603
+ toolbarEl.appendChild(createModeButton('Split', iconSplit(), 'split'));
604
+ toolbarEl.appendChild(createModeButton('Preview', iconPreview(), 'preview'));
605
+ root.appendChild(toolbarEl);
606
+ }
607
+ // ── Body ──────────────────────────────────────────────────────────
608
+ const body = document.createElement('div');
609
+ body.className = 'nova-editor__body';
610
+ applyModeClass();
611
+ // ── Write Pane ────────────────────────────────────────────────────
612
+ const writePane = document.createElement('div');
613
+ writePane.className = 'nova-editor__pane-write';
614
+ // Line numbers gutter
615
+ let gutterEl = null;
616
+ if (lineNumbers) {
617
+ gutterEl = document.createElement('div');
618
+ gutterEl.className = 'nova-editor__line-numbers';
619
+ gutterEl.setAttribute('aria-hidden', 'true');
620
+ writePane.appendChild(gutterEl);
621
+ }
622
+ const textarea = document.createElement('textarea');
623
+ textarea.className = 'nova-editor__textarea';
624
+ textarea.value = initialValue;
625
+ textarea.placeholder = placeholder;
626
+ textarea.setAttribute('aria-label', 'Markdown input');
627
+ textarea.setAttribute('spellcheck', 'true');
628
+ writePane.appendChild(textarea);
629
+ // ── Divider ───────────────────────────────────────────────────────
630
+ const divider = document.createElement('div');
631
+ divider.className = 'nova-editor__divider';
632
+ // ── Preview Pane ──────────────────────────────────────────────────
633
+ const previewPane = document.createElement('div');
634
+ previewPane.className = 'nova-editor__pane-preview';
635
+ const previewContent = document.createElement('div');
636
+ previewContent.className = 'nova-editor__preview';
637
+ previewContent.setAttribute('role', 'document');
638
+ previewContent.setAttribute('aria-label', 'Markdown preview');
639
+ previewPane.appendChild(previewContent);
640
+ body.appendChild(writePane);
641
+ body.appendChild(divider);
642
+ body.appendChild(previewPane);
643
+ root.appendChild(body);
644
+ container.appendChild(root);
645
+ // ── Internal Helpers ──────────────────────────────────────────────
646
+ function applyModeClass() {
647
+ body.className = `nova-editor__body nova-editor__body--${currentMode}`;
648
+ // Toggle divider visibility
649
+ divider.style.display = currentMode === 'split' ? '' : 'none';
650
+ }
651
+ function updateModeButtons() {
652
+ for (const [mode, btn] of modeButtons) {
653
+ btn.setAttribute('aria-pressed', String(currentMode === mode));
654
+ }
655
+ }
656
+ function updatePreview() {
657
+ if (currentMode === 'write')
658
+ return;
659
+ // Rendered from trusted internal Nova markdown parser
660
+ previewContent.innerHTML = parse(textarea.value);
661
+ }
662
+ function updateLineNumbers() {
663
+ if (!gutterEl)
664
+ return;
665
+ const count = textarea.value.split('\n').length;
666
+ // Clear existing numbers
667
+ while (gutterEl.firstChild)
668
+ gutterEl.removeChild(gutterEl.firstChild);
669
+ for (let i = 1; i <= count; i++) {
670
+ const span = document.createElement('span');
671
+ span.textContent = String(i);
672
+ gutterEl.appendChild(span);
673
+ }
674
+ }
675
+ function syncGutterScroll() {
676
+ if (!gutterEl)
677
+ return;
678
+ gutterEl.scrollTop = textarea.scrollTop;
679
+ }
680
+ function schedulePreview() {
681
+ if (debounceTimer !== undefined)
682
+ clearTimeout(debounceTimer);
683
+ debounceTimer = setTimeout(updatePreview, 150);
684
+ }
685
+ function getLineStart(pos) {
686
+ const val = textarea.value;
687
+ const lineStart = val.lastIndexOf('\n', pos - 1);
688
+ return lineStart + 1;
689
+ }
690
+ function getCurrentLine(pos) {
691
+ const val = textarea.value;
692
+ const start = getLineStart(pos);
693
+ const end = val.indexOf('\n', pos);
694
+ return val.substring(start, end === -1 ? val.length : end);
695
+ }
696
+ // ── Editing Operations ────────────────────────────────────────────
697
+ function wrapSelection(before, after) {
698
+ if (destroyed)
699
+ return;
700
+ textarea.focus();
701
+ const start = textarea.selectionStart;
702
+ const end = textarea.selectionEnd;
703
+ const val = textarea.value;
704
+ const selected = val.substring(start, end);
705
+ const replacement = before + selected + after;
706
+ textarea.value = val.substring(0, start) + replacement + val.substring(end);
707
+ // Select the inner text (without the wrapping markers)
708
+ textarea.selectionStart = start + before.length;
709
+ textarea.selectionEnd = start + before.length + selected.length;
710
+ triggerChange();
711
+ }
712
+ function insertAtLineStart(prefix) {
713
+ if (destroyed)
714
+ return;
715
+ textarea.focus();
716
+ const start = textarea.selectionStart;
717
+ const val = textarea.value;
718
+ const lineStart = getLineStart(start);
719
+ textarea.value = val.substring(0, lineStart) + prefix + val.substring(lineStart);
720
+ textarea.selectionStart = start + prefix.length;
721
+ textarea.selectionEnd = start + prefix.length;
722
+ triggerChange();
723
+ }
724
+ function insertLink() {
725
+ if (destroyed)
726
+ return;
727
+ textarea.focus();
728
+ const start = textarea.selectionStart;
729
+ const end = textarea.selectionEnd;
730
+ const val = textarea.value;
731
+ const selected = val.substring(start, end);
732
+ if (selected) {
733
+ const replacement = `[${selected}](url)`;
734
+ textarea.value = val.substring(0, start) + replacement + val.substring(end);
735
+ // Select "url"
736
+ textarea.selectionStart = start + selected.length + 3;
737
+ textarea.selectionEnd = start + selected.length + 6;
738
+ }
739
+ else {
740
+ const replacement = '[text](url)';
741
+ textarea.value = val.substring(0, start) + replacement + val.substring(end);
742
+ // Select "text"
743
+ textarea.selectionStart = start + 1;
744
+ textarea.selectionEnd = start + 5;
745
+ }
746
+ triggerChange();
747
+ }
748
+ function insertImage() {
749
+ if (destroyed)
750
+ return;
751
+ textarea.focus();
752
+ const start = textarea.selectionStart;
753
+ const end = textarea.selectionEnd;
754
+ const val = textarea.value;
755
+ const selected = val.substring(start, end);
756
+ const alt = selected || 'alt';
757
+ const replacement = `![${alt}](url)`;
758
+ textarea.value = val.substring(0, start) + replacement + val.substring(end);
759
+ // Select "url"
760
+ textarea.selectionStart = start + alt.length + 4;
761
+ textarea.selectionEnd = start + alt.length + 7;
762
+ triggerChange();
763
+ }
764
+ function insertCode() {
765
+ if (destroyed)
766
+ return;
767
+ textarea.focus();
768
+ const start = textarea.selectionStart;
769
+ const end = textarea.selectionEnd;
770
+ const val = textarea.value;
771
+ const selected = val.substring(start, end);
772
+ // If selection spans multiple lines, use fenced code block
773
+ if (selected.includes('\n')) {
774
+ const replacement = '```\n' + selected + '\n```';
775
+ textarea.value = val.substring(0, start) + replacement + val.substring(end);
776
+ textarea.selectionStart = start + 4;
777
+ textarea.selectionEnd = start + 4 + selected.length;
778
+ }
779
+ else {
780
+ wrapSelection('`', '`');
781
+ return; // wrapSelection already calls triggerChange
782
+ }
783
+ triggerChange();
784
+ }
785
+ function insertText(text) {
786
+ if (destroyed)
787
+ return;
788
+ textarea.focus();
789
+ const start = textarea.selectionStart;
790
+ const end = textarea.selectionEnd;
791
+ const val = textarea.value;
792
+ textarea.value = val.substring(0, start) + text + val.substring(end);
793
+ textarea.selectionStart = start + text.length;
794
+ textarea.selectionEnd = start + text.length;
795
+ triggerChange();
796
+ }
797
+ function triggerChange() {
798
+ updateLineNumbers();
799
+ schedulePreview();
800
+ onChange?.(textarea.value);
801
+ }
802
+ // ── Event Handlers ────────────────────────────────────────────────
803
+ on(textarea, 'input', () => {
804
+ triggerChange();
805
+ });
806
+ on(textarea, 'scroll', () => {
807
+ syncGutterScroll();
808
+ });
809
+ // Tab key: insert 2 spaces
810
+ on(textarea, 'keydown', ((e) => {
811
+ if (e.key === 'Tab') {
812
+ e.preventDefault();
813
+ insertText(' ');
814
+ return;
815
+ }
816
+ // Enter: auto-continue lists
817
+ if (e.key === 'Enter') {
818
+ const pos = textarea.selectionStart;
819
+ const line = getCurrentLine(pos);
820
+ // Unordered list continuation
821
+ const ulMatch = line.match(/^(\s*[-*+]\s)/);
822
+ if (ulMatch) {
823
+ // If the line is just the prefix (empty item), remove it and break out
824
+ if (line.trim() === '-' || line.trim() === '*' || line.trim() === '+') {
825
+ e.preventDefault();
826
+ const lineStart = getLineStart(pos);
827
+ const lineEnd = textarea.value.indexOf('\n', pos);
828
+ const end = lineEnd === -1 ? textarea.value.length : lineEnd;
829
+ textarea.value = textarea.value.substring(0, lineStart) + textarea.value.substring(end);
830
+ textarea.selectionStart = lineStart;
831
+ textarea.selectionEnd = lineStart;
832
+ triggerChange();
833
+ return;
834
+ }
835
+ e.preventDefault();
836
+ insertText('\n' + ulMatch[1]);
837
+ return;
838
+ }
839
+ // Ordered list continuation
840
+ const olMatch = line.match(/^(\s*)(\d+)\.\s/);
841
+ if (olMatch) {
842
+ // If the line is just the number (empty item), remove it and break out
843
+ if (line.trim().match(/^\d+\.$/)) {
844
+ e.preventDefault();
845
+ const lineStart = getLineStart(pos);
846
+ const lineEnd = textarea.value.indexOf('\n', pos);
847
+ const end = lineEnd === -1 ? textarea.value.length : lineEnd;
848
+ textarea.value = textarea.value.substring(0, lineStart) + textarea.value.substring(end);
849
+ textarea.selectionStart = lineStart;
850
+ textarea.selectionEnd = lineStart;
851
+ triggerChange();
852
+ return;
853
+ }
854
+ e.preventDefault();
855
+ const nextNum = parseInt(olMatch[2], 10) + 1;
856
+ insertText('\n' + olMatch[1] + nextNum + '. ');
857
+ return;
858
+ }
859
+ }
860
+ }));
861
+ // Keyboard shortcuts
862
+ on(textarea, 'keydown', ((e) => {
863
+ const mod = e.metaKey || e.ctrlKey;
864
+ if (!mod)
865
+ return;
866
+ if (e.key === 'b') {
867
+ e.preventDefault();
868
+ wrapSelection('**', '**');
869
+ }
870
+ else if (e.key === 'i') {
871
+ e.preventDefault();
872
+ wrapSelection('*', '*');
873
+ }
874
+ else if (e.key === 'k') {
875
+ e.preventDefault();
876
+ insertLink();
877
+ }
878
+ else if (e.key === 's') {
879
+ e.preventDefault();
880
+ onSave?.(textarea.value);
881
+ }
882
+ else if (e.key === 'p' && e.shiftKey) {
883
+ e.preventDefault();
884
+ setMode(currentMode === 'preview' ? 'write' : 'preview');
885
+ }
886
+ }));
887
+ // Also handle Ctrl+Shift+P and Ctrl+S from anywhere in the editor
888
+ on(root, 'keydown', ((e) => {
889
+ const mod = e.metaKey || e.ctrlKey;
890
+ if (!mod)
891
+ return;
892
+ if (e.key === 'p' && e.shiftKey) {
893
+ e.preventDefault();
894
+ setMode(currentMode === 'preview' ? 'write' : 'preview');
895
+ }
896
+ if (e.key === 's' && !e.shiftKey) {
897
+ e.preventDefault();
898
+ onSave?.(textarea.value);
899
+ }
900
+ }));
901
+ // ── Mode Management ───────────────────────────────────────────────
902
+ function setMode(mode) {
903
+ if (destroyed)
904
+ return;
905
+ currentMode = mode;
906
+ applyModeClass();
907
+ updateModeButtons();
908
+ updatePreview();
909
+ updateLineNumbers();
910
+ }
911
+ // ── Initial Render ────────────────────────────────────────────────
912
+ updatePreview();
913
+ updateLineNumbers();
914
+ // ── Public Instance ───────────────────────────────────────────────
915
+ const instance = {
916
+ getValue() {
917
+ return textarea.value;
918
+ },
919
+ setValue(value) {
920
+ if (destroyed)
921
+ return;
922
+ textarea.value = value;
923
+ triggerChange();
924
+ },
925
+ getMode() {
926
+ return currentMode;
927
+ },
928
+ setMode,
929
+ focus() {
930
+ if (destroyed)
931
+ return;
932
+ textarea.focus();
933
+ },
934
+ insertText(text) {
935
+ insertText(text);
936
+ },
937
+ wrapSelection(before, after) {
938
+ wrapSelection(before, after);
939
+ },
940
+ destroy() {
941
+ if (destroyed)
942
+ return;
943
+ destroyed = true;
944
+ if (debounceTimer !== undefined)
945
+ clearTimeout(debounceTimer);
946
+ for (const [target, evt, fn, capture] of listeners) {
947
+ target.removeEventListener(evt, fn, capture);
948
+ }
949
+ listeners.length = 0;
950
+ root.remove();
951
+ },
952
+ };
953
+ return instance;
954
+ }
955
+ //# sourceMappingURL=editor.js.map