@aleph-alpha/chat-kit 6.3.0 → 6.4.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-gV-lp0ZB.js → CkHistory.vue_vue_type_script_setup_true_lang-B0WUzTHa.js} +11 -5
- package/dist/chat-kit.css +5 -5
- package/dist/{code-palette-DAJUTnR-.js → code-palette-DH_G7mZ5.js} +111 -110
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts +6 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +1 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts +47 -0
- package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +4 -0
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/types.d.ts +20 -0
- package/dist/components/base/CkCodeEditor/types.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/{createCodeMirror-BkV6FEPn.js → createCodeMirror-5iswYetS.js} +70 -2
- package/dist/{createCrepeEditor-Ci5FHHIV.js → createCrepeEditor-C-AQ1w_e.js} +2 -2
- package/dist/{index-Cxs6kPho.js → index-ABUMl7H3.js} +3 -3
- package/dist/{index-DufAP1JF.js → index-BlIc9r0r.js} +3 -3
- package/dist/{index-DxTzaHLr.js → index-BqWO6mvz.js} +4 -4
- package/dist/{index-DhwsowEf.js → index-CFGO7LRz.js} +2 -2
- package/dist/{index-D6ftdiLa.js → index-CG8e3146.js} +1 -1
- package/dist/{index-Dl24CCB3.js → index-CIF4qQzR.js} +4 -4
- package/dist/{index-DgCcFC3N.js → index-CR2kGrpY.js} +3 -3
- package/dist/{index-BoUJGvvQ.js → index-CWp_t8Wl.js} +2 -2
- package/dist/{index-BUCqcTPA.js → index-D0IZW-Up.js} +2 -2
- package/dist/{index-CYk7AB8M.js → index-DKwEe_Tu.js} +4 -4
- package/dist/{index-w2yzH7vs.js → index-DLjsqQHx.js} +3 -3
- package/dist/{index-DqGDenO9.js → index-DPgw0Cf_.js} +2 -2
- package/dist/{index-CB6xZVsf.js → index-Db5-PJG-.js} +2 -2
- package/dist/{index-MfXW4ZyN.js → index-Dbi75pKH.js} +3 -3
- package/dist/{index-BhvbqYsK.js → index-DfLCrnVq.js} +2 -2
- package/dist/{index-ZHg2R9iD.js → index-GpSpJ3ti.js} +3 -3
- package/dist/{index-CN8d3yl1.js → index-Mh3WUU-6.js} +1 -1
- package/dist/{index-BLVAx_f8.js → index-QAs789O5.js} +2 -2
- package/dist/{index-B2tew8uf.js → index-S4ylc8JK.js} +3 -3
- package/dist/{index-CIF93PZF.js → index-XZeturb3.js} +3 -3
- package/dist/{index-BtY1uxn6.js → index-g2X42srv.js} +3 -3
- package/dist/{index-gYt3E3Cp.js → index-hEJ_Bavx.js} +2 -2
- package/dist/{index-BvG6l44n.js → index-hbS5PDHj.js} +3 -3
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +61 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.stories.ts +94 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.vue +8 -2
- package/src/components/base/CkCodeEditor/badgeDecoration.ts +173 -0
- package/src/components/base/CkCodeEditor/createCodeMirror.ts +9 -0
- package/src/components/base/CkCodeEditor/types.ts +26 -0
|
@@ -4,6 +4,32 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
|
4
4
|
import CkCodeEditor from './CkCodeEditor.vue';
|
|
5
5
|
import CkMarkdownRenderer from '../CkMarkdownRenderer/CkMarkdownRenderer.vue';
|
|
6
6
|
|
|
7
|
+
const folderIcon =
|
|
8
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" ' +
|
|
9
|
+
'stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
|
|
10
|
+
'stroke-linejoin="round" width="12" height="12" aria-hidden="true">' +
|
|
11
|
+
'<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>';
|
|
12
|
+
|
|
13
|
+
/** Matches `@collection:<id>` and draws each one as a folder pill. */
|
|
14
|
+
const collectionBadge = {
|
|
15
|
+
pattern: /@collection:([\w-]+)/g,
|
|
16
|
+
render: (match: RegExpExecArray) => ({
|
|
17
|
+
label: (match[1] ?? '').replace(/-/g, ' '),
|
|
18
|
+
icon: folderIcon,
|
|
19
|
+
title: match[0],
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Matches `{{ name }}`, showing that the trigger is whatever you pattern for. */
|
|
24
|
+
const handlebarsBadge = {
|
|
25
|
+
pattern: /\{\{\s*([\w-]+)\s*\}\}/g,
|
|
26
|
+
render: (match: RegExpExecArray) => ({
|
|
27
|
+
label: match[1] ?? '',
|
|
28
|
+
icon: folderIcon,
|
|
29
|
+
title: match[0],
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
|
|
7
33
|
const meta: Meta<typeof CkCodeEditor> = {
|
|
8
34
|
title: 'Base/CkCodeEditor',
|
|
9
35
|
component: CkCodeEditor,
|
|
@@ -31,6 +57,20 @@ const meta: Meta<typeof CkCodeEditor> = {
|
|
|
31
57
|
showLineNumbers: { control: 'boolean' },
|
|
32
58
|
lineWrapping: { control: 'boolean' },
|
|
33
59
|
autofocus: { control: 'boolean' },
|
|
60
|
+
// A regex plus a callback cannot be typed into a control, so the rule is
|
|
61
|
+
// offered as named presets instead. Without this, badges could only be seen
|
|
62
|
+
// on the one story that hard-codes a rule.
|
|
63
|
+
badge: {
|
|
64
|
+
options: ['none', 'collections', 'handlebars'],
|
|
65
|
+
mapping: {
|
|
66
|
+
none: undefined,
|
|
67
|
+
collections: collectionBadge,
|
|
68
|
+
handlebars: handlebarsBadge,
|
|
69
|
+
},
|
|
70
|
+
control: { type: 'radio' },
|
|
71
|
+
description:
|
|
72
|
+
'Pattern plus a render callback turning matched text into inline pills. Pick a preset to try it on any story.',
|
|
73
|
+
},
|
|
34
74
|
writeToClipboard: {
|
|
35
75
|
control: false,
|
|
36
76
|
description:
|
|
@@ -69,6 +109,14 @@ const pythonSample = [
|
|
|
69
109
|
' return f"{opener}, {self.name}!"',
|
|
70
110
|
].join('\n');
|
|
71
111
|
|
|
112
|
+
const badgeSample = [
|
|
113
|
+
'You improve and draft work writing: emails, project summaries and status',
|
|
114
|
+
'updates, tightening structure and matching register to the audience.',
|
|
115
|
+
'',
|
|
116
|
+
'For analyses, use @collection:collection-name-3 and @collection:collection-name-4',
|
|
117
|
+
'and distinguish an opinion from a fact.',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
72
120
|
const defaultTemplateSource = `<script setup lang="ts">
|
|
73
121
|
import { ref } from 'vue';
|
|
74
122
|
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
@@ -149,6 +197,29 @@ const code = ref(longFile);
|
|
|
149
197
|
<CkCodeEditor v-model="code" language="typescript" max-height="16rem" line-wrapping />
|
|
150
198
|
</template>`;
|
|
151
199
|
|
|
200
|
+
const withBadgesTemplateSource = `<script setup lang="ts">
|
|
201
|
+
import { ref } from 'vue';
|
|
202
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
203
|
+
|
|
204
|
+
const prompt = ref('For analyses, use @collection:sales and @collection:support.');
|
|
205
|
+
|
|
206
|
+
// The document keeps the raw text; only the drawing is replaced.
|
|
207
|
+
const badge = {
|
|
208
|
+
pattern: /@collection:([\\w-]+)/g,
|
|
209
|
+
render: (m) => ({ label: m[1], icon: folderIcon, title: m[0] }),
|
|
210
|
+
};
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<template>
|
|
214
|
+
<CkCodeEditor
|
|
215
|
+
v-model="prompt"
|
|
216
|
+
language="markdown"
|
|
217
|
+
:badge="badge"
|
|
218
|
+
:line-wrapping="true"
|
|
219
|
+
aria-label="Prompt"
|
|
220
|
+
/>
|
|
221
|
+
</template>`;
|
|
222
|
+
|
|
152
223
|
const matchesRendererTemplateSource = `<script setup lang="ts">
|
|
153
224
|
import { ref } from 'vue';
|
|
154
225
|
import { CkCodeEditor, CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
|
|
@@ -279,6 +350,29 @@ export const ScrollingAndWrapping: Story = {
|
|
|
279
350
|
parameters: { docs: { source: { code: scrollingTemplateSource } } },
|
|
280
351
|
};
|
|
281
352
|
|
|
353
|
+
/**
|
|
354
|
+
* `badge` draws matched references as inline pills, the way a mention renders in
|
|
355
|
+
* prose. The document still holds the raw `@collection:…` text, so `getCode()`
|
|
356
|
+
* and copy give the real reference; a pill deletes as one unit.
|
|
357
|
+
*/
|
|
358
|
+
export const WithBadges: Story = {
|
|
359
|
+
render: (args) => ({
|
|
360
|
+
components: { CkCodeEditor },
|
|
361
|
+
setup() {
|
|
362
|
+
const code = ref(args.modelValue ?? '');
|
|
363
|
+
return { args, code };
|
|
364
|
+
},
|
|
365
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Prompt" />`,
|
|
366
|
+
}),
|
|
367
|
+
args: {
|
|
368
|
+
modelValue: badgeSample,
|
|
369
|
+
language: 'markdown',
|
|
370
|
+
badge: collectionBadge,
|
|
371
|
+
lineWrapping: true,
|
|
372
|
+
},
|
|
373
|
+
parameters: { docs: { source: { code: withBadgesTemplateSource } } },
|
|
374
|
+
};
|
|
375
|
+
|
|
282
376
|
/**
|
|
283
377
|
* Parity check: the editor on the left, the same snippet rendered through
|
|
284
378
|
* `CkMarkdownRenderer` and `UiCodeBlock` on the right. Border, radius, header,
|
|
@@ -28,6 +28,7 @@ const props = withDefaults(defineProps<CkCodeEditorProps>(), {
|
|
|
28
28
|
lineWrapping: false,
|
|
29
29
|
maxHeight: undefined,
|
|
30
30
|
autofocus: false,
|
|
31
|
+
badge: undefined,
|
|
31
32
|
writeToClipboard: undefined,
|
|
32
33
|
});
|
|
33
34
|
|
|
@@ -84,6 +85,7 @@ onMounted(async () => {
|
|
|
84
85
|
readonly: isLocked.value,
|
|
85
86
|
showLineNumbers: props.showLineNumbers,
|
|
86
87
|
lineWrapping: props.lineWrapping,
|
|
88
|
+
badge: props.badge,
|
|
87
89
|
onChange: handleChange,
|
|
88
90
|
onFocus: () => emit('focus'),
|
|
89
91
|
onBlur: () => emit('blur'),
|
|
@@ -128,6 +130,10 @@ watch(
|
|
|
128
130
|
() => props.lineWrapping,
|
|
129
131
|
(wrap) => editor?.setLineWrapping(wrap),
|
|
130
132
|
);
|
|
133
|
+
watch(
|
|
134
|
+
() => props.badge,
|
|
135
|
+
(rule) => editor?.setBadge(rule),
|
|
136
|
+
);
|
|
131
137
|
|
|
132
138
|
onBeforeUnmount(() => {
|
|
133
139
|
destroyed = true;
|
|
@@ -188,9 +194,9 @@ defineExpose({ focus, getCode });
|
|
|
188
194
|
>
|
|
189
195
|
<div
|
|
190
196
|
v-if="showHeader"
|
|
191
|
-
class="
|
|
197
|
+
class="bg-background-surface-muted-subtle flex h-9 items-center justify-between px-3"
|
|
192
198
|
>
|
|
193
|
-
<span class="text-content-on-surface-secondary
|
|
199
|
+
<span class="text-content-on-surface-secondary label-12-regular">
|
|
194
200
|
{{ headerLabel }}
|
|
195
201
|
</span>
|
|
196
202
|
<UiButton
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Extension, Range } from '@codemirror/state';
|
|
2
|
+
import {
|
|
3
|
+
Decoration,
|
|
4
|
+
type DecorationSet,
|
|
5
|
+
EditorView,
|
|
6
|
+
MatchDecorator,
|
|
7
|
+
ViewPlugin,
|
|
8
|
+
type ViewUpdate,
|
|
9
|
+
WidgetType,
|
|
10
|
+
} from '@codemirror/view';
|
|
11
|
+
import DOMPurify from 'dompurify';
|
|
12
|
+
|
|
13
|
+
/** What a matched reference should look like once it is drawn as a pill. */
|
|
14
|
+
export interface CkCodeEditorBadgeContent {
|
|
15
|
+
/** Text inside the pill. */
|
|
16
|
+
label: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional inline SVG markup for a leading icon, sized to 12px. This is
|
|
19
|
+
* developer-supplied configuration rather than document content, and it is
|
|
20
|
+
* sanitised before it reaches the DOM.
|
|
21
|
+
*/
|
|
22
|
+
icon?: string;
|
|
23
|
+
/** Native tooltip, useful for showing the underlying reference. */
|
|
24
|
+
title?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Classes replacing the default pill styling, which mirrors `UiBadge`'s
|
|
27
|
+
* `outline` variant at `small` size.
|
|
28
|
+
*/
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Rule turning matched text in the document into inline pills. */
|
|
33
|
+
export interface CkCodeEditorBadgeRule {
|
|
34
|
+
/**
|
|
35
|
+
* Pattern matched within each line. A non-global pattern is promoted to a
|
|
36
|
+
* global one, and matching never crosses a line break.
|
|
37
|
+
*/
|
|
38
|
+
pattern: RegExp;
|
|
39
|
+
/**
|
|
40
|
+
* Maps a match to its pill, or returns `null` to leave that occurrence as
|
|
41
|
+
* ordinary text. The match array carries any capture groups.
|
|
42
|
+
*/
|
|
43
|
+
render: (match: RegExpExecArray) => CkCodeEditorBadgeContent | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The badge as the ChatKit design file specifies it: the shadcn `secondary`
|
|
47
|
+
// badge at a 6px radius, 8px/2px padding, a 4px gap and a 12px label. Written
|
|
48
|
+
// out because a widget builds DOM imperatively and cannot mount a Vue component
|
|
49
|
+
// cheaply; these class names sit in chat-kit source, so UnoCSS emits them into
|
|
50
|
+
// the package stylesheet like any other.
|
|
51
|
+
const DEFAULT_BADGE_CLASS =
|
|
52
|
+
'ck-code-badge inline-flex w-fit shrink-0 select-none items-center justify-center gap-1 ' +
|
|
53
|
+
'overflow-hidden whitespace-nowrap rounded-md border border-border-surface-default ' +
|
|
54
|
+
'bg-background-button-secondary-button text-content-on-surface-primary px-2 py-0.5 ' +
|
|
55
|
+
'label-12-regular align-middle';
|
|
56
|
+
|
|
57
|
+
class BadgeWidget extends WidgetType {
|
|
58
|
+
constructor(private readonly content: CkCodeEditorBadgeContent) {
|
|
59
|
+
super();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Decorations are rebuilt on every document and viewport change. Without a
|
|
63
|
+
// real comparison here every pill would be torn down and recreated on each
|
|
64
|
+
// keystroke, which flickers and drops any focus inside the widget.
|
|
65
|
+
override eq(other: BadgeWidget): boolean {
|
|
66
|
+
return (
|
|
67
|
+
other.content.label === this.content.label &&
|
|
68
|
+
other.content.icon === this.content.icon &&
|
|
69
|
+
other.content.title === this.content.title &&
|
|
70
|
+
other.content.class === this.content.class
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toDOM(): HTMLElement {
|
|
75
|
+
const el = document.createElement('span');
|
|
76
|
+
el.className = this.content.class ?? DEFAULT_BADGE_CLASS;
|
|
77
|
+
if (this.content.title) el.title = this.content.title;
|
|
78
|
+
|
|
79
|
+
if (this.content.icon) {
|
|
80
|
+
const icon = document.createElement('span');
|
|
81
|
+
icon.className = 'ck-code-badge__icon inline-flex shrink-0';
|
|
82
|
+
// Config, not document content — but sanitised anyway, since it is the
|
|
83
|
+
// only markup in this file that does not originate here.
|
|
84
|
+
icon.innerHTML = DOMPurify.sanitize(this.content.icon, {
|
|
85
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
86
|
+
});
|
|
87
|
+
el.append(icon);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
el.append(document.createTextNode(this.content.label));
|
|
91
|
+
return el;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The pill is decorative; let the editor handle every event over it. */
|
|
95
|
+
override ignoreEvent(): boolean {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `MatchDecorator` requires a global pattern and mutates `lastIndex`. */
|
|
101
|
+
function toGlobal(pattern: RegExp): RegExp {
|
|
102
|
+
const flags = pattern.flags.includes('g')
|
|
103
|
+
? pattern.flags
|
|
104
|
+
: `${pattern.flags}g`;
|
|
105
|
+
return new RegExp(pattern.source, flags);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Draw matched references as inline pills, the way a mention or a chip renders
|
|
110
|
+
* inside prose.
|
|
111
|
+
*
|
|
112
|
+
* The document keeps the original text: the decoration only replaces what is
|
|
113
|
+
* *drawn*, so reading the value, copying and pasting all still yield the raw
|
|
114
|
+
* reference. The matched range is also registered as atomic, so the caret
|
|
115
|
+
* treats a pill as a single unit and backspace removes the whole reference
|
|
116
|
+
* rather than one character of it.
|
|
117
|
+
*/
|
|
118
|
+
export function badgeExtension(rule: CkCodeEditorBadgeRule): Extension {
|
|
119
|
+
const matcher = new MatchDecorator({
|
|
120
|
+
regexp: toGlobal(rule.pattern),
|
|
121
|
+
decoration: (match) => {
|
|
122
|
+
const content = rule.render(match as RegExpExecArray);
|
|
123
|
+
return content
|
|
124
|
+
? Decoration.replace({ widget: new BadgeWidget(content) })
|
|
125
|
+
: null;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return ViewPlugin.fromClass(
|
|
130
|
+
class {
|
|
131
|
+
badges: DecorationSet;
|
|
132
|
+
|
|
133
|
+
constructor(view: EditorView) {
|
|
134
|
+
this.badges = matcher.createDeco(view);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
update(update: ViewUpdate): void {
|
|
138
|
+
this.badges = matcher.updateDeco(update, this.badges);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
decorations: (plugin) => plugin.badges,
|
|
143
|
+
provide: (plugin) =>
|
|
144
|
+
EditorView.atomicRanges.of(
|
|
145
|
+
(view) => view.plugin(plugin)?.badges ?? Decoration.none,
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Exported for tests: the ranges a rule would decorate in a plain string. */
|
|
152
|
+
export function matchBadgeRanges(
|
|
153
|
+
rule: CkCodeEditorBadgeRule,
|
|
154
|
+
text: string,
|
|
155
|
+
): Range<Decoration>[] {
|
|
156
|
+
const pattern = toGlobal(rule.pattern);
|
|
157
|
+
const ranges: Range<Decoration>[] = [];
|
|
158
|
+
let match: RegExpExecArray | null;
|
|
159
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
160
|
+
const content = rule.render(match);
|
|
161
|
+
if (content) {
|
|
162
|
+
ranges.push(
|
|
163
|
+
Decoration.replace({ widget: new BadgeWidget(content) }).range(
|
|
164
|
+
match.index,
|
|
165
|
+
match.index + match[0].length,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
// A zero-length match would spin forever otherwise.
|
|
170
|
+
if (match[0] === '') pattern.lastIndex += 1;
|
|
171
|
+
}
|
|
172
|
+
return ranges;
|
|
173
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
placeholder as placeholderExt,
|
|
23
23
|
} from '@codemirror/view';
|
|
24
24
|
import { ckCodeMirrorTheme } from '../../../helpers/codemirror/theme';
|
|
25
|
+
import { badgeExtension, type CkCodeEditorBadgeRule } from './badgeDecoration';
|
|
25
26
|
import '../../../helpers/codemirror/code-palette.css';
|
|
26
27
|
|
|
27
28
|
export interface CreateCodeMirrorOptions {
|
|
@@ -34,6 +35,8 @@ export interface CreateCodeMirrorOptions {
|
|
|
34
35
|
readonly: boolean;
|
|
35
36
|
showLineNumbers: boolean;
|
|
36
37
|
lineWrapping: boolean;
|
|
38
|
+
/** Draws matched references as inline pills; see `badgeDecoration.ts`. */
|
|
39
|
+
badge?: CkCodeEditorBadgeRule;
|
|
37
40
|
/** Called synchronously whenever the document changes. */
|
|
38
41
|
onChange: (value: string) => void;
|
|
39
42
|
onFocus: () => void;
|
|
@@ -50,6 +53,7 @@ export interface CodeMirrorHandle {
|
|
|
50
53
|
setReadonly: (readonly: boolean) => void;
|
|
51
54
|
setShowLineNumbers: (show: boolean) => void;
|
|
52
55
|
setLineWrapping: (wrap: boolean) => void;
|
|
56
|
+
setBadge: (rule: CkCodeEditorBadgeRule | undefined) => void;
|
|
53
57
|
focus: () => void;
|
|
54
58
|
destroy: () => void;
|
|
55
59
|
}
|
|
@@ -135,6 +139,7 @@ export async function createCodeMirror(
|
|
|
135
139
|
const readonlySlot = new Compartment();
|
|
136
140
|
const lineNumbersSlot = new Compartment();
|
|
137
141
|
const wrappingSlot = new Compartment();
|
|
142
|
+
const badgeSlot = new Compartment();
|
|
138
143
|
|
|
139
144
|
const support = await resolveLanguage(options.language);
|
|
140
145
|
|
|
@@ -148,6 +153,7 @@ export async function createCodeMirror(
|
|
|
148
153
|
readonlySlot.of(readonlyState(options.readonly)),
|
|
149
154
|
lineNumbersSlot.of(options.showLineNumbers ? lineNumbers() : []),
|
|
150
155
|
wrappingSlot.of(options.lineWrapping ? EditorView.lineWrapping : []),
|
|
156
|
+
badgeSlot.of(options.badge ? badgeExtension(options.badge) : []),
|
|
151
157
|
],
|
|
152
158
|
}),
|
|
153
159
|
});
|
|
@@ -182,6 +188,9 @@ export async function createCodeMirror(
|
|
|
182
188
|
setLineWrapping: (wrap) => {
|
|
183
189
|
reconfigure(wrappingSlot, wrap ? EditorView.lineWrapping : []);
|
|
184
190
|
},
|
|
191
|
+
setBadge: (rule) => {
|
|
192
|
+
reconfigure(badgeSlot, rule ? badgeExtension(rule) : []);
|
|
193
|
+
},
|
|
185
194
|
focus: () => view.focus(),
|
|
186
195
|
destroy: () => view.destroy(),
|
|
187
196
|
};
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { CkCodeEditorBadgeRule } from './badgeDecoration';
|
|
2
|
+
|
|
3
|
+
// Re-exported so consumers get the badge types from the component's entry point.
|
|
4
|
+
export type {
|
|
5
|
+
CkCodeEditorBadgeContent,
|
|
6
|
+
CkCodeEditorBadgeRule,
|
|
7
|
+
} from './badgeDecoration';
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Single-document code editor built on [CodeMirror 6](https://codemirror.net/).
|
|
3
11
|
* Its surface is the one `UiCodeBlock` renders for fenced code inside
|
|
@@ -100,6 +108,24 @@ export interface CkCodeEditorProps {
|
|
|
100
108
|
* @default false
|
|
101
109
|
*/
|
|
102
110
|
autofocus?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Draws matched text as an inline pill, the way a mention renders in prose.
|
|
113
|
+
* Give a pattern and a callback mapping each match to a label and optional
|
|
114
|
+
* icon; return `null` from the callback to leave an occurrence alone.
|
|
115
|
+
*
|
|
116
|
+
* The document keeps the original text, so `getCode()`, copying and pasting
|
|
117
|
+
* still yield the raw reference. A pill behaves as one unit for the caret,
|
|
118
|
+
* so backspace removes the whole reference.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const badge = {
|
|
123
|
+
* pattern: /@collection:([\w-]+)/g,
|
|
124
|
+
* render: (m) => ({ label: m[1], icon: folderSvg }),
|
|
125
|
+
* };
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
badge?: CkCodeEditorBadgeRule;
|
|
103
129
|
/**
|
|
104
130
|
* Clipboard writer for the copy button. Injected for tests and for hosts that
|
|
105
131
|
* proxy clipboard access; defaults to `navigator.clipboard.writeText`.
|