@aleph-alpha/chat-kit 6.3.0 → 6.4.1

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 (73) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-gV-lp0ZB.js → CkHistory.vue_vue_type_script_setup_true_lang-BvRIp3m6.js} +79 -16
  2. package/dist/adapters/responses-api.js +1 -1
  3. package/dist/chat-kit.css +81 -11
  4. package/dist/{code-palette-DAJUTnR-.js → code-palette-BWrZDi47.js} +156 -119
  5. package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts +8 -1
  6. package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts.map +1 -1
  7. package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +2 -0
  8. package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -1
  9. package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts +47 -0
  10. package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts.map +1 -0
  11. package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +19 -1
  12. package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -1
  13. package/dist/components/base/CkCodeEditor/types.d.ts +32 -0
  14. package/dist/components/base/CkCodeEditor/types.d.ts.map +1 -1
  15. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +7 -0
  16. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
  17. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts +9 -0
  18. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
  19. package/dist/components/base/CkMarkdownRenderer/types.d.ts +13 -0
  20. package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
  21. package/dist/components/index.js +1 -1
  22. package/dist/composables/index.js +1 -1
  23. package/dist/createCodeMirror-BO-TWTzn.js +187 -0
  24. package/dist/{createCrepeEditor-Ci5FHHIV.js → createCrepeEditor-BGSlwmKn.js} +4 -4
  25. package/dist/handlers/index.js +2 -2
  26. package/dist/helpers/codemirror/theme.d.ts.map +1 -1
  27. package/dist/helpers/unified/remarkBreaks.d.ts +15 -0
  28. package/dist/helpers/unified/remarkBreaks.d.ts.map +1 -0
  29. package/dist/{index-w2yzH7vs.js → index--Yonjzfi.js} +3 -3
  30. package/dist/{index-CN8d3yl1.js → index-2YPxXIaf.js} +1 -1
  31. package/dist/{index-BoUJGvvQ.js → index-B4uqe-fL.js} +2 -2
  32. package/dist/{index-gYt3E3Cp.js → index-Bfo4U-Al.js} +2 -2
  33. package/dist/{index-BtY1uxn6.js → index-Bw39pbRL.js} +3 -3
  34. package/dist/{index-Dl24CCB3.js → index-CGEgwupi.js} +4 -4
  35. package/dist/{index-MfXW4ZyN.js → index-CZmdzLaU.js} +3 -3
  36. package/dist/{index-DgCcFC3N.js → index-CeaT_FBl.js} +3 -3
  37. package/dist/{index-BhvbqYsK.js → index-CefLTKmi.js} +2 -2
  38. package/dist/{index-CLdXUpL8.js → index-CfVHzirA.js} +5 -5
  39. package/dist/{index-BUCqcTPA.js → index-CrBanggA.js} +2 -2
  40. package/dist/{index-DufAP1JF.js → index-Cx5ThXzs.js} +3 -3
  41. package/dist/{index-CZFx44C7.js → index-D0DQjdsq.js} +2 -2
  42. package/dist/{index-DqGDenO9.js → index-DH9e_hqJ.js} +2 -2
  43. package/dist/{index-BvG6l44n.js → index-DTomeh-p.js} +3 -3
  44. package/dist/{index-ZHg2R9iD.js → index-DZGfsazg.js} +3 -3
  45. package/dist/{index-BLVAx_f8.js → index-DlLnTKv3.js} +2 -2
  46. package/dist/{index-Cxs6kPho.js → index-DqUiBDRo.js} +3 -3
  47. package/dist/{index-DhwsowEf.js → index-GDUM7K5V.js} +2 -2
  48. package/dist/{index-CB6xZVsf.js → index-Hro6EPZS.js} +2 -2
  49. package/dist/{index-D6ftdiLa.js → index-KgumaL1c.js} +1 -1
  50. package/dist/{index-CIF93PZF.js → index-TemHfU1M.js} +3 -3
  51. package/dist/{index-CYk7AB8M.js → index-g_Bs6dNr.js} +4 -4
  52. package/dist/{index-B2tew8uf.js → index-oO_X5wU0.js} +3 -3
  53. package/dist/{index-DxTzaHLr.js → index-ptbBQ53d.js} +4 -4
  54. package/dist/index.js +5 -5
  55. package/dist/{markdownToPlainText-BOtlQ_BY.js → markdownToPlainText-DN_G-I6g.js} +1 -1
  56. package/dist/{useChatKitLabels-CatQSpfL.js → useChatKitLabels-DxW0N5U-.js} +1 -1
  57. package/dist/{useMessageHandler-nnsort0-.js → useMessageHandler-Bkzrdgfj.js} +1 -1
  58. package/package.json +3 -3
  59. package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +161 -1
  60. package/src/components/base/CkCodeEditor/CkCodeEditor.stories.ts +97 -1
  61. package/src/components/base/CkCodeEditor/CkCodeEditor.vue +135 -12
  62. package/src/components/base/CkCodeEditor/badgeDecoration.ts +173 -0
  63. package/src/components/base/CkCodeEditor/createCodeMirror.ts +54 -8
  64. package/src/components/base/CkCodeEditor/types.ts +38 -0
  65. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +40 -0
  66. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +31 -0
  67. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +3 -0
  68. package/src/components/base/CkMarkdownRenderer/types.ts +13 -0
  69. package/src/components/base/CkMilkdownEditor/milkdown-theme.css +6 -3
  70. package/src/helpers/codemirror/code-palette.css +23 -3
  71. package/src/helpers/codemirror/theme.ts +44 -8
  72. package/src/helpers/unified/remarkBreaks.ts +32 -0
  73. package/dist/createCodeMirror-BkV6FEPn.js +0 -101
@@ -1,6 +1,6 @@
1
- import { L as LRParser, E as ExternalTokenizer, C as ContextTracker } from "./index-CN8d3yl1.js";
2
- import { u as styleTags, v as tags, q as LanguageSupport, L as LRLanguage, t as foldNodeProp, A as foldInside, r as indentNodeProp, s as continuedIndent } from "./code-palette-DAJUTnR-.js";
3
- import { defineCSSCompletionSource } from "./index-BhvbqYsK.js";
1
+ import { L as LRParser, E as ExternalTokenizer, C as ContextTracker } from "./index-2YPxXIaf.js";
2
+ import { s as styleTags, t as tags, a as LanguageSupport, L as LRLanguage, f as foldNodeProp, g as foldInside, i as indentNodeProp, c as continuedIndent } from "./code-palette-BWrZDi47.js";
3
+ import { defineCSSCompletionSource } from "./index-CefLTKmi.js";
4
4
  const indent = 168, dedent = 169, descendantOp = 170, InterpolationEnd = 1, InterpolationContinue = 2, Unit = 3, callee = 171, identifier = 172, VariableName = 4, queryIdentifier = 173, InterpolationStart = 5, newline = 174, blankLineStart = 175, eof = 176, whitespace = 177, LineComment = 6, Comment = 7, IndentedMixin = 8, IndentedInclude = 9, Dialect_indented = 0;
5
5
  const space = [
6
6
  9,
@@ -1,5 +1,5 @@
1
- import { L as LRParser, E as ExternalTokenizer } from "./index-CN8d3yl1.js";
2
- import { u as styleTags, v as tags, q as LanguageSupport, L as LRLanguage, r as indentNodeProp, s as continuedIndent, t as foldNodeProp, A as foldInside } from "./code-palette-DAJUTnR-.js";
1
+ import { L as LRParser, E as ExternalTokenizer } from "./index-2YPxXIaf.js";
2
+ import { s as styleTags, t as tags, a as LanguageSupport, L as LRLanguage, i as indentNodeProp, c as continuedIndent, f as foldNodeProp, g as foldInside } from "./code-palette-BWrZDi47.js";
3
3
  const closureParamDelim = 1, tpOpen = 2, tpClose = 3, RawString = 4, Float = 5;
4
4
  const _b = 98, _e = 101, _f = 102, _r = 114, _E = 69, Zero = 48, Dot = 46, Plus = 43, Minus = 45, Hash = 35, Quote = 34, Pipe = 124, LessThan = 60, GreaterThan = 62;
5
5
  function isNum(ch) {
@@ -1,5 +1,5 @@
1
- import { L as LRParser } from "./index-CN8d3yl1.js";
2
- import { u as styleTags, v as tags, q as LanguageSupport, L as LRLanguage, r as indentNodeProp, s as continuedIndent, t as foldNodeProp, A as foldInside } from "./code-palette-DAJUTnR-.js";
1
+ import { L as LRParser } from "./index-2YPxXIaf.js";
2
+ import { s as styleTags, t as tags, a as LanguageSupport, L as LRLanguage, i as indentNodeProp, c as continuedIndent, f as foldNodeProp, g as foldInside } from "./code-palette-BWrZDi47.js";
3
3
  const jsonHighlighting = styleTags({
4
4
  String: tags.string,
5
5
  Number: tags.number,
@@ -1,4 +1,4 @@
1
- import { w as syntaxTree, T as Transaction, B as Text, S as StateEffect, D as codePointAt, x as EditorSelection, j as indentUnit, F as Annotation, G as Decoration, H as StateField, E as EditorView, P as Prec, k as keymap, I as codePointSize, R as RangeSet, J as showTooltip, V as ViewPlugin, K as logException, M as getTooltip, N as Facet, O as combineConfig, Q as fromCodePoint, U as MapMode, W as WidgetType, X as RangeValue, Y as Direction, Z as CharCategory } from "./code-palette-DAJUTnR-.js";
1
+ import { b as syntaxTree, T as Transaction, h as Text, S as StateEffect, j as codePointAt, d as EditorSelection, k as indentUnit, A as Annotation, D as Decoration, l as StateField, E as EditorView, P as Prec, m as keymap, n as codePointSize, R as RangeSet, o as showTooltip, V as ViewPlugin, q as logException, r as getTooltip, F as Facet, u as combineConfig, v as fromCodePoint, M as MapMode, W as WidgetType, w as RangeValue, x as Direction, C as CharCategory } from "./code-palette-BWrZDi47.js";
2
2
  class CompletionContext {
3
3
  /**
4
4
  Create a new completion context. (Mostly useful for testing
@@ -1,6 +1,6 @@
1
- import { q as LanguageSupport, L as LRLanguage, r as indentNodeProp, s as continuedIndent, t as foldNodeProp, A as foldInside, u as styleTags, v as tags } from "./code-palette-DAJUTnR-.js";
2
- import { defineCSSCompletionSource } from "./index-BhvbqYsK.js";
3
- import { L as LRParser, E as ExternalTokenizer } from "./index-CN8d3yl1.js";
1
+ import { a as LanguageSupport, L as LRLanguage, i as indentNodeProp, c as continuedIndent, f as foldNodeProp, g as foldInside, s as styleTags, t as tags } from "./code-palette-BWrZDi47.js";
2
+ import { defineCSSCompletionSource } from "./index-CefLTKmi.js";
3
+ import { L as LRParser, E as ExternalTokenizer } from "./index-2YPxXIaf.js";
4
4
  const descendantOp = 110, Unit = 1, openArgList = 2;
5
5
  const space = [
6
6
  9,
@@ -1,7 +1,7 @@
1
- import { L as LRLanguage, q as LanguageSupport, y as parseMixed, u as styleTags, v as tags } from "./code-palette-DAJUTnR-.js";
2
- import { html } from "./index-DxTzaHLr.js";
3
- import { javascriptLanguage } from "./index-DufAP1JF.js";
4
- import { L as LRParser, E as ExternalTokenizer } from "./index-CN8d3yl1.js";
1
+ import { L as LRLanguage, a as LanguageSupport, p as parseMixed, s as styleTags, t as tags } from "./code-palette-BWrZDi47.js";
2
+ import { html } from "./index-ptbBQ53d.js";
3
+ import { javascriptLanguage } from "./index-Cx5ThXzs.js";
4
+ import { L as LRParser, E as ExternalTokenizer } from "./index-2YPxXIaf.js";
5
5
  const Text = 1, attributeContentSingle = 33, attributeContentDouble = 34, scriptAttributeContentSingle = 35, scriptAttributeContentDouble = 36;
6
6
  const text = /* @__PURE__ */ new ExternalTokenizer((input) => {
7
7
  let start = input.pos;
@@ -1,6 +1,6 @@
1
- import { E as EditorView, x as EditorSelection, q as LanguageSupport, y as parseMixed, w as syntaxTree, L as LRLanguage, u as styleTags, v as tags, r as indentNodeProp, z as delimitedIndent, t as foldNodeProp } from "./code-palette-DAJUTnR-.js";
2
- import { html } from "./index-DxTzaHLr.js";
3
- import { L as LRParser, E as ExternalTokenizer } from "./index-CN8d3yl1.js";
1
+ import { E as EditorView, d as EditorSelection, a as LanguageSupport, p as parseMixed, b as syntaxTree, L as LRLanguage, s as styleTags, t as tags, i as indentNodeProp, e as delimitedIndent, f as foldNodeProp } from "./code-palette-BWrZDi47.js";
2
+ import { html } from "./index-ptbBQ53d.js";
3
+ import { L as LRParser, E as ExternalTokenizer } from "./index-2YPxXIaf.js";
4
4
  const interpolationStart = 1, tagStart = 2, endTagStart = 3, text = 180, endrawTagStart = 4, rawText = 181, endcommentTagStart = 5, commentText = 182, InlineComment = 6;
5
5
  function wordChar(code) {
6
6
  return code >= 65 && code <= 90 || code >= 97 && code <= 122;
@@ -1,7 +1,7 @@
1
- import { L as LRParser, E as ExternalTokenizer, C as ContextTracker } from "./index-CN8d3yl1.js";
2
- import { y as parseMixed, u as styleTags, v as tags, E as EditorView, w as syntaxTree, x as EditorSelection, q as LanguageSupport, L as LRLanguage, r as indentNodeProp, t as foldNodeProp, af as bracketMatchingHandle } from "./code-palette-DAJUTnR-.js";
3
- import { cssLanguage, css } from "./index-BhvbqYsK.js";
4
- import { typescriptLanguage, jsxLanguage, tsxLanguage, javascriptLanguage, javascript } from "./index-DufAP1JF.js";
1
+ import { L as LRParser, E as ExternalTokenizer, C as ContextTracker } from "./index-2YPxXIaf.js";
2
+ import { p as parseMixed, s as styleTags, t as tags, E as EditorView, b as syntaxTree, d as EditorSelection, a as LanguageSupport, L as LRLanguage, i as indentNodeProp, f as foldNodeProp, ag as bracketMatchingHandle } from "./code-palette-BWrZDi47.js";
3
+ import { cssLanguage, css } from "./index-CefLTKmi.js";
4
+ import { typescriptLanguage, jsxLanguage, tsxLanguage, javascriptLanguage, javascript } from "./index-Cx5ThXzs.js";
5
5
  const scriptText = 55, StartCloseScriptTag = 1, styleText = 56, StartCloseStyleTag = 2, textareaText = 57, StartCloseTextareaTag = 3, EndTag = 4, SelfClosingEndTag = 5, StartTag = 6, StartScriptTag = 7, StartStyleTag = 8, StartTextareaTag = 9, StartSelfClosingTag = 10, StartCloseTag = 11, NoMatchStartCloseTag = 12, MismatchedStartCloseTag = 13, missingCloseTag = 58, IncompleteTag = 14, IncompleteCloseTag = 15, commentContent$1 = 59, Element = 21, TagName = 23, Attribute = 24, AttributeName = 25, AttributeValue = 27, UnquotedAttributeValue = 28, ScriptText = 29, StyleText = 32, TextareaText = 35, OpenTag = 37, CloseTag = 38, Dialect_noMatch = 0, Dialect_selfClosing = 1;
6
6
  const selfClosers$1 = {
7
7
  area: true,
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { u } from "./useMessageHandler-nnsort0-.js";
2
- import { C, d, p, u as u2, b } from "./useChatKitLabels-CatQSpfL.js";
3
- import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-CZFx44C7.js";
4
- import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B, D, E } from "./CkHistory.vue_vue_type_script_setup_true_lang-gV-lp0ZB.js";
5
- import { m as m2 } from "./markdownToPlainText-BOtlQ_BY.js";
1
+ import { u } from "./useMessageHandler-Bkzrdgfj.js";
2
+ import { C, d, p, u as u2, b } from "./useChatKitLabels-DxW0N5U-.js";
3
+ import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-D0DQjdsq.js";
4
+ import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B, D, E } from "./CkHistory.vue_vue_type_script_setup_true_lang-BvRIp3m6.js";
5
+ import { m as m2 } from "./markdownToPlainText-DN_G-I6g.js";
6
6
  export {
7
7
  C as CHAT_KIT_LABELS,
8
8
  _ as CkAgentMessage,
@@ -1,4 +1,4 @@
1
- import { u as unified, r as remarkParse, a as remarkGfm } from "./index-CLdXUpL8.js";
1
+ import { u as unified, r as remarkParse, a as remarkGfm } from "./index-CfVHzirA.js";
2
2
  function generateId() {
3
3
  return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : String(Date.now());
4
4
  }
@@ -1,6 +1,6 @@
1
1
  import { computed, toValue, inject, provide } from "vue";
2
2
  import { p as purify } from "./katex.min-md74IFlY.js";
3
- import { b as asciiAlphanumeric, o as ok, l as longestStreak, f as factorySpace, m as markdownLineEnding, v as visit, c as convert, d as visitParents, S as SKIP, e as ccount, u as unified, r as remarkParse, a as remarkGfm, g as findAndReplace } from "./index-CLdXUpL8.js";
3
+ import { b as asciiAlphanumeric, o as ok, l as longestStreak, c as factorySpace, m as markdownLineEnding, v as visit, d as convert, e as visitParents, S as SKIP, g as ccount, u as unified, r as remarkParse, a as remarkGfm, f as findAndReplace } from "./index-CfVHzirA.js";
4
4
  function normalizeUri(value) {
5
5
  const result = [];
6
6
  let index = -1;
@@ -1,4 +1,4 @@
1
- import { g as generateId } from "./markdownToPlainText-BOtlQ_BY.js";
1
+ import { g as generateId } from "./markdownToPlainText-DN_G-I6g.js";
2
2
  import { reactive, ref, computed, readonly } from "vue";
3
3
  function createEmptyTree() {
4
4
  return { nodes: {}, rootId: null };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/chat-kit",
3
- "version": "6.3.0",
3
+ "version": "6.4.1",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -115,9 +115,9 @@
115
115
  "vue": "^3.5.27",
116
116
  "vue-tsc": "^3.2.4",
117
117
  "wait-on": "9.0.3",
118
+ "@aleph-alpha/eslint-config-frontend": "0.7.1",
118
119
  "@aleph-alpha/prettier-config-frontend": "2.1.1",
119
- "@aleph-alpha/tsconfig-frontend": "0.7.3",
120
- "@aleph-alpha/eslint-config-frontend": "0.7.1"
120
+ "@aleph-alpha/tsconfig-frontend": "0.7.3"
121
121
  },
122
122
  "scripts": {
123
123
  "build": "vite build && vue-tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
@@ -1,7 +1,8 @@
1
1
  import { render, screen, waitFor } from '@testing-library/vue';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import CkCodeEditor from './CkCodeEditor.vue';
4
- import { resolveLanguage } from './createCodeMirror';
4
+ import { detectLineSeparator, resolveLanguage } from './createCodeMirror';
5
+ import { matchBadgeRanges } from './badgeDecoration';
5
6
 
6
7
  // CodeMirror measures its own layout on construction; jsdom has none of the
7
8
  // geometry APIs it calls. The stubs stay in this spec rather than the global
@@ -199,6 +200,124 @@ describe('CkCodeEditor', () => {
199
200
  );
200
201
  });
201
202
 
203
+ it('draws matched references as pills while the document keeps the text', async () => {
204
+ const { container } = await renderEditor({
205
+ modelValue: 'use @collection:alpha here',
206
+ badge: {
207
+ pattern: /@collection:([\w-]+)/g,
208
+ render: (m: RegExpExecArray) => ({ label: m[1] as string }),
209
+ },
210
+ });
211
+
212
+ const pill = container.querySelector('.ck-code-badge');
213
+ expect(pill?.textContent).toBe('alpha');
214
+ // The raw reference is what a consumer reads back, not the label.
215
+ expect(container.querySelector('.cm-content')?.textContent).not.toContain(
216
+ '@collection:alpha',
217
+ );
218
+ });
219
+
220
+ it('leaves a match alone when the rule returns null', async () => {
221
+ const { container } = await renderEditor({
222
+ modelValue: '@collection:alpha and @collection:skip',
223
+ badge: {
224
+ pattern: /@collection:([\w-]+)/g,
225
+ render: (m: RegExpExecArray) =>
226
+ m[1] === 'skip' ? null : { label: m[1] as string },
227
+ },
228
+ });
229
+
230
+ const pills = [...container.querySelectorAll('.ck-code-badge')].map(
231
+ (e) => e.textContent,
232
+ );
233
+ expect(pills).toEqual(['alpha']);
234
+ });
235
+
236
+ it('keeps Windows line endings instead of rewriting them', async () => {
237
+ const crlf = 'alpha\r\nbeta\r\ngamma';
238
+ const { emitted } = await renderEditor({ modelValue: crlf });
239
+
240
+ const content = document.querySelector('.cm-content') as HTMLElement;
241
+ await userEvent.click(content);
242
+ await userEvent.type(content, '!');
243
+
244
+ await waitFor(() => expect(emitted()['update:modelValue']).toBeTruthy());
245
+ const updates = emitted()['update:modelValue'] as string[][];
246
+ const value = updates.at(-1)?.[0] ?? '';
247
+ // Every original separator survives, rather than being flattened to \n.
248
+ expect(value).toContain('alpha\r\nbeta\r\ngamma');
249
+ expect(value).not.toContain('alpha\nbeta');
250
+ });
251
+
252
+ it('leaves a Unix document on Unix endings', async () => {
253
+ const { emitted } = await renderEditor({ modelValue: 'alpha\nbeta' });
254
+
255
+ const content = document.querySelector('.cm-content') as HTMLElement;
256
+ await userEvent.click(content);
257
+ await userEvent.type(content, '!');
258
+
259
+ await waitFor(() => expect(emitted()['update:modelValue']).toBeTruthy());
260
+ const updates = emitted()['update:modelValue'] as string[][];
261
+ expect(updates.at(-1)?.[0]).not.toContain('\r');
262
+ });
263
+
264
+ it('wraps the code in the design system fade mask', async () => {
265
+ const { container } = await renderEditor({ modelValue: 'a\nb' });
266
+
267
+ const mask = container.querySelector('.ui-fade-mask');
268
+ expect(mask).toBeInTheDocument();
269
+ expect(mask?.querySelector('.ck-code-editor__host')).toBeInTheDocument();
270
+ // jsdom reports no layout, so nothing scrolls and the bands collapse.
271
+ expect(
272
+ (mask as HTMLElement).style.getPropertyValue('--ui-fade-mask-height'),
273
+ ).toBe('0px');
274
+ });
275
+
276
+ it('omits the fade mask when `scrollFade` is off', async () => {
277
+ const { container } = await renderEditor({
278
+ modelValue: 'a\nb',
279
+ scrollFade: false,
280
+ });
281
+
282
+ expect(container.querySelector('.ui-fade-mask')).toBeNull();
283
+ expect(
284
+ container.querySelector('.ck-code-editor__host'),
285
+ ).toBeInTheDocument();
286
+ });
287
+
288
+ // CodeMirror reports focus changes from a 10ms timeout, so a test asserting
289
+ // that no event followed has to outlast it.
290
+ const settleFocus = () => new Promise((resolve) => setTimeout(resolve, 30));
291
+
292
+ it('keeps focus while it moves between the code and the header', async () => {
293
+ const { emitted } = await renderEditor({ modelValue: 'x' });
294
+
295
+ const content = document.querySelector('.cm-content') as HTMLElement;
296
+ await userEvent.click(content);
297
+ await waitFor(() => expect(emitted().focus).toBeTruthy());
298
+
299
+ // The copy button is inside the component, so the editor has not been left.
300
+ await userEvent.click(screen.getByRole('button', { name: 'Copy code' }));
301
+ await settleFocus();
302
+ expect(emitted().blur).toBeUndefined();
303
+ });
304
+
305
+ it('emits `blur` once focus leaves the component entirely', async () => {
306
+ const outside = document.createElement('button');
307
+ document.body.append(outside);
308
+ const { emitted } = await renderEditor({ modelValue: 'x' });
309
+
310
+ const content = document.querySelector('.cm-content') as HTMLElement;
311
+ await userEvent.click(content);
312
+ await userEvent.click(outside);
313
+
314
+ await waitFor(() => expect(emitted().blur).toHaveLength(1));
315
+ // And exactly once, not again from CodeMirror's own focus tracking.
316
+ await settleFocus();
317
+ expect(emitted().blur).toHaveLength(1);
318
+ outside.remove();
319
+ });
320
+
202
321
  it('does not throw when unmounted before or after the editor is created', async () => {
203
322
  const early = render(CkCodeEditor, { props: { modelValue: 'x' } });
204
323
  expect(() => early.unmount()).not.toThrow();
@@ -221,3 +340,44 @@ describe('resolveLanguage', () => {
221
340
  await expect(resolveLanguage('not-a-language')).resolves.toBeNull();
222
341
  });
223
342
  });
343
+
344
+ describe('matchBadgeRanges', () => {
345
+ const rule = {
346
+ pattern: /@collection:([\w-]+)/g,
347
+ render: (m: RegExpExecArray) => ({ label: m[1] as string }),
348
+ };
349
+
350
+ it('finds every occurrence, with or without the global flag', () => {
351
+ const text = '@collection:a then @collection:b';
352
+ expect(matchBadgeRanges(rule, text)).toHaveLength(2);
353
+ expect(
354
+ matchBadgeRanges({ ...rule, pattern: /@collection:([\w-]+)/ }, text),
355
+ ).toHaveLength(2);
356
+ });
357
+
358
+ it('spans exactly the matched text', () => {
359
+ const [range] = matchBadgeRanges(rule, 'x @collection:a y');
360
+ expect(range?.from).toBe(2);
361
+ expect(range?.to).toBe('x @collection:a'.length);
362
+ });
363
+
364
+ it('skips matches the rule declines', () => {
365
+ expect(
366
+ matchBadgeRanges({ ...rule, render: () => null }, '@collection:a'),
367
+ ).toHaveLength(0);
368
+ });
369
+ });
370
+
371
+ describe('detectLineSeparator', () => {
372
+ it('reports the separator a document already uses', () => {
373
+ expect(detectLineSeparator('a\nb')).toBe('\n');
374
+ expect(detectLineSeparator('a\r\nb')).toBe('\r\n');
375
+ expect(detectLineSeparator('no newlines at all')).toBe('\n');
376
+ expect(detectLineSeparator('')).toBe('\n');
377
+ });
378
+
379
+ it('follows the majority in a mixed document', () => {
380
+ expect(detectLineSeparator('a\r\nb\r\nc\nd')).toBe('\r\n');
381
+ expect(detectLineSeparator('a\nb\nc\r\nd')).toBe('\n');
382
+ });
383
+ });
@@ -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,21 @@ const meta: Meta<typeof CkCodeEditor> = {
31
57
  showLineNumbers: { control: 'boolean' },
32
58
  lineWrapping: { control: 'boolean' },
33
59
  autofocus: { control: 'boolean' },
60
+ scrollFade: { control: 'boolean' },
61
+ // A regex plus a callback cannot be typed into a control, so the rule is
62
+ // offered as named presets instead. Without this, badges could only be seen
63
+ // on the one story that hard-codes a rule.
64
+ badge: {
65
+ options: ['none', 'collections', 'handlebars'],
66
+ mapping: {
67
+ none: undefined,
68
+ collections: collectionBadge,
69
+ handlebars: handlebarsBadge,
70
+ },
71
+ control: { type: 'radio' },
72
+ description:
73
+ 'Pattern plus a render callback turning matched text into inline pills. Pick a preset to try it on any story.',
74
+ },
34
75
  writeToClipboard: {
35
76
  control: false,
36
77
  description:
@@ -69,6 +110,14 @@ const pythonSample = [
69
110
  ' return f"{opener}, {self.name}!"',
70
111
  ].join('\n');
71
112
 
113
+ const badgeSample = [
114
+ 'You improve and draft work writing: emails, project summaries and status',
115
+ 'updates, tightening structure and matching register to the audience.',
116
+ '',
117
+ 'For analyses, use @collection:collection-name-3 and @collection:collection-name-4',
118
+ 'and distinguish an opinion from a fact.',
119
+ ].join('\n');
120
+
72
121
  const defaultTemplateSource = `<script setup lang="ts">
73
122
  import { ref } from 'vue';
74
123
  import { CkCodeEditor } from '@aleph-alpha/chat-kit';
@@ -149,6 +198,29 @@ const code = ref(longFile);
149
198
  <CkCodeEditor v-model="code" language="typescript" max-height="16rem" line-wrapping />
150
199
  </template>`;
151
200
 
201
+ const withBadgesTemplateSource = `<script setup lang="ts">
202
+ import { ref } from 'vue';
203
+ import { CkCodeEditor } from '@aleph-alpha/chat-kit';
204
+
205
+ const prompt = ref('For analyses, use @collection:sales and @collection:support.');
206
+
207
+ // The document keeps the raw text; only the drawing is replaced.
208
+ const badge = {
209
+ pattern: /@collection:([\\w-]+)/g,
210
+ render: (m) => ({ label: m[1], icon: folderIcon, title: m[0] }),
211
+ };
212
+ </script>
213
+
214
+ <template>
215
+ <CkCodeEditor
216
+ v-model="prompt"
217
+ language="markdown"
218
+ :badge="badge"
219
+ :line-wrapping="true"
220
+ aria-label="Prompt"
221
+ />
222
+ </template>`;
223
+
152
224
  const matchesRendererTemplateSource = `<script setup lang="ts">
153
225
  import { ref } from 'vue';
154
226
  import { CkCodeEditor, CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
@@ -253,7 +325,8 @@ export const WithoutHeader: Story = {
253
325
 
254
326
  /**
255
327
  * `maxHeight` caps the scrollable area and `lineWrapping` folds long lines
256
- * instead of scrolling sideways.
328
+ * instead of scrolling sideways. Scroll it: the code fades at whichever edge
329
+ * has more of the document beyond it, and neither edge fades once you reach it.
257
330
  */
258
331
  export const ScrollingAndWrapping: Story = {
259
332
  render: (args) => ({
@@ -279,6 +352,29 @@ export const ScrollingAndWrapping: Story = {
279
352
  parameters: { docs: { source: { code: scrollingTemplateSource } } },
280
353
  };
281
354
 
355
+ /**
356
+ * `badge` draws matched references as inline pills, the way a mention renders in
357
+ * prose. The document still holds the raw `@collection:…` text, so `getCode()`
358
+ * and copy give the real reference; a pill deletes as one unit.
359
+ */
360
+ export const WithBadges: Story = {
361
+ render: (args) => ({
362
+ components: { CkCodeEditor },
363
+ setup() {
364
+ const code = ref(args.modelValue ?? '');
365
+ return { args, code };
366
+ },
367
+ template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Prompt" />`,
368
+ }),
369
+ args: {
370
+ modelValue: badgeSample,
371
+ language: 'markdown',
372
+ badge: collectionBadge,
373
+ lineWrapping: true,
374
+ },
375
+ parameters: { docs: { source: { code: withBadgesTemplateSource } } },
376
+ };
377
+
282
378
  /**
283
379
  * Parity check: the editor on the left, the same snippet rendered through
284
380
  * `CkMarkdownRenderer` and `UiCodeBlock` on the right. Border, radius, header,