@1771technologies/lytenyte-pro 2.0.4-dev.4 → 2.1.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 (28) hide show
  1. package/README.md +54 -11
  2. package/css/components/expression-editor.css +2 -1
  3. package/dist/components/expressions-editor/create-completion-provider.d.ts +8 -1
  4. package/dist/components/expressions-editor/create-completion-provider.js +273 -43
  5. package/dist/components/expressions-editor/cursor-position/use-cursor-position.d.ts +2 -2
  6. package/dist/components/expressions-editor/cursor-position/use-cursor-position.js +24 -8
  7. package/dist/components/expressions-editor/expression-editor.js +29 -9
  8. package/dist/components/expressions-editor/index.d.ts +1 -0
  9. package/dist/components/expressions-editor/intellisence/completion-context.d.ts +3 -2
  10. package/dist/components/expressions-editor/intellisence/completion-popover.js +20 -3
  11. package/dist/components/expressions-editor/token-highlighter-default.js +2 -0
  12. package/dist/components/expressions-editor/types.d.ts +1 -0
  13. package/dist/components/expressions-editor/use-keyboard-navigation.d.ts +2 -2
  14. package/dist/components/expressions-editor/use-keyboard-navigation.js +25 -4
  15. package/dist/expressions/index.d.ts +3 -1
  16. package/dist/expressions/index.js +3 -1
  17. package/dist/expressions/parser/types.d.ts +5 -1
  18. package/dist/expressions/plugins/date-identifier.d.ts +4 -0
  19. package/dist/expressions/plugins/date-identifier.js +29 -0
  20. package/dist/expressions/plugins/date-literal.d.ts +2 -0
  21. package/dist/expressions/plugins/date-literal.js +71 -0
  22. package/dist/expressions/plugins/quoted-identifier.d.ts +2 -0
  23. package/dist/expressions/plugins/quoted-identifier.js +35 -0
  24. package/dist/expressions/plugins/standard.d.ts +3 -1
  25. package/dist/expressions/plugins/standard.js +5 -1
  26. package/package.json +6 -5
  27. package/dist/components/expressions-editor/(expressions)/expression.play.d.ts +0 -4
  28. package/dist/components/expressions-editor/(expressions)/expression.play.js +0 -69
package/README.md CHANGED
@@ -35,7 +35,7 @@ to **LyteNyte Grid PRO** for advanced enterprise features and support.
35
35
  [![NPM Version](https://img.shields.io/npm/v/%401771technologies%2Flytenyte-core?label=Core&style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/@1771technologies/lytenyte-core)
36
36
  [![GitHub branch status](https://img.shields.io/github/checks-status/1771-Technologies/lytenyte/main?style=flat&logo=github)](https://github.com/1771-Technologies/lytenyte/tree/main)
37
37
  [![GitHub Repo stars](https://img.shields.io/github/stars/1771-Technologies/lytenyte?style=flat&logo=github)](https://github.com/1771-Technologies/lytenyte)
38
- [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=flat)](code_of_conduct.md)
38
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=flat)](https://github.com/1771-Technologies/lytenyte/blob/main/CODE_OF_CONDUCT.md)
39
39
  [![CI Status](https://img.shields.io/github/actions/workflow/status/1771-Technologies/lytenyte/release.yml?branch=main&style=flat&label=CI&logo=github)](https://github.com/1771-Technologies/lytenyte/actions/workflows/release.yml?query=branch%3Amain)
40
40
  [![Follow on X](https://img.shields.io/twitter/follow/1771tech?label=follow+1771tech&style=social&logo=x&logoColor=black)](https://x.com/1771tech)
41
41
  [![LinkedIn](https://img.shields.io/badge/LinkedIn-Follow-0A66C2?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/1771technologies)
@@ -55,14 +55,34 @@ to **LyteNyte Grid PRO** for advanced enterprise features and support.
55
55
 
56
56
  LyteNyte Grid is available in two editions:
57
57
 
58
- 1. **LyteNyte Grid Core: Free, open-source edition** for building high-performance data tables.
59
- Includes sorting, filtering, editing, row selection, detail views, and more. Also includes advanced
60
- features such as aggregation, row grouping, and data export, often locked behind paywalls in other grids.
58
+ 1. **LyteNyte Grid Core:** Free, **open-source edition** for building high-performance data tables. Includes sorting,
59
+ filtering, editing, row selection, detail views, and more. Also includes advanced features such
60
+ as aggregation, row grouping, and cell range selection, often locked behind paywalls in other grids.
61
61
 
62
- 2. **LyteNyte Grid PRO: Commercial edition** that extends Core with powerful enterprise
63
- features such as server-side data loading, pivoting, tree data, component managers,
64
- and advanced filtering. Build custom menus, popovers and dialogs,
65
- and workflow-driven UIs with ease.
62
+ 2. **LyteNyte Grid PRO: Commercial edition** that extends Core with powerful enterprise features,
63
+ including server-side data loading, pivoting, tree data, component managers, and advanced
64
+ filtering. Build custom menus, popovers, expressions, and workflow-driven UIs with ease.
65
+
66
+ ## AI-Enhanced Workflows
67
+
68
+ **LyteNyte Grid AI Skills** are structured context files for Claude Code, Windsurf, Cursor, and other AI coding agents.
69
+ Skills provide agents with accurate, curated context for LyteNyte Grid, allowing them to generate complex
70
+ grid implementations with greater speed and reliability.
71
+
72
+ Instead of building the grid manually, developers can describe the
73
+ desired result and let the agent generate the implementation.
74
+
75
+ **Install LyteNyte Grid AI Skills**
76
+
77
+ ```bash
78
+ npx skills add 1771-Technologies/lytenyte
79
+ ```
80
+
81
+ Once installed, the Skill activates automatically whenever you work on a LyteNyte Grid task. No
82
+ special prompting is needed. Skills are actively maintained by the 1771 Technologies
83
+ team and versioned alongside each LyteNyte Grid release.
84
+
85
+ See our [AI Skills Overview guide](https://1771technologies.com/docs/ai-skills-overview) to learn more.
66
86
 
67
87
  ## Features
68
88
 
@@ -79,6 +99,16 @@ LyteNyte Grid is available in two editions:
79
99
 
80
100
  </details>
81
101
 
102
+ <details>
103
+ <summary><b>🤖&nbsp; AI Enhanced Workflows</b></summary>
104
+ <br>
105
+
106
+ | Feature | Core | PRO |
107
+ | --------------------------------------------------------------------- | :--: | :-: |
108
+ | [AI Skills](https://www.1771technologies.com/docs/ai-skills-overview) | ✅ | ✅ |
109
+
110
+ </details>
111
+
82
112
  <details>
83
113
  <summary><b>🔌&nbsp; API Extensions</b></summary>
84
114
  <br>
@@ -198,8 +228,8 @@ LyteNyte Grid is available in two editions:
198
228
  | &nbsp;&nbsp;&nbsp;↳ [Checkbox Row Selection](https://www.1771technologies.com/docs/row-selection#checkbox-selection) | ✅ | ✅ |
199
229
  | &nbsp;&nbsp;&nbsp;↳ [Isolated Row Selection](https://www.1771technologies.com/docs/row-selection#isolated-row-selection) | ✅ | ✅ |
200
230
  | &nbsp;&nbsp;&nbsp;↳ [Linked Row Selection](https://www.1771technologies.com/docs/row-selection#linked-row-selection) | ✅ | ✅ |
201
- | [Cell Selection](https://www.1771technologies.com/docs/cell-selection#single-range-selection) | | ✅ |
202
- | [Cell Range Selection](https://www.1771technologies.com/docs/cell-selection#multi-range-selection) | | ✅ |
231
+ | [Cell Selection](https://www.1771technologies.com/docs/cell-selection#single-range-selection) | | ✅ |
232
+ | [Cell Range Selection](https://www.1771technologies.com/docs/cell-selection#multi-range-selection) | | ✅ |
203
233
 
204
234
  </details>
205
235
 
@@ -239,6 +269,19 @@ LyteNyte Grid is available in two editions:
239
269
 
240
270
  </details>
241
271
 
272
+ <details>
273
+ <summary><b>🧮&nbsp; Expressions</b> &nbsp;</summary>
274
+ <br>
275
+
276
+ | Feature | Core | PRO |
277
+ | -------------------------------------------------------------------------------- | :--: | :-: |
278
+ | [Expressions Engine](https://www.1771technologies.com/docs/expressions-overview) | — | ✅ |
279
+ | [Filter Expressions](https://www.1771technologies.com/docs/expression-filters) | — | ✅ |
280
+ | [Expression Editor](https://www.1771technologies.com/docs/expression-editor) | — | ✅ |
281
+ | [Expression Plugins](https://www.1771technologies.com/docs/expression-plugins) | — | ✅ |
282
+
283
+ </details>
284
+
242
285
  <details>
243
286
  <summary><b>📦&nbsp; Grouping & Aggregations</b></summary>
244
287
  <br>
@@ -377,7 +420,7 @@ npm install --save @1771technologies/lytenyte-pro
377
420
 
378
421
  > [!NOTE]
379
422
  > You can install and use LyteNyte Grid PRO
380
- > without license but a watermark will be displayed.
423
+ > without a license, but a watermark will be displayed.
381
424
 
382
425
  **Core:**
383
426
 
@@ -2,6 +2,7 @@
2
2
  [data-ln-expression-editor] {
3
3
  --ln-expr-number: light-dark(#b45309, #fab387);
4
4
  --ln-expr-string: light-dark(#15803d, #a6e3a1);
5
+ --ln-expr-date: light-dark(#0e7490, #89dceb);
5
6
  --ln-expr-keyword: light-dark(#7c3aed, #cba6f7);
6
7
  --ln-expr-identifier: light-dark(#0369a1, #89b4fa);
7
8
  --ln-expr-operator: light-dark(#be185d, #f38ba8);
@@ -13,7 +14,7 @@
13
14
  [data-ln-expression-editor] {
14
15
  font-family:
15
16
  ui-monospace, "Cascadia Code", "Segoe UI Mono", "Liberation Mono", Menlo, Monaco, Consolas, monospace;
16
- font-size: 16px;
17
+ font-size: var(--ln-font-sm);
17
18
  white-space: nowrap;
18
19
  word-break: normal;
19
20
  overflow-wrap: normal;
@@ -1,3 +1,10 @@
1
1
  import type { Token } from "../../expressions/lexer/types.js";
2
2
  import type { CompletionItem } from "./types.js";
3
- export declare function createCompletionProvider(context: Record<string, unknown>): (tokens: Token[], cursorPosition: number) => CompletionItem[];
3
+ export type ContextEntry = {
4
+ value: unknown;
5
+ type: "string" | "number" | "array" | "object" | "function" | "boolean" | "date";
6
+ return?: "string" | "number" | "array" | "object" | "date";
7
+ };
8
+ type CompletionContext = Record<string, ContextEntry>;
9
+ export declare function createCompletionProvider(context: CompletionContext): (tokens: Token[], cursorPosition: number) => CompletionItem[];
10
+ export {};
@@ -48,28 +48,250 @@ const ARRAY_METHODS = [
48
48
  { label: "sort", kind: "function" },
49
49
  { label: "values", kind: "function" },
50
50
  ];
51
+ const DATE_METHODS = [
52
+ { label: "getTime", kind: "function" },
53
+ { label: "toISOString", kind: "function" },
54
+ ];
55
+ const NUMBER_METHODS = [
56
+ { label: "toExponential", kind: "function" },
57
+ { label: "toFixed", kind: "function" },
58
+ { label: "toLocaleString", kind: "function" },
59
+ { label: "toPrecision", kind: "function" },
60
+ { label: "toString", kind: "function" },
61
+ { label: "valueOf", kind: "function" },
62
+ ];
51
63
  function isDot(token) {
52
64
  return (token.type === "Punctuation" && token.value === ".") || token.type === "OptionalChain";
53
65
  }
54
66
  function isClosingBracket(token) {
55
67
  return token.type === "Punctuation" && token.value === "]";
56
68
  }
69
+ function isCompleteValue(token) {
70
+ return (token.type === "Identifier" ||
71
+ token.type === "QuotedIdentifier" ||
72
+ token.type === "Number" ||
73
+ token.type === "String" ||
74
+ token.type === "TemplateLiteral" ||
75
+ (token.type === "Punctuation" && (token.value === ")" || token.value === "]")));
76
+ }
57
77
  function isStringLiteral(token) {
58
78
  return token.type === "String" || token.type === "TemplateLiteral";
59
79
  }
60
- function analyzeTokens(tokens, cursorPosition) {
80
+ const BINARY_OPERATORS = [
81
+ { label: "+ Plus", kind: "operator", value: "+" },
82
+ { label: "- Minus", kind: "operator", value: "-" },
83
+ { label: "* Multiply", kind: "operator", value: "*" },
84
+ { label: "/ Divide", kind: "operator", value: "/" },
85
+ { label: "% Modulus", kind: "operator", value: "%" },
86
+ { label: "** Exponentiation", kind: "operator", value: "**" },
87
+ { label: "== Equal To", kind: "operator", value: "==" },
88
+ { label: "!= Not Equal To", kind: "operator", value: "!=" },
89
+ { label: "< Less Than", kind: "operator", value: "<" },
90
+ { label: "<= Less Than Or Equal To", kind: "operator", value: "<=" },
91
+ { label: "> Greater Than", kind: "operator", value: ">" },
92
+ { label: ">= Greater Than Or Equal To", kind: "operator", value: ">=" },
93
+ { label: "&& AND", kind: "operator", value: "&&" },
94
+ { label: "|| OR", kind: "operator", value: "||" },
95
+ { label: "?? OR if Null", kind: "operator", value: "??" },
96
+ { label: "|> Pipe", kind: "operator", value: "|>" },
97
+ ];
98
+ const STRING_METHOD_RETURNS = {
99
+ at: "string",
100
+ charAt: "string",
101
+ charCodeAt: "number",
102
+ endsWith: "unknown",
103
+ includes: "unknown",
104
+ indexOf: "number",
105
+ lastIndexOf: "number",
106
+ match: "array",
107
+ padEnd: "string",
108
+ padStart: "string",
109
+ repeat: "string",
110
+ replace: "string",
111
+ replaceAll: "string",
112
+ slice: "string",
113
+ split: "array",
114
+ startsWith: "unknown",
115
+ substring: "string",
116
+ toLowerCase: "string",
117
+ toUpperCase: "string",
118
+ trim: "string",
119
+ trimEnd: "string",
120
+ trimStart: "string",
121
+ };
122
+ const ARRAY_METHOD_RETURNS = {
123
+ at: "unknown",
124
+ concat: "array",
125
+ entries: "unknown",
126
+ every: "unknown",
127
+ filter: "array",
128
+ find: "unknown",
129
+ findIndex: "number",
130
+ flat: "array",
131
+ flatMap: "array",
132
+ forEach: "unknown",
133
+ includes: "unknown",
134
+ indexOf: "number",
135
+ join: "string",
136
+ keys: "unknown",
137
+ map: "array",
138
+ reduce: "unknown",
139
+ reduceRight: "unknown",
140
+ reverse: "array",
141
+ slice: "array",
142
+ some: "unknown",
143
+ sort: "array",
144
+ values: "unknown",
145
+ };
146
+ const DATE_METHOD_RETURNS = {
147
+ getTime: "number",
148
+ toISOString: "string",
149
+ };
150
+ function kindOf(value) {
151
+ if (typeof value === "function")
152
+ return "function";
153
+ if (Array.isArray(value))
154
+ return "array";
155
+ if (typeof value === "string")
156
+ return "string";
157
+ if (typeof value === "number")
158
+ return "number";
159
+ if (typeof value === "boolean")
160
+ return "boolean";
161
+ if (typeof value === "object" && value !== null)
162
+ return "object";
163
+ return "unknown";
164
+ }
165
+ function isContextEntry(v) {
166
+ return v != null && typeof v === "object" && "type" in v && "value" in v;
167
+ }
168
+ function resolveEntry(context, path) {
169
+ if (path.length === 0)
170
+ return undefined;
171
+ let entry = context[path[0]];
172
+ for (let i = 1; i < path.length; i++) {
173
+ if (!entry || entry.value == null || typeof entry.value !== "object")
174
+ return undefined;
175
+ const nested = entry.value[path[i]];
176
+ if (isContextEntry(nested)) {
177
+ entry = nested;
178
+ }
179
+ else if (nested !== undefined) {
180
+ entry = { value: nested, type: kindOf(nested) };
181
+ }
182
+ else {
183
+ return undefined;
184
+ }
185
+ }
186
+ return entry;
187
+ }
188
+ function findMatchingOpenParen(relevant, closeIndex) {
189
+ let depth = 1;
190
+ let i = closeIndex - 1;
191
+ while (i >= 0) {
192
+ if (relevant[i].type === "Punctuation") {
193
+ if (relevant[i].value === ")")
194
+ depth++;
195
+ else if (relevant[i].value === "(") {
196
+ if (--depth === 0)
197
+ return i;
198
+ }
199
+ }
200
+ i--;
201
+ }
202
+ return -1;
203
+ }
204
+ function buildPathFromTokens(relevant, i) {
205
+ const tokenName = (t) => (t.type === "QuotedIdentifier" ? t.value.slice(2, -1) : t.value);
206
+ const token = relevant[i];
207
+ if (!token || (token.type !== "Identifier" && token.type !== "QuotedIdentifier"))
208
+ return null;
209
+ const path = [tokenName(token)];
210
+ let j = i - 1;
211
+ while (j >= 1 && isDot(relevant[j])) {
212
+ j--;
213
+ if (relevant[j].type !== "Identifier" && relevant[j].type !== "QuotedIdentifier")
214
+ break;
215
+ path.unshift(tokenName(relevant[j]));
216
+ j--;
217
+ }
218
+ return path;
219
+ }
220
+ function resolveTypeOf(relevant, i, context) {
221
+ const token = relevant[i];
222
+ if (!token)
223
+ return "unknown";
224
+ if (token.type === "DateLiteral")
225
+ return "date";
226
+ if (isStringLiteral(token))
227
+ return "string";
228
+ if (isClosingBracket(token))
229
+ return "array";
230
+ if (token.type === "Punctuation" && token.value === ")") {
231
+ const openIdx = findMatchingOpenParen(relevant, i);
232
+ if (openIdx < 1)
233
+ return "unknown";
234
+ const methodToken = relevant[openIdx - 1];
235
+ if (methodToken?.type !== "Identifier")
236
+ return "unknown";
237
+ const hasDot = openIdx >= 2 && isDot(relevant[openIdx - 2]);
238
+ if (!hasDot) {
239
+ // Top-level function call: fn(...)
240
+ const funcEntry = context[methodToken.value];
241
+ if (funcEntry?.type === "function" && funcEntry.return)
242
+ return funcEntry.return;
243
+ return "unknown";
244
+ }
245
+ // Method call: receiver.method(...)
246
+ // First try resolving the receiver as a context object to find a typed method entry
247
+ const receiverPath = buildPathFromTokens(relevant, openIdx - 3);
248
+ if (receiverPath) {
249
+ const receiverEntry = resolveEntry(context, receiverPath);
250
+ if (receiverEntry?.type === "object" && receiverEntry.value != null) {
251
+ const methodVal = receiverEntry.value[methodToken.value];
252
+ const methodEntry = isContextEntry(methodVal) ? methodVal : undefined;
253
+ if (methodEntry?.type === "function" && methodEntry.return)
254
+ return methodEntry.return;
255
+ }
256
+ }
257
+ // Fall back to builtin method return-type maps (string/array prototype methods)
258
+ const receiverType = resolveTypeOf(relevant, openIdx - 3, context);
259
+ if (receiverType === "string")
260
+ return STRING_METHOD_RETURNS[methodToken.value] ?? "unknown";
261
+ if (receiverType === "array")
262
+ return ARRAY_METHOD_RETURNS[methodToken.value] ?? "unknown";
263
+ if (receiverType === "date")
264
+ return DATE_METHOD_RETURNS[methodToken.value] ?? "unknown";
265
+ return "unknown";
266
+ }
267
+ if (token.type === "Identifier" || token.type === "QuotedIdentifier") {
268
+ const path = buildPathFromTokens(relevant, i);
269
+ if (!path)
270
+ return "unknown";
271
+ const entry = resolveEntry(context, path);
272
+ const kind = entry?.type ?? "unknown";
273
+ if (kind === "string" || kind === "number" || kind === "array" || kind === "object")
274
+ return kind;
275
+ return "unknown";
276
+ }
277
+ return "unknown";
278
+ }
279
+ function analyzeTokens(tokens, cursorPosition, context) {
61
280
  const relevant = tokens.filter((t) => t.end <= cursorPosition && t.type !== "EOF" && t.type !== "Whitespace");
62
281
  if (relevant.length === 0)
63
282
  return { kind: "top-level" };
64
283
  let i = relevant.length - 1;
65
- // Skip the partial word the user is currently typing
66
- if (relevant[i].type === "Identifier")
284
+ // Skip the partial word the user is currently typing (only if cursor is at its end)
285
+ if (relevant[i].type === "Identifier" && relevant[i].end === cursorPosition)
67
286
  i--;
68
287
  if (i < 0)
69
288
  return { kind: "top-level" };
70
- // If the token before the word isn't a dot, we're at the top level
71
- if (!isDot(relevant[i]))
289
+ // If the token before the word isn't a dot, check if it's a complete value
290
+ if (!isDot(relevant[i])) {
291
+ if (isCompleteValue(relevant[i]))
292
+ return { kind: "after-value" };
72
293
  return { kind: "top-level" };
294
+ }
73
295
  // Step over the dot
74
296
  i--;
75
297
  if (i < 0)
@@ -81,79 +303,87 @@ function analyzeTokens(tokens, cursorPosition) {
81
303
  // [1,2,3]. → array methods
82
304
  if (isClosingBracket(beforeDot))
83
305
  return { kind: "array-literal" };
84
- // identifier chain: walk back through alternating Identifier / dot tokens
85
- if (beforeDot.type === "Identifier") {
86
- const path = [beforeDot.value];
306
+ // d"2023-02-11". date methods
307
+ if (beforeDot.type === "DateLiteral")
308
+ return { kind: "date-value" };
309
+ // fn(). → resolve call return type recursively
310
+ if (beforeDot.type === "Punctuation" && beforeDot.value === ")") {
311
+ const resultType = resolveTypeOf(relevant, i, context);
312
+ if (resultType === "string")
313
+ return { kind: "string-literal" };
314
+ if (resultType === "number")
315
+ return { kind: "number-value" };
316
+ if (resultType === "array")
317
+ return { kind: "array-literal" };
318
+ if (resultType === "date")
319
+ return { kind: "date-value" };
320
+ return { kind: "none" };
321
+ }
322
+ // identifier chain: walk back through alternating Identifier / QuotedIdentifier / dot tokens
323
+ if (beforeDot.type === "Identifier" || beforeDot.type === "QuotedIdentifier") {
324
+ const tokenName = (t) => (t.type === "QuotedIdentifier" ? t.value.slice(2, -1) : t.value);
325
+ const path = [tokenName(beforeDot)];
87
326
  i--;
88
327
  while (i >= 1) {
89
328
  if (!isDot(relevant[i]))
90
329
  break;
91
330
  i--;
92
- if (relevant[i].type !== "Identifier")
331
+ if (relevant[i].type !== "Identifier" && relevant[i].type !== "QuotedIdentifier")
93
332
  break;
94
- path.unshift(relevant[i].value);
333
+ path.unshift(tokenName(relevant[i]));
95
334
  i--;
96
335
  }
97
336
  return { kind: "context-path", path };
98
337
  }
99
338
  return { kind: "none" };
100
339
  }
101
- function resolveValue(context, path) {
102
- let current = context;
103
- for (const key of path) {
104
- if (current == null || typeof current !== "object")
105
- return undefined;
106
- current = current[key];
107
- }
108
- return current;
109
- }
110
- function kindOf(value) {
111
- if (typeof value === "function")
112
- return "function";
113
- if (Array.isArray(value))
114
- return "array";
115
- if (typeof value === "string")
116
- return "string";
117
- if (typeof value === "number")
118
- return "number";
119
- if (typeof value === "boolean")
120
- return "boolean";
121
- if (typeof value === "object" && value !== null)
122
- return "object";
123
- return "unknown";
124
- }
340
+ const VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
125
341
  function objectCompletions(obj) {
126
342
  return Object.entries(obj).map(([key, val]) => ({
127
343
  label: key,
128
- kind: kindOf(val),
344
+ kind: isContextEntry(val) ? val.type : kindOf(val),
129
345
  id: key,
346
+ value: VALID_IDENTIFIER.test(key) ? undefined : `@"${key}"`,
130
347
  }));
131
348
  }
132
349
  function builtinCompletions(methods) {
133
- return methods.map((m) => ({ label: m.label, kind: m.kind, id: m.label }));
350
+ return methods.map((m) => ({ label: m.label, kind: m.kind, id: m.label, value: m.value }));
351
+ }
352
+ function binaryOperatorCompletions() {
353
+ return builtinCompletions(BINARY_OPERATORS);
134
354
  }
135
355
  export function createCompletionProvider(context) {
136
356
  return function completionProvider(tokens, cursorPosition) {
137
- const analysis = analyzeTokens(tokens, cursorPosition);
357
+ const analysis = analyzeTokens(tokens, cursorPosition, context);
138
358
  switch (analysis.kind) {
139
359
  case "top-level":
140
360
  return objectCompletions(context);
141
361
  case "string-literal":
142
362
  return builtinCompletions(STRING_METHODS);
363
+ case "number-value":
364
+ return builtinCompletions(NUMBER_METHODS);
365
+ case "date-value":
366
+ return builtinCompletions(DATE_METHODS);
143
367
  case "array-literal":
144
368
  return builtinCompletions(ARRAY_METHODS);
145
369
  case "context-path": {
146
- const value = resolveValue(context, analysis.path);
147
- if (value == null)
370
+ const entry = resolveEntry(context, analysis.path);
371
+ if (!entry)
148
372
  return [];
149
- if (typeof value === "string")
373
+ if (entry.type === "string")
150
374
  return builtinCompletions(STRING_METHODS);
151
- if (Array.isArray(value))
375
+ if (entry.type === "number")
376
+ return builtinCompletions(NUMBER_METHODS);
377
+ if (entry.type === "array")
152
378
  return builtinCompletions(ARRAY_METHODS);
153
- if (typeof value === "object")
154
- return objectCompletions(value);
379
+ if (entry.type === "date")
380
+ return builtinCompletions(DATE_METHODS);
381
+ if (entry.type === "object" && entry.value != null && typeof entry.value === "object")
382
+ return objectCompletions(entry.value);
155
383
  return [];
156
384
  }
385
+ case "after-value":
386
+ return binaryOperatorCompletions();
157
387
  case "none":
158
388
  return [];
159
389
  }
@@ -1,5 +1,5 @@
1
- import type { CursorCoordinates } from "../types";
1
+ import type { VirtualElement } from "../../external/floating-ui.js";
2
2
  export declare function useCursorPosition(textareaRef: React.RefObject<HTMLTextAreaElement | null>): {
3
- coordinates: CursorCoordinates;
3
+ virtualElement: VirtualElement;
4
4
  update: () => void;
5
5
  };
@@ -1,17 +1,33 @@
1
- import { useState, useCallback } from "react";
1
+ import { useRef, useCallback } from "react";
2
2
  import { measureCursorPosition } from "./measure-cursor-at-pointer.js";
3
3
  export function useCursorPosition(textareaRef) {
4
- const [coordinates, setCoordinates] = useState({
5
- top: 0,
6
- left: 0,
7
- lineHeight: 22,
4
+ const cursorIndexRef = useRef(0);
5
+ const virtualElement = useRef({
6
+ getBoundingClientRect() {
7
+ const textarea = textareaRef.current;
8
+ if (!textarea)
9
+ return { top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, width: 0, height: 0 };
10
+ const { top, left, lineHeight } = measureCursorPosition(textarea, cursorIndexRef.current);
11
+ return {
12
+ top,
13
+ left,
14
+ right: left,
15
+ bottom: top + lineHeight,
16
+ x: left,
17
+ y: top,
18
+ width: 0,
19
+ height: lineHeight,
20
+ };
21
+ },
22
+ get contextElement() {
23
+ return textareaRef.current ?? undefined;
24
+ },
8
25
  });
9
26
  const update = useCallback(() => {
10
27
  const textarea = textareaRef.current;
11
28
  if (!textarea)
12
29
  return;
13
- const coords = measureCursorPosition(textarea, textarea.selectionStart);
14
- setCoordinates(coords);
30
+ cursorIndexRef.current = textarea.selectionStart;
15
31
  }, [textareaRef]);
16
- return { coordinates, update };
32
+ return { virtualElement: virtualElement.current, update };
17
33
  }
@@ -17,7 +17,7 @@ const sharedFontStyle = {
17
17
  lineHeight: "inherit",
18
18
  padding: "0",
19
19
  };
20
- const triggerCharacters = ["."];
20
+ const triggerCharacters = [".", " "];
21
21
  export function ExpressionEditor({ value, onChange: onValueChange, tokenize, highlight: Highlighter = DefaultTokenHighlighter, completionProvider, placeholder, disabled, readOnly, className, style, children, }) {
22
22
  // TODO @Lee: we should eventually allow multiline expressions but for now there isn't really a valid
23
23
  // use case that requires multiline expressions without also allow variable support.
@@ -27,11 +27,6 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
27
27
  const tokens = useMemo(() => tokenize(value), [value, tokenize]);
28
28
  const completions = useCompletions(completionProvider);
29
29
  const cursorPosition = useCursorPosition(textareaRef);
30
- const navigation = useKeyboardNavigation({
31
- onValueChange,
32
- onDismiss: completions.dismiss,
33
- textareaRef,
34
- });
35
30
  const trigger = useCompletionTrigger({
36
31
  triggerCharacters,
37
32
  onTrigger: (toks, pos, word) => {
@@ -40,6 +35,18 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
40
35
  },
41
36
  tokenize,
42
37
  });
38
+ const navigation = useKeyboardNavigation({
39
+ onValueChange,
40
+ onAccepted: (value, cursorPosition) => {
41
+ if (completionProvider) {
42
+ trigger.triggerManually(value, cursorPosition + 1);
43
+ }
44
+ else {
45
+ completions.dismiss();
46
+ }
47
+ },
48
+ textareaRef,
49
+ });
43
50
  depsRef.current = {
44
51
  isOpen: completions.isOpen,
45
52
  selectedItem: completions.selectedItem,
@@ -80,7 +87,20 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
80
87
  deps.onValueChange(newValue);
81
88
  const word = getWordAtCursor(newValue, selectionStart);
82
89
  if (deps.isOpen) {
83
- deps.updateFilter(word.word);
90
+ const charBefore = newValue[selectionStart - 1] || "";
91
+ if (word.word.length > 0) {
92
+ deps.updateFilter(word.word);
93
+ }
94
+ else if (charBefore === '"' || charBefore === "'") {
95
+ // String delimiter typed — close the popover rather than re-triggering.
96
+ deps.dismiss();
97
+ deps.cancel();
98
+ }
99
+ else if (!triggerCharacters.includes(charBefore)) {
100
+ // Non-word, non-trigger char (e.g. an operator) - re-fetch so the list
101
+ // reflects the new syntactic context rather than keeping stale results.
102
+ deps.triggerManually(newValue, selectionStart);
103
+ }
84
104
  }
85
105
  deps.handleInputChange(newValue, selectionStart);
86
106
  }, []);
@@ -107,8 +127,8 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
107
127
  boxSizing: "border-box",
108
128
  };
109
129
  const popoverValue = useMemo(() => {
110
- return { isOpen: completions.isOpen, coordinates: cursorPosition.coordinates };
111
- }, [completions.isOpen, cursorPosition.coordinates]);
130
+ return { isOpen: completions.isOpen, referenceElement: cursorPosition.virtualElement };
131
+ }, [completions.isOpen, cursorPosition.virtualElement]);
112
132
  const listValue = useMemo(() => {
113
133
  return {
114
134
  items: completions.filteredItems,
@@ -15,3 +15,4 @@ export declare const ExpressionEditor: {
15
15
  };
16
16
  export { createCompletionProvider } from "./create-completion-provider.js";
17
17
  export type { CompletionItem } from "./types.js";
18
+ export type { ContextEntry } from "./create-completion-provider.js";
@@ -1,7 +1,8 @@
1
- import type { CompletionItem, CursorCoordinates } from "../types";
1
+ import type { VirtualElement } from "../../external/floating-ui.js";
2
+ import type { CompletionItem } from "../types";
2
3
  interface ContextPopover {
3
4
  readonly isOpen: boolean;
4
- readonly coordinates: CursorCoordinates;
5
+ readonly referenceElement: VirtualElement | null;
5
6
  }
6
7
  export declare const CompletionPopoverProvider: import("react").Provider<ContextPopover>;
7
8
  export declare const useCompletionPopover: () => ContextPopover;
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRef, useEffect, forwardRef } from "react";
3
3
  import { useCompletionPopover } from "./completion-context.js";
4
4
  import { useCombinedRefs } from "@1771technologies/lytenyte-core/internal";
5
+ import { autoUpdate, computePosition, offset, shift } from "../../external/floating-ui.js";
5
6
  function CompletionPopoverImpl({ children, ...props }, ref) {
6
- const { isOpen, coordinates } = useCompletionPopover();
7
+ const { isOpen, referenceElement } = useCompletionPopover();
7
8
  const popoverRef = useRef(null);
8
9
  useEffect(() => {
9
10
  const el = popoverRef.current;
@@ -18,11 +19,27 @@ function CompletionPopoverImpl({ children, ...props }, ref) {
18
19
  el.hidePopover();
19
20
  }
20
21
  }, [isOpen]);
22
+ useEffect(() => {
23
+ const el = popoverRef.current;
24
+ if (!isOpen || !el || !referenceElement)
25
+ return;
26
+ function update() {
27
+ computePosition(referenceElement, el, {
28
+ strategy: "fixed",
29
+ placement: "bottom-start",
30
+ middleware: [offset(4), shift({ padding: 8 })],
31
+ }).then(({ x, y }) => {
32
+ el.style.left = `${x}px`;
33
+ el.style.top = `${y}px`;
34
+ });
35
+ }
36
+ return autoUpdate(referenceElement, el, update);
37
+ }, [isOpen, referenceElement]);
21
38
  const combined = useCombinedRefs(ref, popoverRef);
22
39
  return (_jsx("div", { ...props, ref: combined, popover: "manual", "data-ln-expression-popover": true, style: {
23
40
  position: "fixed",
24
- top: coordinates.top + coordinates.lineHeight,
25
- left: coordinates.left,
41
+ top: 0,
42
+ left: 0,
26
43
  margin: 0,
27
44
  inset: "unset",
28
45
  }, children: isOpen ? children : null }));
@@ -3,12 +3,14 @@ const tokenToSyntaxColor = {
3
3
  Number: "number",
4
4
  String: "string",
5
5
  TemplateLiteral: "string",
6
+ DateLiteral: "date",
6
7
  Spread: "operator",
7
8
  Punctuation: "operator",
8
9
  Pipe: "operator",
9
10
  Operator: "operator",
10
11
  Arrow: "operator",
11
12
  Identifier: "identifier",
13
+ QuotedIdentifier: "identifier",
12
14
  ExpressionError: "error",
13
15
  Unparsed: "punctuation",
14
16
  };
@@ -4,6 +4,7 @@ export type CompletionItem = {
4
4
  label: string;
5
5
  kind: string;
6
6
  id: string;
7
+ value?: string;
7
8
  };
8
9
  export type KeybindingConfig = {
9
10
  accept?: string[];
@@ -1,10 +1,10 @@
1
1
  import type { CompletionItem, WordAtCursor } from "./types";
2
2
  interface NavigationDeps {
3
3
  readonly onValueChange: (value: string) => void;
4
- readonly onDismiss: () => void;
4
+ readonly onAccepted: (value: string, cursorPosition: number) => void;
5
5
  readonly textareaRef: React.RefObject<HTMLTextAreaElement | null>;
6
6
  }
7
- export declare function useKeyboardNavigation({ onValueChange, onDismiss, textareaRef }: NavigationDeps): {
7
+ export declare function useKeyboardNavigation({ onValueChange, onAccepted, textareaRef }: NavigationDeps): {
8
8
  acceptCompletion: (item: CompletionItem, word: WordAtCursor) => void;
9
9
  };
10
10
  export {};
@@ -1,16 +1,37 @@
1
1
  import { useCallback } from "react";
2
2
  import { replaceWordAtCursor } from "./replace-word-at-cursor.js";
3
- export function useKeyboardNavigation({ onValueChange, onDismiss, textareaRef }) {
3
+ export function useKeyboardNavigation({ onValueChange, onAccepted, textareaRef }) {
4
4
  const acceptCompletion = useCallback((item, word) => {
5
5
  const textarea = textareaRef.current;
6
6
  if (!textarea)
7
7
  return;
8
- const { value, cursorPosition } = replaceWordAtCursor(textarea.value, word, item.label);
8
+ // Extend the replacement range to consume any @"..." / @'...' prefix the user already typed.
9
+ // The partial word may contain spaces (e.g. @"Age Gr → word is "Gr"), so scan backwards
10
+ // through word chars and spaces to find the opening @" / @'.
11
+ let adjustedWord = word;
12
+ const text = textarea.value;
13
+ let scan = word.start - 1;
14
+ // skip back over any chars that could be part of a quoted identifier value (spaces included)
15
+ while (scan > 0 && text[scan] !== '"' && text[scan] !== "'")
16
+ scan--;
17
+ if (scan >= 1 && (text[scan] === '"' || text[scan] === "'") && text[scan - 1] === "@") {
18
+ adjustedWord = { ...word, start: scan - 1 };
19
+ }
20
+ const charBeforeWord = adjustedWord.start > 0 ? text[adjustedWord.start - 1] : "";
21
+ const noSpaceBefore = charBeforeWord === "" ||
22
+ charBeforeWord === " " ||
23
+ charBeforeWord === "." ||
24
+ charBeforeWord === "(" ||
25
+ charBeforeWord === "[" ||
26
+ charBeforeWord === "{";
27
+ const rawInsertText = item.value ?? item.label;
28
+ const insertText = noSpaceBefore ? rawInsertText : " " + rawInsertText;
29
+ const { value, cursorPosition } = replaceWordAtCursor(text, adjustedWord, insertText);
9
30
  textarea.value = value;
10
31
  textarea.selectionStart = cursorPosition;
11
32
  textarea.selectionEnd = cursorPosition;
12
33
  onValueChange(value);
13
- onDismiss();
14
- }, [onValueChange, onDismiss, textareaRef]);
34
+ onAccepted(value, cursorPosition);
35
+ }, [onValueChange, onAccepted, textareaRef]);
15
36
  return { acceptCompletion };
16
37
  }
@@ -1,5 +1,7 @@
1
1
  export { Evaluator } from "./evaluator/evaluate.js";
2
2
  export type { Plugin, EvalFn } from "./plugin.js";
3
3
  export type { Token } from "./lexer/types.js";
4
- export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, } from "./plugins/standard.js";
4
+ export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, quotedIdentifierPlugin, } from "./plugins/standard.js";
5
5
  export { createResolvedIdentifierPlugin } from "./plugins/resolved-identifier.js";
6
+ export { dateLiteralPlugin } from "./plugins/date-literal.js";
7
+ export { createDateIdentifierPlugin } from "./plugins/date-identifier.js";
@@ -1,3 +1,5 @@
1
1
  export { Evaluator } from "./evaluator/evaluate.js";
2
- export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, } from "./plugins/standard.js";
2
+ export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, quotedIdentifierPlugin, } from "./plugins/standard.js";
3
3
  export { createResolvedIdentifierPlugin } from "./plugins/resolved-identifier.js";
4
+ export { dateLiteralPlugin } from "./plugins/date-literal.js";
5
+ export { createDateIdentifierPlugin } from "./plugins/date-identifier.js";
@@ -93,5 +93,9 @@ export interface ArrowFunctionExpression extends BaseNode {
93
93
  params: string[];
94
94
  body: ASTNode;
95
95
  }
96
- export type ASTNode = NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral | UndefinedLiteral | Identifier | BinaryExpression | UnaryExpression | ConditionalExpression | MemberExpression | OptionalMemberExpression | ArrayLiteral | ObjectLiteral | CallExpression | PipeExpression | TemplateLiteral | SpreadElement | ArrowFunctionExpression;
96
+ export interface DateLiteralNode extends BaseNode {
97
+ type: "DateLiteralNode";
98
+ value: string;
99
+ }
100
+ export type ASTNode = NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral | UndefinedLiteral | Identifier | BinaryExpression | UnaryExpression | ConditionalExpression | MemberExpression | OptionalMemberExpression | ArrayLiteral | ObjectLiteral | CallExpression | PipeExpression | TemplateLiteral | SpreadElement | ArrowFunctionExpression | DateLiteralNode;
97
101
  export {};
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "../plugin.js";
2
+ export declare function createDateIdentifierPlugin(options: {
3
+ identifiers: string[];
4
+ }): Plugin;
@@ -0,0 +1,29 @@
1
+ function toDate(value) {
2
+ if (value instanceof Date)
3
+ return value;
4
+ if (typeof value === "string" || typeof value === "number") {
5
+ const d = new Date(value);
6
+ return isNaN(d.getTime()) ? null : d;
7
+ }
8
+ return null;
9
+ }
10
+ export function createDateIdentifierPlugin(options) {
11
+ const identifierSet = new Set(options.identifiers);
12
+ return {
13
+ name: "date-identifier",
14
+ evaluate: (node, context) => {
15
+ if (node.type !== "Identifier")
16
+ return null;
17
+ if (!identifierSet.has(node.name))
18
+ return null;
19
+ const raw = context[node.name];
20
+ const date = toDate(raw);
21
+ if (date === null) {
22
+ if (raw === undefined || raw === null)
23
+ return { value: null };
24
+ throw new Error(`Context value for "${node.name}" cannot be coerced to a Date`);
25
+ }
26
+ return { value: date };
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "../plugin.js";
2
+ export declare const dateLiteralPlugin: Plugin;
@@ -0,0 +1,71 @@
1
+ import { scanString } from "../lexer/tokenize/scan-string.js";
2
+ import { current, advance } from "../parser/parser-context.js";
3
+ import { ExpressionError } from "../errors/expression-error.js";
4
+ const DATE_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
5
+ export const dateLiteralPlugin = {
6
+ name: "date-literal",
7
+ scan(source, pos) {
8
+ if (source[pos] !== "d")
9
+ return null;
10
+ const next = source[pos + 1];
11
+ if (next !== '"' && next !== "'")
12
+ return null;
13
+ try {
14
+ const r = scanString(source, pos + 1);
15
+ // Store the full source text (d"2023-02-11") so the editor overlay renders it correctly.
16
+ return { type: "DateLiteral", value: source.slice(pos, r.end), end: r.end };
17
+ }
18
+ catch (e) {
19
+ if (e instanceof ExpressionError) {
20
+ throw new ExpressionError(e.rawMessage, { source, start: pos, end: e.end });
21
+ }
22
+ throw e;
23
+ }
24
+ },
25
+ parsePrefix(ctx) {
26
+ const tok = current(ctx);
27
+ if (tok.type !== "DateLiteral")
28
+ return null;
29
+ advance(ctx);
30
+ // tok.value is the full d"2023-02-11" — strip the d" prefix and closing quote for the AST.
31
+ const dateStr = tok.value.slice(2, -1);
32
+ return { type: "DateLiteralNode", value: dateStr, start: tok.start, end: tok.end };
33
+ },
34
+ evaluate(node, context, evalFn) {
35
+ if (node.type === "DateLiteralNode") {
36
+ const d = new Date(node.value);
37
+ if (isNaN(d.getTime())) {
38
+ throw new Error(`Invalid date literal: "${node.value}"`);
39
+ }
40
+ return { value: d };
41
+ }
42
+ // Intercept comparison operators when at least one operand is a Date.
43
+ // This must run before comparisonPlugin (place dateLiteralPlugin first in the array).
44
+ if (node.type === "BinaryExpression") {
45
+ const op = node.operator;
46
+ if (!DATE_OPS.has(op))
47
+ return null;
48
+ const left = evalFn(node.left, context);
49
+ const right = evalFn(node.right, context);
50
+ if (!(left instanceof Date) && !(right instanceof Date))
51
+ return null;
52
+ const lt = left instanceof Date ? left.getTime() : Number(left);
53
+ const rt = right instanceof Date ? right.getTime() : Number(right);
54
+ switch (op) {
55
+ case "==":
56
+ return { value: lt === rt };
57
+ case "!=":
58
+ return { value: lt !== rt };
59
+ case "<":
60
+ return { value: lt < rt };
61
+ case ">":
62
+ return { value: lt > rt };
63
+ case "<=":
64
+ return { value: lt <= rt };
65
+ case ">=":
66
+ return { value: lt >= rt };
67
+ }
68
+ }
69
+ return null;
70
+ },
71
+ };
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "../plugin.js";
2
+ export declare const quotedIdentifierPlugin: Plugin;
@@ -0,0 +1,35 @@
1
+ import { scanString } from "../lexer/tokenize/scan-string.js";
2
+ import { current, advance } from "../parser/parser-context.js";
3
+ import { ExpressionError } from "../errors/expression-error.js";
4
+ export const quotedIdentifierPlugin = {
5
+ name: "quoted-identifier",
6
+ scan(source, pos) {
7
+ if (source[pos] !== "@")
8
+ return null;
9
+ const next = source[pos + 1];
10
+ if (next !== '"' && next !== "'")
11
+ return null;
12
+ try {
13
+ const r = scanString(source, pos + 1);
14
+ // Store the full source text (@"Age Group") so the token overlay renders correctly.
15
+ return { type: "QuotedIdentifier", value: source.slice(pos, r.end), end: r.end };
16
+ }
17
+ catch (e) {
18
+ // Rethrow with start=pos so the ExpressionError token covers the leading @ character.
19
+ // Without this, scanString reports start at the opening quote (pos+1), leaving @ uncovered.
20
+ if (e instanceof ExpressionError) {
21
+ throw new ExpressionError(e.rawMessage, { source, start: pos, end: e.end });
22
+ }
23
+ throw e;
24
+ }
25
+ },
26
+ parsePrefix(ctx) {
27
+ const tok = current(ctx);
28
+ if (tok.type !== "QuotedIdentifier")
29
+ return null;
30
+ advance(ctx);
31
+ // Strip the leading @" (or @') and the trailing quote to get the bare identifier name.
32
+ const name = tok.value.slice(2, -1);
33
+ return { type: "Identifier", name, start: tok.start, end: tok.end };
34
+ },
35
+ };
@@ -9,5 +9,7 @@ import { membershipPlugin } from "./membership.js";
9
9
  import { accessPlugin } from "./access.js";
10
10
  import { arrowsPlugin } from "./arrows.js";
11
11
  import { collectionsPlugin } from "./collections.js";
12
+ import { quotedIdentifierPlugin } from "./quoted-identifier.js";
13
+ import { dateLiteralPlugin } from "./date-literal.js";
12
14
  export declare const standardPlugins: Plugin[];
13
- export { stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, };
15
+ export { dateLiteralPlugin, stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, quotedIdentifierPlugin, };
@@ -8,7 +8,10 @@ import { membershipPlugin } from "./membership.js";
8
8
  import { accessPlugin } from "./access.js";
9
9
  import { arrowsPlugin } from "./arrows.js";
10
10
  import { collectionsPlugin } from "./collections.js";
11
+ import { quotedIdentifierPlugin } from "./quoted-identifier.js";
12
+ import { dateLiteralPlugin } from "./date-literal.js";
11
13
  export const standardPlugins = [
14
+ dateLiteralPlugin,
12
15
  arrowsPlugin,
13
16
  collectionsPlugin,
14
17
  stringsPlugin,
@@ -19,5 +22,6 @@ export const standardPlugins = [
19
22
  ternaryPlugin,
20
23
  pipePlugin,
21
24
  accessPlugin,
25
+ quotedIdentifierPlugin,
22
26
  ];
23
- export { stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, };
27
+ export { dateLiteralPlugin, stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, quotedIdentifierPlugin, };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@1771technologies/lytenyte-pro",
3
3
  "description": "Blazingly fast headless React data grid with 100s of features.",
4
- "version": "2.0.4-dev.4",
4
+ "version": "2.1.0",
5
5
  "type": "module",
6
6
  "license": "COMMERCIAL",
7
7
  "files": [
8
8
  "dist",
9
9
  "css",
10
+ "skills",
10
11
  "LICENSE",
11
12
  "SECURITY.md",
12
13
  "README.md"
@@ -85,12 +86,12 @@
85
86
  "access": "public"
86
87
  },
87
88
  "dependencies": {
88
- "@1771technologies/lytenyte-shared": "2.0.4-dev.4",
89
- "@1771technologies/lytenyte-core": "2.0.4-dev.4",
90
- "@1771technologies/lytenyte-design": "2.0.4-dev.4"
89
+ "@1771technologies/lytenyte-core": "2.1.0",
90
+ "@1771technologies/lytenyte-shared": "2.1.0",
91
+ "@1771technologies/lytenyte-design": "2.1.0"
91
92
  },
92
93
  "devDependencies": {
93
- "@1771technologies/grid-sample-data": "2.0.4-dev.4"
94
+ "@1771technologies/grid-sample-data": "2.1.0"
94
95
  },
95
96
  "peerDependencies": {
96
97
  "react": "18.0.0 || ^19.0.0",
@@ -1,4 +0,0 @@
1
- import "../../../../css/light-dark.css";
2
- import "../../../../css/components/expression-editor.css";
3
- import "./expression.css";
4
- export default function App(): import("react/jsx-runtime").JSX.Element;
@@ -1,69 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useMemo } from "react";
3
- import { createCompletionProvider, ExpressionEditor } from "../index.js";
4
- import { Evaluator, standardPlugins } from "../../../expressions/index.js";
5
- import "../../../../css/light-dark.css";
6
- import "../../../../css/components/expression-editor.css";
7
- import "./expression.css";
8
- import { CompletionPopover } from "../intellisence/completion-popover.js";
9
- import { CompletionList } from "../intellisence/completion-list.js";
10
- import { CompletionListItem } from "../intellisence/completion-item.js";
11
- const CONTEXT = {
12
- user: {
13
- name: "Alice",
14
- age: 30,
15
- email: "alice@example.com",
16
- address: {
17
- city: "Portland",
18
- state: "OR",
19
- zip: "97201",
20
- },
21
- },
22
- order: {
23
- total: 99.99,
24
- status: "shipped",
25
- items: 3,
26
- },
27
- Math: {
28
- PI: 3.14159,
29
- abs: "function",
30
- max: "function",
31
- min: "function",
32
- round: "function",
33
- },
34
- };
35
- const EXAMPLES = [
36
- {
37
- label: "Property access",
38
- value: "user.address.city",
39
- },
40
- {
41
- label: "Math",
42
- value: "Math.round(order.total * 1.08)",
43
- },
44
- {
45
- label: "Conditional",
46
- value: "if order.total > 100 and user.age >= 18",
47
- },
48
- {
49
- label: "Object literal",
50
- value: "{name: user.name, total: order.total, tax: order.total * 0.08}",
51
- },
52
- {
53
- label: "Error token",
54
- value: "user.name or undefined",
55
- },
56
- ];
57
- export default function App() {
58
- const [value, setValue] = useState("user.name");
59
- const evaluator = useMemo(() => new Evaluator(standardPlugins), []);
60
- const tokenize = useCallback((input) => evaluator.tokensSafe(input, true), [evaluator]);
61
- const provider = useMemo(() => {
62
- return createCompletionProvider(CONTEXT);
63
- }, []);
64
- return (_jsxs("div", { className: "demo-container", children: [_jsx("label", { className: "demo-label", children: "Single-line" }), _jsx(ExpressionEditor.Root, { value: value, onChange: setValue, tokenize: tokenize, completionProvider: provider, placeholder: "Type an expression...", children: _jsx(CompletionPopover, { children: _jsx(CompletionList, { children: ({ items }) => {
65
- return items.map((item, index) => {
66
- return (_jsxs(CompletionListItem, { item: item, index: index, className: "completion-item", children: [_jsx("span", { className: "completion-kind", children: item.kind }), _jsx("span", { className: "completion-label", children: item.label })] }, item.id));
67
- });
68
- } }) }) }), _jsx("div", { className: "examples-row", children: EXAMPLES.map((ex) => (_jsx("button", { className: `example-btn${value === ex.value ? "active" : ""}`, onClick: () => setValue(ex.value), children: ex.label }, ex.label))) })] }));
69
- }