@agentix-e/spel-editor 1.1.3 → 1.2.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/CHANGELOG.md CHANGED
@@ -2,31 +2,127 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [0.1.1] - 2026-07-14
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
7
 
8
+ ## [1.2.0] — 2026-09-13
9
+ ### Added
10
+ - `docs/integration.md` — embedding, theming, accessibility, and how to call a provider
11
+ without shipping its key to the browser.
12
+ - Ten `--spel-token-*` custom properties, one per token class, so the syntax palette can be
13
+ replaced from CSS.
14
+
15
+ ### Changed
16
+ - **`@agentix-e/spel-ts` moves to `^2.0.0`.** This package uses spel-ts at runtime — the
17
+ tokenizer and `TokenKind` for the grammar, the parser behind lint and hover, and the
18
+ completion, formatter and diagnostic engines — so the major bump is a real dependency
19
+ change rather than a formality. `TokenKind`'s ordinals are renumbered in 2.0.0 and an
20
+ unsuffixed integer literal above `int` range is no longer accepted. The grammar refers
21
+ to members by name and no literal above `int` range appears anywhere in this package's
22
+ sources or tests, so neither change should reach its behaviour — and the suite, not
23
+ that reasoning, is what establishes it.
24
+ - `typecheck` now also checks the tests, which `tsconfig.json` had excluded by including
25
+ `src` alone. Fifty-two errors surfaced, all of them real, and are fixed.
26
+ - Coverage thresholds raised to 95 on all four dimensions, from 85/80/85/85.
27
+
28
+ ### Fixed
29
+ - **`true`, `false`, `null`, `and`, `or`, `matches`, `between`, `instanceof` and `new`
30
+ were painted as ordinary variables.** spel-ts 2.0.0 moved those words out of its
31
+ tokenizer — it resolves them in the parser, with `equalsIgnoreCase`, which is also
32
+ what lets a field be called `and` — so all nine now arrive as `IDENTIFIER` instead of
33
+ token kinds of their own. The grammar decides them from the text, and only when they
34
+ are not names: `#and` is still a variable and `obj.and` still a property.
35
+ - **`div` was painted as nothing.** spel-ts 2.0.0 gives the textual division operator a
36
+ token kind of its own and the style table had no case for it, so `a div b` left it
37
+ unstyled. `a mod b` was already covered; the two now behave alike.
38
+ - **Syntax highlighting did nothing.** `spelLanguage()` wrapped the stream parser in a
39
+ `LanguageSupport` without a highlight style, and `LanguageSupport` installs none of its
40
+ own, so every token was tagged and none was coloured. The style is now part of the
41
+ language support.
42
+ - **Accepting a completion typed a placeholder into the document.** The engine describes
43
+ insertions as snippets (`T($1)`, `between {$1, $2}`, `$1 ? $2 : $3`), which were passed
44
+ to CodeMirror's `apply` as plain strings. They are translated to CodeMirror's field
45
+ syntax and applied as snippets, so the placeholders became tab-through fields.
46
+ - **A completion replaced only the ASCII tail of a reference.** The prefix was matched with
47
+ `\w`, so completing `#order.amo` inserted after `amo` instead of replacing the
48
+ reference, and a field named in Chinese was not treated as part of the token at all.
49
+ - **The completion list stayed open where no token continues.** `validFor` was `() => true`,
50
+ which claims every position — including the space that ends a name.
51
+ - **Assigning `value` did not reach the document.** `setValue()` dispatched, but
52
+ `el.value = …` and the `value` attribute only updated the element's state, so a
53
+ declarative binding showed the previous expression.
54
+ - **The highlighter's token cache was shared by every document.** The tokenizer, token list
55
+ and cursor lived in a closure rather than in the state CodeMirror passes per document, so
56
+ two parses interleaved and each replaced the other's tokens.
57
+ - **Two nested textboxes for one editable region.** The wrapper carried `role="textbox"`
58
+ and `tabindex="0"` while CodeMirror's content already carries `role="textbox"`; the label
59
+ and disabled state now live on the content element, and the wrapper is no longer a
60
+ separate tab stop.
61
+ - **Gutter text was below the contrast floor.** `#9ca3af` on `#f9fafb` measured 2.43:1;
62
+ the default is now `#5b6472`, which measures 5.72:1.
63
+ - **A diagnostic outside the document would have taken out every mark.** Ranges from the
64
+ engine are clamped to the document, which CodeMirror requires.
65
+ - Test fixtures that did not match the interfaces they claimed: `variables` was written as a
66
+ list, `MethodSchema`/`TypeSchema`/`FunctionSchema` were given a `type` field they do not
67
+ have, and a method key of `toString()` produced the label `toString()()`.
68
+
69
+ ## [1.1.3] — 2026-07-20
70
+ ### Changed
71
+ - Unified badge style with CI/Docs/Coverage/License/TypeScript/Node.js badges
72
+ - Added GitHub Pages deployment job to CI (TypeDoc + coverage)
73
+ - Added `typedoc.json` for API docs generation
74
+ - Added SEO-optimized GitHub Pages landing page
75
+ - Bumped `spel-ts` to `^1.2.2`, `nl2spel` to `^1.3.0`
76
+
77
+ ## [1.1.2] — 2026-07-19
7
78
  ### Changed
8
- - Remove unused imports across source files
9
- - Add `.prettierrc` configuration for consistent formatting
10
- - Tighten `tsconfig.json` strictness checks
79
+ - Updated `nl2spel` dependency to `^1.2.2`
11
80
 
81
+ ## [1.1.1] — 2026-07-19
82
+ ### Fixed
83
+ - Removed arbitrary depth limit
84
+
85
+ ## [1.1.0] — 2026-07-18
86
+ ### Added
87
+ - Recursive context schema extraction
88
+ - NL2SpEL integration documentation and interactive demo page
89
+
90
+ ## [1.0.0] — 2026-07-17
12
91
  ### Added
13
- - Playwright browser integration tests (`test:browser` / `test:browser:ui`)
14
- - nl2spel integration tests with DeepSeek provider
15
- - `DEEPSEEK_API_KEY` secret handling for integration tests
92
+ - 100% language service coverage (completion, diagnostics, hover, formatting)
93
+ - OIDC npm provenance publishing
94
+ - Playwright browser tests + nl2spel integration tests
95
+ - `getEditorView()` method for direct CodeMirror access
96
+
97
+ ### Changed
98
+ - `strictFunctionTypes` enabled
99
+ - Coverage thresholds raised to 85/80/85/85
100
+ - Vitest upgraded to v3.x
101
+ - CI: fixed browser-tests job (ESM needs bundler)
102
+
103
+ ## [0.1.1] — 2026-07-14
104
+ ### Changed
105
+ - Code quality: unused imports, config consistency, type safety
106
+ - CI: explicit pnpm version 10.28.2
107
+ - Restored npm badge
16
108
 
17
- ## [0.1.0] - 2026-07-12
109
+ ### Fixed
110
+ - Package.json repo URL
111
+ - Cross-repo ecosystem coherence (links, consistency, badges)
18
112
 
113
+ ## [0.1.0] — 2026-07-12
19
114
  ### Added
20
- - Initial release of `@agentix-e/spel-editor`
21
- - `<spel-editor>` Web Component with CodeMirror 6 integration
22
- - Syntax highlighting via spel-ts Tokenizer + StreamLanguage adapter
23
- - Auto-completion with keyword, operator, and context-aware suggestions
24
- - Real-time diagnostics (syntax, semantic, and context validation)
25
- - Hover tooltips showing node type information
26
- - Non-invasive theming via 12 CSS custom properties
27
- - Programmatic API: `getValue()`, `setValue()`, `validate()`, `format()`, `insertSnippet()`
115
+ - Initial release: Web-embeddable SpEL editor Web Component
116
+ - CodeMirror 6 integration with syntax highlighting
117
+ - Auto-completion adapter (spel-ts powered)
118
+ - Real-time diagnostics / lint adapter
119
+ - Hover tooltips adapter
120
+ - SpEL grammar tokenizer (StreamLanguage)
121
+ - `validate` event with `SpelDiagnostic` detail
28
122
  - `change` event with `value` and `isValid` detail
29
- - Framework-agnostic works with React, Vue, Angular, Svelte, or plain HTML
123
+ - 12 CSS custom properties for non-invasive theming
124
+ - Framework-agnostic Web Component (`<spel-editor>`)
125
+ - Programmatic API: `getValue()`, `setValue()`, `validate()`, `format()`, `insertSnippet()`, `getEditorView()`
30
126
 
31
- [0.1.1]: https://github.com/AgentiX-E/spel-editor/releases/tag/v0.1.1
32
- [0.1.0]: https://github.com/AgentiX-E/spel-editor/releases/tag/v0.1.0
127
+ ---
128
+ *Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).*
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @agentix-e/spel-editor
2
2
 
3
3
  > Web-embeddable Spring Expression Language (SpEL) editor.
4
- > Based on CodeMirror 6 + spel-ts v1.1.0. Framework-agnostic Web Component.
4
+ > Based on CodeMirror 6 + spel-ts v2.0.0. Framework-agnostic Web Component.
5
5
 
6
6
  [![CI](https://github.com/AgentiX-E/spel-editor/actions/workflows/ci.yml/badge.svg)](https://github.com/AgentiX-E/spel-editor/actions/workflows/ci.yml)
7
7
  [![npm](https://img.shields.io/npm/v/@agentix-e/spel-editor?color=blue)](https://www.npmjs.com/package/@agentix-e/spel-editor)
@@ -47,11 +47,12 @@ npm install @agentix-e/spel-editor
47
47
 
48
48
  ## Features
49
49
 
50
- - **Syntax highlighting** — Accurate token-level coloring powered by spel-ts Tokenizer
51
- - **Auto-completion** — Keyword, operator, variable, and context-aware suggestions
50
+ - **Syntax highlighting** — Token-level colouring powered by the spel-ts Tokenizer, with every token class themable through a CSS custom property
51
+ - **Auto-completion** — Keyword, operator, variable, and context-aware suggestions, inserted as CodeMirror snippets with tab-through fields
52
52
  - **Real-time diagnostics** — Syntax, semantic, and context validation
53
53
  - **Hover tooltips** — Node type information on hover
54
- - **Non-invasive theming** — 12 CSS custom properties for complete visual control
54
+ - **Non-invasive theming** — 22 CSS custom properties for complete visual control
55
+ - **Declarative-friendly** — Assigning `value` (property or attribute) updates the visible document
55
56
  - **Framework agnostic** — Works with React, Vue, Angular, Svelte, or plain HTML
56
57
 
57
58
  ## API Reference
@@ -99,11 +100,27 @@ npm install @agentix-e/spel-editor
99
100
  | `--spel-line-highlight` | `#f3f4f6` | Active line highlight color |
100
101
  | `--spel-selection-bg` | `#bfdbfe` | Selection background color |
101
102
  | `--spel-gutter-bg` | `#f9fafb` | Gutter background |
102
- | `--spel-gutter-fg` | `#9ca3af` | Gutter foreground (line numbers) |
103
+ | `--spel-gutter-fg` | `#5b6472` | Gutter foreground (line numbers). 5.72:1 on the gutter background |
103
104
  | `--spel-border-width` | `1px` | Editor border width |
104
105
  | `--spel-border-color` | `#d0d5dd` | Editor border color |
105
106
  | `--spel-border-radius` | `6px` | Editor border radius |
106
107
 
108
+ Every colour a token can be painted in is a custom property too, so the palette can be
109
+ replaced without a rebuild:
110
+
111
+ | Property | Default | Token |
112
+ |----------|---------|-------|
113
+ | `--spel-token-keyword` | `#7c3aed` | `null`, `true`, `matches`, `between`, `instanceof` |
114
+ | `--spel-token-bool` | `#b45309` | Boolean literals |
115
+ | `--spel-token-number` | `#0f766e` | Integer, long, real and hex literals |
116
+ | `--spel-token-string` | `#15803d` | String literals |
117
+ | `--spel-token-variable` | `#1d4ed8` | `#variable` |
118
+ | `--spel-token-property` | `#0369a1` | `.property` and `?.property` |
119
+ | `--spel-token-operator` | `#475569` | Arithmetic, comparison and logical operators |
120
+ | `--spel-token-type` | `#9333ea` | `@bean`, `&@factory`, `T` |
121
+ | `--spel-token-punctuation` | `#64748b` | Brackets, commas, dots |
122
+ | `--spel-token-operator-keyword` | `#7c3aed` | Selection and projection (`.?[ ]`, `.![ ]`, `.^[ ]`, `.*[ ]`) |
123
+
107
124
  ## NL Integration — Natural Language → SpEL
108
125
 
109
126
  `@agentix-e/spel-editor` accepts `@agentix-e/nl2spel` as an **optional peer dependency**.
@@ -153,11 +170,17 @@ npm install @agentix-e/nl2spel-openai
153
170
  const editor = document.querySelector('#editor');
154
171
  const engine = new NL2SpelEngine();
155
172
 
156
- // Register DeepSeek as the LLM provider
173
+ // Route provider calls through your own server, which holds the API key.
174
+ // A key placed here would be compiled into a public bundle — see
175
+ // docs/integration.md §5 for the proxy contract and the browser-local alternative.
157
176
  engine.registerProvider(
158
177
  new OpenAICompatibleProvider({
159
- provider: 'deepseek',
160
- apiKey: 'sk-your-deepseek-key-here',
178
+ custom: {
179
+ name: 'spel-proxy',
180
+ baseURL: '/api/spel-llm',
181
+ apiKey: 'not-a-secret',
182
+ model: 'deepseek-chat',
183
+ },
161
184
  })
162
185
  );
163
186
 
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { closeBrackets, autocompletion } from "@codemirror/autocomplete";
27
27
  import { linter } from "@codemirror/lint";
28
28
 
29
29
  // src/cm6/spel-language.ts
30
- import { LanguageSupport } from "@codemirror/language";
30
+ import { LanguageSupport, syntaxHighlighting } from "@codemirror/language";
31
31
 
32
32
  // src/cm6/spel-grammar.ts
33
33
  import { StreamLanguage } from "@codemirror/language";
@@ -42,6 +42,7 @@ function tokenKindToStyle(kind) {
42
42
  case TokenKind.MATCHES:
43
43
  case TokenKind.BETWEEN:
44
44
  case TokenKind.INSTANCEOF:
45
+ case TokenKind.DIV:
45
46
  case TokenKind.MOD:
46
47
  case TokenKind.NEW:
47
48
  return "keyword";
@@ -112,103 +113,140 @@ function tokenKindToStyle(kind) {
112
113
  return "";
113
114
  }
114
115
  }
116
+ var PARSER_RESOLVED_WORDS = /* @__PURE__ */ new Map([
117
+ ["true", "bool"],
118
+ ["false", "bool"],
119
+ ["null", "keyword"],
120
+ ["and", "operator"],
121
+ ["or", "operator"],
122
+ ["matches", "keyword"],
123
+ ["between", "keyword"],
124
+ ["instanceof", "keyword"],
125
+ ["new", "keyword"]
126
+ ]);
115
127
  function createSpelStreamParser() {
116
128
  return StreamLanguage.define(createTokenParser());
117
129
  }
130
+ function spansFor(line) {
131
+ let tokens;
132
+ try {
133
+ tokens = new SpelTokenizer(line).tokenize();
134
+ } catch {
135
+ return [];
136
+ }
137
+ const spans = [];
138
+ for (let index = 0; index < tokens.length; index++) {
139
+ const token = tokens[index];
140
+ if (token.kind === TokenKind.EOF) break;
141
+ const style = styleForToken(line, tokens, index);
142
+ if (style === "") continue;
143
+ spans.push({ from: token.startPos, to: token.endPos, style });
144
+ }
145
+ return spans;
146
+ }
147
+ function styleForToken(line, tokens, index) {
148
+ const token = tokens[index];
149
+ if (token.kind === TokenKind.IDENTIFIER) {
150
+ const previous = index > 0 ? tokens[index - 1] : void 0;
151
+ if (previous?.kind === TokenKind.HASH) return "variableName";
152
+ if (previous?.kind === TokenKind.DOT || previous?.kind === TokenKind.SAFE_NAV) {
153
+ return "propertyName";
154
+ }
155
+ const word = line.slice(token.startPos, token.endPos).toLowerCase();
156
+ return PARSER_RESOLVED_WORDS.get(word) ?? "variableName";
157
+ }
158
+ return tokenKindToStyle(token.kind);
159
+ }
118
160
  function createTokenParser() {
119
- let _tokenizer = null;
120
- let _tokens = [];
121
- let _tokenIndex = 0;
122
- const startState = () => null;
123
- const token = (stream) => {
124
- if (!_tokenizer || _tokenIndex === 0) {
125
- _tokenizer = new SpelTokenizer(stream.string);
126
- let rawTokens;
127
- try {
128
- rawTokens = _tokenizer.tokenize();
129
- } catch {
130
- _tokens = [];
131
- _tokenIndex = 0;
132
- stream.skipToEnd();
133
- return null;
134
- }
135
- _tokens = [];
136
- _tokenIndex = 0;
137
- for (let i = 0; i < rawTokens.length - 1; i++) {
138
- const tok = rawTokens[i];
139
- const style = tokenKindToStyle(tok.kind);
140
- if (tok.kind === TokenKind.IDENTIFIER) {
141
- const prevTok = i > 0 ? rawTokens[i - 1] : null;
142
- if (prevTok?.kind === TokenKind.HASH) {
143
- _tokens.push({
144
- from: tok.startPos,
145
- to: tok.endPos,
146
- style: "variableName"
147
- });
148
- continue;
149
- }
150
- if (prevTok?.kind === TokenKind.DOT || prevTok?.kind === TokenKind.SAFE_NAV) {
151
- _tokens.push({
152
- from: tok.startPos,
153
- to: tok.endPos,
154
- style: "propertyName"
155
- });
156
- continue;
157
- }
158
- }
159
- _tokens.push({ from: tok.startPos, to: tok.endPos, style });
160
- }
161
+ const startState = () => ({ line: null, spans: [], index: 0 });
162
+ const token = (stream, state) => {
163
+ if (state.line !== stream.string || stream.pos === 0) {
164
+ state.line = stream.string;
165
+ state.spans = spansFor(stream.string);
166
+ state.index = 0;
161
167
  }
162
- while (_tokenIndex < _tokens.length) {
163
- const t = _tokens[_tokenIndex];
164
- if (t.from >= stream.pos) {
165
- stream.pos = t.to;
166
- _tokenIndex++;
167
- return t.style || null;
168
+ while (state.index < state.spans.length) {
169
+ const span = state.spans[state.index];
170
+ state.index += 1;
171
+ if (span.to > stream.pos) {
172
+ stream.pos = span.to;
173
+ return span.style;
168
174
  }
169
- _tokenIndex++;
170
175
  }
171
176
  stream.skipToEnd();
172
- _tokenIndex = 0;
173
177
  return null;
174
178
  };
175
179
  return { startState, token };
176
180
  }
177
181
 
182
+ // src/cm6/spel-highlight.ts
183
+ import { HighlightStyle } from "@codemirror/language";
184
+ import { tags } from "@lezer/highlight";
185
+ var spelHighlightStyle = HighlightStyle.define([
186
+ { tag: tags.keyword, color: "var(--spel-token-keyword, #7c3aed)" },
187
+ { tag: tags.bool, color: "var(--spel-token-bool, #b45309)" },
188
+ { tag: tags.number, color: "var(--spel-token-number, #0f766e)" },
189
+ { tag: tags.string, color: "var(--spel-token-string, #15803d)" },
190
+ { tag: tags.variableName, color: "var(--spel-token-variable, #1d4ed8)" },
191
+ { tag: tags.propertyName, color: "var(--spel-token-property, #0369a1)" },
192
+ { tag: tags.operator, color: "var(--spel-token-operator, #475569)" },
193
+ { tag: tags.typeName, color: "var(--spel-token-type, #9333ea)" },
194
+ { tag: tags.punctuation, color: "var(--spel-token-punctuation, #64748b)" },
195
+ {
196
+ tag: tags.operatorKeyword,
197
+ color: "var(--spel-token-operator-keyword, #7c3aed)",
198
+ fontStyle: "italic"
199
+ }
200
+ ]);
201
+
178
202
  // src/cm6/spel-language.ts
179
203
  function spelLanguage() {
180
- return new LanguageSupport(createSpelStreamParser());
204
+ return new LanguageSupport(createSpelStreamParser(), [syntaxHighlighting(spelHighlightStyle)]);
181
205
  }
182
206
 
183
207
  // src/cm6/completion-source.ts
184
- import "@codemirror/autocomplete";
208
+ import {
209
+ snippetCompletion
210
+ } from "@codemirror/autocomplete";
185
211
  import { SpelCompletionEngine } from "@agentix-e/spel-ts";
212
+
213
+ // src/cm6/snippet.ts
214
+ function toCm6Snippet(template) {
215
+ return template.replace(/\$(\d+)/g, (_match, digits) => `\${${digits}}`);
216
+ }
217
+
218
+ // src/cm6/completion-source.ts
219
+ var NAME_PREFIX = /[\p{L}\p{N}_$#@.]*$/u;
220
+ var NAME_VALID_FOR = /^[\p{L}\p{N}_$#@.]*$/u;
186
221
  function spelCompletion(getContextSchema) {
187
222
  return (context) => {
188
223
  const expression = context.state.sliceDoc();
189
224
  const position = context.pos;
190
225
  const schema = getContextSchema?.() ?? void 0;
191
226
  const items = SpelCompletionEngine.getCompletions(expression, position, schema);
192
- const validFor = /\w*/;
193
227
  return {
194
- from: context.matchBefore(validFor)?.from ?? position,
228
+ // `matchBefore` answers null when the pattern does not match at the cursor, so the
229
+ // fallback stays even though NAME_PREFIX, which may match empty, always matches
230
+ // today. It guards CodeMirror's contract rather than a case this adapter can reach,
231
+ // and a future narrowing of the pattern would make it live.
232
+ from: context.matchBefore(NAME_PREFIX)?.from ?? position,
195
233
  options: items.map((item) => mapToCM6Completion(item)),
196
- // Allow completions at any position
197
- validFor: () => true
234
+ // Stated rather than omitted, so the list stays open only while the cursor is
235
+ // still inside a name. `() => true` claimed every position continues a token,
236
+ // including the space that ends one.
237
+ validFor: NAME_VALID_FOR
198
238
  };
199
239
  };
200
240
  }
201
241
  function mapToCM6Completion(item) {
202
- const type = mapKindToCM6Type(item.kind);
203
- return {
242
+ return snippetCompletion(toCm6Snippet(item.insertText), {
204
243
  label: item.label,
205
- type,
244
+ type: mapKindToCM6Type(item.kind),
206
245
  detail: item.detail,
207
246
  info: item.documentation,
208
- apply: item.insertText,
209
247
  // Higher priority items appear first
210
248
  boost: item.sortPriority / 100
211
- };
249
+ });
212
250
  }
213
251
  function mapKindToCM6Type(kind) {
214
252
  switch (kind) {
@@ -243,16 +281,18 @@ function spelLint(getContextSchema) {
243
281
  const schema = getContextSchema?.() ?? void 0;
244
282
  if (expression.trim().length === 0) return [];
245
283
  const diagnostics = SpelDiagnosticEngine.validate(expression, schema);
246
- return diagnostics.map((d) => mapToCM6Diagnostic(d));
284
+ return diagnostics.map((diagnostic) => mapToCM6Diagnostic(diagnostic, view.state.doc.length));
247
285
  };
248
286
  }
249
- function mapToCM6Diagnostic(d) {
287
+ function mapToCM6Diagnostic(diagnostic, docLength) {
288
+ const from = Math.max(0, Math.min(diagnostic.from, docLength));
289
+ const to = Math.max(from, Math.min(diagnostic.to, docLength));
250
290
  return {
251
- from: d.from,
252
- to: d.to,
253
- message: d.message,
254
- severity: mapSeverity(d.severity),
255
- source: d.code
291
+ from,
292
+ to,
293
+ message: diagnostic.message,
294
+ severity: mapSeverity(diagnostic.severity),
295
+ source: diagnostic.code
256
296
  };
257
297
  }
258
298
  function mapSeverity(sev) {
@@ -354,11 +394,14 @@ var EDITOR_STYLES = css`
354
394
  }
355
395
  .cm-editor .cm-gutters {
356
396
  background: var(--spel-gutter-bg, #f9fafb);
357
- color: var(--spel-gutter-fg, #9ca3af);
397
+ /* 5.72:1 on the gutter background. The previous #9ca3af measured 2.43:1, under
398
+ the 4.5:1 WCAG AA requires for text, which made line numbers and lint markers
399
+ unreadable at reduced contrast sensitivity. */
400
+ color: var(--spel-gutter-fg, #5b6472);
358
401
  border: none;
359
402
  }
360
403
  `;
361
- var _editorView, _diagnosticCache, _diagnosticDebounceTimer, _SpelEditor_instances, scheduleDiagnostics_fn, runDiagnostics_fn, createEditor_fn, updateEditorState_fn, fireChange_fn;
404
+ var _editorView, _diagnosticCache, _diagnosticDebounceTimer, _SpelEditor_instances, reflectValue_fn, scheduleDiagnostics_fn, runDiagnostics_fn, contentAttributes_fn, createEditor_fn, updateEditorState_fn, fireChange_fn;
362
405
  var SpelEditor = class extends LitElement {
363
406
  constructor() {
364
407
  super(...arguments);
@@ -377,11 +420,6 @@ var SpelEditor = class extends LitElement {
377
420
  return html`
378
421
  <div
379
422
  class="cm-container"
380
- role="textbox"
381
- aria-label=${this.placeholder}
382
- aria-readonly=${this.readonly ? "true" : "false"}
383
- aria-disabled=${this.disabled ? "true" : "false"}
384
- tabindex="0"
385
423
  style="
386
424
  min-height: ${this.minHeight};
387
425
  border: var(--spel-border-width, 1px) solid var(--spel-border-color, #d0d5dd);
@@ -395,12 +433,15 @@ var SpelEditor = class extends LitElement {
395
433
  __privateMethod(this, _SpelEditor_instances, createEditor_fn).call(this);
396
434
  }
397
435
  updated(changed) {
398
- if (changed.has("disabled") || changed.has("readonly")) {
436
+ if (changed.has("disabled") || changed.has("readonly") || changed.has("placeholder")) {
399
437
  __privateMethod(this, _SpelEditor_instances, updateEditorState_fn).call(this);
400
438
  }
401
439
  if (!__privateGet(this, _editorView) && this.containerEl) {
402
440
  __privateMethod(this, _SpelEditor_instances, createEditor_fn).call(this);
403
441
  }
442
+ if (changed.has("value")) {
443
+ __privateMethod(this, _SpelEditor_instances, reflectValue_fn).call(this);
444
+ }
404
445
  if (changed.has("contextSchema")) {
405
446
  __privateMethod(this, _SpelEditor_instances, scheduleDiagnostics_fn).call(this);
406
447
  }
@@ -493,6 +534,29 @@ _editorView = new WeakMap();
493
534
  _diagnosticCache = new WeakMap();
494
535
  _diagnosticDebounceTimer = new WeakMap();
495
536
  _SpelEditor_instances = new WeakSet();
537
+ /**
538
+ * Push the current `value` into the editor, if the two have diverged.
539
+ *
540
+ * Typing sets `value` from the document, so this is a no-op for the common case and
541
+ * only acts on a value assigned from outside — which is how every declarative
542
+ * consumer sets it. Without this, `el.value = 'a > 1'` updated the element's state
543
+ * while the visible document kept showing the previous expression.
544
+ *
545
+ * The guard is what keeps it from looping: the change listener writes the document
546
+ * text back into `value`, and a dispatch here is skipped whenever they already
547
+ * agree, so the cursor is never reset mid-edit.
548
+ */
549
+ reflectValue_fn = function() {
550
+ const view = __privateGet(this, _editorView);
551
+ if (!view) return;
552
+ const current = view.state.sliceDoc();
553
+ if (current === this.value) return;
554
+ view.dispatch({
555
+ changes: { from: 0, to: view.state.doc.length, insert: this.value },
556
+ selection: { anchor: this.value.length }
557
+ });
558
+ __privateMethod(this, _SpelEditor_instances, scheduleDiagnostics_fn).call(this);
559
+ };
496
560
  /**
497
561
  * Schedule a debounced diagnostic run (300ms).
498
562
  * Avoids redundant computation on rapid typing.
@@ -526,6 +590,21 @@ runDiagnostics_fn = function() {
526
590
  })
527
591
  );
528
592
  };
593
+ /**
594
+ * Attributes for the editable region.
595
+ *
596
+ * CodeMirror already renders `.cm-content` with `role="textbox"`,
597
+ * `aria-multiline="true"` and — when the state is read-only — `aria-readonly`, so
598
+ * that element is the component's one textbox. The label and the disabled state
599
+ * belong on it as well: declaring them on the wrapper as well produced two nested
600
+ * textboxes and two tab stops for a single editable region.
601
+ */
602
+ contentAttributes_fn = function() {
603
+ return {
604
+ "aria-label": this.placeholder,
605
+ ...this.disabled ? { "aria-disabled": "true" } : {}
606
+ };
607
+ };
529
608
  createEditor_fn = function() {
530
609
  const extensions = [
531
610
  spelLanguage(),
@@ -536,6 +615,7 @@ createEditor_fn = function() {
536
615
  closeBrackets(),
537
616
  keymap.of([...defaultKeymap, ...historyKeymap]),
538
617
  cmPlaceholder(this.placeholder),
618
+ EditorView.contentAttributes.of(__privateMethod(this, _SpelEditor_instances, contentAttributes_fn).call(this)),
539
619
  EditorView.updateListener.of((update) => {
540
620
  if (update.docChanged) {
541
621
  this.value = update.state.sliceDoc();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentix-e/spel-editor",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Web-embeddable Spring Expression Language (SpEL) editor — CodeMirror 6 + spel-ts powered, framework-agnostic Web Component with NL2SpEL integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "test:browser:ui": "playwright test --ui",
25
25
  "test:all": "pnpm test && pnpm test:browser",
26
26
  "test:watch": "vitest",
27
- "typecheck": "tsc --noEmit",
27
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
28
28
  "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
29
29
  "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
30
30
  "clean": "rm -rf dist",
@@ -34,13 +34,14 @@
34
34
  "node": ">=18"
35
35
  },
36
36
  "dependencies": {
37
- "@agentix-e/spel-ts": "^1.2.2",
37
+ "@agentix-e/spel-ts": "^2.0.0",
38
38
  "@codemirror/autocomplete": "^6.0.0",
39
39
  "@codemirror/commands": "^6.0.0",
40
40
  "@codemirror/language": "^6.0.0",
41
41
  "@codemirror/lint": "^6.0.0",
42
42
  "@codemirror/state": "^6.0.0",
43
43
  "@codemirror/view": "^6.0.0",
44
+ "@lezer/highlight": "^1.2.3",
44
45
  "lit": "^3.0.0"
45
46
  },
46
47
  "peerDependencies": {