@aleph-alpha/chat-kit 6.2.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.
Files changed (83) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-DPCpIxWc.js → CkHistory.vue_vue_type_script_setup_true_lang-B0WUzTHa.js} +351 -164
  2. package/dist/chat-kit.css +111 -28
  3. package/dist/code-palette-DH_G7mZ5.js +21599 -0
  4. package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts +49 -0
  5. package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts.map +1 -0
  6. package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +40 -0
  7. package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -0
  8. package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts +47 -0
  9. package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts.map +1 -0
  10. package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +48 -0
  11. package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -0
  12. package/dist/components/base/CkCodeEditor/index.d.ts +3 -0
  13. package/dist/components/base/CkCodeEditor/index.d.ts.map +1 -0
  14. package/dist/components/base/CkCodeEditor/types.d.ts +149 -0
  15. package/dist/components/base/CkCodeEditor/types.d.ts.map +1 -0
  16. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts +6 -0
  17. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts.map +1 -1
  18. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts +1 -0
  19. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts.map +1 -1
  20. package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts +1 -0
  21. package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts.map +1 -1
  22. package/dist/components/base/CkMilkdownEditor/types.d.ts +8 -0
  23. package/dist/components/base/CkMilkdownEditor/types.d.ts.map +1 -1
  24. package/dist/components/base/index.d.ts +2 -0
  25. package/dist/components/base/index.d.ts.map +1 -1
  26. package/dist/components/index.d.ts +2 -2
  27. package/dist/components/index.d.ts.map +1 -1
  28. package/dist/components/index.js +27 -26
  29. package/dist/createCodeMirror-5iswYetS.js +169 -0
  30. package/dist/{createCrepeEditor-C9seqnzm.js → createCrepeEditor-C-AQ1w_e.js} +1433 -24739
  31. package/dist/handlers/index.js +1 -1
  32. package/dist/handlers/useChatKitLabels.d.ts +6 -0
  33. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  34. package/dist/{components/base/CkMilkdownEditor/codeMirrorTheme.d.ts → helpers/codemirror/theme.d.ts} +1 -1
  35. package/dist/helpers/codemirror/theme.d.ts.map +1 -0
  36. package/dist/{index-DO8xk00f.js → index-ABUMl7H3.js} +3 -3
  37. package/dist/{index-DfvAv_Y2.js → index-BlIc9r0r.js} +3 -2
  38. package/dist/{index-VbqLf82s.js → index-BqWO6mvz.js} +4 -4
  39. package/dist/{index-Bxg0Q_mk.js → index-CFGO7LRz.js} +2 -2
  40. package/dist/index-CG8e3146.js +1825 -0
  41. package/dist/{index-C90_LSx6.js → index-CIF4qQzR.js} +4 -4
  42. package/dist/{index-DXTRMb09.js → index-CR2kGrpY.js} +3 -3
  43. package/dist/{index-B0nlcqqF.js → index-CWp_t8Wl.js} +2 -2
  44. package/dist/{index-CHtY7ztn.js → index-D0IZW-Up.js} +2 -2
  45. package/dist/{index-By7WdPbk.js → index-DKwEe_Tu.js} +4 -4
  46. package/dist/{index-DINJn4Mn.js → index-DLjsqQHx.js} +3 -3
  47. package/dist/{index-eZunhxNH.js → index-DPgw0Cf_.js} +2 -2
  48. package/dist/{index-D85elIIr.js → index-Db5-PJG-.js} +2 -2
  49. package/dist/{index-CFdcEeXx.js → index-Dbi75pKH.js} +3 -2
  50. package/dist/{index-8mYiDHFC.js → index-DfLCrnVq.js} +2 -2
  51. package/dist/{index-6PiPEjJd.js → index-GpSpJ3ti.js} +3 -2
  52. package/dist/{index-reQfKGuJ.js → index-Mh3WUU-6.js} +1 -1
  53. package/dist/{index-DLZcs4H8.js → index-QAs789O5.js} +2 -2
  54. package/dist/{index-DzpUblMx.js → index-S4ylc8JK.js} +3 -3
  55. package/dist/{index-DbVRgXxM.js → index-XZeturb3.js} +3 -3
  56. package/dist/{index-U4LYutvz.js → index-g2X42srv.js} +3 -2
  57. package/dist/{index-DNxEOsDD.js → index-hEJ_Bavx.js} +2 -2
  58. package/dist/{index-C5UC1wP7.js → index-hbS5PDHj.js} +3 -2
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +29 -28
  62. package/dist/{useChatKitLabels-Bz58tas9.js → useChatKitLabels-CatQSpfL.js} +2 -0
  63. package/package.json +4 -2
  64. package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +284 -0
  65. package/src/components/base/CkCodeEditor/CkCodeEditor.stories.ts +405 -0
  66. package/src/components/base/CkCodeEditor/CkCodeEditor.vue +244 -0
  67. package/src/components/base/CkCodeEditor/badgeDecoration.ts +173 -0
  68. package/src/components/base/CkCodeEditor/createCodeMirror.ts +197 -0
  69. package/src/components/base/CkCodeEditor/index.ts +2 -0
  70. package/src/components/base/CkCodeEditor/types.ts +154 -0
  71. package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.spec.ts +13 -0
  72. package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.ts +34 -0
  73. package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.vue +3 -1
  74. package/src/components/base/CkMilkdownEditor/createCrepeEditor.ts +2 -1
  75. package/src/components/base/CkMilkdownEditor/milkdown-theme.css +39 -26
  76. package/src/components/base/CkMilkdownEditor/types.ts +8 -0
  77. package/src/components/base/index.ts +6 -0
  78. package/src/components/index.ts +4 -0
  79. package/src/handlers/useChatKitLabels.ts +6 -0
  80. package/src/helpers/codemirror/code-palette.css +49 -0
  81. package/src/{components/base/CkMilkdownEditor/codeMirrorTheme.ts → helpers/codemirror/theme.ts} +23 -12
  82. package/src/index.ts +4 -0
  83. package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts.map +0 -1
@@ -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
+ }
@@ -0,0 +1,197 @@
1
+ import {
2
+ defaultKeymap,
3
+ history,
4
+ historyKeymap,
5
+ indentWithTab,
6
+ } from '@codemirror/commands';
7
+ import {
8
+ bracketMatching,
9
+ indentOnInput,
10
+ indentUnit,
11
+ type LanguageSupport,
12
+ } from '@codemirror/language';
13
+ import { languages } from '@codemirror/language-data';
14
+ import { Compartment, EditorState, type Extension } from '@codemirror/state';
15
+ import {
16
+ EditorView,
17
+ drawSelection,
18
+ highlightActiveLine,
19
+ highlightSpecialChars,
20
+ keymap,
21
+ lineNumbers,
22
+ placeholder as placeholderExt,
23
+ } from '@codemirror/view';
24
+ import { ckCodeMirrorTheme } from '../../../helpers/codemirror/theme';
25
+ import { badgeExtension, type CkCodeEditorBadgeRule } from './badgeDecoration';
26
+ import '../../../helpers/codemirror/code-palette.css';
27
+
28
+ export interface CreateCodeMirrorOptions {
29
+ /** Element the view is mounted into. */
30
+ parent: HTMLElement;
31
+ doc: string;
32
+ language: string;
33
+ placeholder?: string;
34
+ ariaLabel?: string;
35
+ readonly: boolean;
36
+ showLineNumbers: boolean;
37
+ lineWrapping: boolean;
38
+ /** Draws matched references as inline pills; see `badgeDecoration.ts`. */
39
+ badge?: CkCodeEditorBadgeRule;
40
+ /** Called synchronously whenever the document changes. */
41
+ onChange: (value: string) => void;
42
+ onFocus: () => void;
43
+ onBlur: () => void;
44
+ }
45
+
46
+ /** Imperative surface `CkCodeEditor` drives; keeps CodeMirror out of the SFC. */
47
+ export interface CodeMirrorHandle {
48
+ view: EditorView;
49
+ getCode: () => string;
50
+ /** Replace the whole document, keeping the cursor where it still fits. */
51
+ setCode: (value: string) => void;
52
+ setLanguage: (language: string) => Promise<void>;
53
+ setReadonly: (readonly: boolean) => void;
54
+ setShowLineNumbers: (show: boolean) => void;
55
+ setLineWrapping: (wrap: boolean) => void;
56
+ setBadge: (rule: CkCodeEditorBadgeRule | undefined) => void;
57
+ focus: () => void;
58
+ destroy: () => void;
59
+ }
60
+
61
+ /** Plain text, and the value `UiCodeBlock` also treats as "no language". */
62
+ const PLAIN_ALIASES = new Set(['text', 'txt', 'plain', 'plaintext', '']);
63
+
64
+ /**
65
+ * Resolve a language id the way a reader would expect: by name, by alias, or by
66
+ * file extension, all case-insensitively. Returns `null` for plain text and for
67
+ * anything CodeMirror does not know, so an unrecognised id degrades to
68
+ * unhighlighted code instead of throwing.
69
+ */
70
+ export async function resolveLanguage(
71
+ language: string,
72
+ ): Promise<LanguageSupport | null> {
73
+ const id = language.trim().toLowerCase();
74
+ if (PLAIN_ALIASES.has(id)) return null;
75
+
76
+ const description = languages.find(
77
+ (candidate) =>
78
+ candidate.name.toLowerCase() === id ||
79
+ candidate.alias.includes(id) ||
80
+ candidate.extensions.includes(id),
81
+ );
82
+ if (!description) return null;
83
+
84
+ try {
85
+ return await description.load();
86
+ } catch {
87
+ // A grammar that fails to load must not take the editor down with it.
88
+ return null;
89
+ }
90
+ }
91
+
92
+ // `readOnly` rejects edits while keeping the text selectable; `editable`
93
+ // additionally drops `contenteditable`, which hides the caret and keeps the
94
+ // editor out of the tab order.
95
+ function readonlyState(readonly: boolean): Extension {
96
+ return [EditorState.readOnly.of(readonly), EditorView.editable.of(!readonly)];
97
+ }
98
+
99
+ function baseExtensions(options: CreateCodeMirrorOptions): Extension[] {
100
+ return [
101
+ // Deliberately not CodeMirror's `basicSetup`: it also brings autocomplete,
102
+ // search, folding and bracket closing, none of which suit a snippet field,
103
+ // and its fold gutter would sit next to the line numbers.
104
+ history(),
105
+ drawSelection(),
106
+ highlightSpecialChars(),
107
+ highlightActiveLine(),
108
+ bracketMatching(),
109
+ indentOnInput(),
110
+ indentUnit.of(' '),
111
+ keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
112
+ ckCodeMirrorTheme,
113
+ EditorView.contentAttributes.of({
114
+ role: 'textbox',
115
+ 'aria-multiline': 'true',
116
+ ...(options.ariaLabel ? { 'aria-label': options.ariaLabel } : {}),
117
+ }),
118
+ EditorView.updateListener.of((update) => {
119
+ if (update.docChanged) options.onChange(update.state.doc.toString());
120
+ if (update.focusChanged) {
121
+ if (update.view.hasFocus) options.onFocus();
122
+ else options.onBlur();
123
+ }
124
+ }),
125
+ ...(options.placeholder ? [placeholderExt(options.placeholder)] : []),
126
+ ];
127
+ }
128
+
129
+ /**
130
+ * Build a CodeMirror view themed like `UiCodeBlock`. Loaded lazily by
131
+ * `CkCodeEditor` so CodeMirror stays out of the package's main chunk.
132
+ */
133
+ export async function createCodeMirror(
134
+ options: CreateCodeMirrorOptions,
135
+ ): Promise<CodeMirrorHandle> {
136
+ // One set per view: a compartment is the key a reconfigure targets, so
137
+ // sharing them across editors would make the instances interfere.
138
+ const languageSlot = new Compartment();
139
+ const readonlySlot = new Compartment();
140
+ const lineNumbersSlot = new Compartment();
141
+ const wrappingSlot = new Compartment();
142
+ const badgeSlot = new Compartment();
143
+
144
+ const support = await resolveLanguage(options.language);
145
+
146
+ const view = new EditorView({
147
+ parent: options.parent,
148
+ state: EditorState.create({
149
+ doc: options.doc,
150
+ extensions: [
151
+ ...baseExtensions(options),
152
+ languageSlot.of(support ? [support] : []),
153
+ readonlySlot.of(readonlyState(options.readonly)),
154
+ lineNumbersSlot.of(options.showLineNumbers ? lineNumbers() : []),
155
+ wrappingSlot.of(options.lineWrapping ? EditorView.lineWrapping : []),
156
+ badgeSlot.of(options.badge ? badgeExtension(options.badge) : []),
157
+ ],
158
+ }),
159
+ });
160
+
161
+ function reconfigure(slot: Compartment, extension: Extension): void {
162
+ view.dispatch({ effects: slot.reconfigure(extension) });
163
+ }
164
+
165
+ return {
166
+ view,
167
+ getCode: () => view.state.doc.toString(),
168
+ setCode: (value) => {
169
+ if (value === view.state.doc.toString()) return;
170
+ // Replacing the whole document would otherwise send the cursor home
171
+ // whenever a parent echoes the value back; clamp it to the new length.
172
+ const cursor = Math.min(view.state.selection.main.head, value.length);
173
+ view.dispatch({
174
+ changes: { from: 0, to: view.state.doc.length, insert: value },
175
+ selection: { anchor: cursor },
176
+ });
177
+ },
178
+ setLanguage: async (language) => {
179
+ const next = await resolveLanguage(language);
180
+ reconfigure(languageSlot, next ? [next] : []);
181
+ },
182
+ setReadonly: (readonly) => {
183
+ reconfigure(readonlySlot, readonlyState(readonly));
184
+ },
185
+ setShowLineNumbers: (show) => {
186
+ reconfigure(lineNumbersSlot, show ? lineNumbers() : []);
187
+ },
188
+ setLineWrapping: (wrap) => {
189
+ reconfigure(wrappingSlot, wrap ? EditorView.lineWrapping : []);
190
+ },
191
+ setBadge: (rule) => {
192
+ reconfigure(badgeSlot, rule ? badgeExtension(rule) : []);
193
+ },
194
+ focus: () => view.focus(),
195
+ destroy: () => view.destroy(),
196
+ };
197
+ }
@@ -0,0 +1,2 @@
1
+ export { default as CkCodeEditor } from './CkCodeEditor.vue';
2
+ export type * from './types';
@@ -0,0 +1,154 @@
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
+
9
+ /**
10
+ * Single-document code editor built on [CodeMirror 6](https://codemirror.net/).
11
+ * Its surface is the one `UiCodeBlock` renders for fenced code inside
12
+ * {@link CkMarkdownRenderer}: the same bordered, rounded panel, the same muted
13
+ * header carrying the language and a copy button, the same monospace metrics,
14
+ * and syntax colours approximating Shiki's `github-light` / `github-dark`. A
15
+ * snippet therefore looks the same whether it is being read in a reply or
16
+ * edited here.
17
+ *
18
+ * Use it wherever a whole document *is* code — a snippet field, a config or
19
+ * template editor, a "create skill" form. For prose that merely contains code,
20
+ * use {@link CkMilkdownEditor}, whose fenced blocks share this same theme.
21
+ *
22
+ * Grammars load on demand from CodeMirror's language data, so only the
23
+ * languages actually opened are fetched, and the editor itself loads lazily on
24
+ * mount so importing the component costs nothing until one is shown.
25
+ *
26
+ * @category Form Inputs
27
+ * @useCases edit a code snippet, config or template editor, code field in a form, review and tweak generated code
28
+ * @keywords code, editor, codemirror, syntax highlighting, snippet, v-model, monospace, language
29
+ * @related CkMilkdownEditor, CkMarkdownRenderer
30
+ */
31
+ export interface CkCodeEditorProps {
32
+ /**
33
+ * The code. Supports `v-model`.
34
+ *
35
+ * Unlike {@link CkMilkdownEditor}, updates are emitted synchronously on every
36
+ * change, with no debounce and no reformatting of what you pass in.
37
+ *
38
+ * @default ''
39
+ */
40
+ modelValue?: string;
41
+ /**
42
+ * Language id used for both syntax highlighting and the header label, for
43
+ * example `typescript`, `python`, `json` or `bash`. Matching is on the names,
44
+ * aliases and extensions CodeMirror knows; an unknown value still renders as
45
+ * plain text rather than failing.
46
+ *
47
+ * @default 'text'
48
+ */
49
+ language?: string;
50
+ /**
51
+ * Filename shown in the header instead of the language, mirroring the prop of
52
+ * the same name on `UiCodeBlock`.
53
+ */
54
+ filename?: string;
55
+ /** Text shown while the document is empty. */
56
+ placeholder?: string;
57
+ /**
58
+ * Render the code without allowing edits. The caret and the active-line
59
+ * highlight are hidden, but the text stays selectable and copyable.
60
+ *
61
+ * @default false
62
+ */
63
+ readonly?: boolean;
64
+ /**
65
+ * Like `readonly`, but also mutes the editor and marks it `aria-disabled`.
66
+ *
67
+ * @default false
68
+ */
69
+ disabled?: boolean;
70
+ /** Accessible name for the editable region. */
71
+ ariaLabel?: string;
72
+ /**
73
+ * Show the header bar with the language or filename and the copy button. Set
74
+ * it to `false` for a bare editing surface.
75
+ *
76
+ * @default true
77
+ */
78
+ showHeader?: boolean;
79
+ /**
80
+ * Show the copy button in the header.
81
+ *
82
+ * @default true
83
+ */
84
+ showCopyButton?: boolean;
85
+ /**
86
+ * Show the line-number gutter. On by default, which is what a dedicated code
87
+ * editor is expected to do; `UiCodeBlock` and `CkMilkdownEditor` default to
88
+ * `false` because their code is being read rather than written.
89
+ *
90
+ * @default true
91
+ */
92
+ showLineNumbers?: boolean;
93
+ /**
94
+ * Wrap long lines instead of scrolling horizontally. `UiCodeBlock` scrolls,
95
+ * so the default keeps the two consistent.
96
+ *
97
+ * @default false
98
+ */
99
+ lineWrapping?: boolean;
100
+ /**
101
+ * Maximum height of the scrollable code area, as any CSS length, for example
102
+ * `'24rem'`. Unset, the editor grows with its content.
103
+ */
104
+ maxHeight?: string;
105
+ /**
106
+ * Move focus into the editor once it has been created.
107
+ *
108
+ * @default false
109
+ */
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;
129
+ /**
130
+ * Clipboard writer for the copy button. Injected for tests and for hosts that
131
+ * proxy clipboard access; defaults to `navigator.clipboard.writeText`.
132
+ */
133
+ writeToClipboard?: (text: string) => Promise<void>;
134
+ }
135
+
136
+ /** Events emitted by {@link CkCodeEditor}. */
137
+ export interface CkCodeEditorEmits {
138
+ /** Emitted on every document change, with the full code. */
139
+ 'update:modelValue': [value: string];
140
+ /** Emitted when the editing area gains focus. */
141
+ focus: [];
142
+ /** Emitted when focus leaves the editor entirely, header included. */
143
+ blur: [];
144
+ /** Emitted after the copy button runs, whether or not the write succeeded. */
145
+ copy: [payload: { success: boolean }];
146
+ }
147
+
148
+ /** Methods exposed by {@link CkCodeEditor} through a template ref. */
149
+ export interface CkCodeEditorExposed {
150
+ /** Focus the editing area. No-op until the editor has been created. */
151
+ focus: () => void;
152
+ /** Read the current document synchronously. */
153
+ getCode: () => string;
154
+ }
@@ -142,6 +142,19 @@ describe('CkMilkdownEditor', () => {
142
142
  ).toBe('false');
143
143
  });
144
144
 
145
+ it('toggles the line-number gutter with `showLineNumbers`', async () => {
146
+ const { container, rerender } = await renderEditor({
147
+ modelValue: 'Hello',
148
+ showLineNumbers: true,
149
+ });
150
+
151
+ const root = () => container.querySelector('.ck-milkdown');
152
+ expect(root()?.className).toContain('ck-milkdown--line-numbers');
153
+
154
+ await rerender({ modelValue: 'Hello', showLineNumbers: false });
155
+ expect(root()?.className).not.toContain('ck-milkdown--line-numbers');
156
+ });
157
+
145
158
  it('exposes the editable region as a labelled textbox', async () => {
146
159
  const { container } = await renderEditor({
147
160
  modelValue: 'Hello',
@@ -25,6 +25,7 @@ const meta: Meta<typeof CkMilkdownEditor> = {
25
25
  readonly: { control: 'boolean' },
26
26
  disabled: { control: 'boolean' },
27
27
  autofocus: { control: 'boolean' },
28
+ showLineNumbers: { control: 'boolean' },
28
29
  },
29
30
  };
30
31
 
@@ -109,6 +110,21 @@ const markdown = '# Read me\\n\\nThis document **cannot** be edited.';
109
110
  <CkMilkdownEditor :model-value="markdown" readonly aria-label="Read-only document" />
110
111
  </template>`;
111
112
 
113
+ const withLineNumbersTemplateSource = `<script setup lang="ts">
114
+ import { ref } from 'vue';
115
+ import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
116
+
117
+ const markdown = ref('# Notes\\n\\nA fenced block with a gutter.');
118
+ </script>
119
+
120
+ <template>
121
+ <CkMilkdownEditor
122
+ v-model="markdown"
123
+ show-line-numbers
124
+ aria-label="Editor with line numbers"
125
+ />
126
+ </template>`;
127
+
112
128
  const disabledTemplateSource = `<script setup lang="ts">
113
129
  import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
114
130
 
@@ -210,6 +226,24 @@ export const Readonly: Story = {
210
226
  parameters: { docs: { source: { code: readonlyTemplateSource } } },
211
227
  };
212
228
 
229
+ /**
230
+ * `showLineNumbers` reveals the line-number gutter on fenced code blocks. It
231
+ * matches the prop of the same name on `UiCodeBlock`, so the same flag can keep
232
+ * the editor and the rendered message consistent.
233
+ */
234
+ export const WithLineNumbers: Story = {
235
+ render: (args) => ({
236
+ components: { CkMilkdownEditor },
237
+ setup() {
238
+ const markdown = ref(args.modelValue ?? '');
239
+ return { args, markdown };
240
+ },
241
+ template: `<CkMilkdownEditor v-bind="args" v-model="markdown" aria-label="Editor with line numbers" />`,
242
+ }),
243
+ args: { modelValue: richContent, showLineNumbers: true },
244
+ parameters: { docs: { source: { code: withLineNumbersTemplateSource } } },
245
+ };
246
+
213
247
  /**
214
248
  * `disabled` also mutes the content and blocks pointer interaction, and sets
215
249
  * `aria-disabled` on the root.
@@ -22,6 +22,7 @@ const props = withDefaults(defineProps<CkMilkdownEditorProps>(), {
22
22
  ariaLabel: undefined,
23
23
  classes: () => ({}),
24
24
  autofocus: false,
25
+ showLineNumbers: false,
25
26
  });
26
27
 
27
28
  const emit = defineEmits<CkMilkdownEditorEmits>();
@@ -132,9 +133,10 @@ defineExpose({ focus, getMarkdown });
132
133
  <div
133
134
  ref="rootRef"
134
135
  :class="[
135
- 'ck-milkdown min-w-0 break-words',
136
+ 'ck-milkdown ck-code-surface min-w-0 break-words',
136
137
  isReady && 'ck-milkdown--ready',
137
138
  disabled && 'ck-milkdown--disabled',
139
+ showLineNumbers && 'ck-milkdown--line-numbers',
138
140
  ]"
139
141
  :aria-disabled="disabled || undefined"
140
142
  @focusin="onFocusIn"
@@ -6,7 +6,7 @@ import {
6
6
  type ChatKitLabels,
7
7
  } from '../../../handlers/useChatKitLabels';
8
8
  import type { CkMarkdownElementClasses } from '../CkMarkdownRenderer';
9
- import { ckCodeMirrorTheme } from './codeMirrorTheme';
9
+ import { ckCodeMirrorTheme } from '../../../helpers/codemirror/theme';
10
10
  import { crepeIcons } from './crepeIcons';
11
11
  import { applyMarkdownClasses } from './markdownClassAttrs';
12
12
 
@@ -24,6 +24,7 @@ import '@milkdown/crepe/theme/common/list-item.css';
24
24
  import '@milkdown/crepe/theme/common/link-tooltip.css';
25
25
  import '@milkdown/crepe/theme/common/toolbar.css';
26
26
  import '@milkdown/crepe/theme/common/code-mirror.css';
27
+ import '../../../helpers/codemirror/code-palette.css';
27
28
  import './milkdown-theme.css';
28
29
 
29
30
  export interface CreateCrepeEditorOptions {