@aleph-alpha/chat-kit 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-DPCpIxWc.js → CkHistory.vue_vue_type_script_setup_true_lang-gV-lp0ZB.js} +345 -164
- package/dist/chat-kit.css +111 -28
- package/dist/code-palette-DAJUTnR-.js +21598 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts +43 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +39 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +44 -0
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/index.d.ts +3 -0
- package/dist/components/base/CkCodeEditor/index.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/types.d.ts +129 -0
- package/dist/components/base/CkCodeEditor/types.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts +6 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts +1 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts +1 -0
- package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/types.d.ts +8 -0
- package/dist/components/base/CkMilkdownEditor/types.d.ts.map +1 -1
- package/dist/components/base/index.d.ts +2 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +27 -26
- package/dist/createCodeMirror-BkV6FEPn.js +101 -0
- package/dist/{createCrepeEditor-C9seqnzm.js → createCrepeEditor-Ci5FHHIV.js} +1433 -24739
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +6 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/{components/base/CkMilkdownEditor/codeMirrorTheme.d.ts → helpers/codemirror/theme.d.ts} +1 -1
- package/dist/helpers/codemirror/theme.d.ts.map +1 -0
- package/dist/{index-DzpUblMx.js → index-B2tew8uf.js} +3 -3
- package/dist/{index-DLZcs4H8.js → index-BLVAx_f8.js} +2 -2
- package/dist/{index-CHtY7ztn.js → index-BUCqcTPA.js} +2 -2
- package/dist/{index-8mYiDHFC.js → index-BhvbqYsK.js} +2 -2
- package/dist/{index-B0nlcqqF.js → index-BoUJGvvQ.js} +2 -2
- package/dist/{index-U4LYutvz.js → index-BtY1uxn6.js} +3 -2
- package/dist/{index-C5UC1wP7.js → index-BvG6l44n.js} +3 -2
- package/dist/{index-D85elIIr.js → index-CB6xZVsf.js} +2 -2
- package/dist/{index-DbVRgXxM.js → index-CIF93PZF.js} +3 -3
- package/dist/{index-reQfKGuJ.js → index-CN8d3yl1.js} +1 -1
- package/dist/{index-By7WdPbk.js → index-CYk7AB8M.js} +4 -4
- package/dist/{index-DO8xk00f.js → index-Cxs6kPho.js} +3 -3
- package/dist/index-D6ftdiLa.js +1825 -0
- package/dist/{index-DXTRMb09.js → index-DgCcFC3N.js} +3 -3
- package/dist/{index-Bxg0Q_mk.js → index-DhwsowEf.js} +2 -2
- package/dist/{index-C90_LSx6.js → index-Dl24CCB3.js} +4 -4
- package/dist/{index-eZunhxNH.js → index-DqGDenO9.js} +2 -2
- package/dist/{index-DfvAv_Y2.js → index-DufAP1JF.js} +3 -2
- package/dist/{index-VbqLf82s.js → index-DxTzaHLr.js} +4 -4
- package/dist/{index-CFdcEeXx.js → index-MfXW4ZyN.js} +3 -2
- package/dist/{index-6PiPEjJd.js → index-ZHg2R9iD.js} +3 -2
- package/dist/{index-DNxEOsDD.js → index-gYt3E3Cp.js} +2 -2
- package/dist/{index-DINJn4Mn.js → index-w2yzH7vs.js} +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -28
- package/dist/{useChatKitLabels-Bz58tas9.js → useChatKitLabels-CatQSpfL.js} +2 -0
- package/package.json +5 -3
- package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +223 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.stories.ts +311 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.vue +238 -0
- package/src/components/base/CkCodeEditor/createCodeMirror.ts +188 -0
- package/src/components/base/CkCodeEditor/index.ts +2 -0
- package/src/components/base/CkCodeEditor/types.ts +128 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.spec.ts +13 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.ts +34 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.vue +3 -1
- package/src/components/base/CkMilkdownEditor/createCrepeEditor.ts +2 -1
- package/src/components/base/CkMilkdownEditor/milkdown-theme.css +39 -26
- package/src/components/base/CkMilkdownEditor/types.ts +8 -0
- package/src/components/base/index.ts +6 -0
- package/src/components/index.ts +4 -0
- package/src/handlers/useChatKitLabels.ts +6 -0
- package/src/helpers/codemirror/code-palette.css +49 -0
- package/src/{components/base/CkMilkdownEditor/codeMirrorTheme.ts → helpers/codemirror/theme.ts} +23 -12
- package/src/index.ts +4 -0
- package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts.map +0 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { UiCodeBlock } from '@aleph-alpha/ui-library';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
4
|
+
import CkCodeEditor from './CkCodeEditor.vue';
|
|
5
|
+
import CkMarkdownRenderer from '../CkMarkdownRenderer/CkMarkdownRenderer.vue';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof CkCodeEditor> = {
|
|
8
|
+
title: 'Base/CkCodeEditor',
|
|
9
|
+
component: CkCodeEditor,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
modelValue: { control: 'text', description: 'The code (`v-model`).' },
|
|
13
|
+
language: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description:
|
|
16
|
+
'Language id for highlighting and the header label, matched on names, aliases and extensions.',
|
|
17
|
+
},
|
|
18
|
+
filename: {
|
|
19
|
+
control: 'text',
|
|
20
|
+
description: 'Shown in the header instead of the language.',
|
|
21
|
+
},
|
|
22
|
+
maxHeight: {
|
|
23
|
+
control: 'text',
|
|
24
|
+
description: 'CSS length capping the scrollable area, e.g. `24rem`.',
|
|
25
|
+
},
|
|
26
|
+
placeholder: { control: 'text' },
|
|
27
|
+
readonly: { control: 'boolean' },
|
|
28
|
+
disabled: { control: 'boolean' },
|
|
29
|
+
showHeader: { control: 'boolean' },
|
|
30
|
+
showCopyButton: { control: 'boolean' },
|
|
31
|
+
showLineNumbers: { control: 'boolean' },
|
|
32
|
+
lineWrapping: { control: 'boolean' },
|
|
33
|
+
autofocus: { control: 'boolean' },
|
|
34
|
+
writeToClipboard: {
|
|
35
|
+
control: false,
|
|
36
|
+
description:
|
|
37
|
+
'Clipboard writer for tests or hosts that proxy clipboard access.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
|
|
45
|
+
const typescriptSample = [
|
|
46
|
+
'export interface Greeting {',
|
|
47
|
+
' name: string;',
|
|
48
|
+
' formal?: boolean;',
|
|
49
|
+
'}',
|
|
50
|
+
'',
|
|
51
|
+
'// Returns a greeting suited to the audience.',
|
|
52
|
+
'export function greet({ name, formal = false }: Greeting): string {',
|
|
53
|
+
' const opener = formal ? "Good day" : "Hello";',
|
|
54
|
+
' return `${opener}, ${name}!`;',
|
|
55
|
+
'}',
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const pythonSample = [
|
|
59
|
+
'from dataclasses import dataclass',
|
|
60
|
+
'',
|
|
61
|
+
'',
|
|
62
|
+
'@dataclass',
|
|
63
|
+
'class Greeting:',
|
|
64
|
+
' name: str',
|
|
65
|
+
' formal: bool = False',
|
|
66
|
+
'',
|
|
67
|
+
' def render(self) -> str:',
|
|
68
|
+
' opener = "Good day" if self.formal else "Hello"',
|
|
69
|
+
' return f"{opener}, {self.name}!"',
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
73
|
+
import { ref } from 'vue';
|
|
74
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
75
|
+
|
|
76
|
+
const code = ref('export const answer = 42;');
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<CkCodeEditor v-model="code" language="typescript" aria-label="Code" />
|
|
81
|
+
</template>`;
|
|
82
|
+
|
|
83
|
+
const withFilenameTemplateSource = `<script setup lang="ts">
|
|
84
|
+
import { ref } from 'vue';
|
|
85
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
86
|
+
|
|
87
|
+
const code = ref('print("hello")');
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<CkCodeEditor
|
|
92
|
+
v-model="code"
|
|
93
|
+
language="python"
|
|
94
|
+
filename="main.py"
|
|
95
|
+
aria-label="main.py"
|
|
96
|
+
/>
|
|
97
|
+
</template>`;
|
|
98
|
+
|
|
99
|
+
const plainTextTemplateSource = `<script setup lang="ts">
|
|
100
|
+
import { ref } from 'vue';
|
|
101
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
102
|
+
|
|
103
|
+
const notes = ref('');
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<CkCodeEditor
|
|
108
|
+
v-model="notes"
|
|
109
|
+
placeholder="Paste your snippet…"
|
|
110
|
+
:show-line-numbers="false"
|
|
111
|
+
aria-label="Snippet"
|
|
112
|
+
/>
|
|
113
|
+
</template>`;
|
|
114
|
+
|
|
115
|
+
const readonlyTemplateSource = `<script setup lang="ts">
|
|
116
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
117
|
+
|
|
118
|
+
const code = 'export const answer = 42;';
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<CkCodeEditor :model-value="code" language="typescript" readonly aria-label="Code" />
|
|
123
|
+
</template>`;
|
|
124
|
+
|
|
125
|
+
const bareTemplateSource = `<script setup lang="ts">
|
|
126
|
+
import { ref } from 'vue';
|
|
127
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
128
|
+
|
|
129
|
+
const code = ref('SELECT * FROM users;');
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<template>
|
|
133
|
+
<CkCodeEditor
|
|
134
|
+
v-model="code"
|
|
135
|
+
language="sql"
|
|
136
|
+
:show-header="false"
|
|
137
|
+
aria-label="Query"
|
|
138
|
+
/>
|
|
139
|
+
</template>`;
|
|
140
|
+
|
|
141
|
+
const scrollingTemplateSource = `<script setup lang="ts">
|
|
142
|
+
import { ref } from 'vue';
|
|
143
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
144
|
+
|
|
145
|
+
const code = ref(longFile);
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<CkCodeEditor v-model="code" language="typescript" max-height="16rem" line-wrapping />
|
|
150
|
+
</template>`;
|
|
151
|
+
|
|
152
|
+
const matchesRendererTemplateSource = `<script setup lang="ts">
|
|
153
|
+
import { ref } from 'vue';
|
|
154
|
+
import { CkCodeEditor, CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
|
|
155
|
+
import { UiCodeBlock } from '@aleph-alpha/ui-library';
|
|
156
|
+
|
|
157
|
+
const code = ref('export const answer = 42;');
|
|
158
|
+
const asMarkdown = computed(() => '\\\`\\\`\\\`typescript\\n' + code.value + '\\n\\\`\\\`\\\`');
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<template>
|
|
162
|
+
<div class="text-content-on-surface-primary grid gap-6 text-base leading-7 md:grid-cols-2">
|
|
163
|
+
<CkCodeEditor v-model="code" language="typescript" aria-label="Editor" />
|
|
164
|
+
<CkMarkdownRenderer :content="asMarkdown">
|
|
165
|
+
<template #code-block="{ code: c, language }">
|
|
166
|
+
<UiCodeBlock :code="c" :language="language || 'text'" />
|
|
167
|
+
</template>
|
|
168
|
+
</CkMarkdownRenderer>
|
|
169
|
+
</div>
|
|
170
|
+
</template>`;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A TypeScript snippet with the header, copy button and line numbers. Type in
|
|
174
|
+
* it — changes are emitted synchronously, with no debounce.
|
|
175
|
+
*/
|
|
176
|
+
export const Default: Story = {
|
|
177
|
+
render: (args) => ({
|
|
178
|
+
components: { CkCodeEditor },
|
|
179
|
+
setup() {
|
|
180
|
+
const code = ref(args.modelValue ?? '');
|
|
181
|
+
return { args, code };
|
|
182
|
+
},
|
|
183
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Code" />`,
|
|
184
|
+
}),
|
|
185
|
+
args: { modelValue: typescriptSample, language: 'typescript' },
|
|
186
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `filename` replaces the language in the header, matching the prop of the same
|
|
191
|
+
* name on `UiCodeBlock`. The grammar still comes from `language`.
|
|
192
|
+
*/
|
|
193
|
+
export const WithFilename: Story = {
|
|
194
|
+
render: (args) => ({
|
|
195
|
+
components: { CkCodeEditor },
|
|
196
|
+
setup() {
|
|
197
|
+
const code = ref(args.modelValue ?? '');
|
|
198
|
+
return { args, code };
|
|
199
|
+
},
|
|
200
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="main.py" />`,
|
|
201
|
+
}),
|
|
202
|
+
args: { modelValue: pythonSample, language: 'python', filename: 'main.py' },
|
|
203
|
+
parameters: { docs: { source: { code: withFilenameTemplateSource } } },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* With no language the header label is blank and nothing is highlighted, the
|
|
208
|
+
* same treatment `UiCodeBlock` gives plain text. Shown here with a placeholder
|
|
209
|
+
* and no gutter, as a lightweight snippet field.
|
|
210
|
+
*/
|
|
211
|
+
export const PlainText: Story = {
|
|
212
|
+
render: (args) => ({
|
|
213
|
+
components: { CkCodeEditor },
|
|
214
|
+
setup() {
|
|
215
|
+
const code = ref(args.modelValue ?? '');
|
|
216
|
+
return { args, code };
|
|
217
|
+
},
|
|
218
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Snippet" />`,
|
|
219
|
+
}),
|
|
220
|
+
args: {
|
|
221
|
+
modelValue: '',
|
|
222
|
+
placeholder: 'Paste your snippet…',
|
|
223
|
+
showLineNumbers: false,
|
|
224
|
+
},
|
|
225
|
+
parameters: { docs: { source: { code: plainTextTemplateSource } } },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* `readonly` keeps the code selectable and copyable but drops the caret and the
|
|
230
|
+
* active-line band. Use it while a request is in flight.
|
|
231
|
+
*/
|
|
232
|
+
export const Readonly: Story = {
|
|
233
|
+
args: {
|
|
234
|
+
modelValue: typescriptSample,
|
|
235
|
+
language: 'typescript',
|
|
236
|
+
readonly: true,
|
|
237
|
+
},
|
|
238
|
+
parameters: { docs: { source: { code: readonlyTemplateSource } } },
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `showHeader: false` gives a bare editing surface, for forms that already
|
|
243
|
+
* label the field themselves.
|
|
244
|
+
*/
|
|
245
|
+
export const WithoutHeader: Story = {
|
|
246
|
+
args: {
|
|
247
|
+
modelValue: 'SELECT id, email\nFROM users\nWHERE active = true;',
|
|
248
|
+
language: 'sql',
|
|
249
|
+
showHeader: false,
|
|
250
|
+
},
|
|
251
|
+
parameters: { docs: { source: { code: bareTemplateSource } } },
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* `maxHeight` caps the scrollable area and `lineWrapping` folds long lines
|
|
256
|
+
* instead of scrolling sideways.
|
|
257
|
+
*/
|
|
258
|
+
export const ScrollingAndWrapping: Story = {
|
|
259
|
+
render: (args) => ({
|
|
260
|
+
components: { CkCodeEditor },
|
|
261
|
+
setup() {
|
|
262
|
+
const code = ref(args.modelValue ?? '');
|
|
263
|
+
return { args, code };
|
|
264
|
+
},
|
|
265
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Long file" />`,
|
|
266
|
+
}),
|
|
267
|
+
args: {
|
|
268
|
+
modelValue: [
|
|
269
|
+
typescriptSample,
|
|
270
|
+
'',
|
|
271
|
+
'// A deliberately long line that has to wrap rather than scroll sideways, so you can see how lineWrapping behaves in a narrow column.',
|
|
272
|
+
'',
|
|
273
|
+
pythonSample,
|
|
274
|
+
].join('\n'),
|
|
275
|
+
language: 'typescript',
|
|
276
|
+
maxHeight: '16rem',
|
|
277
|
+
lineWrapping: true,
|
|
278
|
+
},
|
|
279
|
+
parameters: { docs: { source: { code: scrollingTemplateSource } } },
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Parity check: the editor on the left, the same snippet rendered through
|
|
284
|
+
* `CkMarkdownRenderer` and `UiCodeBlock` on the right. Border, radius, header,
|
|
285
|
+
* font and syntax colours should read as one component in both themes.
|
|
286
|
+
*/
|
|
287
|
+
export const MatchesTheRenderer: Story = {
|
|
288
|
+
render: (args) => ({
|
|
289
|
+
components: { CkCodeEditor, CkMarkdownRenderer, UiCodeBlock },
|
|
290
|
+
setup() {
|
|
291
|
+
const code = ref(args.modelValue ?? '');
|
|
292
|
+
return { args, code };
|
|
293
|
+
},
|
|
294
|
+
template: `
|
|
295
|
+
<div class="text-content-on-surface-primary grid gap-6 text-base leading-7 md:grid-cols-2">
|
|
296
|
+
<CkCodeEditor v-bind="args" v-model="code" aria-label="Editor" />
|
|
297
|
+
<CkMarkdownRenderer :content="'\\\`\\\`\\\`typescript\\n' + code + '\\n\\\`\\\`\\\`'">
|
|
298
|
+
<template #code-block="{ code: c, language }">
|
|
299
|
+
<UiCodeBlock :code="c" :language="language || 'text'" />
|
|
300
|
+
</template>
|
|
301
|
+
</CkMarkdownRenderer>
|
|
302
|
+
</div>
|
|
303
|
+
`,
|
|
304
|
+
}),
|
|
305
|
+
args: {
|
|
306
|
+
modelValue: typescriptSample,
|
|
307
|
+
language: 'typescript',
|
|
308
|
+
showLineNumbers: false,
|
|
309
|
+
},
|
|
310
|
+
parameters: { docs: { source: { code: matchesRendererTemplateSource } } },
|
|
311
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
onMounted,
|
|
6
|
+
ref,
|
|
7
|
+
useTemplateRef,
|
|
8
|
+
watch,
|
|
9
|
+
} from 'vue';
|
|
10
|
+
import { UiButton, UiIcon } from '@aleph-alpha/ui-library';
|
|
11
|
+
import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
12
|
+
import type { CodeMirrorHandle } from './createCodeMirror';
|
|
13
|
+
import type { CkCodeEditorEmits, CkCodeEditorProps } from './types';
|
|
14
|
+
|
|
15
|
+
defineOptions({ name: 'CkCodeEditor' });
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<CkCodeEditorProps>(), {
|
|
18
|
+
modelValue: '',
|
|
19
|
+
language: 'text',
|
|
20
|
+
filename: undefined,
|
|
21
|
+
placeholder: undefined,
|
|
22
|
+
readonly: false,
|
|
23
|
+
disabled: false,
|
|
24
|
+
ariaLabel: undefined,
|
|
25
|
+
showHeader: true,
|
|
26
|
+
showCopyButton: true,
|
|
27
|
+
showLineNumbers: true,
|
|
28
|
+
lineWrapping: false,
|
|
29
|
+
maxHeight: undefined,
|
|
30
|
+
autofocus: false,
|
|
31
|
+
writeToClipboard: undefined,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<CkCodeEditorEmits>();
|
|
35
|
+
|
|
36
|
+
const labels = useChatKitLabels();
|
|
37
|
+
const rootRef = useTemplateRef<HTMLDivElement>('rootRef');
|
|
38
|
+
const hostRef = useTemplateRef<HTMLDivElement>('hostRef');
|
|
39
|
+
const isLocked = computed(() => props.readonly || props.disabled);
|
|
40
|
+
const isReady = ref(false);
|
|
41
|
+
const isCopied = ref(false);
|
|
42
|
+
|
|
43
|
+
// `UiCodeBlock` labels the block with the filename when there is one and hides
|
|
44
|
+
// the label entirely for plain text; mirror that so the two headers match.
|
|
45
|
+
const headerLabel = computed(() => {
|
|
46
|
+
const label = props.filename || props.language;
|
|
47
|
+
return label === 'text' ? '' : label;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const copyLabel = computed(() =>
|
|
51
|
+
isCopied.value
|
|
52
|
+
? (labels.value.codeEditorCopied ?? 'Copied')
|
|
53
|
+
: (labels.value.codeEditorCopy ?? 'Copy code'),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
let editor: CodeMirrorHandle | undefined;
|
|
57
|
+
let destroyed = false;
|
|
58
|
+
// Last value emitted or applied, so an echo from the parent is not re-applied.
|
|
59
|
+
let lastKnownCode = props.modelValue;
|
|
60
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
61
|
+
|
|
62
|
+
function handleChange(value: string): void {
|
|
63
|
+
if (value === lastKnownCode) return;
|
|
64
|
+
lastKnownCode = value;
|
|
65
|
+
emit('update:modelValue', value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onMounted(async () => {
|
|
69
|
+
const parent = hostRef.value;
|
|
70
|
+
if (!parent) return;
|
|
71
|
+
|
|
72
|
+
// Lazy: CodeMirror and its grammars load only once an editor is shown.
|
|
73
|
+
const { createCodeMirror } = await import('./createCodeMirror');
|
|
74
|
+
if (destroyed) return;
|
|
75
|
+
|
|
76
|
+
const initialCode = props.modelValue;
|
|
77
|
+
lastKnownCode = initialCode;
|
|
78
|
+
const handle = await createCodeMirror({
|
|
79
|
+
parent,
|
|
80
|
+
doc: initialCode,
|
|
81
|
+
language: props.language,
|
|
82
|
+
placeholder: props.placeholder,
|
|
83
|
+
ariaLabel: props.ariaLabel,
|
|
84
|
+
readonly: isLocked.value,
|
|
85
|
+
showLineNumbers: props.showLineNumbers,
|
|
86
|
+
lineWrapping: props.lineWrapping,
|
|
87
|
+
onChange: handleChange,
|
|
88
|
+
onFocus: () => emit('focus'),
|
|
89
|
+
onBlur: () => emit('blur'),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Unmounted while the grammar was loading.
|
|
93
|
+
if (destroyed) {
|
|
94
|
+
handle.destroy();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
editor = handle;
|
|
99
|
+
// Props may have moved on while the grammar loaded; catch up.
|
|
100
|
+
if (props.modelValue !== initialCode) handle.setCode(props.modelValue);
|
|
101
|
+
isReady.value = true;
|
|
102
|
+
if (props.autofocus) handle.focus();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
watch(
|
|
106
|
+
() => props.modelValue,
|
|
107
|
+
(next) => {
|
|
108
|
+
if (next === lastKnownCode) return;
|
|
109
|
+
lastKnownCode = next;
|
|
110
|
+
editor?.setCode(next);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
watch(
|
|
115
|
+
() => props.language,
|
|
116
|
+
(next) => {
|
|
117
|
+
// Loading a grammar is async; nothing waits on it, and a failure already
|
|
118
|
+
// falls back to plain text inside `setLanguage`.
|
|
119
|
+
editor?.setLanguage(next).catch(() => undefined);
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
watch(isLocked, (locked) => editor?.setReadonly(locked));
|
|
123
|
+
watch(
|
|
124
|
+
() => props.showLineNumbers,
|
|
125
|
+
(show) => editor?.setShowLineNumbers(show),
|
|
126
|
+
);
|
|
127
|
+
watch(
|
|
128
|
+
() => props.lineWrapping,
|
|
129
|
+
(wrap) => editor?.setLineWrapping(wrap),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
onBeforeUnmount(() => {
|
|
133
|
+
destroyed = true;
|
|
134
|
+
clearTimeout(copyTimer);
|
|
135
|
+
editor?.destroy();
|
|
136
|
+
editor = undefined;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
async function handleCopy(): Promise<void> {
|
|
140
|
+
const write =
|
|
141
|
+
props.writeToClipboard ??
|
|
142
|
+
((text: string) => globalThis.navigator.clipboard.writeText(text));
|
|
143
|
+
try {
|
|
144
|
+
await write(getCode());
|
|
145
|
+
isCopied.value = true;
|
|
146
|
+
clearTimeout(copyTimer);
|
|
147
|
+
copyTimer = setTimeout(() => {
|
|
148
|
+
isCopied.value = false;
|
|
149
|
+
}, 2000);
|
|
150
|
+
emit('copy', { success: true });
|
|
151
|
+
} catch {
|
|
152
|
+
emit('copy', { success: false });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isInsideRoot(target: EventTarget | null): boolean {
|
|
157
|
+
return target instanceof Node && (rootRef.value?.contains(target) ?? false);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// CodeMirror reports focus on its own content; the header's copy button is
|
|
161
|
+
// outside that, so only a move out of the whole component counts as a blur.
|
|
162
|
+
function onFocusOut(event: FocusEvent): void {
|
|
163
|
+
if (!isInsideRoot(event.relatedTarget)) emit('blur');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function focus(): void {
|
|
167
|
+
editor?.focus();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getCode(): string {
|
|
171
|
+
return editor?.getCode() ?? props.modelValue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
defineExpose({ focus, getCode });
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<div
|
|
179
|
+
ref="rootRef"
|
|
180
|
+
:class="[
|
|
181
|
+
'ck-code-editor ck-code-surface border-border-surface-default min-w-0 overflow-hidden rounded-lg border',
|
|
182
|
+
isReady && 'ck-code-editor--ready',
|
|
183
|
+
isLocked && 'ck-code-editor--readonly',
|
|
184
|
+
disabled && 'ck-code-editor--disabled',
|
|
185
|
+
]"
|
|
186
|
+
:aria-disabled="disabled || undefined"
|
|
187
|
+
@focusout="onFocusOut"
|
|
188
|
+
>
|
|
189
|
+
<div
|
|
190
|
+
v-if="showHeader"
|
|
191
|
+
class="border-border-surface-default bg-background-surface-muted flex items-center justify-between border-b px-4 py-2"
|
|
192
|
+
>
|
|
193
|
+
<span class="text-content-on-surface-secondary font-mono text-sm">
|
|
194
|
+
{{ headerLabel }}
|
|
195
|
+
</span>
|
|
196
|
+
<UiButton
|
|
197
|
+
v-if="showCopyButton"
|
|
198
|
+
variant="ghost"
|
|
199
|
+
size="icon-sm"
|
|
200
|
+
:aria-label="copyLabel"
|
|
201
|
+
@click="handleCopy"
|
|
202
|
+
>
|
|
203
|
+
<UiIcon :name="isCopied ? 'check' : 'copy'" :size="14" />
|
|
204
|
+
</UiButton>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div
|
|
208
|
+
ref="hostRef"
|
|
209
|
+
class="ck-code-editor__host"
|
|
210
|
+
:style="maxHeight ? { '--ck-code-max-height': maxHeight } : undefined"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
</template>
|
|
214
|
+
|
|
215
|
+
<style scoped>
|
|
216
|
+
/* The editor fills the panel; `:deep` is needed because CodeMirror builds its
|
|
217
|
+
* own DOM, which scoped styles do not reach. */
|
|
218
|
+
.ck-code-editor__host :deep(.cm-editor) {
|
|
219
|
+
background: var(--ck-cm-bg);
|
|
220
|
+
color: var(--ck-cm-foreground);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.ck-code-editor__host :deep(.cm-scroller) {
|
|
224
|
+
max-height: var(--ck-code-max-height, none);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* Nothing is being edited, so the active-line band would only be noise. */
|
|
228
|
+
.ck-code-editor--readonly .ck-code-editor__host :deep(.cm-activeLine),
|
|
229
|
+
.ck-code-editor--readonly .ck-code-editor__host :deep(.cm-activeLineGutter) {
|
|
230
|
+
background: transparent;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.ck-code-editor--disabled {
|
|
234
|
+
opacity: 0.6;
|
|
235
|
+
cursor: not-allowed;
|
|
236
|
+
pointer-events: none;
|
|
237
|
+
}
|
|
238
|
+
</style>
|