@cocoar/vue-ui 3.1.0 → 3.2.0-beta.6

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 (179) hide show
  1. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  3. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  5. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  6. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  7. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  9. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  10. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  11. package/dist/components/data-list/index.d.ts +12 -0
  12. package/dist/components/data-list/index.d.ts.map +1 -0
  13. package/dist/components/data-list/internal/compare.d.ts +14 -0
  14. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  15. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  17. package/dist/components/data-list/internal/search.d.ts +16 -0
  18. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  19. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  21. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  23. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  25. package/dist/components/data-list/types.d.ts +185 -0
  26. package/dist/components/data-list/types.d.ts.map +1 -0
  27. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  28. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  29. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  30. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  31. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  32. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  33. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  34. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  36. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  38. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  40. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  42. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  44. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  45. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  46. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  47. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  48. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  50. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  52. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  54. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  56. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  58. package/dist/composables/dragRegistry.d.ts +12 -0
  59. package/dist/composables/dragRegistry.d.ts.map +1 -1
  60. package/dist/composables/index.d.ts +2 -2
  61. package/dist/composables/index.d.ts.map +1 -1
  62. package/dist/composables/useDragDrop.d.ts +62 -6
  63. package/dist/composables/useDragDrop.d.ts.map +1 -1
  64. package/dist/composables/useVirtualList.d.ts +21 -1
  65. package/dist/composables/useVirtualList.d.ts.map +1 -1
  66. package/dist/index.css +1 -1
  67. package/dist/index.d.ts +4 -2
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4508 -2039
  70. package/package.json +5 -3
  71. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  72. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  73. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  74. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  75. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  76. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +353 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1645 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +475 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  86. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  87. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  89. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  90. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  91. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  99. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  100. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  101. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  102. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  103. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  104. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  105. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  106. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  110. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  111. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  112. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  116. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  117. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  118. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  119. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  120. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  121. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  122. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  127. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  128. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  129. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  130. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  131. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  132. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  133. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  134. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  140. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  141. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  142. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  143. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  144. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  145. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  146. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  147. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  148. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  149. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  150. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  151. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  152. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  153. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  154. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  155. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  156. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  157. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  158. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  159. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  160. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  161. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  162. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  163. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  164. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  165. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  166. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  167. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  171. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  172. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  173. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  174. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  175. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  176. package/skills/cocoar-vue-ui/references/guide/getting-started.md +119 -0
  177. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  179. package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
@@ -0,0 +1,1271 @@
1
+ <!-- Generated from apps/docs/components/script-editor.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # Script Editor
4
+
5
+ A Monaco-based code editor for Vue 3 with Cocoar Design System theming. Supports **TypeScript, JavaScript, and JSON**, with first-class support for user-supplied type definitions (IntelliSense for your domain types).
6
+
7
+ > **Info: Separate Package**
8
+ >
9
+ ```bash
10
+ pnpm add @cocoar/vue-script-editor monaco-editor
11
+ ```
12
+ > `monaco-editor` is a peer dependency — consumers install and configure it themselves. This keeps the library bundle small and lets each app decide which Monaco languages and features to ship.
13
+
14
+ ## Worker Setup
15
+
16
+ Monaco offloads language services to Web Workers. Register them once **before any editor mounts**. Pick the pattern that matches your app's shape.
17
+
18
+ ### SPA (client-only, Vite)
19
+
20
+ The common case. Register at application entry (`src/main.ts` or equivalent):
21
+
22
+ ```ts
23
+ import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
24
+ import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
25
+ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
26
+
27
+ self.MonacoEnvironment = {
28
+ getWorker(_id, label) {
29
+ if (label === 'typescript' || label === 'javascript') return new TsWorker();
30
+ if (label === 'json') return new JsonWorker();
31
+ return new EditorWorker();
32
+ },
33
+ };
34
+ ```
35
+
36
+ Omit the JSON branch if your app never uses `language="json"`.
37
+
38
+ ### SSR / static-generation (VitePress, Nuxt, Astro)
39
+
40
+ Monaco touches `window` and the DOM, so it cannot run during server-side rendering. Defer both worker registration and the editor import to `onMounted`, and wrap the template in `<ClientOnly>`:
41
+
42
+ ```vue
43
+ <template>
44
+ <ClientOnly>
45
+ <component :is="Editor" v-if="Editor" v-model="code" />
46
+ </ClientOnly>
47
+ </template>
48
+
49
+ <script setup lang="ts">
50
+ import { onMounted, ref, shallowRef, type Component } from 'vue';
51
+
52
+ const Editor = shallowRef<Component | null>(null);
53
+ const code = ref('// ...');
54
+
55
+ onMounted(async () => {
56
+ const [mod, editorWorker, tsWorker, jsonWorker] = await Promise.all([
57
+ import('@cocoar/vue-script-editor'),
58
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
59
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
60
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
61
+ ]);
62
+ self.MonacoEnvironment = {
63
+ getWorker(_id, label) {
64
+ if (label === 'typescript' || label === 'javascript') return new tsWorker.default();
65
+ if (label === 'json') return new jsonWorker.default();
66
+ return new editorWorker.default();
67
+ },
68
+ };
69
+ Editor.value = mod.CoarScriptEditor;
70
+ });
71
+ </script>
72
+ ```
73
+
74
+ > **Info: Where does `<ClientOnly>` come from?**
75
+ >
76
+ > VitePress and Nuxt register `<ClientOnly>` globally. In a plain Vite SPA you don't need it — the SPA pattern above is simpler. In Astro use `client:only="vue"` on the component instead.
77
+
78
+ Other bundlers (Webpack, Rollup, esbuild, or a CDN setup) use the same `self.MonacoEnvironment.getWorker` contract with different worker-import syntax — see [Monaco's official docs](https://github.com/microsoft/monaco-editor/tree/main/docs).
79
+
80
+ ## Basic Usage
81
+
82
+ The editor exposes a plain `v-model` for the source text. Use `language` to choose between `'typescript'` (default), `'javascript'`, and `'json'`.
83
+
84
+ **Demo — `script-editor/demos/ScriptEditorBasic.vue`**
85
+
86
+ ```vue
87
+ <template>
88
+ <ClientOnly>
89
+ <component
90
+ :is="Editor"
91
+ v-if="Editor"
92
+ v-model="code"
93
+ language="typescript"
94
+ style="height: 320px"
95
+ />
96
+ <div v-else class="loading">Loading editor…</div>
97
+ </ClientOnly>
98
+ </template>
99
+
100
+ <script setup lang="ts">
101
+ import { onMounted, ref, shallowRef, type Component } from 'vue';
102
+
103
+ const code = ref(`function greet(name: string): string {
104
+ return \`Hello, \${name}!\`;
105
+ }
106
+
107
+ const message = greet('World');
108
+ console.log(message);
109
+ `);
110
+
111
+ const Editor = shallowRef<Component | null>(null);
112
+
113
+ onMounted(async () => {
114
+ const [mod, editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
115
+ import('@cocoar/vue-script-editor'),
116
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
117
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
118
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
119
+ ]);
120
+ (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
121
+ getWorker(_id: string, label: string) {
122
+ if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
123
+ if (label === 'json') return new jsonWorkerMod.default();
124
+ return new editorWorkerMod.default();
125
+ },
126
+ };
127
+ Editor.value = mod.CoarScriptEditor;
128
+ });
129
+ </script>
130
+
131
+ <style scoped>
132
+ .loading {
133
+ padding: 24px;
134
+ text-align: center;
135
+ color: var(--coar-text-neutral-tertiary, #6b7280);
136
+ font-size: 13px;
137
+ }
138
+ </style>
139
+ ```
140
+
141
+ ```vue
142
+ <template>
143
+ <CoarScriptEditor v-model="code" language="typescript" style="height: 320px" />
144
+ </template>
145
+
146
+ <script setup lang="ts">
147
+ import { ref } from 'vue';
148
+ import { CoarScriptEditor } from '@cocoar/vue-script-editor';
149
+
150
+ const code = ref(`function greet(name: string) {\n return \`Hello, \${name}\`;\n}\n`);
151
+ </script>
152
+ ```
153
+
154
+ > **Tip: Sizing**
155
+ >
156
+ > The editor fills its parent container. Either pass a `height` prop (`"160px"`, `240`, `"40vh"`) or wrap in a parent with explicit height. The editor's own default `min-height: 200px` only applies when no parent height is set.
157
+
158
+ ## Form Integration
159
+
160
+ `CoarScriptEditor` is a full citizen of the Cocoar form ecosystem. Drop it inside `CoarFormField` and label, error message, `aria-describedby` wiring, and disabled state propagate automatically — the same way `CoarTextInput` and `CoarSelect` behave. Use `variant="inline"` for a compact form-field look (no line numbers, no gutter, tight padding) and `script-mode` to suppress the "top-level return/await" errors Monaco normally emits for full `.ts` programs.
161
+
162
+ `preamble` gives you per-editor type context without polluting the global TS namespace: the declaration lines render invisibly above the user script (hidden + locked), and `modelValue` only round-trips the user portion.
163
+
164
+ **Demo — `script-editor/demos/ScriptEditorInForm.vue`**
165
+
166
+ ```vue
167
+ <template>
168
+ <ClientOnly>
169
+ <div v-if="Editor && FormField && Button" class="form-demo">
170
+ <component :is="FormField" label="Script name" :error="nameError" required>
171
+ <component :is="TextInput" v-model="form.name" placeholder="mytask" />
172
+ </component>
173
+
174
+ <component :is="FormField" label="Handler script" :error="scriptError" hint="TypeScript body. `query` is pre-declared." required>
175
+ <component
176
+ :is="Editor"
177
+ v-model="form.script"
178
+ variant="inline"
179
+ language="typescript"
180
+ height="180px"
181
+ placeholder="// return query.filter(...)"
182
+ script-mode
183
+ preamble="declare const query: TodoQuery;"
184
+ :extra-libs="extraLibs"
185
+ />
186
+ </component>
187
+
188
+ <div class="row">
189
+ <component :is="Button" type="primary" @clicked="onSubmit">Save</component>
190
+ <component :is="Button" @clicked="onReset">Reset</component>
191
+ </div>
192
+
193
+ <pre class="preview">{{ preview }}</pre>
194
+ </div>
195
+ <div v-else class="loading">Loading editor…</div>
196
+ </ClientOnly>
197
+ </template>
198
+
199
+ <script setup lang="ts">
200
+ import { computed, onMounted, reactive, ref, shallowRef, type Component } from 'vue';
201
+
202
+ const form = reactive({
203
+ name: '',
204
+ script: '',
205
+ });
206
+
207
+ const nameError = computed(() => (form.name.length === 0 ? 'Required.' : ''));
208
+ const scriptError = computed(() =>
209
+ form.script.trim().length === 0 ? 'Provide a handler body.' : '',
210
+ );
211
+ const preview = computed(() => JSON.stringify(form, null, 2));
212
+
213
+ function onSubmit() {
214
+ if (nameError.value || scriptError.value) return;
215
+ // eslint-disable-next-line no-console
216
+ console.log('submit', form);
217
+ }
218
+
219
+ function onReset() {
220
+ form.name = '';
221
+ form.script = '';
222
+ }
223
+
224
+ const extraLibs = [
225
+ {
226
+ filePath: 'file:///types/todo-query.d.ts',
227
+ content: `
228
+ interface Todo { id: string; title: string; done: boolean; }
229
+ interface TodoQuery {
230
+ filter(predicate: (todo: Todo) => boolean): Todo[];
231
+ find(predicate: (todo: Todo) => boolean): Todo | undefined;
232
+ map<T>(mapper: (todo: Todo) => T): T[];
233
+ }
234
+ `.trim(),
235
+ },
236
+ ];
237
+
238
+ const Editor = shallowRef<Component | null>(null);
239
+ const FormField = shallowRef<Component | null>(null);
240
+ const TextInput = shallowRef<Component | null>(null);
241
+ const Button = shallowRef<Component | null>(null);
242
+
243
+ onMounted(async () => {
244
+ const [mod, ui, editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
245
+ import('@cocoar/vue-script-editor'),
246
+ import('@cocoar/vue-ui'),
247
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
248
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
249
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
250
+ ]);
251
+ (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
252
+ getWorker(_id: string, label: string) {
253
+ if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
254
+ if (label === 'json') return new jsonWorkerMod.default();
255
+ return new editorWorkerMod.default();
256
+ },
257
+ };
258
+ Editor.value = mod.CoarScriptEditor;
259
+ FormField.value = ui.CoarFormField;
260
+ TextInput.value = ui.CoarTextInput;
261
+ Button.value = ui.CoarButton;
262
+ });
263
+ </script>
264
+
265
+ <style scoped>
266
+ .form-demo {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: 16px;
270
+ }
271
+
272
+ .row {
273
+ display: flex;
274
+ gap: 8px;
275
+ }
276
+
277
+ .preview {
278
+ background: var(--coar-surface-neutral-secondary, #f9fafb);
279
+ border: 1px solid var(--coar-border-neutral-tertiary, #e5e7eb);
280
+ border-radius: 4px;
281
+ padding: 12px;
282
+ font-size: 12px;
283
+ color: var(--coar-text-neutral-secondary, #4b5563);
284
+ overflow: auto;
285
+ margin: 0;
286
+ }
287
+
288
+ .loading {
289
+ padding: 24px;
290
+ text-align: center;
291
+ color: var(--coar-text-neutral-tertiary, #6b7280);
292
+ font-size: 13px;
293
+ }
294
+ </style>
295
+ ```
296
+
297
+ ```vue
298
+ <template>
299
+ <CoarFormField label="Handler script" :error="scriptError" required>
300
+ <CoarScriptEditor
301
+ v-model="form.script"
302
+ variant="inline"
303
+ language="typescript"
304
+ height="180px"
305
+ placeholder="// return query.filter(...)"
306
+ script-mode
307
+ preamble="declare const query: TodoQuery;"
308
+ :extra-libs="[{ filePath: 'file:///types/todo-query.d.ts', content: todoQueryTypes }]"
309
+ />
310
+ </CoarFormField>
311
+ </template>
312
+ ```
313
+
314
+ ### `preamble` — per-editor type context
315
+
316
+ `preamble` is a hidden, auto-locked prefix prepended to the editor content. It's rendered invisibly, can't be edited or cursored into, and never appears in the emitted `modelValue`. The TypeScript service sees it as normal source, so IntelliSense resolves symbols declared inside it.
317
+
318
+ Typical use-case: your runtime executes the user's script as a function body with a specific set of bindings (`query`, `ctx`, `request`, …). Declare them in the preamble so the editor IntelliSense matches the runtime shape exactly:
319
+
320
+ ```vue
321
+ <CoarScriptEditor
322
+ v-model="form.script"
323
+ preamble="declare const query: TodoQuery;\ndeclare const ctx: WorkflowCtx;"
324
+ />
325
+ ```
326
+
327
+ **When to use `preamble` vs `extraLibs`:**
328
+
329
+ | Signal | Use `preamble` | Use `extraLibs` |
330
+ | ------------------------------------------------- | ---------------------------------------- | ---------------------------------- |
331
+ | Scope should be limited to this editor instance | ✅ | ❌ (globally ambient) |
332
+ | Different editors need different variable names | ✅ | ❌ |
333
+ | App-wide shared domain types, interfaces | ❌ | ✅ |
334
+ | You want the declaration to match a runtime shape | ✅ (declaration is literal code) | 〰️ (works if wrapped in `declare global`) |
335
+
336
+ The two are complementary — most real forms use `extraLibs` for interfaces (`TodoQuery`, `Todo`, …) and `preamble` for the bindings that actually exist at runtime (`declare const query: TodoQuery`).
337
+
338
+ ### `script-mode` — suppress script-body diagnostics
339
+
340
+ Enables suppression of the diagnostic codes TypeScript emits for "script body" constructs that are invalid in a full program but expected in an executable snippet:
341
+
342
+ | Code | Suppressed meaning |
343
+ | ------ | ----------------------------------------------------------------------------- |
344
+ | `1108` | `return` statement outside a function |
345
+ | `1208` | Cannot use `export` in a non-module (`export {}` forces module scope) |
346
+ | `1375` | `await` allowed only in async functions |
347
+ | `2304` | Cannot find name … (when the user relies on pre-injected globals) |
348
+ | `2695` | Left-hand side of assignment is invalid |
349
+ | `7027` | Unreachable code detected |
350
+
351
+ > **Warning: Global side-effect**
352
+ >
353
+ > `script-mode` calls `monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions` which is **shared across every TS/JS editor on the page**. The codes are additive — Cocoar merges them into the existing ignore list and never clears them, so toggling `script-mode` off does not restore the diagnostics. If your app mixes "full program" editors with "script body" editors, use the escape-hatch (`monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions`) from the consumer side for finer control.
354
+
355
+ ### Compact `variant="inline"`
356
+
357
+ Flipping `variant` to `'inline'` restyles Monaco for form-field use — no line numbers, no glyph margin, folding off, context menu off, tight 8px padding, word wrap on, and a hover/focus ring that matches `CoarTextInput`.
358
+
359
+ ```vue
360
+ <CoarScriptEditor v-model="code" variant="inline" height="160px" />
361
+ ```
362
+
363
+ Use `'editor'` (the default) whenever the editor is the page focus — IDE-like experience, line numbers, full gutter.
364
+
365
+ ## Custom Type Definitions (`extraLibs`)
366
+
367
+ Inject `.d.ts` contents into the editor's TypeScript service to get autocomplete, hover types, and inline diagnostics for your domain objects — without polluting the global TS server.
368
+
369
+ **Demo — `script-editor/demos/ScriptEditorExtraLibs.vue`**
370
+
371
+ ```vue
372
+ <template>
373
+ <ClientOnly>
374
+ <component
375
+ :is="Editor"
376
+ v-if="Editor"
377
+ v-model="code"
378
+ language="typescript"
379
+ :extra-libs="extraLibs"
380
+ style="height: 360px"
381
+ />
382
+ <div v-else class="loading">Loading editor…</div>
383
+ </ClientOnly>
384
+ </template>
385
+
386
+ <script setup lang="ts">
387
+ import { onMounted, ref, shallowRef, type Component } from 'vue';
388
+
389
+ type ExtraLib = { content: string; filePath: string };
390
+
391
+ const extraLibs: ExtraLib[] = [
392
+ {
393
+ content: `declare interface AppContext {
394
+ user: { id: string; name: string; email: string };
395
+ tenant: { id: string; name: string; plan: 'free' | 'pro' | 'enterprise' };
396
+ }
397
+
398
+ declare function getContext(): AppContext;
399
+ `,
400
+ filePath: 'file:///types/app-context.d.ts',
401
+ },
402
+ ];
403
+
404
+ const code = ref(`// Try typing \`ctx.\` to get autocompletion from the injected types.
405
+ const ctx = getContext();
406
+ const greeting = \`Welcome \${ctx.user.name} from \${ctx.tenant.name}\`;
407
+ `);
408
+
409
+ const Editor = shallowRef<Component | null>(null);
410
+
411
+ onMounted(async () => {
412
+ const [mod, editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
413
+ import('@cocoar/vue-script-editor'),
414
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
415
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
416
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
417
+ ]);
418
+ (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
419
+ getWorker(_id: string, label: string) {
420
+ if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
421
+ if (label === 'json') return new jsonWorkerMod.default();
422
+ return new editorWorkerMod.default();
423
+ },
424
+ };
425
+ Editor.value = mod.CoarScriptEditor;
426
+ });
427
+ </script>
428
+
429
+ <style scoped>
430
+ .loading {
431
+ padding: 24px;
432
+ text-align: center;
433
+ color: var(--coar-text-neutral-tertiary, #6b7280);
434
+ font-size: 13px;
435
+ }
436
+ </style>
437
+ ```
438
+
439
+ ```vue
440
+ <script setup lang="ts">
441
+ import { ref } from 'vue';
442
+ import { CoarScriptEditor, type CoarScriptEditorExtraLib } from '@cocoar/vue-script-editor';
443
+
444
+ const extraLibs: CoarScriptEditorExtraLib[] = [
445
+ {
446
+ content: `declare interface AppContext {
447
+ user: { id: string; name: string };
448
+ tenant: { id: string; name: string };
449
+ }
450
+
451
+ declare function getContext(): AppContext;`,
452
+ filePath: 'file:///types/app-context.d.ts',
453
+ },
454
+ ];
455
+
456
+ const code = ref(`const ctx = getContext();\nconsole.log(ctx.user.name);\n`);
457
+ </script>
458
+
459
+ <template>
460
+ <CoarScriptEditor v-model="code" language="typescript" :extra-libs="extraLibs" />
461
+ </template>
462
+ ```
463
+
464
+ Each entry maps to `monaco.languages.typescript.typescriptDefaults.addExtraLib(...)` (or `javascriptDefaults` in JS mode). Use a stable, unique `filePath` per lib — Monaco keys its entries on the path.
465
+
466
+ ## Runtime lib configuration
467
+
468
+ Monaco ships with a default lib set of `es5 + dom + webworker.importscripts + scripthost` — surfacing thousands of browser APIs (`document.*`, `fetch`, `localStorage`, `WScript`, …) in IntelliSense. For script editors backed by a non-browser runtime (e.g. Jint/Edge.js in a .NET host), autocompleting those APIs would lure users into writing code that crashes at execution time.
469
+
470
+ `CoarScriptEditor` therefore forces Monaco to `lib: ['es2024']` the first time it's mounted, dropping the browser-specific libs and keeping only the standard ECMAScript surface. It also applies to both TS and JS defaults and sets `target: ES2024`, `allowNonTsExtensions: true`, `noResolve: true`.
471
+
472
+ Host-specific globals (e.g. your runtime's `fetch`, `require`, `exit`) should be layered on top via `extraLibs` — opt-in and explicit, so what Monaco shows matches what Jint can run.
473
+
474
+ If you need a different lib set for non-Jint scenarios, call `monaco.languages.typescript.typescriptDefaults.setCompilerOptions(...)` yourself **after** the first `CoarScriptEditor` has mounted — `setCompilerOptions` is a module-global last-writer-wins, so your override takes effect immediately for every editor on the page.
475
+
476
+ > **Warning: `filePath` must start with `file:///`**
477
+ >
478
+ > Monaco's TypeScript service silently ignores declarations registered under any other URI scheme, so a value like `'types/foo.d.ts'` will compile without error but produce no IntelliSense. In development mode the component emits a `console.warn` when it detects this, but there's no runtime error — it's easy to miss in production. Always prefix with `file:///`.
479
+
480
+ > **Warning: Untrusted content**
481
+ >
482
+ > `extraLibs.content` is parsed by Monaco's TypeScript service as a `.d.ts` file — it is not `eval`'d, so arbitrary code in `content` cannot execute in the browser. Declaration files *can* however expose surprising types / module augmentations. If the content comes from untrusted sources (e.g. another tenant's template), treat it as you would any other user-generated data: validate server-side and consider sandboxing the editor in an iframe with a restrictive CSP.
483
+
484
+ ## JSON mode
485
+
486
+ Set `language="json"` to edit JSON with Monaco's native JSON services — syntax validation, bracket matching, format-on-save, and optional schema-based IntelliSense all work out of the box.
487
+
488
+ **Demo — `script-editor/demos/ScriptEditorJson.vue`**
489
+
490
+ ```vue
491
+ <template>
492
+ <ClientOnly>
493
+ <component
494
+ :is="Editor"
495
+ v-if="Editor"
496
+ v-model="code"
497
+ language="json"
498
+ style="height: 280px"
499
+ />
500
+ <div v-else class="loading">Loading editor…</div>
501
+ </ClientOnly>
502
+ </template>
503
+
504
+ <script setup lang="ts">
505
+ import { onMounted, ref, shallowRef, type Component } from 'vue';
506
+
507
+ const code = ref(`{
508
+ "name": "my-app",
509
+ "version": "1.0.0",
510
+ "scripts": {
511
+ "dev": "vite",
512
+ "build": "vite build"
513
+ },
514
+ "dependencies": {
515
+ "vue": "^3.5.0"
516
+ }
517
+ }`);
518
+
519
+ const Editor = shallowRef<Component | null>(null);
520
+
521
+ onMounted(async () => {
522
+ const [mod, editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
523
+ import('@cocoar/vue-script-editor'),
524
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
525
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
526
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
527
+ ]);
528
+ (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
529
+ getWorker(_id: string, label: string) {
530
+ if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
531
+ if (label === 'json') return new jsonWorkerMod.default();
532
+ return new editorWorkerMod.default();
533
+ },
534
+ };
535
+ Editor.value = mod.CoarScriptEditor;
536
+ });
537
+ </script>
538
+
539
+ <style scoped>
540
+ .loading {
541
+ padding: 24px;
542
+ text-align: center;
543
+ color: var(--coar-text-neutral-tertiary, #6b7280);
544
+ font-size: 13px;
545
+ }
546
+ </style>
547
+ ```
548
+
549
+ ```vue
550
+ <script setup lang="ts">
551
+ import { ref } from 'vue';
552
+ import { CoarScriptEditor } from '@cocoar/vue-script-editor';
553
+
554
+ const config = ref(`{
555
+ "name": "example",
556
+ "version": "1.0.0"
557
+ }`);
558
+ </script>
559
+
560
+ <template>
561
+ <CoarScriptEditor v-model="config" language="json" />
562
+ </template>
563
+ ```
564
+
565
+ > **Tip: JSON schemas**
566
+ >
567
+ > `extraLibs` is TypeScript-specific and is ignored in JSON mode. For schema-driven validation and autocompletion, call Monaco's JSON defaults directly — typically once at app entry:
568
+ >
569
+ ```ts
570
+ import * as monaco from 'monaco-editor';
571
+
572
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
573
+ validate: true,
574
+ schemas: [
575
+ {
576
+ uri: 'https://my-app/schemas/config.json',
577
+ fileMatch: ['*.json'],
578
+ schema: { type: 'object', required: ['name'], properties: { name: { type: 'string' } } },
579
+ },
580
+ ],
581
+ });
582
+ ```
583
+ >
584
+ > If you need per-editor schema attachment, use the `getEditor()` escape hatch to access the model's URI and register matching schemas.
585
+
586
+ ## Read-only & Minimap
587
+
588
+ Both boolean flags default to `false`. Toggle `readonly` for viewer-style contexts and `minimap` when long files benefit from the overview gutter.
589
+
590
+ **Demo — `script-editor/demos/ScriptEditorReadonly.vue`**
591
+
592
+ ```vue
593
+ <template>
594
+ <ClientOnly>
595
+ <component
596
+ :is="Editor"
597
+ v-if="Editor"
598
+ v-model="code"
599
+ language="javascript"
600
+ readonly
601
+ minimap
602
+ style="height: 260px"
603
+ />
604
+ <div v-else class="loading">Loading editor…</div>
605
+ </ClientOnly>
606
+ </template>
607
+
608
+ <script setup lang="ts">
609
+ import { onMounted, ref, shallowRef, type Component } from 'vue';
610
+
611
+ const code = ref(`// Read-only JavaScript snippet with minimap enabled.
612
+ const items = [1, 2, 3, 4, 5];
613
+ const doubled = items.map((n) => n * 2);
614
+ console.log(doubled);
615
+ `);
616
+
617
+ const Editor = shallowRef<Component | null>(null);
618
+
619
+ onMounted(async () => {
620
+ const [mod, editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
621
+ import('@cocoar/vue-script-editor'),
622
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
623
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
624
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
625
+ ]);
626
+ (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
627
+ getWorker(_id: string, label: string) {
628
+ if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
629
+ if (label === 'json') return new jsonWorkerMod.default();
630
+ return new editorWorkerMod.default();
631
+ },
632
+ };
633
+ Editor.value = mod.CoarScriptEditor;
634
+ });
635
+ </script>
636
+
637
+ <style scoped>
638
+ .loading {
639
+ padding: 24px;
640
+ text-align: center;
641
+ color: var(--coar-text-neutral-tertiary, #6b7280);
642
+ font-size: 13px;
643
+ }
644
+ </style>
645
+ ```
646
+
647
+ ## Constrained Mode (Protected Lines)
648
+
649
+ Any line of the source that contains `// @locked` is protected: the user cannot edit it, merge it with a neighbour, or delete it. Everything else — other lines, file-top imports, helpers between functions — is freely editable.
650
+
651
+ The TypeScript language service sees the whole file as one document, so IntelliSense, Auto-Import, and domain types resolve exactly as in a normal `.ts` file. Markers are plain line comments, so the stored value is also a valid `.ts` file in any toolchain.
652
+
653
+ **Demo — `script-editor/demos/ScriptEditorConstrained.vue`**
654
+
655
+ ```vue
656
+ <template>
657
+ <ClientOnly>
658
+ <div v-if="Editor" class="constrained-demo">
659
+ <div class="toolbar">
660
+ <label>
661
+ <input v-model="authoring" type="checkbox" />
662
+ Authoring mode — enforcement {{ authoring ? 'OFF' : 'ON' }}
663
+ </label>
664
+ </div>
665
+ <component
666
+ :is="Editor"
667
+ v-model="code"
668
+ :authoring="authoring"
669
+ language="typescript"
670
+ :extra-libs="extraLibs"
671
+ style="height: 320px"
672
+ />
673
+ <details>
674
+ <summary>Persisted value (<code>v-model</code>)</summary>
675
+ <pre>{{ code }}</pre>
676
+ </details>
677
+ </div>
678
+ <div v-else class="loading">Loading editor…</div>
679
+ </ClientOnly>
680
+ </template>
681
+
682
+ <script setup lang="ts">
683
+ import { onMounted, ref, shallowRef, type Component } from 'vue';
684
+
685
+ type ExtraLib = { content: string; filePath: string };
686
+
687
+ const code = ref(`declare interface Order {
688
+ id: string;
689
+ total: number;
690
+ customer: { id: string; name: string };
691
+ }
692
+
693
+ function describeOrder(order: Order): string { // @locked
694
+ return \`Order \${order.id} for \${order.customer.name}\`;
695
+ } // @locked
696
+
697
+ function orderTotal(orders: Order[]): number { // @locked
698
+ return orders.reduce((sum, o) => sum + o.total, 0);
699
+ } // @locked
700
+ `);
701
+
702
+ const authoring = ref(false);
703
+
704
+ const extraLibs: ExtraLib[] = [
705
+ {
706
+ content: `declare interface Order {
707
+ id: string;
708
+ total: number;
709
+ customer: { id: string; name: string };
710
+ }`,
711
+ filePath: 'file:///types/order.d.ts',
712
+ },
713
+ ];
714
+
715
+ const Editor = shallowRef<Component | null>(null);
716
+
717
+ onMounted(async () => {
718
+ const [mod, editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
719
+ import('@cocoar/vue-script-editor'),
720
+ import('monaco-editor/esm/vs/editor/editor.worker?worker'),
721
+ import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
722
+ import('monaco-editor/esm/vs/language/json/json.worker?worker'),
723
+ ]);
724
+ (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
725
+ getWorker(_id: string, label: string) {
726
+ if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
727
+ if (label === 'json') return new jsonWorkerMod.default();
728
+ return new editorWorkerMod.default();
729
+ },
730
+ };
731
+ Editor.value = mod.CoarScriptEditor;
732
+ });
733
+ </script>
734
+
735
+ <style scoped>
736
+ .constrained-demo {
737
+ display: flex;
738
+ flex-direction: column;
739
+ gap: 8px;
740
+ }
741
+
742
+ .toolbar {
743
+ padding: 6px 10px;
744
+ font-size: 12px;
745
+ background: var(--coar-background-neutral-tertiary, #f3f4f6);
746
+ border-radius: 4px;
747
+ }
748
+
749
+ .toolbar label {
750
+ display: flex;
751
+ align-items: center;
752
+ gap: 8px;
753
+ cursor: pointer;
754
+ }
755
+
756
+ details {
757
+ font-size: 11px;
758
+ }
759
+
760
+ details pre {
761
+ margin: 8px 0 0;
762
+ padding: 8px;
763
+ background: var(--coar-background-neutral-tertiary, #f3f4f6);
764
+ border-radius: 4px;
765
+ white-space: pre-wrap;
766
+ word-break: break-word;
767
+ }
768
+
769
+ .loading {
770
+ padding: 24px;
771
+ text-align: center;
772
+ color: var(--coar-text-neutral-tertiary, #6b7280);
773
+ font-size: 13px;
774
+ }
775
+ </style>
776
+ ```
777
+
778
+ ```vue
779
+ <script setup lang="ts">
780
+ import { ref } from 'vue';
781
+ import { CoarScriptEditor } from '@cocoar/vue-script-editor';
782
+
783
+ const code = ref(`declare interface Order {
784
+ id: string;
785
+ total: number;
786
+ }
787
+
788
+ function describeOrder(order: Order): string { // @locked
789
+ return \`Order \${order.id}\`;
790
+ } // @locked
791
+ `);
792
+ </script>
793
+
794
+ <template>
795
+ <CoarScriptEditor v-model="code" language="typescript" />
796
+ </template>
797
+ ```
798
+
799
+ ### Why line-based `@locked` (and not open/close tags)
800
+
801
+ Three real-world benefits:
802
+
803
+ - **Auto-Import works.** When the user types an unresolved type, TypeScript's quickfix inserts the `import` at the top of the file — a non-locked line, so the edit passes. Locked lines below simply shift down with the rest of the text; Monaco's native text handling does the bookkeeping for us.
804
+ - **No pairing errors.** Each `// @locked` marker is self-contained; you can't accidentally leave one open or nest them wrong. Copy-pasting a function across templates "just works".
805
+ - **Helpers fit naturally.** The user can declare local types, utility functions, or blank lines between the locked signatures without asking permission.
806
+
807
+ ### The model value is the persistence format
808
+
809
+ `v-model` always contains the full source *including* the `// @locked` comments. Save and reload the exact same string — nothing to serialize:
810
+
811
+ ```ts
812
+ // Save
813
+ localStorage.setItem('snippet', code.value);
814
+
815
+ // Load (later)
816
+ code.value = localStorage.getItem('snippet') ?? defaultTemplate;
817
+ ```
818
+
819
+ > **Tip: Free mode**
820
+ >
821
+ > Without any `// @locked` markers the editor behaves like a regular code editor — no guards, no decorations. Adding a marker to any line turns enforcement on for just that line.
822
+
823
+ ### Reacting to rejected edits
824
+
825
+ Bind `@reject` to surface user feedback when an edit was rolled back:
826
+
827
+ ```vue
828
+ <script setup lang="ts">
829
+ import type { CoarScriptEditorRejectEvent } from '@cocoar/vue-script-editor';
830
+
831
+ function onReject(event: CoarScriptEditorRejectEvent) {
832
+ // Show a toast, trigger a shake animation, flash the affected line, etc.
833
+ const line = event.range?.startLineNumber;
834
+ toast.warn(`This line is protected.${line != null ? ` (line ${line})` : ''}`);
835
+ }
836
+ </script>
837
+
838
+ <template>
839
+ <CoarScriptEditor v-model="code" @reject="onReject" />
840
+ </template>
841
+ ```
842
+
843
+ ### Authoring mode (`authoring` prop)
844
+
845
+ Template authors need to edit the protected parts too. Pass `:authoring="true"` to suspend enforcement: locked lines become editable, markers render at full size in a warm accent colour, and the author can add new markers or remove existing ones. Toggle the prop back to `false` and enforcement resumes with whatever markers are currently in the text.
846
+
847
+ ```vue
848
+ <script setup lang="ts">
849
+ const editing = ref(false);
850
+ </script>
851
+
852
+ <template>
853
+ <button @click="editing = !editing">
854
+ {{ editing ? 'Exit authoring mode' : 'Enter authoring mode' }}
855
+ </button>
856
+ <CoarScriptEditor v-model="code" :authoring="editing" />
857
+ </template>
858
+ ```
859
+
860
+ The demo above exposes the same toggle inline so you can flip between the modes.
861
+
862
+ ### How it works
863
+
864
+ - **Marker detection**: `/\/\/\s*@locked\b/` — a line is locked if it contains `// @locked` anywhere. Match is case-sensitive; `@lockedx` does not count.
865
+ - **Protected range**: every locked line is protected inclusively, including its trailing newline. Backspace at the start of a line below a locked one, Delete at the end of a line above — both blocked.
866
+ - **Overlap check**: if any change in the batch intersects any protected range, the entire multi-cursor batch is rolled back via `editor.trigger('undo')`. An internal stack boundary keeps the rejected edit from coalescing with surrounding legal edits.
867
+ - **Cursor guard**: cursors that land inside a locked line snap to the nearer free boundary. Silenced when `authoring` is on.
868
+ - **Diagnostics filter**: error-severity markers emitted on locked lines are suppressed — an in-progress body that makes TypeScript mark the signature as broken won't surface that to the user, because they can't fix it anyway. Warnings and info remain; the filter also stands down in `authoring` mode so authors see everything.
869
+ - **Auto-features policy**: Monaco's `formatOnType`, `formatOnPaste`, and `linkedEditing` are turned off in constrained mode so cross-boundary reformats don't generate confusing rejections. Auto-Import (the lightbulb quickfix) is deliberately left on — its edits go to the file top, which is virtually always outside any lock.
870
+ - **Rejections**: `@reject` emits an object `{ reason, range? }` so you can surface a toast, shake animation, or highlight the affected line range. See the Events section below for the full payload.
871
+
872
+ > **Warning: Editor-option override**
873
+ >
874
+ > The auto-feature overrides above run on mount when constrained mode activates. If you call `editorRef.value?.getEditor().updateOptions({ formatOnType: true })` from consumer code, the override will re-apply next time constrained mode is set up (e.g. when `modelValue` gains or loses `// @locked` markers). If you need to re-enable these features for a constrained-mode editor, file an issue describing your use case — we may add an opt-out prop.
875
+
876
+ ### Styling
877
+
878
+ Two CSS variable groups, set per editor root and overridden automatically by the `coar-script-editor--authoring` class:
879
+
880
+ ```css
881
+ .coar-script-editor {
882
+ --coar-script-editor-marker-scale: 0.6; /* relative to editor font */
883
+ --coar-script-editor-marker-opacity: 0.45;
884
+ --coar-script-editor-marker-color: var(--coar-text-neutral-tertiary);
885
+ --coar-script-editor-locked-line-bg: /* subtle tint */;
886
+ }
887
+ .coar-script-editor--authoring {
888
+ --coar-script-editor-marker-scale: 1;
889
+ --coar-script-editor-marker-opacity: 0.85;
890
+ --coar-script-editor-marker-color: var(--coar-text-warning);
891
+ --coar-script-editor-locked-line-bg: /* warm tint */;
892
+ }
893
+ ```
894
+
895
+ Override per-editor via inline style or globally via your theme stylesheet.
896
+
897
+ ### Pure helpers
898
+
899
+ Available without mounting an editor — use them for validation, on the server, or in tests:
900
+
901
+ ```ts
902
+ import {
903
+ hasLockedMarkers,
904
+ scanLockedLines,
905
+ computeProtectedRanges,
906
+ getEditableSegments,
907
+ getSlots,
908
+ getSlot,
909
+ editIsProtected,
910
+ snapOffsetAwayFromLocked,
911
+ countLockedLines,
912
+ isEverySegmentNonEmpty,
913
+ validateSource,
914
+ SLOT_MARKER_PATTERN,
915
+ } from '@cocoar/vue-script-editor';
916
+
917
+ // Structural queries
918
+ if (hasLockedMarkers(source)) { /* ... constrained ... */ }
919
+ const n = countLockedLines(source);
920
+ const lines = scanLockedLines(source); // per locked line
921
+ const ranges = computeProtectedRanges(lines); // merged blocks for overlap/snap
922
+
923
+ // Segmentation
924
+ const segments = getEditableSegments(source); // stretches between locks
925
+
926
+ // Named slots (per-region access by name; see the Named slots section)
927
+ const allSlots = getSlots(source); // { slotName: bodyContent }
928
+ const fn2Body = getSlot(source, 'fn2'); // string | undefined
929
+
930
+ // Submit-gating
931
+ if (isEverySegmentNonEmpty(source)) {
932
+ submit(source);
933
+ }
934
+
935
+ // Soft validation (non-throwing, surfaces informational warnings)
936
+ const v: SourceValidation = validateSource(source);
937
+ // v.ok — no warnings surfaced
938
+ // v.lockedLineCount — number of // @locked lines
939
+ // v.segmentCount — number of editable stretches between locks
940
+ // v.warnings — e.g. "source starts with a locked line" (imports can't be added above)
941
+ ```
942
+
943
+ The full type shapes:
944
+
945
+ ```ts
946
+ interface SourceValidation {
947
+ ok: boolean;
948
+ lockedLineCount: number;
949
+ segmentCount: number;
950
+ warnings: string[];
951
+ }
952
+
953
+ interface LockedLine {
954
+ lineIndex: number; // 0-based
955
+ lineStart: number; // char offset of first line char
956
+ lineEnd: number; // offset of trailing `\n` (or source.length for last line)
957
+ protectedStart: number; // inclusive start of the protected range
958
+ protectedEnd: number; // inclusive end (covers the `\n`)
959
+ snapBefore: number | null; // cursor-snap target before, null for first line
960
+ snapAfter: number | null; // cursor-snap target after, null for last line
961
+ slotName?: string; // name parsed from @slot:NAME on this locked line
962
+ }
963
+
964
+ interface ProtectedRange {
965
+ start: number;
966
+ end: number;
967
+ snapBefore: number | null;
968
+ snapAfter: number | null;
969
+ }
970
+ ```
971
+
972
+ ### Named slots (`@slot:NAME`)
973
+
974
+ Templates with multiple fillable regions — e.g. an event-handler script with three function bodies the user may or may not fill in — need a way to identify *which* body belongs to *which* function in the persisted source. Line-based locking alone tells you "this stretch is editable" but not "this is the body of `onLoad`".
975
+
976
+ The `@slot:NAME` attribute solves it. Placed on a `// @locked` line, it **names the editable segment that follows** (up to the next locked line or EOF):
977
+
978
+ ```ts
979
+ function fn1(input) { // @locked @slot:fn1
980
+ } // @locked
981
+
982
+ function fn2(input) { // @locked @slot:fn2
983
+ } // @locked
984
+
985
+ function fn3(input) { // @locked @slot:fn3
986
+ return input * 2;
987
+ } // @locked
988
+ ```
989
+
990
+ Because the marker sits on a **locked line**, the user cannot delete or move it — the slot anchor survives whatever edits the user makes to the bodies. Auto-Import inserts at the file top shift all slot markers down with the rest of the text; the scanner re-derives positions on each call.
991
+
992
+ #### Reading slot content
993
+
994
+ Two helpers, both pure functions over the source string:
995
+
996
+ ```ts
997
+ import { getSlots, getSlot } from '@cocoar/vue-script-editor';
998
+
999
+ // All slots as a dictionary, slot name → body content
1000
+ const slots = getSlots(code.value);
1001
+ // { fn1: '', fn2: '', fn3: ' return input * 2;' }
1002
+
1003
+ // A single slot, or `undefined` if the template does not declare it
1004
+ const fn2Body = getSlot(code.value, 'fn2');
1005
+ // '' — declared but not filled in
1006
+
1007
+ const missing = getSlot(code.value, 'fn4');
1008
+ // undefined — template does not have this slot
1009
+ ```
1010
+
1011
+ - **Empty string** (`''`) = slot exists but body is whitespace-only (user skipped it). Check with `content.trim().length === 0`.
1012
+ - **`undefined`** = slot not declared in the template. Lets callers distinguish "not part of the template" from "part of the template but empty".
1013
+ - **First-wins on duplicates** — if two locked lines declare the same slot name, the first one's segment is returned. `validateSource()` warns on duplicates during template authoring.
1014
+ - **Content trim rule**: leading and trailing blank lines are stripped; indentation of the remaining content is preserved, so multi-line bodies keep their shape.
1015
+
1016
+ #### Submit-gating by slot
1017
+
1018
+ ```ts
1019
+ const slots = getSlots(code.value);
1020
+ const filled = Object.entries(slots)
1021
+ .filter(([, body]) => body.trim().length > 0)
1022
+ .map(([name]) => name);
1023
+
1024
+ if (filled.length === 0) {
1025
+ // Block save — user hasn't filled in anything.
1026
+ }
1027
+ ```
1028
+
1029
+ #### Symmetric parsing on the server
1030
+
1031
+ The slot format is regex-matchable, so consumers running the saved script server-side (e.g. a C# Jint host) can extract the same info without shipping JS. The regex is exported as `SLOT_MARKER_PATTERN`:
1032
+
1033
+ ```ts
1034
+ import { SLOT_MARKER_PATTERN } from '@cocoar/vue-script-editor';
1035
+ // '\/\/\s*@locked\b[^\n]*?@slot:([A-Za-z_][A-Za-z0-9_-]*)'
1036
+ ```
1037
+
1038
+ Drop the helper below into your backend project as-is. It matches the JS implementation one-to-one: same regexes, same first-wins rule on duplicates, same CRLF normalization, same blank-line trimming:
1039
+
1040
+ ```csharp
1041
+ using System.Text.RegularExpressions;
1042
+
1043
+ public static class ScriptSlots
1044
+ {
1045
+ private static readonly Regex LockedMarker =
1046
+ new(@"//\s*@locked\b", RegexOptions.Compiled);
1047
+
1048
+ private static readonly Regex SlotMarker =
1049
+ new(@"//\s*@locked\b[^\n]*?@slot:([A-Za-z_][A-Za-z0-9_-]*)", RegexOptions.Compiled);
1050
+
1051
+ /// <summary>
1052
+ /// All named slots in the source keyed by slot name.
1053
+ /// Empty string = slot exists but body is whitespace-only.
1054
+ /// First-wins on duplicates.
1055
+ /// </summary>
1056
+ public static Dictionary<string, string> GetSlots(string source)
1057
+ {
1058
+ // Normalize CRLF so Windows-saved sources parse identically.
1059
+ var lines = source.Replace("\r\n", "\n").Split('\n');
1060
+ var result = new Dictionary<string, string>(StringComparer.Ordinal);
1061
+
1062
+ for (int i = 0; i < lines.Length; i++)
1063
+ {
1064
+ var match = SlotMarker.Match(lines[i]);
1065
+ if (!match.Success) continue;
1066
+
1067
+ var name = match.Groups[1].Value;
1068
+ if (result.ContainsKey(name)) continue; // first-wins
1069
+
1070
+ // Find the next locked line (or EOF).
1071
+ int end = i + 1;
1072
+ while (end < lines.Length && !LockedMarker.IsMatch(lines[end])) end++;
1073
+
1074
+ var bodyLines = lines.Skip(i + 1).Take(end - i - 1);
1075
+ result[name] = TrimBlankLines(string.Join("\n", bodyLines));
1076
+ }
1077
+
1078
+ return result;
1079
+ }
1080
+
1081
+ /// <summary>
1082
+ /// Content of a single slot. Returns null when no locked line declares that name.
1083
+ /// Returns "" when the slot exists but its body is empty — callers distinguish
1084
+ /// "not declared" from "declared but empty".
1085
+ /// </summary>
1086
+ public static string? GetSlot(string source, string name)
1087
+ => GetSlots(source).TryGetValue(name, out var v) ? v : null;
1088
+
1089
+ private static string TrimBlankLines(string raw)
1090
+ {
1091
+ var lines = raw.Split('\n');
1092
+ int start = 0, end = lines.Length;
1093
+ while (start < end && string.IsNullOrWhiteSpace(lines[start])) start++;
1094
+ while (end > start && string.IsNullOrWhiteSpace(lines[end - 1])) end--;
1095
+ return string.Join("\n", lines.Skip(start).Take(end - start));
1096
+ }
1097
+ }
1098
+ ```
1099
+
1100
+ With this helper, the Jint host can decide per-function whether to invoke it:
1101
+
1102
+ ```csharp
1103
+ var source = await dbContext.Scripts
1104
+ .Where(s => s.Id == id)
1105
+ .Select(s => s.SourceCode)
1106
+ .FirstAsync();
1107
+
1108
+ var slots = ScriptSlots.GetSlots(source);
1109
+ var engine = new Engine().Execute(source);
1110
+
1111
+ foreach (var (name, body) in slots)
1112
+ {
1113
+ if (!string.IsNullOrWhiteSpace(body))
1114
+ engine.Invoke(name, input);
1115
+ }
1116
+ ```
1117
+
1118
+ Or pull a single handler directly:
1119
+
1120
+ ```csharp
1121
+ var onSave = ScriptSlots.GetSlot(source, "onSave");
1122
+ if (!string.IsNullOrWhiteSpace(onSave))
1123
+ engine.Invoke("onSave", input);
1124
+ ```
1125
+
1126
+ The C# port mirrors the JS behaviour exactly, so you can reuse the 13 slot-related test cases from `LockedLineScanner.test.ts` as a parity check — same input strings must produce the same outputs.
1127
+
1128
+ #### Slot name rules
1129
+
1130
+ - Must match `[A-Za-z_][A-Za-z0-9_-]*` — starts with a letter or underscore, then letters / digits / underscores / hyphens.
1131
+ - Names that don't match (e.g. `@slot:1bad`) are ignored silently — the locked line still locks, but no slot is registered.
1132
+ - Must sit on a `// @locked` line. A lone `// @slot:X` on a free line is not recognised, because the user could delete it.
1133
+
1134
+ ### Limitations (v1)
1135
+
1136
+ - **Languages**: TypeScript, JavaScript, JSON. Other Monaco-supported languages (CSS, HTML, Markdown, SQL, etc.) work too if you register their workers, but the component is only tested against these three.
1137
+ - **Per-line granularity.** Locking a specific *character range* inside a line is not supported; the whole line is locked.
1138
+ - **Monaco auto-edits that cross a boundary are blocked.** Format Document over a locked line, Rename Symbol touching both a locked and a free stretch — the whole operation is rolled back. Usually what you want; flip `authoring` on if not.
1139
+ - **Authoring toggle + stale markers**: when `authoring` flips from `false` to `true`, previously-suppressed error markers on locked lines reappear only after the next TypeScript analysis pass (i.e. the next edit). This is a minor UX quirk of Monaco's marker model and does not affect correctness.
1140
+
1141
+ ## API
1142
+
1143
+ ### Props
1144
+
1145
+ | Prop | Type | Default | Description |
1146
+ | -------------- | --------------------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
1147
+ | `modelValue` | `string` | `''` | Editor source. Any line containing `// @locked` is protected. |
1148
+ | `authoring` | `boolean` | `false` | Authoring mode — suspends enforcement so template authors can modify locked lines or markers. |
1149
+ | `language` | `'typescript' \| 'javascript' \| 'json'` | `'typescript'` | Language mode. Changing it switches the model live. |
1150
+ | `readonly` | `boolean` | `false` | Viewer mode — user cannot edit but selection / copy / navigation still work. |
1151
+ | `disabled` | `boolean` | `false` | Non-interactive form state. Dimmed, pointer-events suppressed, picked up from `CoarFormField`. |
1152
+ | `error` | `boolean` | `false` | Error state — red border. Auto-picked up from `CoarFormField.error`. |
1153
+ | `placeholder` | `string` | `''` | Placeholder shown when the editor is empty and not focused. |
1154
+ | `required` | `boolean` | `false` | Sets `aria-required="true"`. Does not enforce submission. |
1155
+ | `autofocus` | `boolean` | `false` | Focus the editor after mount. |
1156
+ | `id` | `string` | `''` | HTML id. Auto-generated if omitted; `CoarFormField.id` takes precedence. |
1157
+ | `name` | `string` | `''` | Informational. Emitted as `data-name` (the editor is not a native form control). |
1158
+ | `height` | `string \| number` | `undefined` | Explicit height — CSS string (`"160px"`, `"40%"`) or pixels as number. |
1159
+ | `variant` | `'editor' \| 'inline'` | `'editor'` | UI preset. `'editor'` = full IDE chrome. `'inline'` = compact form-field look. |
1160
+ | `lineNumbers` | `boolean` | `undefined` | Explicit line-numbers toggle. Overrides the variant default. Off-state keeps a small left margin so text doesn't hit the border. |
1161
+ | `scriptMode` | `boolean` | `false` | Suppresses TS/JS diagnostics for "script body" code. Global side-effect — see Form Integration. |
1162
+ | `preamble` | `string` | `''` | Hidden + locked prefix providing per-editor type context. Does not round-trip through `modelValue`. |
1163
+ | `minimap` | `boolean` | `false` | Show the Monaco minimap gutter. |
1164
+ | `theme` | `'auto' \| 'light' \| 'dark'` | `'auto'` | `auto` tracks `.dark-mode` class on `<html>`/`<body>`, `data-theme="dark"`, then OS `prefers-color-scheme` — reactively. See Theming below. |
1165
+ | `extraLibs` | `CoarScriptEditorExtraLib[]` | `[]` | TypeScript declarations available for IntelliSense. |
1166
+
1167
+ ### Events
1168
+
1169
+ | Event | Payload | Description |
1170
+ | ------------------- | ------------------------------------ | ------------------------------------------------------------------------------------ |
1171
+ | `update:modelValue` | `string` | Full editor text. Markers stay in the value so it round-trips. Preamble is stripped before emit. |
1172
+ | `reject` | `CoarScriptEditorRejectEvent` | Emitted when an edit was rolled back. See the payload shape below. |
1173
+ | `focused` | `void` | Fired when the editor widget gains focus (including suggestion popup). |
1174
+ | `blurred` | `void` | Fired when the editor widget loses focus — use this to trigger form-touched state. |
1175
+
1176
+ ```ts
1177
+ interface CoarScriptEditorRejectEvent {
1178
+ reason: CoarScriptEditorRejectReason;
1179
+ /** 1-based line range of the rejected edit (from Monaco). */
1180
+ range?: { startLineNumber: number; endLineNumber: number };
1181
+ }
1182
+
1183
+ // Currently a single value; the type is an open union so consumers pattern-match forward-compatibly.
1184
+ type CoarScriptEditorRejectReason = 'edit-overlaps-locked-line';
1185
+ ```
1186
+
1187
+ ### Types
1188
+
1189
+ ```ts
1190
+ interface CoarScriptEditorExtraLib {
1191
+ content: string; // .d.ts source
1192
+ filePath: string; // e.g. 'file:///types/app-context.d.ts'
1193
+ }
1194
+
1195
+ type CoarScriptEditorLanguage = 'typescript' | 'javascript' | 'json';
1196
+ type CoarScriptEditorTheme = 'auto' | 'light' | 'dark';
1197
+ ```
1198
+
1199
+ ### Exposed Methods
1200
+
1201
+ ```ts
1202
+ const editorRef = ref<InstanceType<typeof CoarScriptEditor> | null>(null);
1203
+
1204
+ // Standard helper
1205
+ editorRef.value?.focus();
1206
+
1207
+ // Escape-hatch access to the raw Monaco editor instance and its text model
1208
+ editorRef.value?.getEditor();
1209
+ editorRef.value?.getModel();
1210
+ ```
1211
+
1212
+ Use `getEditor()` / `getModel()` for APIs not covered by the declarative props — markers, custom commands, folding ranges, formatting actions, etc.
1213
+
1214
+ ## Theming
1215
+
1216
+ Two Monaco themes ship with the package — `coar-light` and `coar-dark`. They're registered via `monaco.editor.defineTheme` the first time any editor mounts.
1217
+
1218
+ ### How `theme="auto"` decides
1219
+
1220
+ Monaco's theme is not CSS-driven — it's switched via an imperative `monaco.editor.setTheme()` call. `auto` mode watches the page for common dark-mode signals and calls `setTheme` whenever any of them changes. The resolution order is:
1221
+
1222
+ 1. `.dark-mode` class on `<html>` or `<body>` → dark (Cocoar convention)
1223
+ 2. `.dark` class on `<html>` or `<body>` → dark
1224
+ 3. `data-theme="dark"` / `data-theme="light"` attribute on `<html>` or `<body>`
1225
+ 4. OS-level `prefers-color-scheme`
1226
+
1227
+ All four sources are watched live via `MutationObserver` + `matchMedia` listeners, so toggling your app's theme switcher flips the editor in the same frame.
1228
+
1229
+ > **Tip: Custom theme switchers**
1230
+ >
1231
+ > If your app uses a different convention (e.g. a Pinia store driving a root attribute), skip `auto` and bind the prop directly:
1232
+ >
1233
+ ```vue
1234
+ <CoarScriptEditor v-model="code" :theme="isDark ? 'dark' : 'light'" />
1235
+ ```
1236
+ >
1237
+ > Monaco's `setTheme` is called whenever the prop changes, so this is the cheapest integration.
1238
+
1239
+ The editor container surfaces these CSS custom properties:
1240
+
1241
+ ```css
1242
+ .coar-script-editor {
1243
+ --coar-border-neutral-tertiary: /* editor border */;
1244
+ --coar-background-neutral-primary: /* editor surface */;
1245
+ --coar-radius-xs: /* rounded corners */;
1246
+ }
1247
+ ```
1248
+
1249
+ To register your own Monaco theme, call `monaco.editor.defineTheme('my-theme', {...})` anywhere in your app and pass it via the underlying editor instance (`editorRef.value?.getEditor().updateOptions({ theme: 'my-theme' })`).
1250
+
1251
+ ### Font
1252
+
1253
+ The editor renders code in **Cascadia Code** (Microsoft's ligature-enabled coding font) with `Consolas`, `Monaco`, `Courier New` as fallback. Ligatures are enabled by default, so `!=`, `=>`, `===`, and `&&` render as combined glyphs. This matches `CoarCodeBlock` — both components share the same font stack.
1254
+
1255
+ Cascadia Code is bundled via `@cocoar/vue-ui/fonts` (weights 400 / 600 / 700). If your app imports that stylesheet — the standard Cocoar setup — the font loads automatically:
1256
+
1257
+ ```ts
1258
+ import '@cocoar/vue-ui/fonts'
1259
+ import '@cocoar/vue-ui/styles'
1260
+ ```
1261
+
1262
+ If you don't import `@cocoar/vue-ui/fonts` (e.g. an app that only uses the script editor), Monaco falls back to Consolas/Monaco/Courier New. The editor keeps working; you just don't get the Cascadia Code glyphs or ligatures.
1263
+
1264
+ To override the font — e.g. to a custom corporate monospace — use the `getEditor()` escape hatch:
1265
+
1266
+ ```ts
1267
+ editorRef.value?.getEditor()?.updateOptions({
1268
+ fontFamily: "'JetBrains Mono', monospace",
1269
+ fontLigatures: true,
1270
+ });
1271
+ ```