@fw-components/formula-editor 2.0.3-formula-editor.2 → 2.0.7-cline-formulaeditor.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.
@@ -0,0 +1,461 @@
1
+ import Big from "big.js";
2
+ import { Expectation, Queue, Stack } from "./helpers.js";
3
+ import { Recommender } from "./recommendor.js";
4
+ export class Parser {
5
+ static { this.MAX_FORMULA_LENGTH = 1000; }
6
+ static { this.CACHE_SIZE = 100; }
7
+ constructor(variables, minSuggestionLen) {
8
+ this._parseCache = new Map();
9
+ this._rpnCache = new Map();
10
+ this.mathematicalOperators = new Set(["^", "+", "-", "*", "/", "sin", "cos", "tan", "sqrt", "log"]);
11
+ this.constants = new Map([
12
+ ["pi", Math.PI],
13
+ ["e", Math.E]
14
+ ]);
15
+ this.operatorPrecedence = {
16
+ "^": 3,
17
+ "/": 2,
18
+ "*": 2,
19
+ "+": 1,
20
+ "-": 1,
21
+ };
22
+ this.variables = variables;
23
+ this._recommender = new Recommender(this.variables, minSuggestionLen);
24
+ }
25
+ clearCacheIfNeeded() {
26
+ if (this._parseCache.size > Parser.CACHE_SIZE) {
27
+ const entriesToDelete = Array.from(this._parseCache.keys())
28
+ .slice(0, Math.floor(Parser.CACHE_SIZE / 2));
29
+ entriesToDelete.forEach(key => this._parseCache.delete(key));
30
+ }
31
+ if (this._rpnCache.size > Parser.CACHE_SIZE) {
32
+ const entriesToDelete = Array.from(this._rpnCache.keys())
33
+ .slice(0, Math.floor(Parser.CACHE_SIZE / 2));
34
+ entriesToDelete.forEach(key => this._rpnCache.delete(key));
35
+ }
36
+ }
37
+ parseInput(formula, prevCurPos = null, recommendation = null) {
38
+ // Check formula length
39
+ if (formula.length > Parser.MAX_FORMULA_LENGTH) {
40
+ return {
41
+ recommendations: null,
42
+ formattedContent: null,
43
+ formattedString: null,
44
+ newCursorPosition: prevCurPos ?? -1,
45
+ errorString: `Formula length exceeds maximum limit of ${Parser.MAX_FORMULA_LENGTH} characters`
46
+ };
47
+ }
48
+ // Check cache for exact matches without recommendation
49
+ const cacheKey = `${formula}-${prevCurPos}`;
50
+ if (!recommendation && this._parseCache.has(cacheKey)) {
51
+ return { ...this._parseCache.get(cacheKey) };
52
+ }
53
+ let tokens = formula.match(/'[^']*'|\d+|[A-Za-z_][A-Za-z0-9_]*|[-+(),*^/:?\s]|sin|cos|tan|sqrt|log/g);
54
+ // Stores the positions of opening parentheses. This allows us to
55
+ // show "Unclosed parenthesis error" for positions which are far behind
56
+ // our current token
57
+ let parentheses = new Stack();
58
+ // The HTML formatted string which we eventually show on the view.
59
+ let formattedString = ``;
60
+ // The expectation that we have for the current token.
61
+ let expectation = Expectation.VARIABLE;
62
+ // Position of the current token in the formula string.
63
+ let currentPosition = 0;
64
+ // Previous 'token' (not a space or a new line) that we just encountered.
65
+ let previousToken = "";
66
+ let currentTokens = "";
67
+ // The object that we return as the output of the parsing result.
68
+ let parseOutput = {
69
+ recommendations: null,
70
+ formattedContent: null,
71
+ formattedString: null,
72
+ newCursorPosition: prevCurPos ?? -1,
73
+ errorString: null,
74
+ };
75
+ if (!formula.trim()) {
76
+ if (recommendation) {
77
+ formattedString = `<span class="wysiwygInternals">${recommendation}</span>`;
78
+ currentPosition += recommendation.length;
79
+ const parser = new DOMParser();
80
+ const doc = parser.parseFromString(formattedString, "text/html");
81
+ parseOutput.formattedContent = doc.querySelector("body");
82
+ parseOutput.formattedString = formattedString;
83
+ parseOutput.newCursorPosition = recommendation.length;
84
+ return parseOutput;
85
+ }
86
+ }
87
+ tokens?.forEach((token) => {
88
+ // It is a number if it's in the defined variables, constants, or
89
+ // it's a valid number literal.
90
+ let isNumber = this.variables.has(token) || this.constants.has(token) || !Number.isNaN(Number(token)), isOperator = this.mathematicalOperators.has(token), isSpace = token.trim() == "", isBracket = token == "(" || token == ")";
91
+ // We don't really want anything for the spaces, other than simply
92
+ // adding them back to the view.
93
+ if (isSpace) {
94
+ formattedString = `${formattedString}${token}`;
95
+ currentPosition += token.length;
96
+ return;
97
+ }
98
+ // If the cursor position is 'inside` the current token:
99
+ //
100
+ // 1. If we've got a recommendation to add, simply replace the
101
+ // word with the recommendation.
102
+ // 2. Ask the recommendor to fetch recommendations for this specific
103
+ // token/word.
104
+ if (currentPosition <= prevCurPos &&
105
+ currentPosition + token.length >= prevCurPos) {
106
+ // If a recommendation was provided, replace the correspoding
107
+ // word with it and move the cursor forward, accordingly.
108
+ if (recommendation) {
109
+ // Since we are sure that the recommendation will always correspond
110
+ // to a variable.
111
+ isNumber = true;
112
+ if (this.mathematicalOperators.has(token)) {
113
+ recommendation = token + recommendation;
114
+ }
115
+ // If the new cursor length somehow becomes larger than the
116
+ // length of the formula string, setting the caret to that
117
+ // length will move the caret to the start. Although this overflow
118
+ // won't happen, but still, this check prevents that.
119
+ parseOutput.newCursorPosition = Math.min(parseOutput.newCursorPosition +
120
+ recommendation.length -
121
+ token.length, formula.length + recommendation.length - token.length);
122
+ token = recommendation;
123
+ recommendation = null;
124
+ }
125
+ // Fetch recommendations nonetheless.
126
+ parseOutput.recommendations =
127
+ this._recommender.getRecommendation(token);
128
+ }
129
+ let tokenClassName = "";
130
+ // Don't check for errors if an error has already been encountered.
131
+ if (expectation != Expectation.UNDEFINED) {
132
+ // Unnecessary closing parenthesis
133
+ if (parentheses.isEmpty() && token == ")") {
134
+ parseOutput.errorString = `Unexpected ')' at position ${currentPosition}`;
135
+ tokenClassName += " error";
136
+ expectation = Expectation.UNDEFINED;
137
+ }
138
+ // Operator or ) after an operator. Eg: `23 / *` or `23 / )`
139
+ // Unary `+` and `-` are not an error as they might represent
140
+ // a positive or negative number respectively. But they will still
141
+ // be an error if the formula ends with them.
142
+ else if (expectation == Expectation.VARIABLE &&
143
+ !isNumber &&
144
+ token != "(" &&
145
+ !((token == "-" || token == "+") &&
146
+ (!currentTokens.trim() || this.mathematicalOperators.has(previousToken)))) {
147
+ parseOutput.errorString = `Expected variable/number at position ${currentPosition}`;
148
+ tokenClassName += " error";
149
+ expectation = Expectation.UNDEFINED;
150
+ }
151
+ // Number/Variable after the same. Eg: `a a` or `420 420`.
152
+ // Having a ) is fine. Eg: `23)` might be representing `(23 + 23)
153
+ else if (expectation == Expectation.OPERATOR &&
154
+ !isOperator &&
155
+ token != ")") {
156
+ parseOutput.errorString = `Expected mathematical operator at position ${currentPosition}`;
157
+ tokenClassName += " error";
158
+ expectation = Expectation.UNDEFINED;
159
+ }
160
+ // Unknown symbol/variable/word
161
+ else if (!(isNumber || isOperator || isBracket)) {
162
+ parseOutput.errorString = `Unknown word at position ${currentPosition}`;
163
+ tokenClassName += " error";
164
+ expectation = Expectation.UNDEFINED;
165
+ }
166
+ // The case of division by zero. Since we can't know if an expression evaluates
167
+ // to zero or not, that case can only be handled during calculation.
168
+ else if (isNumber &&
169
+ previousToken == "/" &&
170
+ (this.variables.get(token) == 0 || Number(token) == 0)) {
171
+ parseOutput.errorString = `Division by zero at position ${currentPosition}`;
172
+ tokenClassName += " error";
173
+ expectation = Expectation.UNDEFINED;
174
+ }
175
+ // Empty brackets. Default might be takn as 0, but that will only make sense
176
+ // in addition and subtraction and not in other operators, so making this
177
+ // case an error makes more sense.
178
+ else if (previousToken == "(" && token == ")") {
179
+ parseOutput.errorString = `Empty brackets at position ${currentPosition}`;
180
+ tokenClassName += " error";
181
+ expectation = Expectation.UNDEFINED;
182
+ }
183
+ }
184
+ // Setting the expectation for the next token, if we have not encountered an
185
+ // error already.
186
+ if (expectation != Expectation.UNDEFINED) {
187
+ if (token == "(" || isOperator) {
188
+ expectation = Expectation.VARIABLE;
189
+ }
190
+ else if (token == ")" || isNumber) {
191
+ expectation = Expectation.OPERATOR;
192
+ }
193
+ }
194
+ if (token == "(") {
195
+ parentheses.push(currentPosition);
196
+ tokenClassName += " bracket";
197
+ }
198
+ else if (token == ")") {
199
+ parentheses.pop();
200
+ tokenClassName += " bracket";
201
+ }
202
+ else if (isOperator) {
203
+ tokenClassName += " operator";
204
+ }
205
+ else if (expectation == Expectation.UNDEFINED) {
206
+ tokenClassName += " error";
207
+ }
208
+ // Since not using ShadowDOM, having these specific class names will prevent
209
+ // name collision.
210
+ formattedString = `${formattedString}<span class="wysiwygInternals ${tokenClassName}">${token}</span>`;
211
+ currentPosition += token.length;
212
+ previousToken = token;
213
+ currentTokens += token;
214
+ });
215
+ if (recommendation) {
216
+ parseOutput.newCursorPosition = Math.min(parseOutput.newCursorPosition +
217
+ recommendation.length, formula.length + recommendation.length);
218
+ formattedString = `${formattedString}<span class="wysiwygInternals">${recommendation}</span>`;
219
+ }
220
+ // If the formula ends with a mathematical operator, or has unclosed `(`
221
+ if (this.mathematicalOperators.has(previousToken)) {
222
+ // parseOutput.errorString = "Unexpected ending of formula.";
223
+ parseOutput.recommendations = Array.from(this.variables.keys());
224
+ }
225
+ else if (!parentheses.isEmpty()) {
226
+ parseOutput.errorString = `Unclosed '(' at position: ${parentheses.top()}`;
227
+ }
228
+ const parser = new DOMParser();
229
+ const doc = parser.parseFromString(formattedString, "text/html");
230
+ parseOutput.formattedContent = doc.querySelector("body");
231
+ parseOutput.formattedString = formattedString;
232
+ // Cache the result if no recommendation was used
233
+ if (!recommendation) {
234
+ this._parseCache.set(cacheKey, { ...parseOutput });
235
+ this.clearCacheIfNeeded();
236
+ }
237
+ return parseOutput;
238
+ }
239
+ buildRPN(formula) {
240
+ // Check cache
241
+ if (this._rpnCache.has(formula)) {
242
+ const cached = this._rpnCache.get(formula);
243
+ return cached ? cached.clone() : null;
244
+ }
245
+ if (this.parseInput(formula).errorString) {
246
+ return null;
247
+ }
248
+ const tokens = formula
249
+ .match(/'[^']*'|\d+|[A-Za-z_][A-Za-z0-9_]*|[-+(),*^/:?\s]/g)
250
+ ?.filter((el) => !/\s+/.test(el) && el !== "");
251
+ // Handling the special case of unary `-` and `+`.
252
+ let previousToken = "";
253
+ let carriedToken = null;
254
+ const parsedTokens = [];
255
+ let currentTokens = "";
256
+ for (const token of tokens) {
257
+ if ((token == "+" || token == "-") &&
258
+ (!currentTokens.trim() || this.mathematicalOperators.has(previousToken))) {
259
+ carriedToken = token;
260
+ }
261
+ else if (carriedToken) {
262
+ parsedTokens.push(carriedToken + token);
263
+ carriedToken = null;
264
+ }
265
+ else {
266
+ parsedTokens.push(token);
267
+ }
268
+ previousToken = token;
269
+ currentTokens += token;
270
+ }
271
+ /**
272
+ * Shunting Yard Algorithm (EW Dijkstra)
273
+ */
274
+ const operatorStack = new Stack();
275
+ const outputQueue = new Queue();
276
+ for (const token of parsedTokens) {
277
+ if (token == "(") {
278
+ operatorStack.push("(");
279
+ }
280
+ else if (token == ")") {
281
+ while (operatorStack.top() != "(") {
282
+ outputQueue.enqueue(operatorStack.pop());
283
+ }
284
+ operatorStack.pop();
285
+ }
286
+ else if (this.mathematicalOperators.has(token)) {
287
+ while (this.mathematicalOperators.has(operatorStack.top()) &&
288
+ this.operatorPrecedence[token] <=
289
+ this.operatorPrecedence[operatorStack.top()]) {
290
+ outputQueue.enqueue(operatorStack.pop());
291
+ }
292
+ operatorStack.push(token);
293
+ }
294
+ else if (!Number.isNaN(token) && token != "") {
295
+ outputQueue.enqueue(token);
296
+ }
297
+ }
298
+ while (operatorStack.top()) {
299
+ outputQueue.enqueue(operatorStack.pop());
300
+ }
301
+ // Cache the result
302
+ this._rpnCache.set(formula, outputQueue ? outputQueue.clone() : null);
303
+ this.clearCacheIfNeeded();
304
+ return outputQueue;
305
+ }
306
+ addParentheses(formula) {
307
+ const rpn = this.buildRPN(formula);
308
+ if (!rpn) {
309
+ return null;
310
+ }
311
+ const lexedRPN = [];
312
+ while (!rpn.isEmpty()) {
313
+ lexedRPN.push(rpn.dequeue());
314
+ }
315
+ // Stores the operators that we encounter in the RPN
316
+ let operatorStack = new Stack();
317
+ // Stores the `results`, which are essentially individual groups
318
+ // of tokens showing a meaningful value.
319
+ let resultStack = new Stack();
320
+ lexedRPN.forEach((symbol) => {
321
+ let parsedLeftExpression, parsedRightExpression;
322
+ // If we encounter a number or a variable in the RPN, it is itself
323
+ // a calculated entity (say a result in itself), needs no modification
324
+ // and can be directly put into the result stack.
325
+ if (this.variables.has(symbol) ||
326
+ (!isNaN(parseFloat(symbol)) && isFinite(parseFloat(symbol)))) {
327
+ resultStack.push(symbol);
328
+ operatorStack.push(null);
329
+ }
330
+ // If it is not a number/variable then it is an operator. We will
331
+ // take out previous operators from the `operatorStack`, compare
332
+ // them with the current one, adds brackets accordingly to the `results`
333
+ // around it, and then finally add it to the `operatorStack` for
334
+ // future reference.
335
+ else if (Object.keys(this.operatorPrecedence).includes(symbol)) {
336
+ let [rightExpression, leftExpression, operatorA, operatorB] = [
337
+ resultStack.pop(),
338
+ resultStack.pop(),
339
+ operatorStack.pop(),
340
+ operatorStack.pop(),
341
+ ];
342
+ // The conditions that govern when to show a parenthesis.
343
+ if (this.operatorPrecedence[operatorB] <=
344
+ this.operatorPrecedence[symbol] ||
345
+ (this.operatorPrecedence[operatorB] ===
346
+ this.operatorPrecedence[symbol] &&
347
+ ["/", "-"].includes(symbol))) {
348
+ parsedLeftExpression = `(${leftExpression})`;
349
+ }
350
+ else {
351
+ parsedLeftExpression = leftExpression;
352
+ }
353
+ if (this.operatorPrecedence[operatorA] <=
354
+ this.operatorPrecedence[symbol] ||
355
+ (this.operatorPrecedence[operatorA] ===
356
+ this.operatorPrecedence[symbol] &&
357
+ ["/", "-"].includes(symbol))) {
358
+ parsedRightExpression = `(${rightExpression})`;
359
+ }
360
+ else {
361
+ parsedRightExpression = rightExpression;
362
+ }
363
+ // The bracket included expression is now itself a `result`
364
+ resultStack.push(`${parsedLeftExpression} ${symbol} ${parsedRightExpression}`);
365
+ operatorStack.push(symbol);
366
+ }
367
+ else
368
+ throw `${symbol} is not a recognized symbol`;
369
+ });
370
+ if (!resultStack.isEmpty()) {
371
+ return resultStack.pop();
372
+ }
373
+ else
374
+ throw `${lexedRPN} is not a correct RPN`;
375
+ }
376
+ evaluateFunction(name, value) {
377
+ switch (name) {
378
+ case 'sin': return Math.sin(value);
379
+ case 'cos': return Math.cos(value);
380
+ case 'tan': return Math.tan(value);
381
+ case 'sqrt':
382
+ if (value < 0)
383
+ throw new Error('Cannot calculate square root of negative number');
384
+ return Math.sqrt(value);
385
+ case 'log':
386
+ if (value <= 0)
387
+ throw new Error('Cannot calculate logarithm of non-positive number');
388
+ return Math.log(value);
389
+ default: throw new Error(`Unknown function: ${name}`);
390
+ }
391
+ }
392
+ calculate(formula) {
393
+ let rpn = this.buildRPN(formula);
394
+ let calculationResult = {
395
+ result: undefined,
396
+ errorString: null,
397
+ };
398
+ if (!rpn) {
399
+ return calculationResult;
400
+ }
401
+ let calcStack = new Stack();
402
+ while (!rpn.isEmpty()) {
403
+ const frontItem = rpn.dequeue();
404
+ if (!this.mathematicalOperators.has(frontItem)) {
405
+ calcStack.push(Big(Number.parseFloat(this.variables.get(frontItem)?.toString() ?? frontItem)));
406
+ }
407
+ else {
408
+ let operator = frontItem;
409
+ let numB = calcStack.pop();
410
+ let numA = calcStack.pop();
411
+ try {
412
+ // Handle constants
413
+ if (this.constants.has(frontItem)) {
414
+ calcStack.push(Big(this.constants.get(frontItem)));
415
+ continue;
416
+ }
417
+ // Handle functions
418
+ if (['sin', 'cos', 'tan', 'sqrt', 'log'].includes(frontItem)) {
419
+ const value = calcStack.pop();
420
+ try {
421
+ calcStack.push(Big(this.evaluateFunction(frontItem, parseFloat(value.toString()))));
422
+ }
423
+ catch (error) {
424
+ calculationResult.errorString = error.message;
425
+ return calculationResult;
426
+ }
427
+ continue;
428
+ }
429
+ switch (operator) {
430
+ case "+":
431
+ calcStack.push(Big(numA).add(Big(numB)));
432
+ break;
433
+ case "-":
434
+ calcStack.push(Big(numA).sub(Big(numB)));
435
+ break;
436
+ case "*":
437
+ calcStack.push(Big(numA).mul(Big(numB)));
438
+ break;
439
+ case "/":
440
+ if (parseFloat(Big(numB).toString()) == 0) {
441
+ calculationResult.errorString = "Division by zero encountered";
442
+ return calculationResult;
443
+ }
444
+ calcStack.push(Big(numA).div(Big(numB)));
445
+ break;
446
+ // Big.js doesn't support exponentiating a Big to a Big, which
447
+ // is obvious due to performance overheads. Use this case with care.
448
+ case "^":
449
+ calcStack.push(Big(numA).pow(parseFloat(Big(numB).toString())));
450
+ }
451
+ }
452
+ catch (error) {
453
+ calculationResult.errorString = error;
454
+ return calculationResult;
455
+ }
456
+ }
457
+ }
458
+ calculationResult.result = parseFloat(calcStack.top().toString());
459
+ return calculationResult;
460
+ }
461
+ }
@@ -0,0 +1,18 @@
1
+ import { matchSorter } from 'match-sorter';
2
+ export class Recommender {
3
+ constructor(variables, minSuggestionLen) {
4
+ this._minimumSuggestionLength = minSuggestionLen > 0 ? minSuggestionLen : 1;
5
+ this.variableList = Array.from(variables.keys());
6
+ }
7
+ getRecommendation(word) {
8
+ if (word.length < this._minimumSuggestionLength) {
9
+ return null;
10
+ }
11
+ const recommendations = matchSorter(this.variableList, word);
12
+ if (recommendations.length === 0 ||
13
+ (recommendations.length === 1 && recommendations[0] === word)) {
14
+ return null;
15
+ }
16
+ return recommendations;
17
+ }
18
+ }
@@ -0,0 +1,149 @@
1
+ import { css } from "lit";
2
+ export const FormulaEditorStyles = css `
3
+
4
+ .formula-editor-label {
5
+ display: block;
6
+ font-size: var(--fe-label-font-size, 0.8rem);
7
+ color: var(--fe-label-color, #515151);
8
+ margin-bottom: var(--fe-label-margin-bottom, 1px);
9
+ }
10
+
11
+ :host {
12
+ display: block;
13
+ position: relative;
14
+ }
15
+
16
+ #wysiwyg-editor {
17
+ display: inline-block;
18
+ padding: var(--fe-padding, 8px);
19
+ caret-color: var(--fe-caret-color, #fff);
20
+ color: var(--fe-text-color, #f7f1ff);
21
+ line-height: 1.1;
22
+ width: var(--fe-width, 100%);
23
+ height: var(--fe-height, 60%);
24
+ border-radius: var(--fe-border-radius, 4px);
25
+ overflow: auto;
26
+ border: var(--fe-border, 2px solid black);
27
+ border-bottom: var(--fe-border-bottom, 0px solid black);
28
+ outline: none;
29
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
30
+ white-space: pre-wrap;
31
+ background-color: var(--fe-background-color, #222222);
32
+ margin: 0px;
33
+ box-sizing: border-box;
34
+ /* position: relative; */
35
+ }
36
+
37
+ #wysiwyg-editor:empty:before {
38
+ content: attr(placeholder);
39
+ color: var(--fe-placeholder-color,grey);
40
+ pointer-events: none;
41
+ }
42
+
43
+ #wysiwyg-editor:focus {
44
+ border-color: var(--fe-focus-border-color, #7c5dfa);
45
+ box-shadow: 0 0 0 2px var(--fe-focus-shadow-color, rgba(124, 93, 250, 0.2));
46
+ }
47
+
48
+ /* Responsive styles */
49
+ @media (max-width: 768px) {
50
+ #wysiwyg-editor {
51
+ font-size: var(--fe-mobile-font-size, 14px);
52
+ padding: var(--fe-mobile-padding, 6px);
53
+ }
54
+ }
55
+
56
+ .wysiwygInternals.error {
57
+ text-decoration: underline;
58
+ -webkit-text-decoration-color: var(--fe-err-underline-color, #fc514f);
59
+ text-decoration-color: var(--fe-err-underline-color, #fc514f);
60
+ -webkit-text-decoration-style: wavy;
61
+ text-decoration-style: wavy;
62
+ /* text-decoration-thickness: 1px; */
63
+ text-decoration-color: var(--fe-err-underline-color, red);
64
+ }
65
+
66
+ .wysiwygInternals.bracket {
67
+ color: var(--fe-bracket-color, #fc514f);
68
+ transition: color 0.2s ease;
69
+ }
70
+
71
+ .wysiwygInternals.operator {
72
+ font-weight: bold;
73
+ color: var(--fe-operator-color, #fc618d);
74
+ transition: color 0.2s ease;
75
+ }
76
+
77
+ .wysiwygInternals.variable {
78
+ color: var(--fe-variable-color, #fc618d);
79
+ transition: color 0.2s ease;
80
+ }
81
+
82
+ .wysiwygInternals.function {
83
+ color: var(--fe-function-color, #82aaff);
84
+ font-style: italic;
85
+ transition: color 0.2s ease;
86
+ }
87
+
88
+ .wysiwygInternals.constant {
89
+ color: var(--fe-constant-color, #c3e88d);
90
+ font-weight: 500;
91
+ transition: color 0.2s ease;
92
+ }
93
+
94
+ /* High contrast mode support */
95
+ @media (forced-colors: active) {
96
+ .wysiwygInternals.error {
97
+ forced-color-adjust: none;
98
+ text-decoration-color: CanvasText;
99
+ }
100
+
101
+ #wysiwyg-editor:focus {
102
+ outline: 2px solid Highlight;
103
+ }
104
+ }
105
+
106
+ .error-message {
107
+ color: var(--fe-error-color, #fc514f);
108
+ background-color: var(--fe-error-background, rgba(252, 81, 79, 0.1));
109
+ padding: var(--fe-error-padding, 8px);
110
+ border-radius: var(--fe-error-border-radius, 4px);
111
+ margin-top: var(--fe-error-margin-top, 4px);
112
+ animation: fadeIn 0.2s ease;
113
+ font-size: var(--fe-error-font-size, 0.8rem);
114
+ margin-top: var(--fe-error-margin-top, 4px);
115
+ min-height: var(--fe-error-min-height, 1.2em);
116
+ }
117
+
118
+ @keyframes fadeIn {
119
+ from {
120
+ opacity: 0;
121
+ transform: translateY(-4px);
122
+ }
123
+ to {
124
+ opacity: 1;
125
+ transform: translateY(0);
126
+ }
127
+ }
128
+
129
+ /* Loading state */
130
+ .loading::after {
131
+ content: '';
132
+ position: absolute;
133
+ right: 8px;
134
+ top: 50%;
135
+ transform: translateY(-50%);
136
+ width: 12px;
137
+ height: 12px;
138
+ border: 2px solid var(--fe-loading-color, #fc618d);
139
+ border-radius: 50%;
140
+ border-top-color: transparent;
141
+ animation: spin 0.8s linear infinite;
142
+ }
143
+
144
+ @keyframes spin {
145
+ to {
146
+ transform: translateY(-50%) rotate(360deg);
147
+ }
148
+ }
149
+ `;
@@ -0,0 +1,24 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, html } from "lit";
8
+ import { customElement, property } from "lit/decorators.js";
9
+ import { Operator } from "../helpers/types";
10
+ let OperatorInput = class OperatorInput extends LitElement {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.operator = Operator.NONE;
14
+ }
15
+ render() {
16
+ return html `<span> ${this.operator} </span>`;
17
+ }
18
+ };
19
+ __decorate([
20
+ property()
21
+ ], OperatorInput.prototype, "operator", void 0);
22
+ OperatorInput = __decorate([
23
+ customElement("operator-input")
24
+ ], OperatorInput);