@masterteam/components 0.0.74 → 0.0.76

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 (31) hide show
  1. package/assets/common.css +1 -1
  2. package/fesm2022/masterteam-components-color-picker-field.mjs +2 -2
  3. package/fesm2022/masterteam-components-color-picker-field.mjs.map +1 -1
  4. package/fesm2022/masterteam-components-date-field.mjs +2 -2
  5. package/fesm2022/masterteam-components-date-field.mjs.map +1 -1
  6. package/fesm2022/masterteam-components-dynamic-drawer.mjs +0 -2
  7. package/fesm2022/masterteam-components-dynamic-drawer.mjs.map +1 -1
  8. package/fesm2022/masterteam-components-formula.mjs +1126 -0
  9. package/fesm2022/masterteam-components-formula.mjs.map +1 -0
  10. package/fesm2022/masterteam-components-module-summary-card.mjs +1 -1
  11. package/fesm2022/masterteam-components-module-summary-card.mjs.map +1 -1
  12. package/fesm2022/masterteam-components-page-header.mjs +2 -2
  13. package/fesm2022/masterteam-components-page-header.mjs.map +1 -1
  14. package/fesm2022/masterteam-components-page.mjs +2 -2
  15. package/fesm2022/masterteam-components-page.mjs.map +1 -1
  16. package/fesm2022/masterteam-components-pick-list-field.mjs +1 -1
  17. package/fesm2022/masterteam-components-pick-list-field.mjs.map +1 -1
  18. package/fesm2022/masterteam-components-table.mjs +4 -5
  19. package/fesm2022/masterteam-components-table.mjs.map +1 -1
  20. package/fesm2022/masterteam-components-toggle-field.mjs +21 -5
  21. package/fesm2022/masterteam-components-toggle-field.mjs.map +1 -1
  22. package/fesm2022/masterteam-components-topbar.mjs +2 -2
  23. package/fesm2022/masterteam-components-topbar.mjs.map +1 -1
  24. package/fesm2022/masterteam-components-tree.mjs +1 -1
  25. package/fesm2022/masterteam-components-tree.mjs.map +1 -1
  26. package/fesm2022/masterteam-components.mjs +7 -2
  27. package/fesm2022/masterteam-components.mjs.map +1 -1
  28. package/package.json +6 -1
  29. package/types/masterteam-components-formula.d.ts +500 -0
  30. package/types/masterteam-components-toggle-field.d.ts +8 -1
  31. package/types/masterteam-components.d.ts +16 -9
@@ -0,0 +1,1126 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, signal, computed, effect, forwardRef, Component } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import * as i1$1 from '@angular/forms';
5
+ import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
6
+ import * as i1 from '@angular/cdk/drag-drop';
7
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
8
+
9
+ /**
10
+ * Flat Formula Token Model
11
+ *
12
+ * The formula is represented as a FLAT array of tokens.
13
+ * This approach provides:
14
+ * - Better performance (no deep component nesting)
15
+ * - Solid drag-drop behavior (functions move as complete units)
16
+ * - Simple serialization (just join token values)
17
+ *
18
+ * STRUCTURE RULES:
19
+ * - fn-open: "IF(" - function name + opening paren as ONE token
20
+ * - fn-close: ")" - closing paren, belongs to a function
21
+ * - fn-separator: "," - argument separator, belongs to a function
22
+ * - Dragging any structural token moves the ENTIRE function
23
+ * - Content tokens (property, operator, literal) can move individually
24
+ */
25
+ // ============ UTILITY FUNCTIONS ============
26
+ /** Generate unique token ID */
27
+ function generateTokenId() {
28
+ return `tok_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
29
+ }
30
+ /** Generate unique function ID */
31
+ function generateFunctionId() {
32
+ return `fn_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
33
+ }
34
+ /** Parse function signature like "SUM(values)" or "IF(cond, a, b)" */
35
+ function parseSignature(signature) {
36
+ const match = signature.match(/^(\w+)\((.*)\)$/);
37
+ if (!match)
38
+ return { name: signature, args: [] };
39
+ const name = match[1];
40
+ const argsStr = match[2].trim();
41
+ if (!argsStr)
42
+ return { name, args: [] };
43
+ const args = [];
44
+ let current = '';
45
+ let depth = 0;
46
+ for (const char of argsStr) {
47
+ if (char === '(')
48
+ depth++;
49
+ else if (char === ')')
50
+ depth--;
51
+ else if (char === ',' && depth === 0) {
52
+ args.push(current.trim());
53
+ current = '';
54
+ continue;
55
+ }
56
+ current += char;
57
+ }
58
+ if (current.trim())
59
+ args.push(current.trim());
60
+ return { name, args };
61
+ }
62
+ /**
63
+ * Create a function's tokens from signature
64
+ * e.g., "IF(condition, trueVal, falseVal)" creates:
65
+ * - fn-open: "IF("
66
+ * - fn-separator: "," (x2)
67
+ * - fn-close: ")"
68
+ */
69
+ function createFunctionTokens(signature, depth = 0) {
70
+ const parsed = parseSignature(signature);
71
+ const functionId = generateFunctionId();
72
+ const tokens = [];
73
+ // Opening token: "NAME("
74
+ tokens.push({
75
+ id: generateTokenId(),
76
+ type: 'fn-open',
77
+ value: `${parsed.name}(`,
78
+ functionId,
79
+ functionName: parsed.name,
80
+ depth,
81
+ dragBehavior: 'function',
82
+ });
83
+ // Separators for arguments (one less than arg count)
84
+ for (let i = 0; i < parsed.args.length - 1; i++) {
85
+ tokens.push({
86
+ id: generateTokenId(),
87
+ type: 'fn-separator',
88
+ value: ',',
89
+ functionId,
90
+ depth,
91
+ argIndex: i, // Separator AFTER this argument
92
+ dragBehavior: 'function',
93
+ });
94
+ }
95
+ // Closing token: ")"
96
+ tokens.push({
97
+ id: generateTokenId(),
98
+ type: 'fn-close',
99
+ value: ')',
100
+ functionId,
101
+ depth,
102
+ dragBehavior: 'function',
103
+ });
104
+ return tokens;
105
+ }
106
+ /**
107
+ * Create a property token
108
+ */
109
+ function createPropertyToken(propertyKey, propertyType = 'current', depth = 0, functionId, argIndex) {
110
+ const prefix = propertyType === 'current'
111
+ ? '@'
112
+ : propertyType === 'children'
113
+ ? '*.'
114
+ : '$.';
115
+ return {
116
+ id: generateTokenId(),
117
+ type: 'property',
118
+ value: `${prefix}${propertyKey}`,
119
+ propertyType,
120
+ depth,
121
+ functionId,
122
+ argIndex,
123
+ dragBehavior: 'single',
124
+ };
125
+ }
126
+ /**
127
+ * Create an operator token
128
+ */
129
+ function createOperatorToken(symbol, depth = 0, functionId, argIndex) {
130
+ return {
131
+ id: generateTokenId(),
132
+ type: 'operator',
133
+ value: symbol,
134
+ depth,
135
+ functionId,
136
+ argIndex,
137
+ dragBehavior: 'single',
138
+ };
139
+ }
140
+ /**
141
+ * Create a literal token
142
+ */
143
+ function createLiteralToken(value, depth = 0, functionId, argIndex) {
144
+ let displayValue = String(value);
145
+ let literalType = 'string';
146
+ if (typeof value === 'number') {
147
+ literalType = 'number';
148
+ }
149
+ else if (typeof value === 'boolean') {
150
+ literalType = 'boolean';
151
+ }
152
+ else if (typeof value === 'string' && !value.startsWith('"')) {
153
+ displayValue = `"${value}"`;
154
+ }
155
+ return {
156
+ id: generateTokenId(),
157
+ type: 'literal',
158
+ value: displayValue,
159
+ literalType,
160
+ depth,
161
+ functionId,
162
+ argIndex,
163
+ dragBehavior: 'single',
164
+ };
165
+ }
166
+ // ============ TOKEN ARRAY OPERATIONS ============
167
+ /**
168
+ * Find the complete range of a function in the token array
169
+ * This includes ALL tokens between fn-open and fn-close (including nested content)
170
+ * Returns [startIndex, endIndex] inclusive
171
+ */
172
+ function findFunctionRange(tokens, functionId) {
173
+ // Find the fn-open token with this functionId
174
+ const openIndex = tokens.findIndex((t) => t.functionId === functionId && t.type === 'fn-open');
175
+ if (openIndex === -1)
176
+ return null;
177
+ // Now find the matching fn-close by counting depth
178
+ // Start from fn-open and find matching close paren
179
+ let depth = 1;
180
+ for (let i = openIndex + 1; i < tokens.length; i++) {
181
+ const token = tokens[i];
182
+ if (token.type === 'fn-open') {
183
+ depth++;
184
+ }
185
+ else if (token.type === 'fn-close') {
186
+ depth--;
187
+ if (depth === 0) {
188
+ // Found the matching close
189
+ return [openIndex, i];
190
+ }
191
+ }
192
+ }
193
+ // Fallback: find fn-close with same functionId
194
+ const closeIndex = tokens.findIndex((t) => t.functionId === functionId && t.type === 'fn-close');
195
+ if (closeIndex === -1)
196
+ return null;
197
+ return [openIndex, closeIndex];
198
+ }
199
+ /**
200
+ * Get all tokens belonging to a function (including nested content)
201
+ */
202
+ function getFunctionTokens(tokens, functionId) {
203
+ const range = findFunctionRange(tokens, functionId);
204
+ if (!range)
205
+ return [];
206
+ return tokens.slice(range[0], range[1] + 1);
207
+ }
208
+ /**
209
+ * Validate if a drop position is allowed
210
+ */
211
+ function isValidDropPosition(tokens, dropIndex, draggedTokens) {
212
+ // Can always drop at start or end
213
+ if (dropIndex === 0 || dropIndex === tokens.length)
214
+ return true;
215
+ const _prevToken = tokens[dropIndex - 1];
216
+ const _nextToken = tokens[dropIndex];
217
+ // If dragging a function, check if it would create invalid nesting
218
+ const isDraggingFunction = draggedTokens.some((t) => t.type === 'fn-open' || t.type === 'fn-close');
219
+ if (isDraggingFunction) {
220
+ // Don't allow dropping a function that would break structure
221
+ // For now, allow all - we can refine later
222
+ }
223
+ return true;
224
+ }
225
+ /**
226
+ * Serialize tokens to formula string
227
+ */
228
+ function serializeTokens(tokens) {
229
+ const parts = [];
230
+ let lastType = null;
231
+ for (const token of tokens) {
232
+ // Add space between tokens (except after fn-open, before fn-close, after separator)
233
+ if (lastType &&
234
+ lastType !== 'fn-open' &&
235
+ token.type !== 'fn-close' &&
236
+ token.type !== 'fn-separator' &&
237
+ lastType !== 'fn-separator') {
238
+ parts.push(' ');
239
+ }
240
+ parts.push(token.value);
241
+ lastType = token.type;
242
+ }
243
+ return parts.join('');
244
+ }
245
+ /**
246
+ * Clone a token with new ID
247
+ */
248
+ function cloneToken(token) {
249
+ return {
250
+ ...token,
251
+ id: generateTokenId(),
252
+ };
253
+ }
254
+ /**
255
+ * Clone multiple tokens with new IDs and new function IDs
256
+ */
257
+ function cloneTokens(tokens) {
258
+ // Map old function IDs to new ones
259
+ const functionIdMap = new Map();
260
+ return tokens.map((token) => {
261
+ const newToken = cloneToken(token);
262
+ // Remap function IDs
263
+ if (token.functionId) {
264
+ if (!functionIdMap.has(token.functionId)) {
265
+ functionIdMap.set(token.functionId, generateFunctionId());
266
+ }
267
+ newToken.functionId = functionIdMap.get(token.functionId);
268
+ }
269
+ return newToken;
270
+ });
271
+ }
272
+ /**
273
+ * Update depth for all tokens (after move)
274
+ */
275
+ function recalculateDepths(tokens) {
276
+ let currentDepth = 0;
277
+ const result = [];
278
+ for (const token of tokens) {
279
+ if (token.type === 'fn-open') {
280
+ result.push({ ...token, depth: currentDepth });
281
+ currentDepth++;
282
+ }
283
+ else if (token.type === 'fn-close') {
284
+ currentDepth--;
285
+ result.push({ ...token, depth: currentDepth });
286
+ }
287
+ else {
288
+ result.push({ ...token, depth: currentDepth });
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+ /**
294
+ * Find which argument index a position belongs to within a function
295
+ */
296
+ function getArgumentIndexAtPosition(tokens, functionId, position) {
297
+ const range = findFunctionRange(tokens, functionId);
298
+ if (!range)
299
+ return 0;
300
+ let argIndex = 0;
301
+ for (let i = range[0] + 1; i < position && i <= range[1]; i++) {
302
+ if (tokens[i].type === 'fn-separator' &&
303
+ tokens[i].functionId === functionId) {
304
+ argIndex++;
305
+ }
306
+ }
307
+ return argIndex;
308
+ }
309
+
310
+ /**
311
+ * Smart Formula Block - Types and Factory Functions
312
+ * Defines the block structure and creation helpers
313
+ */
314
+ /** Generate unique block ID */
315
+ function generateSmartBlockId() {
316
+ return `sb_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
317
+ }
318
+ /** Create a function block from signature */
319
+ function createFunctionBlock(signature, name) {
320
+ const parsed = parseSignature(signature);
321
+ return {
322
+ id: generateSmartBlockId(),
323
+ type: 'function',
324
+ value: parsed.name,
325
+ functionName: name || parsed.name,
326
+ signature,
327
+ arguments: parsed.args.map((arg) => ({
328
+ id: generateSmartBlockId(),
329
+ name: arg,
330
+ blocks: [],
331
+ placeholder: arg,
332
+ })),
333
+ };
334
+ }
335
+ /** Create a property block */
336
+ function createPropertyBlock(propertyKey, propertyType = 'current') {
337
+ const prefix = propertyType === 'current'
338
+ ? '@'
339
+ : propertyType === 'children'
340
+ ? '*.'
341
+ : '$.';
342
+ return {
343
+ id: generateSmartBlockId(),
344
+ type: 'property',
345
+ value: `${prefix}${propertyKey}`,
346
+ propertyType,
347
+ propertyKey,
348
+ };
349
+ }
350
+ /** Create an operator block */
351
+ function createOperatorBlock(symbol) {
352
+ return {
353
+ id: generateSmartBlockId(),
354
+ type: 'operator',
355
+ value: symbol,
356
+ operatorSymbol: symbol,
357
+ };
358
+ }
359
+ /** Create a literal block */
360
+ function createLiteralBlock(value) {
361
+ return {
362
+ id: generateSmartBlockId(),
363
+ type: 'literal',
364
+ value: String(value),
365
+ literalValue: value,
366
+ };
367
+ }
368
+ /** Deep clone a block with new IDs */
369
+ function cloneBlock(block) {
370
+ const newBlock = { ...block, id: generateSmartBlockId() };
371
+ if (block.arguments) {
372
+ newBlock.arguments = block.arguments.map((arg) => ({
373
+ ...arg,
374
+ id: generateSmartBlockId(),
375
+ blocks: arg.blocks.map((b) => cloneBlock(b)),
376
+ }));
377
+ }
378
+ return newBlock;
379
+ }
380
+
381
+ /**
382
+ * Validation Result Models
383
+ * API response types for formula validation
384
+ */
385
+
386
+ /**
387
+ * Formula Block Model - Types for formula functions and operators
388
+ */
389
+
390
+ // Models index
391
+
392
+ class FormulaEditor {
393
+ // ============ INPUTS ============
394
+ placeholder = input('Click toolbar items to build formula...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
395
+ initialTokens = input([], ...(ngDevMode ? [{ debugName: "initialTokens" }] : []));
396
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
397
+ // ============ OUTPUTS ============
398
+ formulaChange = output();
399
+ tokensChange = output();
400
+ onBlur = output();
401
+ onFocus = output();
402
+ // ============ STATE ============
403
+ tokens = signal([], ...(ngDevMode ? [{ debugName: "tokens" }] : []));
404
+ /** Index range being dragged [start, end] inclusive */
405
+ draggedRange = null;
406
+ /** Is currently dragging */
407
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
408
+ /** Hovered function ID for highlighting */
409
+ hoveredFunctionId = signal(null, ...(ngDevMode ? [{ debugName: "hoveredFunctionId" }] : []));
410
+ /** Is focused */
411
+ isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : []));
412
+ // ============ CVA ============
413
+ onChange = () => { };
414
+ onTouched = () => { };
415
+ // ============ COMPUTED ============
416
+ /** Line numbers based on token count */
417
+ lines = computed(() => {
418
+ const count = Math.max(1, Math.ceil(this.tokens().length / 8));
419
+ return Array(count).fill(0);
420
+ }, ...(ngDevMode ? [{ debugName: "lines" }] : []));
421
+ /** Serialized formula string */
422
+ formulaString = computed(() => serializeTokens(this.tokens()), ...(ngDevMode ? [{ debugName: "formulaString" }] : []));
423
+ constructor() {
424
+ // Initialize from input
425
+ effect(() => {
426
+ const initial = this.initialTokens();
427
+ if (initial.length > 0) {
428
+ this.tokens.set(initial);
429
+ }
430
+ });
431
+ }
432
+ // ============ CONTROL VALUE ACCESSOR ============
433
+ writeValue(value) {
434
+ if (value === null || value === undefined) {
435
+ this.tokens.set([]);
436
+ return;
437
+ }
438
+ // Value should be tokens array
439
+ if (Array.isArray(value)) {
440
+ this.tokens.set(value);
441
+ }
442
+ }
443
+ registerOnChange(fn) {
444
+ this.onChange = fn;
445
+ }
446
+ registerOnTouched(fn) {
447
+ this.onTouched = fn;
448
+ }
449
+ // ============ PUBLIC API ============
450
+ /**
451
+ * Add a block from toolbar (converts SmartBlock to tokens)
452
+ */
453
+ addBlock(block) {
454
+ if (this.disabled())
455
+ return;
456
+ const newTokens = this.smartBlockToTokens(block);
457
+ this.tokens.update((tokens) => [...tokens, ...cloneTokens(newTokens)]);
458
+ this.emit();
459
+ }
460
+ /**
461
+ * Add tokens directly
462
+ */
463
+ addTokens(newTokens) {
464
+ if (this.disabled())
465
+ return;
466
+ this.tokens.update((tokens) => [...tokens, ...cloneTokens(newTokens)]);
467
+ this.emit();
468
+ }
469
+ /**
470
+ * Clear all tokens
471
+ */
472
+ clear() {
473
+ if (this.disabled())
474
+ return;
475
+ this.tokens.set([]);
476
+ this.emit();
477
+ }
478
+ /**
479
+ * Get current formula string
480
+ */
481
+ serialize() {
482
+ return this.formulaString();
483
+ }
484
+ /**
485
+ * Get current tokens
486
+ */
487
+ getTokens() {
488
+ return this.tokens();
489
+ }
490
+ // ============ FOCUS HANDLERS ============
491
+ handleFocus() {
492
+ this.isFocused.set(true);
493
+ this.onFocus.emit();
494
+ }
495
+ handleBlur() {
496
+ this.isFocused.set(false);
497
+ this.onTouched();
498
+ this.onBlur.emit();
499
+ }
500
+ // ============ DRAG HANDLERS ============
501
+ /**
502
+ * Handle drag start - determine what tokens to drag
503
+ */
504
+ onDragStart(event, token, index) {
505
+ if (this.disabled())
506
+ return;
507
+ this.isDragging.set(true);
508
+ if (token.dragBehavior === 'function' && token.functionId) {
509
+ // Drag entire function (all tokens from fn-open to fn-close)
510
+ const range = findFunctionRange(this.tokens(), token.functionId);
511
+ if (range) {
512
+ this.draggedRange = range;
513
+ }
514
+ else {
515
+ // Fallback to single token if range not found
516
+ this.draggedRange = [index, index];
517
+ }
518
+ }
519
+ else {
520
+ // Drag single token
521
+ this.draggedRange = [index, index];
522
+ }
523
+ }
524
+ /**
525
+ * Handle drop - reorder tokens in the formula
526
+ */
527
+ onDrop(event) {
528
+ if (this.disabled())
529
+ return;
530
+ this.isDragging.set(false);
531
+ const { previousIndex, currentIndex } = event;
532
+ // No movement
533
+ if (previousIndex === currentIndex) {
534
+ this.resetDragState();
535
+ return;
536
+ }
537
+ // No drag data (shouldn't happen but be safe)
538
+ if (!this.draggedRange) {
539
+ this.resetDragState();
540
+ return;
541
+ }
542
+ const [start, end] = this.draggedRange;
543
+ const count = end - start + 1;
544
+ const isSingleToken = start === end;
545
+ // Block dropping inside own range (only for multi-token drag)
546
+ if (!isSingleToken && currentIndex > start && currentIndex <= end) {
547
+ this.resetDragState();
548
+ return;
549
+ }
550
+ const tokens = [...this.tokens()];
551
+ if (isSingleToken) {
552
+ // Simple single-token move using CDK utility
553
+ moveItemInArray(tokens, previousIndex, currentIndex);
554
+ }
555
+ else {
556
+ // Multi-token move (function with all content)
557
+ // 1. Extract the tokens we're moving
558
+ const moving = tokens.splice(start, count);
559
+ // 2. Calculate insert position
560
+ let insertAt;
561
+ if (currentIndex <= start) {
562
+ // Moving left/backward - insert at drop position
563
+ insertAt = currentIndex;
564
+ }
565
+ else {
566
+ // Moving right/forward
567
+ // After removal, adjust for removed count and insert AFTER drop target
568
+ insertAt = currentIndex - count + 1;
569
+ }
570
+ // 3. Clamp and insert
571
+ insertAt = Math.max(0, Math.min(insertAt, tokens.length));
572
+ tokens.splice(insertAt, 0, ...moving);
573
+ }
574
+ // Update state with recalculated depths
575
+ this.tokens.set(recalculateDepths(tokens));
576
+ this.resetDragState();
577
+ this.emit();
578
+ }
579
+ /**
580
+ * Reset drag state
581
+ */
582
+ resetDragState() {
583
+ this.draggedRange = null;
584
+ }
585
+ /**
586
+ * Handle drag end
587
+ * NOTE: CDK fires cdkDragEnded BEFORE cdkDropListDropped
588
+ * So we don't reset draggedRange here - onDrop handles it
589
+ */
590
+ onDragEnd() {
591
+ this.isDragging.set(false);
592
+ }
593
+ // ============ DELETE ============
594
+ /**
595
+ * Remove a token or function
596
+ */
597
+ removeToken(token, index) {
598
+ if (this.disabled())
599
+ return;
600
+ if (token.dragBehavior === 'function' && token.functionId) {
601
+ // Remove entire function
602
+ const range = findFunctionRange(this.tokens(), token.functionId);
603
+ if (range) {
604
+ this.tokens.update((tokens) => {
605
+ const newTokens = [...tokens];
606
+ newTokens.splice(range[0], range[1] - range[0] + 1);
607
+ return recalculateDepths(newTokens);
608
+ });
609
+ }
610
+ }
611
+ else {
612
+ // Remove single token
613
+ this.tokens.update((tokens) => tokens.filter((_, i) => i !== index));
614
+ }
615
+ this.emit();
616
+ }
617
+ // ============ HOVER EFFECTS ============
618
+ onTokenHover(token) {
619
+ if (token.functionId && token.dragBehavior === 'function') {
620
+ this.hoveredFunctionId.set(token.functionId);
621
+ }
622
+ }
623
+ onTokenLeave() {
624
+ this.hoveredFunctionId.set(null);
625
+ }
626
+ /**
627
+ * Check if token should be highlighted
628
+ * Only structural tokens (fn-open, fn-close, fn-separator) get highlighted
629
+ * Content tokens (property, operator, literal) inside function are NOT highlighted
630
+ */
631
+ isHighlighted(token) {
632
+ const hoveredId = this.hoveredFunctionId();
633
+ if (!hoveredId)
634
+ return false;
635
+ // Only highlight structural tokens of the function, not content inside
636
+ const isStructural = token.type === 'fn-open' ||
637
+ token.type === 'fn-close' ||
638
+ token.type === 'fn-separator';
639
+ return isStructural && token.functionId === hoveredId;
640
+ }
641
+ // ============ STYLING ============
642
+ /** Base token classes */
643
+ baseTokenClasses = 'inline-flex items-center px-2 py-1 rounded text-sm font-medium cursor-grab select-none whitespace-nowrap active:cursor-grabbing';
644
+ /**
645
+ * Get Tailwind classes for a token
646
+ */
647
+ getTokenClasses(token) {
648
+ const base = this.baseTokenClasses;
649
+ // Type-based styling using Tailwind
650
+ switch (token.type) {
651
+ case 'fn-open':
652
+ // Function opening: "IF(" - rounded left only
653
+ return `${base} bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold hover:bg-slate-300 dark:hover:bg-slate-600 rounded-r-none pr-0.5`;
654
+ case 'fn-close':
655
+ // Function closing: ")" - rounded right only
656
+ return `${base} bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold hover:bg-slate-300 dark:hover:bg-slate-600 rounded-l-none pl-0.5`;
657
+ case 'fn-separator':
658
+ // Function separator: ","
659
+ return `${base} bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold hover:bg-slate-300 dark:hover:bg-slate-600 rounded-sm px-1`;
660
+ case 'property':
661
+ // Property tokens - amber colors
662
+ return `${base} bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 font-semibold hover:bg-amber-200 dark:hover:bg-amber-900/50`;
663
+ case 'operator':
664
+ // Operator tokens - sky colors
665
+ return `${base} bg-sky-100 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300 font-bold hover:bg-sky-200 dark:hover:bg-sky-900/50 px-2`;
666
+ case 'literal':
667
+ // Literal tokens - emerald colors
668
+ return `${base} bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-200 dark:hover:bg-emerald-900/50`;
669
+ default:
670
+ return base;
671
+ }
672
+ }
673
+ // ============ CONVERSION ============
674
+ /**
675
+ * Convert SmartBlock to FormulaTokens
676
+ */
677
+ smartBlockToTokens(block) {
678
+ switch (block.type) {
679
+ case 'function':
680
+ return createFunctionTokens(block.signature || `${block.functionName}()`, 0);
681
+ case 'property':
682
+ return [
683
+ createPropertyToken(block.propertyKey || block.value, block.propertyType),
684
+ ];
685
+ case 'operator':
686
+ return [createOperatorToken(block.operatorSymbol || block.value)];
687
+ case 'literal':
688
+ return [createLiteralToken(block.literalValue ?? block.value)];
689
+ default:
690
+ return [createLiteralToken(block.value)];
691
+ }
692
+ }
693
+ // ============ EMIT ============
694
+ emit() {
695
+ const formula = this.formulaString();
696
+ const tokens = this.tokens();
697
+ this.tokensChange.emit(tokens);
698
+ this.formulaChange.emit(formula);
699
+ this.onChange(tokens);
700
+ }
701
+ // ============ CDK HELPERS ============
702
+ /**
703
+ * Track function for ngFor
704
+ */
705
+ trackByToken(_index, token) {
706
+ return token.id;
707
+ }
708
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
709
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormulaEditor, isStandalone: true, selector: "mt-formula-editor", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, initialTokens: { classPropertyName: "initialTokens", publicName: "initialTokens", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formulaChange: "formulaChange", tokensChange: "tokensChange", onBlur: "onBlur", onFocus: "onFocus" }, providers: [
710
+ {
711
+ provide: NG_VALUE_ACCESSOR,
712
+ useExisting: forwardRef(() => FormulaEditor),
713
+ multi: true,
714
+ },
715
+ ], ngImport: i0, template: "<div\r\n class=\"flex border border-surface-200 dark:border-surface-700 rounded-md bg-white dark:bg-surface-800 min-h-20 font-mono text-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/10\"\r\n [class.opacity-60]=\"disabled()\"\r\n [class.pointer-events-none]=\"disabled()\"\r\n (focus)=\"handleFocus()\"\r\n (blur)=\"handleBlur()\"\r\n tabindex=\"0\"\r\n>\r\n <!-- Line Numbers -->\r\n <div\r\n class=\"flex flex-col px-1.5 py-2 bg-surface-50 dark:bg-surface-900 border-r border-surface-200 dark:border-surface-700 rounded-l-md select-none min-w-6 text-right\"\r\n >\r\n @for (line of lines(); track $index) {\r\n <div class=\"text-surface-400 dark:text-surface-500 text-sm leading-6\">\r\n {{ $index + 1 }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Editor Content - Single Drop Zone -->\r\n <div\r\n class=\"flex-1 p-2 flex flex-wrap gap-0.5 items-start content-start min-h-14 cursor-text\"\r\n cdkDropList\r\n cdkDropListOrientation=\"mixed\"\r\n [cdkDropListData]=\"tokens()\"\r\n [cdkDropListDisabled]=\"disabled()\"\r\n (cdkDropListDropped)=\"onDrop($event)\"\r\n >\r\n @if (tokens().length === 0) {\r\n <div class=\"text-surface-400 dark:text-surface-500 text-sm italic\">\r\n {{ placeholder() }}\r\n </div>\r\n } @else {\r\n @for (token of tokens(); track token.id; let i = $index) {\r\n <div\r\n class=\"relative inline-flex items-center group\"\r\n cdkDrag\r\n [cdkDragData]=\"token\"\r\n [cdkDragDisabled]=\"disabled()\"\r\n (cdkDragStarted)=\"onDragStart($event, token, i)\"\r\n (cdkDragEnded)=\"onDragEnd()\"\r\n >\r\n <!-- Drag Placeholder (thin cursor line) -->\r\n <div\r\n *cdkDragPlaceholder\r\n class=\"w-2 min-w-2 h-5 bg-primary rounded-sm opacity-80\"\r\n ></div>\r\n\r\n <!-- Token -->\r\n <span\r\n [class]=\"getTokenClasses(token)\"\r\n [class.border-b-2]=\"!isDragging() && isHighlighted(token)\"\r\n [class.border-primary]=\"!isDragging() && isHighlighted(token)\"\r\n (mouseenter)=\"onTokenHover(token)\"\r\n (mouseleave)=\"onTokenLeave()\"\r\n >\r\n {{ token.value }}\r\n </span>\r\n\r\n <!-- Delete Button (shows on hover, hidden during drag) -->\r\n @if (!isDragging() && !disabled()) {\r\n <button\r\n type=\"button\"\r\n class=\"absolute -top-1.5 -right-1.5 w-3.5 h-3.5 flex items-center justify-center bg-red-500 hover:bg-red-600 text-white rounded-full cursor-pointer scale-75 hover:scale-100 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto z-10\"\r\n (click)=\"removeToken(token, i); $event.stopPropagation()\"\r\n [title]=\"\r\n token.dragBehavior === 'function' ? 'Remove function' : 'Remove'\r\n \"\r\n >\r\n <svg\r\n class=\"h-2.5 w-2.5\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"3\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 18L18 6M6 6l12 12\"\r\n />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n</div>\r\n", styles: [".cdk-drag-preview{width:20px!important;height:20px!important;min-width:20px!important;min-height:20px!important;max-width:20px!important;max-height:20px!important;border-radius:4px;opacity:.9;overflow:hidden!important;cursor:grabbing}.cdk-drag-animating{transition:none!important}.cdk-drag-dragging{opacity:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }] });
716
+ }
717
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaEditor, decorators: [{
718
+ type: Component,
719
+ args: [{ selector: 'mt-formula-editor', standalone: true, imports: [CommonModule, DragDropModule, FormsModule], providers: [
720
+ {
721
+ provide: NG_VALUE_ACCESSOR,
722
+ useExisting: forwardRef(() => FormulaEditor),
723
+ multi: true,
724
+ },
725
+ ], template: "<div\r\n class=\"flex border border-surface-200 dark:border-surface-700 rounded-md bg-white dark:bg-surface-800 min-h-20 font-mono text-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/10\"\r\n [class.opacity-60]=\"disabled()\"\r\n [class.pointer-events-none]=\"disabled()\"\r\n (focus)=\"handleFocus()\"\r\n (blur)=\"handleBlur()\"\r\n tabindex=\"0\"\r\n>\r\n <!-- Line Numbers -->\r\n <div\r\n class=\"flex flex-col px-1.5 py-2 bg-surface-50 dark:bg-surface-900 border-r border-surface-200 dark:border-surface-700 rounded-l-md select-none min-w-6 text-right\"\r\n >\r\n @for (line of lines(); track $index) {\r\n <div class=\"text-surface-400 dark:text-surface-500 text-sm leading-6\">\r\n {{ $index + 1 }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Editor Content - Single Drop Zone -->\r\n <div\r\n class=\"flex-1 p-2 flex flex-wrap gap-0.5 items-start content-start min-h-14 cursor-text\"\r\n cdkDropList\r\n cdkDropListOrientation=\"mixed\"\r\n [cdkDropListData]=\"tokens()\"\r\n [cdkDropListDisabled]=\"disabled()\"\r\n (cdkDropListDropped)=\"onDrop($event)\"\r\n >\r\n @if (tokens().length === 0) {\r\n <div class=\"text-surface-400 dark:text-surface-500 text-sm italic\">\r\n {{ placeholder() }}\r\n </div>\r\n } @else {\r\n @for (token of tokens(); track token.id; let i = $index) {\r\n <div\r\n class=\"relative inline-flex items-center group\"\r\n cdkDrag\r\n [cdkDragData]=\"token\"\r\n [cdkDragDisabled]=\"disabled()\"\r\n (cdkDragStarted)=\"onDragStart($event, token, i)\"\r\n (cdkDragEnded)=\"onDragEnd()\"\r\n >\r\n <!-- Drag Placeholder (thin cursor line) -->\r\n <div\r\n *cdkDragPlaceholder\r\n class=\"w-2 min-w-2 h-5 bg-primary rounded-sm opacity-80\"\r\n ></div>\r\n\r\n <!-- Token -->\r\n <span\r\n [class]=\"getTokenClasses(token)\"\r\n [class.border-b-2]=\"!isDragging() && isHighlighted(token)\"\r\n [class.border-primary]=\"!isDragging() && isHighlighted(token)\"\r\n (mouseenter)=\"onTokenHover(token)\"\r\n (mouseleave)=\"onTokenLeave()\"\r\n >\r\n {{ token.value }}\r\n </span>\r\n\r\n <!-- Delete Button (shows on hover, hidden during drag) -->\r\n @if (!isDragging() && !disabled()) {\r\n <button\r\n type=\"button\"\r\n class=\"absolute -top-1.5 -right-1.5 w-3.5 h-3.5 flex items-center justify-center bg-red-500 hover:bg-red-600 text-white rounded-full cursor-pointer scale-75 hover:scale-100 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto z-10\"\r\n (click)=\"removeToken(token, i); $event.stopPropagation()\"\r\n [title]=\"\r\n token.dragBehavior === 'function' ? 'Remove function' : 'Remove'\r\n \"\r\n >\r\n <svg\r\n class=\"h-2.5 w-2.5\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"3\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 18L18 6M6 6l12 12\"\r\n />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n</div>\r\n", styles: [".cdk-drag-preview{width:20px!important;height:20px!important;min-width:20px!important;min-height:20px!important;max-width:20px!important;max-height:20px!important;border-radius:4px;opacity:.9;overflow:hidden!important;cursor:grabbing}.cdk-drag-animating{transition:none!important}.cdk-drag-dragging{opacity:1}\n"] }]
726
+ }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], initialTokens: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialTokens", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], formulaChange: [{ type: i0.Output, args: ["formulaChange"] }], tokensChange: [{ type: i0.Output, args: ["tokensChange"] }], onBlur: [{ type: i0.Output, args: ["onBlur"] }], onFocus: [{ type: i0.Output, args: ["onFocus"] }] } });
727
+
728
+ /**
729
+ * Toolbar Item Component
730
+ * Clickable item in the toolbar (function, property, operator)
731
+ * Items are clicked to insert - no drag from toolbar
732
+ *
733
+ * This is a pure input/output component with no service dependencies.
734
+ */
735
+ class FormulaToolbarItem {
736
+ /** Item type */
737
+ type = input.required(...(ngDevMode ? [{ debugName: "type" }] : []));
738
+ /** Item value (function name, property key, operator symbol) */
739
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
740
+ /** Display value (optional, defaults to value) */
741
+ display = input(...(ngDevMode ? [undefined, { debugName: "display" }] : []));
742
+ /** Description for tooltip */
743
+ description = input('', ...(ngDevMode ? [{ debugName: "description" }] : []));
744
+ /** Function signature (for functions only, e.g., "SUM(values)") */
745
+ signature = input(...(ngDevMode ? [undefined, { debugName: "signature" }] : []));
746
+ /** Property type for properties */
747
+ propertyType = input('current', ...(ngDevMode ? [{ debugName: "propertyType" }] : []));
748
+ /** Insert event - emits SmartBlock */
749
+ onInsert = output();
750
+ /** Computed display value */
751
+ displayValue = computed(() => this.display() ?? this.value(), ...(ngDevMode ? [{ debugName: "displayValue" }] : []));
752
+ /** Build SmartBlock from item data */
753
+ smartBlock = computed(() => {
754
+ switch (this.type()) {
755
+ case 'function':
756
+ return createFunctionBlock(this.signature() || `${this.value()}()`, this.value());
757
+ case 'property':
758
+ return createPropertyBlock(this.value(), this.propertyType());
759
+ case 'operator':
760
+ return createOperatorBlock(this.value());
761
+ default:
762
+ return createOperatorBlock(this.value());
763
+ }
764
+ }, ...(ngDevMode ? [{ debugName: "smartBlock" }] : []));
765
+ /** Item CSS class based on type */
766
+ itemClass = computed(() => {
767
+ switch (this.type()) {
768
+ case 'function':
769
+ return 'bg-slate-200 text-slate-700 hover:bg-slate-300 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600';
770
+ case 'property':
771
+ return 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/50';
772
+ case 'operator':
773
+ return 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-300 dark:hover:bg-sky-900/50';
774
+ default:
775
+ return 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300';
776
+ }
777
+ }, ...(ngDevMode ? [{ debugName: "itemClass" }] : []));
778
+ /** Handle click - emit the SmartBlock */
779
+ handleClick() {
780
+ this.onInsert.emit(this.smartBlock());
781
+ }
782
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaToolbarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
783
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormulaToolbarItem, isStandalone: true, selector: "mt-formula-toolbar-item", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, signature: { classPropertyName: "signature", publicName: "signature", isSignal: true, isRequired: false, transformFunction: null }, propertyType: { classPropertyName: "propertyType", publicName: "propertyType", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onInsert: "onInsert" }, host: { classAttribute: "inline-block" }, ngImport: i0, template: "<button\r\n type=\"button\"\r\n class=\"inline-flex cursor-pointer items-center gap-1 rounded px-2 py-1 text-sm font-medium transition-colors hover:shadow-sm active:scale-95\"\r\n [class]=\"itemClass()\"\r\n [title]=\"description()\"\r\n (click)=\"handleClick()\"\r\n>\r\n @if (type() === \"function\") {\r\n <span class=\"text-sm opacity-60\">\u0192</span>\r\n }\r\n {{ displayValue() }}\r\n</button>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
784
+ }
785
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaToolbarItem, decorators: [{
786
+ type: Component,
787
+ args: [{ selector: 'mt-formula-toolbar-item', standalone: true, imports: [CommonModule], host: {
788
+ class: 'inline-block',
789
+ }, template: "<button\r\n type=\"button\"\r\n class=\"inline-flex cursor-pointer items-center gap-1 rounded px-2 py-1 text-sm font-medium transition-colors hover:shadow-sm active:scale-95\"\r\n [class]=\"itemClass()\"\r\n [title]=\"description()\"\r\n (click)=\"handleClick()\"\r\n>\r\n @if (type() === \"function\") {\r\n <span class=\"text-sm opacity-60\">\u0192</span>\r\n }\r\n {{ displayValue() }}\r\n</button>\r\n" }]
790
+ }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], signature: [{ type: i0.Input, args: [{ isSignal: true, alias: "signature", required: false }] }], propertyType: [{ type: i0.Input, args: [{ isSignal: true, alias: "propertyType", required: false }] }], onInsert: [{ type: i0.Output, args: ["onInsert"] }] } });
791
+
792
+ /**
793
+ * Formula Toolbar Component
794
+ * Vertical tabs toolbar with functions, properties, and operators
795
+ *
796
+ * This is a PURE input/output component with NO service dependencies.
797
+ * All data (functions, operators, properties) must be passed via inputs.
798
+ */
799
+ class FormulaToolbar {
800
+ // ============ INPUTS ============
801
+ /** Known properties to show in properties tab */
802
+ knownProperties = input([], ...(ngDevMode ? [{ debugName: "knownProperties" }] : []));
803
+ /** Function categories (from API or static data) */
804
+ functionCategories = input([], ...(ngDevMode ? [{ debugName: "functionCategories" }] : []));
805
+ /** Operators list */
806
+ operators = input([], ...(ngDevMode ? [{ debugName: "operators" }] : []));
807
+ /** Initial active tab */
808
+ initialTab = input('functions', ...(ngDevMode ? [{ debugName: "initialTab" }] : []));
809
+ /** Placeholder for search input */
810
+ searchPlaceholder = input('Search...', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : []));
811
+ /** Labels */
812
+ labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
813
+ // ============ OUTPUTS ============
814
+ /** Insert event - emits SmartBlock for the editor */
815
+ onBlockInsert = output();
816
+ /** Tab change event */
817
+ onTabChange = output();
818
+ // ============ STATE ============
819
+ /** Active tab */
820
+ activeTab = signal('functions', ...(ngDevMode ? [{ debugName: "activeTab" }] : []));
821
+ /** Tab options */
822
+ tabOptions = [
823
+ { label: 'Functions', value: 'functions' },
824
+ { label: 'Properties', value: 'properties' },
825
+ { label: 'Operators', value: 'operators' },
826
+ ];
827
+ /** Search query */
828
+ searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
829
+ /** Custom value input */
830
+ customValue = signal('', ...(ngDevMode ? [{ debugName: "customValue" }] : []));
831
+ /** Detected type of custom value */
832
+ customValueType = computed(() => {
833
+ const val = this.customValue().trim();
834
+ if (!val)
835
+ return 'text';
836
+ // Check if it's a number
837
+ if (/^-?\d+(\.\d+)?$/.test(val)) {
838
+ return 'number';
839
+ }
840
+ // Check if it's a quoted string
841
+ if ((val.startsWith('"') && val.endsWith('"')) ||
842
+ (val.startsWith("'") && val.endsWith("'"))) {
843
+ return 'string';
844
+ }
845
+ return 'text';
846
+ }, ...(ngDevMode ? [{ debugName: "customValueType" }] : []));
847
+ /** Preview of custom value */
848
+ customValuePreview = computed(() => {
849
+ const val = this.customValue().trim();
850
+ if (!val)
851
+ return '';
852
+ const type = this.customValueType();
853
+ if (type === 'string') {
854
+ // Show the quoted string as-is
855
+ return val;
856
+ }
857
+ if (type === 'number') {
858
+ return val;
859
+ }
860
+ // For unquoted text, show as string preview
861
+ return `"${val}"`;
862
+ }, ...(ngDevMode ? [{ debugName: "customValuePreview" }] : []));
863
+ /** CSS class for custom value type */
864
+ customValueTypeClass = computed(() => {
865
+ switch (this.customValueType()) {
866
+ case 'number':
867
+ return 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-300 dark:hover:bg-rose-900/50';
868
+ case 'string':
869
+ return 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:hover:bg-emerald-900/50';
870
+ default:
871
+ return 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600';
872
+ }
873
+ }, ...(ngDevMode ? [{ debugName: "customValueTypeClass" }] : []));
874
+ constructor() {
875
+ // Initialize active tab from input
876
+ effect(() => {
877
+ const initial = this.initialTab();
878
+ if (initial) {
879
+ this.activeTab.set(initial);
880
+ }
881
+ });
882
+ }
883
+ // ============ COMPUTED ============
884
+ /** Filtered categories based on search */
885
+ filteredCategories = computed(() => {
886
+ const query = this.searchQuery().toLowerCase();
887
+ const categories = this.functionCategories();
888
+ if (!query)
889
+ return categories;
890
+ return categories
891
+ .map((cat) => ({
892
+ ...cat,
893
+ functions: cat.functions.filter((fn) => fn.name.toLowerCase().includes(query) ||
894
+ fn.description.toLowerCase().includes(query)),
895
+ }))
896
+ .filter((cat) => cat.functions.length > 0);
897
+ }, ...(ngDevMode ? [{ debugName: "filteredCategories" }] : []));
898
+ /** Filtered properties based on search */
899
+ filteredProperties = computed(() => {
900
+ const query = this.searchQuery().toLowerCase();
901
+ const props = this.knownProperties();
902
+ if (!query)
903
+ return props;
904
+ return props.filter((p) => p.toLowerCase().includes(query));
905
+ }, ...(ngDevMode ? [{ debugName: "filteredProperties" }] : []));
906
+ /** Filtered operators based on search */
907
+ filteredOperators = computed(() => {
908
+ const query = this.searchQuery().toLowerCase();
909
+ const ops = this.operators();
910
+ if (!query)
911
+ return ops;
912
+ return ops.filter((op) => op.symbol.toLowerCase().includes(query) ||
913
+ op.name.toLowerCase().includes(query) ||
914
+ op.description.toLowerCase().includes(query));
915
+ }, ...(ngDevMode ? [{ debugName: "filteredOperators" }] : []));
916
+ /** Count of filtered items per tab */
917
+ filteredFunctionsCount = computed(() => this.filteredCategories().reduce((sum, cat) => sum + cat.functions.length, 0), ...(ngDevMode ? [{ debugName: "filteredFunctionsCount" }] : []));
918
+ filteredPropertiesCount = computed(() => this.filteredProperties().length, ...(ngDevMode ? [{ debugName: "filteredPropertiesCount" }] : []));
919
+ filteredOperatorsCount = computed(() => this.filteredOperators().length, ...(ngDevMode ? [{ debugName: "filteredOperatorsCount" }] : []));
920
+ /** Smart search effect - switches to tab with results */
921
+ smartSearchEffect = effect(() => {
922
+ const query = this.searchQuery();
923
+ if (!query || query.length < 2)
924
+ return;
925
+ const currentTab = this.activeTab();
926
+ const functionsCount = this.filteredFunctionsCount();
927
+ const propertiesCount = this.filteredPropertiesCount();
928
+ const operatorsCount = this.filteredOperatorsCount();
929
+ // Check if current tab has results
930
+ const currentHasResults = (currentTab === 'functions' && functionsCount > 0) ||
931
+ (currentTab === 'properties' && propertiesCount > 0) ||
932
+ (currentTab === 'operators' && operatorsCount > 0);
933
+ // If current tab has no results, switch to first tab with results
934
+ if (!currentHasResults) {
935
+ if (functionsCount > 0) {
936
+ this.activeTab.set('functions');
937
+ }
938
+ else if (propertiesCount > 0) {
939
+ this.activeTab.set('properties');
940
+ }
941
+ else if (operatorsCount > 0) {
942
+ this.activeTab.set('operators');
943
+ }
944
+ }
945
+ }, ...(ngDevMode ? [{ debugName: "smartSearchEffect" }] : []));
946
+ /** Group operators by type */
947
+ operatorGroups = computed(() => {
948
+ const ops = this.filteredOperators();
949
+ const groups = new Map();
950
+ ops.forEach((op) => {
951
+ const type = op.type || 'other';
952
+ if (!groups.has(type)) {
953
+ groups.set(type, []);
954
+ }
955
+ groups.get(type).push(op);
956
+ });
957
+ return Array.from(groups.entries()).map(([type, operators]) => ({
958
+ type: type.charAt(0).toUpperCase() + type.slice(1),
959
+ operators,
960
+ }));
961
+ }, ...(ngDevMode ? [{ debugName: "operatorGroups" }] : []));
962
+ /** Total item count for current tab */
963
+ itemCount = computed(() => {
964
+ switch (this.activeTab()) {
965
+ case 'functions':
966
+ return this.filteredCategories().reduce((sum, cat) => sum + cat.functions.length, 0);
967
+ case 'properties':
968
+ return this.filteredProperties().length;
969
+ case 'operators':
970
+ return this.filteredOperators().length;
971
+ default:
972
+ return 0;
973
+ }
974
+ }, ...(ngDevMode ? [{ debugName: "itemCount" }] : []));
975
+ // ============ COMPUTED LABELS ============
976
+ labelFunctions = computed(() => this.labels()?.functions ?? 'Functions', ...(ngDevMode ? [{ debugName: "labelFunctions" }] : []));
977
+ labelProperties = computed(() => this.labels()?.properties ?? 'Properties', ...(ngDevMode ? [{ debugName: "labelProperties" }] : []));
978
+ labelOperators = computed(() => this.labels()?.operators ?? 'Operators', ...(ngDevMode ? [{ debugName: "labelOperators" }] : []));
979
+ labelNoFunctions = computed(() => this.labels()?.noFunctionsFound ?? 'No functions found', ...(ngDevMode ? [{ debugName: "labelNoFunctions" }] : []));
980
+ labelNoProperties = computed(() => this.labels()?.noPropertiesAvailable ?? 'No properties available', ...(ngDevMode ? [{ debugName: "labelNoProperties" }] : []));
981
+ labelNoOperators = computed(() => this.labels()?.noOperatorsFound ?? 'No operators found', ...(ngDevMode ? [{ debugName: "labelNoOperators" }] : []));
982
+ // ============ METHODS ============
983
+ /** Get count for a specific tab (filtered when searching) */
984
+ getTabCount(tab) {
985
+ const query = this.searchQuery();
986
+ if (query) {
987
+ // When searching, show filtered count
988
+ switch (tab) {
989
+ case 'functions':
990
+ return this.filteredFunctionsCount();
991
+ case 'properties':
992
+ return this.filteredPropertiesCount();
993
+ case 'operators':
994
+ return this.filteredOperatorsCount();
995
+ default:
996
+ return 0;
997
+ }
998
+ }
999
+ else {
1000
+ // When not searching, show total count
1001
+ switch (tab) {
1002
+ case 'functions':
1003
+ return this.functionCategories().reduce((sum, cat) => sum + cat.functions.length, 0);
1004
+ case 'properties':
1005
+ return this.knownProperties().length;
1006
+ case 'operators':
1007
+ return this.operators().length;
1008
+ default:
1009
+ return 0;
1010
+ }
1011
+ }
1012
+ }
1013
+ /** Check if a tab has results when searching */
1014
+ tabHasResults(tab) {
1015
+ return this.getTabCount(tab) > 0;
1016
+ }
1017
+ /** Set active tab */
1018
+ setActiveTab(tab) {
1019
+ this.activeTab.set(tab);
1020
+ this.onTabChange.emit(tab);
1021
+ }
1022
+ /** Emit block insert event */
1023
+ insertBlock(block) {
1024
+ this.onBlockInsert.emit(block);
1025
+ }
1026
+ /** Clear search */
1027
+ clearSearch() {
1028
+ this.searchQuery.set('');
1029
+ }
1030
+ /** Insert custom value */
1031
+ insertCustomValue() {
1032
+ const val = this.customValue().trim();
1033
+ if (!val)
1034
+ return;
1035
+ const type = this.customValueType();
1036
+ let literalValue;
1037
+ if (type === 'number') {
1038
+ literalValue = parseFloat(val);
1039
+ }
1040
+ else if (type === 'string') {
1041
+ // Remove quotes for storage, keep the content
1042
+ literalValue = val.slice(1, -1);
1043
+ }
1044
+ else {
1045
+ // Treat as string, add quotes
1046
+ literalValue = val;
1047
+ }
1048
+ const block = createLiteralBlock(literalValue);
1049
+ // For strings, ensure the value includes quotes for display
1050
+ if (type === 'string' || type === 'text') {
1051
+ block.value = type === 'string' ? val : `"${val}"`;
1052
+ }
1053
+ this.onBlockInsert.emit(block);
1054
+ this.clearCustomValue();
1055
+ }
1056
+ /** Clear custom value input */
1057
+ clearCustomValue() {
1058
+ this.customValue.set('');
1059
+ }
1060
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1061
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormulaToolbar, isStandalone: true, selector: "mt-formula-toolbar", inputs: { knownProperties: { classPropertyName: "knownProperties", publicName: "knownProperties", isSignal: true, isRequired: false, transformFunction: null }, functionCategories: { classPropertyName: "functionCategories", publicName: "functionCategories", isSignal: true, isRequired: false, transformFunction: null }, operators: { classPropertyName: "operators", publicName: "operators", isSignal: true, isRequired: false, transformFunction: null }, initialTab: { classPropertyName: "initialTab", publicName: "initialTab", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onBlockInsert: "onBlockInsert", onTabChange: "onTabChange" }, ngImport: i0, template: "<div class=\"flex h-52 flex-col border-b border-slate-200 dark:border-slate-700\">\r\n <!-- Top Bar: Search + Quick Insert -->\r\n <div\r\n class=\"flex items-center gap-2 border-b border-slate-200 px-3 py-2 dark:border-slate-700\"\r\n >\r\n <!-- Search Field -->\r\n <div class=\"relative flex-1\">\r\n <svg\r\n class=\"absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\r\n />\r\n </svg>\r\n <input\r\n type=\"text\"\r\n class=\"w-full rounded-lg border border-slate-300 bg-white py-1.5 ps-9 pe-8 text-sm text-slate-700 placeholder:text-slate-400 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all duration-200 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-200 dark:placeholder:text-slate-500\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [(ngModel)]=\"searchQuery\"\r\n />\r\n @if (searchQuery()) {\r\n <button\r\n type=\"button\"\r\n class=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 transition-all duration-200 dark:hover:bg-slate-700\"\r\n (click)=\"clearSearch()\"\r\n >\r\n <svg\r\n class=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 18L18 6M6 6l12 12\"\r\n />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Separator -->\r\n <div class=\"h-6 w-px bg-slate-200 dark:bg-slate-600\"></div>\r\n\r\n <!-- Quick Insert Input -->\r\n <div class=\"relative flex items-center gap-2\">\r\n <span\r\n class=\"text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide\"\r\n >Insert:</span\r\n >\r\n <div class=\"relative\">\r\n <input\r\n #customValueInput\r\n type=\"text\"\r\n class=\"w-32 rounded-md border border-dashed border-slate-300 bg-slate-50 px-3 py-1.5 text-sm font-mono text-slate-700 placeholder:text-slate-400 focus:border-primary focus:bg-white focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all duration-200 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:placeholder:text-slate-500 dark:focus:bg-slate-900\"\r\n placeholder='123 or \"text\"'\r\n [(ngModel)]=\"customValue\"\r\n (keydown.enter)=\"insertCustomValue()\"\r\n (keydown.escape)=\"clearCustomValue()\"\r\n />\r\n </div>\r\n\r\n <!-- Insert Button with Type Preview -->\r\n @if (customValue()) {\r\n <button\r\n type=\"button\"\r\n class=\"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200 hover:shadow-sm active:scale-95\"\r\n [class]=\"customValueTypeClass()\"\r\n (click)=\"insertCustomValue()\"\r\n [title]=\"'Insert ' + customValueType() + ' (Enter)'\"\r\n >\r\n @switch (customValueType()) {\r\n @case (\"number\") {\r\n <svg\r\n class=\"h-3 w-3\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M7 20l4-16m2 16l4-16M6 9h14M4 15h14\"\r\n />\r\n </svg>\r\n }\r\n @case (\"string\") {\r\n <svg\r\n class=\"h-3 w-3\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z\"\r\n />\r\n </svg>\r\n }\r\n @default {\r\n <svg\r\n class=\"h-3 w-3\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M12 4.5v15m7.5-7.5h-15\"\r\n />\r\n </svg>\r\n }\r\n }\r\n <span class=\"max-w-16 truncate\">{{ customValuePreview() }}</span>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Main Content Row -->\r\n <div class=\"flex flex-1 overflow-hidden\">\r\n <!-- Vertical Tabs -->\r\n <div\r\n class=\"flex w-44 flex-col border-e border-slate-200 dark:border-slate-700\"\r\n >\r\n @for (tab of tabOptions; track tab.value) {\r\n <button\r\n type=\"button\"\r\n class=\"flex cursor-pointer items-center gap-2 px-3 py-2.5 text-start text-sm font-medium transition-all duration-200\"\r\n [class]=\"\r\n activeTab() === tab.value\r\n ? 'bg-white border-e-2 border-primary text-primary dark:bg-slate-900'\r\n : searchQuery() && !tabHasResults(tab.value)\r\n ? 'text-slate-300 dark:text-slate-600'\r\n : 'text-slate-600 hover:bg-slate-100 hover:text-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'\r\n \"\r\n (click)=\"setActiveTab(tab.value)\"\r\n >\r\n <!-- Tab Icons -->\r\n @switch (tab.value) {\r\n @case (\"functions\") {\r\n <svg\r\n class=\"h-5 w-5 shrink-0\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5\"\r\n />\r\n </svg>\r\n }\r\n @case (\"properties\") {\r\n <svg\r\n class=\"h-5 w-5 shrink-0\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z\"\r\n />\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 6h.008v.008H6V6z\"\r\n />\r\n </svg>\r\n }\r\n @case (\"operators\") {\r\n <svg\r\n class=\"h-5 w-5 shrink-0\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M15.75 15.75V18m-7.5-6.75h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V13.5zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V18zm2.498-6.75h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V13.5zm0 2.25h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V18zm2.504-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V18zm2.498-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zM8.25 6h7.5v2.25h-7.5V6zM12 2.25c-1.892 0-3.758.11-5.593.322C5.307 2.7 4.5 3.65 4.5 4.757V19.5a2.25 2.25 0 002.25 2.25h10.5a2.25 2.25 0 002.25-2.25V4.757c0-1.108-.806-2.057-1.907-2.185A48.507 48.507 0 0012 2.25z\"\r\n />\r\n </svg>\r\n }\r\n }\r\n <!-- Label (always visible) -->\r\n <span class=\"truncate whitespace-nowrap text-sm\">{{\r\n tab.label\r\n }}</span>\r\n <!-- Count Badge -->\r\n <span\r\n class=\"ms-auto rounded-full px-1.5 py-0.5 text-xs font-semibold\"\r\n [class]=\"\r\n activeTab() === tab.value\r\n ? 'bg-primary/10 text-primary'\r\n : 'bg-slate-200 text-slate-600 dark:bg-slate-600 dark:text-slate-300'\r\n \"\r\n >\r\n {{ getTabCount(tab.value) }}\r\n </span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Content Area -->\r\n <div class=\"flex flex-1 flex-col overflow-hidden\">\r\n <!-- Items Grid -->\r\n <div class=\"flex-1 overflow-y-auto p-2.5\">\r\n @switch (activeTab()) {\r\n @case (\"functions\") {\r\n @if (filteredCategories().length === 0) {\r\n <div\r\n class=\"flex h-full items-center justify-center text-sm text-slate-400 dark:text-slate-500\"\r\n >\r\n {{ labelNoFunctions() }}\r\n </div>\r\n } @else {\r\n <div class=\"space-y-3\">\r\n @for (category of filteredCategories(); track category.name) {\r\n <div>\r\n <div class=\"mb-2 flex items-center gap-2\">\r\n <span\r\n class=\"text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400\"\r\n >\r\n {{ category.name }}\r\n </span>\r\n <span\r\n class=\"h-px flex-1 bg-slate-200/60 dark:bg-slate-700/60\"\r\n ></span>\r\n <span\r\n class=\"text-sm text-slate-400 dark:text-slate-500\"\r\n >{{ category.functions.length }}</span\r\n >\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (fn of category.functions; track fn.name) {\r\n <mt-formula-toolbar-item\r\n type=\"function\"\r\n [value]=\"fn.name\"\r\n [description]=\"fn.description\"\r\n [signature]=\"fn.signature\"\r\n (onInsert)=\"insertBlock($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n }\r\n @case (\"properties\") {\r\n @if (filteredProperties().length === 0) {\r\n <div\r\n class=\"flex h-full items-center justify-center text-sm text-slate-400 dark:text-slate-500\"\r\n >\r\n {{ labelNoProperties() }}\r\n </div>\r\n } @else {\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (prop of filteredProperties(); track prop) {\r\n <mt-formula-toolbar-item\r\n type=\"property\"\r\n [value]=\"prop\"\r\n [description]=\"'Property: ' + prop\"\r\n propertyType=\"current\"\r\n (onInsert)=\"insertBlock($event)\"\r\n />\r\n }\r\n </div>\r\n }\r\n }\r\n @case (\"operators\") {\r\n @if (filteredOperators().length === 0) {\r\n <div\r\n class=\"flex h-full items-center justify-center text-sm text-slate-400 dark:text-slate-500\"\r\n >\r\n {{ labelNoOperators() }}\r\n </div>\r\n } @else {\r\n <div class=\"space-y-3\">\r\n @for (group of operatorGroups(); track group.type) {\r\n <div>\r\n <div class=\"mb-2 flex items-center gap-2\">\r\n <span\r\n class=\"text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400\"\r\n >\r\n {{ group.type }}\r\n </span>\r\n <span\r\n class=\"h-px flex-1 bg-slate-200/60 dark:bg-slate-700/60\"\r\n ></span>\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (op of group.operators; track op.symbol) {\r\n <mt-formula-toolbar-item\r\n type=\"operator\"\r\n [value]=\"op.symbol\"\r\n [description]=\"op.description\"\r\n (onInsert)=\"insertBlock($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: FormulaToolbarItem, selector: "mt-formula-toolbar-item", inputs: ["type", "value", "display", "description", "signature", "propertyType"], outputs: ["onInsert"] }] });
1062
+ }
1063
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaToolbar, decorators: [{
1064
+ type: Component,
1065
+ args: [{ selector: 'mt-formula-toolbar', standalone: true, imports: [CommonModule, FormsModule, FormulaToolbarItem], template: "<div class=\"flex h-52 flex-col border-b border-slate-200 dark:border-slate-700\">\r\n <!-- Top Bar: Search + Quick Insert -->\r\n <div\r\n class=\"flex items-center gap-2 border-b border-slate-200 px-3 py-2 dark:border-slate-700\"\r\n >\r\n <!-- Search Field -->\r\n <div class=\"relative flex-1\">\r\n <svg\r\n class=\"absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\r\n />\r\n </svg>\r\n <input\r\n type=\"text\"\r\n class=\"w-full rounded-lg border border-slate-300 bg-white py-1.5 ps-9 pe-8 text-sm text-slate-700 placeholder:text-slate-400 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all duration-200 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-200 dark:placeholder:text-slate-500\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [(ngModel)]=\"searchQuery\"\r\n />\r\n @if (searchQuery()) {\r\n <button\r\n type=\"button\"\r\n class=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 transition-all duration-200 dark:hover:bg-slate-700\"\r\n (click)=\"clearSearch()\"\r\n >\r\n <svg\r\n class=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 18L18 6M6 6l12 12\"\r\n />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Separator -->\r\n <div class=\"h-6 w-px bg-slate-200 dark:bg-slate-600\"></div>\r\n\r\n <!-- Quick Insert Input -->\r\n <div class=\"relative flex items-center gap-2\">\r\n <span\r\n class=\"text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide\"\r\n >Insert:</span\r\n >\r\n <div class=\"relative\">\r\n <input\r\n #customValueInput\r\n type=\"text\"\r\n class=\"w-32 rounded-md border border-dashed border-slate-300 bg-slate-50 px-3 py-1.5 text-sm font-mono text-slate-700 placeholder:text-slate-400 focus:border-primary focus:bg-white focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all duration-200 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:placeholder:text-slate-500 dark:focus:bg-slate-900\"\r\n placeholder='123 or \"text\"'\r\n [(ngModel)]=\"customValue\"\r\n (keydown.enter)=\"insertCustomValue()\"\r\n (keydown.escape)=\"clearCustomValue()\"\r\n />\r\n </div>\r\n\r\n <!-- Insert Button with Type Preview -->\r\n @if (customValue()) {\r\n <button\r\n type=\"button\"\r\n class=\"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200 hover:shadow-sm active:scale-95\"\r\n [class]=\"customValueTypeClass()\"\r\n (click)=\"insertCustomValue()\"\r\n [title]=\"'Insert ' + customValueType() + ' (Enter)'\"\r\n >\r\n @switch (customValueType()) {\r\n @case (\"number\") {\r\n <svg\r\n class=\"h-3 w-3\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M7 20l4-16m2 16l4-16M6 9h14M4 15h14\"\r\n />\r\n </svg>\r\n }\r\n @case (\"string\") {\r\n <svg\r\n class=\"h-3 w-3\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z\"\r\n />\r\n </svg>\r\n }\r\n @default {\r\n <svg\r\n class=\"h-3 w-3\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M12 4.5v15m7.5-7.5h-15\"\r\n />\r\n </svg>\r\n }\r\n }\r\n <span class=\"max-w-16 truncate\">{{ customValuePreview() }}</span>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Main Content Row -->\r\n <div class=\"flex flex-1 overflow-hidden\">\r\n <!-- Vertical Tabs -->\r\n <div\r\n class=\"flex w-44 flex-col border-e border-slate-200 dark:border-slate-700\"\r\n >\r\n @for (tab of tabOptions; track tab.value) {\r\n <button\r\n type=\"button\"\r\n class=\"flex cursor-pointer items-center gap-2 px-3 py-2.5 text-start text-sm font-medium transition-all duration-200\"\r\n [class]=\"\r\n activeTab() === tab.value\r\n ? 'bg-white border-e-2 border-primary text-primary dark:bg-slate-900'\r\n : searchQuery() && !tabHasResults(tab.value)\r\n ? 'text-slate-300 dark:text-slate-600'\r\n : 'text-slate-600 hover:bg-slate-100 hover:text-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'\r\n \"\r\n (click)=\"setActiveTab(tab.value)\"\r\n >\r\n <!-- Tab Icons -->\r\n @switch (tab.value) {\r\n @case (\"functions\") {\r\n <svg\r\n class=\"h-5 w-5 shrink-0\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5\"\r\n />\r\n </svg>\r\n }\r\n @case (\"properties\") {\r\n <svg\r\n class=\"h-5 w-5 shrink-0\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z\"\r\n />\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 6h.008v.008H6V6z\"\r\n />\r\n </svg>\r\n }\r\n @case (\"operators\") {\r\n <svg\r\n class=\"h-5 w-5 shrink-0\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M15.75 15.75V18m-7.5-6.75h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V13.5zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V18zm2.498-6.75h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V13.5zm0 2.25h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V18zm2.504-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V18zm2.498-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zM8.25 6h7.5v2.25h-7.5V6zM12 2.25c-1.892 0-3.758.11-5.593.322C5.307 2.7 4.5 3.65 4.5 4.757V19.5a2.25 2.25 0 002.25 2.25h10.5a2.25 2.25 0 002.25-2.25V4.757c0-1.108-.806-2.057-1.907-2.185A48.507 48.507 0 0012 2.25z\"\r\n />\r\n </svg>\r\n }\r\n }\r\n <!-- Label (always visible) -->\r\n <span class=\"truncate whitespace-nowrap text-sm\">{{\r\n tab.label\r\n }}</span>\r\n <!-- Count Badge -->\r\n <span\r\n class=\"ms-auto rounded-full px-1.5 py-0.5 text-xs font-semibold\"\r\n [class]=\"\r\n activeTab() === tab.value\r\n ? 'bg-primary/10 text-primary'\r\n : 'bg-slate-200 text-slate-600 dark:bg-slate-600 dark:text-slate-300'\r\n \"\r\n >\r\n {{ getTabCount(tab.value) }}\r\n </span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Content Area -->\r\n <div class=\"flex flex-1 flex-col overflow-hidden\">\r\n <!-- Items Grid -->\r\n <div class=\"flex-1 overflow-y-auto p-2.5\">\r\n @switch (activeTab()) {\r\n @case (\"functions\") {\r\n @if (filteredCategories().length === 0) {\r\n <div\r\n class=\"flex h-full items-center justify-center text-sm text-slate-400 dark:text-slate-500\"\r\n >\r\n {{ labelNoFunctions() }}\r\n </div>\r\n } @else {\r\n <div class=\"space-y-3\">\r\n @for (category of filteredCategories(); track category.name) {\r\n <div>\r\n <div class=\"mb-2 flex items-center gap-2\">\r\n <span\r\n class=\"text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400\"\r\n >\r\n {{ category.name }}\r\n </span>\r\n <span\r\n class=\"h-px flex-1 bg-slate-200/60 dark:bg-slate-700/60\"\r\n ></span>\r\n <span\r\n class=\"text-sm text-slate-400 dark:text-slate-500\"\r\n >{{ category.functions.length }}</span\r\n >\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (fn of category.functions; track fn.name) {\r\n <mt-formula-toolbar-item\r\n type=\"function\"\r\n [value]=\"fn.name\"\r\n [description]=\"fn.description\"\r\n [signature]=\"fn.signature\"\r\n (onInsert)=\"insertBlock($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n }\r\n @case (\"properties\") {\r\n @if (filteredProperties().length === 0) {\r\n <div\r\n class=\"flex h-full items-center justify-center text-sm text-slate-400 dark:text-slate-500\"\r\n >\r\n {{ labelNoProperties() }}\r\n </div>\r\n } @else {\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (prop of filteredProperties(); track prop) {\r\n <mt-formula-toolbar-item\r\n type=\"property\"\r\n [value]=\"prop\"\r\n [description]=\"'Property: ' + prop\"\r\n propertyType=\"current\"\r\n (onInsert)=\"insertBlock($event)\"\r\n />\r\n }\r\n </div>\r\n }\r\n }\r\n @case (\"operators\") {\r\n @if (filteredOperators().length === 0) {\r\n <div\r\n class=\"flex h-full items-center justify-center text-sm text-slate-400 dark:text-slate-500\"\r\n >\r\n {{ labelNoOperators() }}\r\n </div>\r\n } @else {\r\n <div class=\"space-y-3\">\r\n @for (group of operatorGroups(); track group.type) {\r\n <div>\r\n <div class=\"mb-2 flex items-center gap-2\">\r\n <span\r\n class=\"text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400\"\r\n >\r\n {{ group.type }}\r\n </span>\r\n <span\r\n class=\"h-px flex-1 bg-slate-200/60 dark:bg-slate-700/60\"\r\n ></span>\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (op of group.operators; track op.symbol) {\r\n <mt-formula-toolbar-item\r\n type=\"operator\"\r\n [value]=\"op.symbol\"\r\n [description]=\"op.description\"\r\n (onInsert)=\"insertBlock($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n" }]
1066
+ }], ctorParameters: () => [], propDecorators: { knownProperties: [{ type: i0.Input, args: [{ isSignal: true, alias: "knownProperties", required: false }] }], functionCategories: [{ type: i0.Input, args: [{ isSignal: true, alias: "functionCategories", required: false }] }], operators: [{ type: i0.Input, args: [{ isSignal: true, alias: "operators", required: false }] }], initialTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialTab", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], onBlockInsert: [{ type: i0.Output, args: ["onBlockInsert"] }], onTabChange: [{ type: i0.Output, args: ["onTabChange"] }] } });
1067
+
1068
+ /**
1069
+ * Formula Status Bar Component
1070
+ * Displays validation status, complexity, and dependencies
1071
+ *
1072
+ * This is a PURE input/output component with NO service dependencies.
1073
+ */
1074
+ class FormulaStatusBar {
1075
+ // ============ INPUTS ============
1076
+ /** Validation result */
1077
+ validation = input(null, ...(ngDevMode ? [{ debugName: "validation" }] : []));
1078
+ /** Labels for i18n support */
1079
+ labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
1080
+ // ============ COMPUTED ============
1081
+ /** Is formula valid */
1082
+ isValid = computed(() => this.validation()?.isValid ?? true, ...(ngDevMode ? [{ debugName: "isValid" }] : []));
1083
+ /** Complexity score */
1084
+ complexity = computed(() => this.validation()?.complexity ?? 0, ...(ngDevMode ? [{ debugName: "complexity" }] : []));
1085
+ /** Dependencies list */
1086
+ dependencies = computed(() => this.validation()?.dependencies ?? [], ...(ngDevMode ? [{ debugName: "dependencies" }] : []));
1087
+ /** Dependencies as text */
1088
+ dependenciesText = computed(() => {
1089
+ const deps = this.dependencies();
1090
+ if (deps.length <= 3) {
1091
+ return deps.join(', ');
1092
+ }
1093
+ return `${deps.slice(0, 3).join(', ')} +${deps.length - 3}`;
1094
+ }, ...(ngDevMode ? [{ debugName: "dependenciesText" }] : []));
1095
+ /** First error */
1096
+ firstError = computed(() => this.validation()?.errors[0] ?? null, ...(ngDevMode ? [{ debugName: "firstError" }] : []));
1097
+ /** Complexity CSS class */
1098
+ complexityClass = computed(() => {
1099
+ const c = this.complexity();
1100
+ if (c <= 10)
1101
+ return 'text-emerald-600 dark:text-emerald-400';
1102
+ if (c <= 25)
1103
+ return 'text-amber-600 dark:text-amber-400';
1104
+ return 'text-rose-600 dark:text-rose-400';
1105
+ }, ...(ngDevMode ? [{ debugName: "complexityClass" }] : []));
1106
+ // ============ COMPUTED LABELS ============
1107
+ labelValid = computed(() => this.labels()?.valid ?? 'Valid', ...(ngDevMode ? [{ debugName: "labelValid" }] : []));
1108
+ labelInvalid = computed(() => this.labels()?.invalid ?? 'Invalid', ...(ngDevMode ? [{ debugName: "labelInvalid" }] : []));
1109
+ labelComplexity = computed(() => this.labels()?.complexity ?? 'Complexity', ...(ngDevMode ? [{ debugName: "labelComplexity" }] : []));
1110
+ labelDependencies = computed(() => this.labels()?.dependencies ?? 'Dependencies', ...(ngDevMode ? [{ debugName: "labelDependencies" }] : []));
1111
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaStatusBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1112
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormulaStatusBar, isStandalone: true, selector: "mt-formula-status-bar", inputs: { validation: { classPropertyName: "validation", publicName: "validation", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\r\n class=\"flex items-center gap-4 border-t border-slate-200 px-4 py-2 text-sm dark:border-slate-700\"\r\n>\r\n <!-- Validation Status -->\r\n <div class=\"flex items-center gap-1.5\">\r\n @if (isValid()) {\r\n <span\r\n class=\"flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-900 dark:text-emerald-400\"\r\n >\r\n <svg\r\n class=\"h-3.5 w-3.5\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2.5\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M5 13l4 4L19 7\"\r\n />\r\n </svg>\r\n </span>\r\n <span class=\"font-medium text-emerald-600 dark:text-emerald-400\">\r\n {{ labelValid() }}\r\n </span>\r\n } @else {\r\n <span\r\n class=\"flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-rose-600 dark:bg-rose-900 dark:text-rose-400\"\r\n >\r\n <svg\r\n class=\"h-3.5 w-3.5\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2.5\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 18L18 6M6 6l12 12\"\r\n />\r\n </svg>\r\n </span>\r\n <span class=\"font-medium text-rose-600 dark:text-rose-400\">\r\n {{ labelInvalid() }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Separator -->\r\n <div class=\"h-4 w-px bg-slate-300 dark:bg-slate-600\"></div>\r\n\r\n <!-- Complexity -->\r\n <div class=\"flex items-center gap-1.5 text-slate-500 dark:text-slate-400\">\r\n <svg class=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n stroke-width=\"2\"\r\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\r\n />\r\n </svg>\r\n <span>{{ labelComplexity() }}:</span>\r\n <span class=\"font-medium\" [class]=\"complexityClass()\">\r\n {{ complexity() }}\r\n </span>\r\n </div>\r\n\r\n <!-- Dependencies -->\r\n @if (dependencies().length > 0) {\r\n <div class=\"h-4 w-px bg-slate-300 dark:bg-slate-600\"></div>\r\n <div class=\"flex items-center gap-1.5 text-slate-500 dark:text-slate-400\">\r\n <svg\r\n class=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n stroke-width=\"2\"\r\n d=\"M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1\"\r\n />\r\n </svg>\r\n <span>{{ labelDependencies() }}:</span>\r\n <span class=\"font-medium text-slate-700 dark:text-slate-300\">\r\n {{ dependenciesText() }}\r\n </span>\r\n </div>\r\n }\r\n\r\n <!-- Error message (if any) -->\r\n @if (firstError(); as error) {\r\n <div\r\n class=\"ms-auto flex items-center gap-1.5 text-rose-600 dark:text-rose-400\"\r\n >\r\n <svg\r\n class=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n stroke-width=\"2\"\r\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\r\n />\r\n </svg>\r\n <span class=\"text-xs\">{{ error.message }}</span>\r\n <span class=\"text-xs opacity-60\"\r\n >({{ error.line }}:{{ error.column }})</span\r\n >\r\n </div>\r\n }\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1113
+ }
1114
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaStatusBar, decorators: [{
1115
+ type: Component,
1116
+ args: [{ selector: 'mt-formula-status-bar', standalone: true, imports: [CommonModule], template: "<div\r\n class=\"flex items-center gap-4 border-t border-slate-200 px-4 py-2 text-sm dark:border-slate-700\"\r\n>\r\n <!-- Validation Status -->\r\n <div class=\"flex items-center gap-1.5\">\r\n @if (isValid()) {\r\n <span\r\n class=\"flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-900 dark:text-emerald-400\"\r\n >\r\n <svg\r\n class=\"h-3.5 w-3.5\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2.5\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M5 13l4 4L19 7\"\r\n />\r\n </svg>\r\n </span>\r\n <span class=\"font-medium text-emerald-600 dark:text-emerald-400\">\r\n {{ labelValid() }}\r\n </span>\r\n } @else {\r\n <span\r\n class=\"flex h-5 w-5 items-center justify-center rounded-full bg-rose-100 text-rose-600 dark:bg-rose-900 dark:text-rose-400\"\r\n >\r\n <svg\r\n class=\"h-3.5 w-3.5\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2.5\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n d=\"M6 18L18 6M6 6l12 12\"\r\n />\r\n </svg>\r\n </span>\r\n <span class=\"font-medium text-rose-600 dark:text-rose-400\">\r\n {{ labelInvalid() }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Separator -->\r\n <div class=\"h-4 w-px bg-slate-300 dark:bg-slate-600\"></div>\r\n\r\n <!-- Complexity -->\r\n <div class=\"flex items-center gap-1.5 text-slate-500 dark:text-slate-400\">\r\n <svg class=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n stroke-width=\"2\"\r\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\r\n />\r\n </svg>\r\n <span>{{ labelComplexity() }}:</span>\r\n <span class=\"font-medium\" [class]=\"complexityClass()\">\r\n {{ complexity() }}\r\n </span>\r\n </div>\r\n\r\n <!-- Dependencies -->\r\n @if (dependencies().length > 0) {\r\n <div class=\"h-4 w-px bg-slate-300 dark:bg-slate-600\"></div>\r\n <div class=\"flex items-center gap-1.5 text-slate-500 dark:text-slate-400\">\r\n <svg\r\n class=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n stroke-width=\"2\"\r\n d=\"M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1\"\r\n />\r\n </svg>\r\n <span>{{ labelDependencies() }}:</span>\r\n <span class=\"font-medium text-slate-700 dark:text-slate-300\">\r\n {{ dependenciesText() }}\r\n </span>\r\n </div>\r\n }\r\n\r\n <!-- Error message (if any) -->\r\n @if (firstError(); as error) {\r\n <div\r\n class=\"ms-auto flex items-center gap-1.5 text-rose-600 dark:text-rose-400\"\r\n >\r\n <svg\r\n class=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n >\r\n <path\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n stroke-width=\"2\"\r\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\r\n />\r\n </svg>\r\n <span class=\"text-xs\">{{ error.message }}</span>\r\n <span class=\"text-xs opacity-60\"\r\n >({{ error.line }}:{{ error.column }})</span\r\n >\r\n </div>\r\n }\r\n</div>\r\n" }]
1117
+ }], propDecorators: { validation: [{ type: i0.Input, args: [{ isSignal: true, alias: "validation", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }] } });
1118
+
1119
+ // Formula components public API
1120
+
1121
+ /**
1122
+ * Generated bundle index. Do not edit.
1123
+ */
1124
+
1125
+ export { FormulaEditor, FormulaStatusBar, FormulaToolbar, FormulaToolbarItem, cloneBlock, cloneToken, cloneTokens, createFunctionBlock, createFunctionTokens, createLiteralBlock, createLiteralToken, createOperatorBlock, createOperatorToken, createPropertyBlock, createPropertyToken, findFunctionRange, generateFunctionId, generateSmartBlockId, generateTokenId, getArgumentIndexAtPosition, getFunctionTokens, isValidDropPosition, parseSignature, recalculateDepths, serializeTokens };
1126
+ //# sourceMappingURL=masterteam-components-formula.mjs.map