@zentto/report-designer 1.6.4 → 1.6.6

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,656 @@
1
+ // @zentto/report-designer — Formula Editor with field autocomplete
2
+ // Visual editor for Crystal Reports-style expressions: {ds.field}, functions, operators
3
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
6
+ 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;
7
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
8
+ };
9
+ import { LitElement, html, css, nothing } from 'lit';
10
+ import { customElement, property, state } from 'lit/decorators.js';
11
+ const FUNCTIONS = [
12
+ // String
13
+ { name: 'UPPER', args: 'value', desc: 'Convert to uppercase', category: 'String' },
14
+ { name: 'LOWER', args: 'value', desc: 'Convert to lowercase', category: 'String' },
15
+ { name: 'LEN', args: 'value', desc: 'String length', category: 'String' },
16
+ { name: 'TRIM', args: 'value', desc: 'Remove whitespace', category: 'String' },
17
+ { name: 'SUBSTR', args: 'value, start, length', desc: 'Extract substring', category: 'String' },
18
+ { name: 'REPLACE', args: 'value, search, replace', desc: 'Replace text', category: 'String' },
19
+ { name: 'CONCAT', args: 'a, b, ...', desc: 'Concatenate strings', category: 'String' },
20
+ { name: 'LEFT', args: 'value, n', desc: 'Left N characters', category: 'String' },
21
+ { name: 'RIGHT', args: 'value, n', desc: 'Right N characters', category: 'String' },
22
+ // Math
23
+ { name: 'ABS', args: 'value', desc: 'Absolute value', category: 'Math' },
24
+ { name: 'ROUND', args: 'value, decimals', desc: 'Round to decimals', category: 'Math' },
25
+ { name: 'FLOOR', args: 'value', desc: 'Round down', category: 'Math' },
26
+ { name: 'CEIL', args: 'value', desc: 'Round up', category: 'Math' },
27
+ { name: 'SQRT', args: 'value', desc: 'Square root', category: 'Math' },
28
+ { name: 'POW', args: 'base, exp', desc: 'Power', category: 'Math' },
29
+ { name: 'MIN', args: 'a, b', desc: 'Minimum', category: 'Math' },
30
+ { name: 'MAX', args: 'a, b', desc: 'Maximum', category: 'Math' },
31
+ // Aggregate
32
+ { name: 'SUM', args: 'field', desc: 'Sum of values', category: 'Aggregate' },
33
+ { name: 'AVG', args: 'field', desc: 'Average', category: 'Aggregate' },
34
+ { name: 'COUNT', args: 'field', desc: 'Count records', category: 'Aggregate' },
35
+ { name: 'DISTINCT_COUNT', args: 'field', desc: 'Count distinct', category: 'Aggregate' },
36
+ // Date
37
+ { name: 'TODAY', args: '', desc: 'Current date', category: 'Date' },
38
+ { name: 'NOW', args: '', desc: 'Current date/time', category: 'Date' },
39
+ { name: 'YEAR', args: 'date', desc: 'Extract year', category: 'Date' },
40
+ { name: 'MONTH', args: 'date', desc: 'Extract month', category: 'Date' },
41
+ { name: 'DAY', args: 'date', desc: 'Extract day', category: 'Date' },
42
+ { name: 'FORMAT_DATE', args: 'date, format', desc: 'Format date', category: 'Date' },
43
+ { name: 'DATE_ADD', args: 'date, days', desc: 'Add days', category: 'Date' },
44
+ // Logic
45
+ { name: 'IF', args: 'condition, then, else', desc: 'Conditional', category: 'Logic' },
46
+ { name: 'ISNULL', args: 'value, default', desc: 'Null check', category: 'Logic' },
47
+ { name: 'ISBLANK', args: 'value', desc: 'Blank check', category: 'Logic' },
48
+ // Type
49
+ { name: 'CAST', args: 'value, type', desc: 'Type conversion', category: 'Type' },
50
+ { name: 'TYPEOF', args: 'value', desc: 'Get type name', category: 'Type' },
51
+ ];
52
+ const OPERATORS = [
53
+ { op: '+', desc: 'Add' }, { op: '-', desc: 'Subtract' },
54
+ { op: '*', desc: 'Multiply' }, { op: '/', desc: 'Divide' },
55
+ { op: '%', desc: 'Modulo' }, { op: '&', desc: 'Concat' },
56
+ { op: '==', desc: 'Equals' }, { op: '!=', desc: 'Not equals' },
57
+ { op: '<', desc: 'Less' }, { op: '>', desc: 'Greater' },
58
+ { op: '<=', desc: 'Less/eq' }, { op: '>=', desc: 'Greater/eq' },
59
+ { op: 'AND', desc: 'And' }, { op: 'OR', desc: 'Or' }, { op: 'NOT', desc: 'Not' },
60
+ ];
61
+ // ─── Component ────────────────────────────────────────────────────
62
+ let FormulaEditor = class FormulaEditor extends LitElement {
63
+ constructor() {
64
+ super(...arguments);
65
+ this.open = false;
66
+ this.dataSources = [];
67
+ this.existingFormulas = [];
68
+ this.initialName = '';
69
+ this.initialExpression = '';
70
+ /** Which data source this formula belongs to (optional) */
71
+ this.dataSourceId = '';
72
+ this._name = '';
73
+ this._expression = '';
74
+ this._cursorPos = 0;
75
+ this._showAutocomplete = false;
76
+ this._autocompleteItems = [];
77
+ this._autocompleteFilter = '';
78
+ this._selectedAutoIdx = 0;
79
+ this._activeTab = 'fields';
80
+ this._fnCategoryFilter = '';
81
+ this._preview = '';
82
+ this._error = '';
83
+ this._expandedDs = new Set();
84
+ }
85
+ static { this.styles = css `
86
+ :host {
87
+ font-family: 'Segoe UI', Roboto, Arial, sans-serif;
88
+ font-size: 12px; display: block;
89
+ }
90
+
91
+ .overlay {
92
+ position: fixed; inset: 0; background: rgba(0,0,0,0.5);
93
+ z-index: 10000; display: flex; align-items: center; justify-content: center;
94
+ backdrop-filter: blur(2px);
95
+ }
96
+
97
+ .dialog {
98
+ background: var(--zrd-panel-bg, #fff); border-radius: 10px;
99
+ box-shadow: 0 12px 40px rgba(0,0,0,0.25);
100
+ width: 700px; min-width: 400px; max-width: 95vw;
101
+ height: 75vh; min-height: 400px; max-height: 90vh;
102
+ display: flex; flex-direction: column;
103
+ overflow: hidden; resize: both;
104
+ }
105
+
106
+ .dialog-header {
107
+ padding: 16px 20px; border-bottom: 1px solid var(--zrd-border, #eee);
108
+ display: flex; justify-content: space-between; align-items: center;
109
+ }
110
+ .dialog-title { font-size: 15px; font-weight: 600; }
111
+ .close-btn {
112
+ background: none; border: none; font-size: 18px; cursor: pointer;
113
+ color: var(--zrd-text-muted, #999); width: 28px; height: 28px;
114
+ border-radius: 4px; display: flex; align-items: center; justify-content: center;
115
+ }
116
+ .close-btn:hover { background: var(--zrd-hover, #f5f5f5); }
117
+
118
+ .dialog-body { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }
119
+
120
+ /* ─── Name field ──────────────────────────── */
121
+ .name-row { display: flex; gap: 8px; align-items: center; }
122
+ .name-label { font-weight: 600; font-size: 11px; min-width: 60px; }
123
+ .name-input {
124
+ flex: 1; padding: 6px 10px; border: 1px solid var(--zrd-border, #ddd);
125
+ border-radius: 5px; font-size: 12px; box-sizing: border-box;
126
+ }
127
+ .name-input:focus { outline: none; border-color: #1976d2; }
128
+
129
+ /* ─── Expression Editor ───────────────────── */
130
+ .expr-container { position: relative; }
131
+ .expr-textarea {
132
+ width: 100%; min-height: 80px; padding: 10px 12px;
133
+ border: 2px solid var(--zrd-border, #ddd); border-radius: 6px;
134
+ font-family: 'Consolas', 'Monaco', monospace; font-size: 13px;
135
+ resize: vertical; box-sizing: border-box; line-height: 1.5;
136
+ background: #fafafa;
137
+ }
138
+ .expr-textarea:focus { outline: none; border-color: #1976d2; background: #fff; }
139
+
140
+ /* ─── Autocomplete Dropdown ───────────────── */
141
+ .autocomplete {
142
+ position: absolute; top: 100%; left: 0; right: 0;
143
+ background: var(--zrd-panel-bg, #fff); border: 1px solid var(--zrd-border, #ddd);
144
+ border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
145
+ max-height: 200px; overflow-y: auto; z-index: 100;
146
+ }
147
+ .ac-item {
148
+ padding: 5px 10px; cursor: pointer; display: flex; align-items: center; gap: 6px;
149
+ font-size: 11px;
150
+ }
151
+ .ac-item:hover, .ac-item.selected { background: #e3f2fd; }
152
+ .ac-label { flex: 1; font-weight: 500; }
153
+ .ac-type { font-size: 9px; color: var(--zrd-text-muted, #999); }
154
+ .ac-detail { font-size: 9px; color: #888; }
155
+
156
+ /* ─── Bottom Panels (tabs) ────────────────── */
157
+ .panel-tabs {
158
+ display: flex; border-bottom: 1px solid var(--zrd-border, #eee); gap: 0;
159
+ }
160
+ .panel-tab {
161
+ padding: 6px 14px; cursor: pointer; font-size: 11px; font-weight: 500;
162
+ border-bottom: 2px solid transparent; color: var(--zrd-text-muted, #999);
163
+ }
164
+ .panel-tab:hover { color: var(--zrd-text, #333); }
165
+ .panel-tab.active { color: #1976d2; border-bottom-color: #1976d2; }
166
+
167
+ .panel-content { max-height: 200px; overflow-y: auto; }
168
+
169
+ /* ─── Fields List ─────────────────────────── */
170
+ .ds-header {
171
+ padding: 4px 8px; font-weight: 600; font-size: 11px; cursor: pointer;
172
+ display: flex; align-items: center; gap: 4px;
173
+ background: var(--zrd-hover, #f9f9f9);
174
+ }
175
+ .ds-header:hover { background: #eef3f8; }
176
+ .field-item {
177
+ padding: 3px 8px 3px 24px; cursor: pointer; display: flex;
178
+ align-items: center; gap: 4px; font-size: 11px;
179
+ }
180
+ .field-item:hover { background: #e3f2fd; }
181
+ .field-item-icon { color: #1976d2; font-size: 10px; }
182
+ .field-item-name { flex: 1; }
183
+ .field-item-type { font-size: 9px; color: #999; }
184
+
185
+ /* ─── Functions List ──────────────────────── */
186
+ .fn-categories { display: flex; gap: 4px; padding: 6px 8px; flex-wrap: wrap; }
187
+ .fn-cat-btn {
188
+ padding: 2px 8px; border: 1px solid var(--zrd-border, #ddd); border-radius: 3px;
189
+ background: none; cursor: pointer; font-size: 10px;
190
+ }
191
+ .fn-cat-btn:hover { background: #f0f7ff; }
192
+ .fn-cat-btn.active { background: #1976d2; color: #fff; border-color: #1976d2; }
193
+
194
+ .fn-item {
195
+ padding: 4px 8px; cursor: pointer; display: flex; align-items: center; gap: 6px;
196
+ font-size: 11px;
197
+ }
198
+ .fn-item:hover { background: #e3f2fd; }
199
+ .fn-name { font-weight: 600; color: #e65100; font-family: monospace; }
200
+ .fn-args { color: #888; font-family: monospace; font-size: 10px; }
201
+ .fn-desc { flex: 1; text-align: right; font-size: 10px; color: #aaa; }
202
+
203
+ /* ─── Operators ───────────────────────────── */
204
+ .op-grid { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px; }
205
+ .op-btn {
206
+ padding: 4px 10px; border: 1px solid var(--zrd-border, #ddd); border-radius: 4px;
207
+ background: var(--zrd-panel-bg, #fff); cursor: pointer;
208
+ font-family: monospace; font-size: 12px; font-weight: 600;
209
+ }
210
+ .op-btn:hover { background: #e3f2fd; border-color: #1976d2; }
211
+
212
+ /* ─── Preview / Error ─────────────────────── */
213
+ .preview-bar {
214
+ padding: 6px 10px; font-size: 11px; border-radius: 4px;
215
+ }
216
+ .preview-bar.ok { background: #e8f5e9; color: #2e7d32; }
217
+ .preview-bar.err { background: #ffebee; color: #c62828; }
218
+
219
+ /* ─── Footer ──────────────────────────────── */
220
+ .dialog-footer {
221
+ padding: 12px 20px; border-top: 1px solid var(--zrd-border, #eee);
222
+ display: flex; justify-content: flex-end; gap: 8px;
223
+ }
224
+ .btn {
225
+ padding: 7px 18px; border-radius: 5px; border: 1px solid var(--zrd-border, #ddd);
226
+ cursor: pointer; font-size: 12px; font-weight: 500;
227
+ background: var(--zrd-panel-bg, #fff);
228
+ }
229
+ .btn:hover { background: #f5f5f5; }
230
+ .btn-primary { background: #1976d2; color: #fff; border-color: #1976d2; }
231
+ .btn-primary:hover { background: #1565c0; }
232
+ .btn-primary:disabled { opacity: 0.5; }
233
+ `; }
234
+ // ─── Lifecycle ────────────────────────────────────────────────
235
+ updated(changed) {
236
+ if (changed.has('open') && this.open) {
237
+ this._name = this.initialName || `formula_${Date.now()}`;
238
+ this._expression = this.initialExpression || '=';
239
+ this._error = '';
240
+ this._preview = '';
241
+ }
242
+ }
243
+ // ─── Autocomplete ─────────────────────────────────────────────
244
+ _onExprInput(e) {
245
+ const ta = e.target;
246
+ this._expression = ta.value;
247
+ this._cursorPos = ta.selectionStart || 0;
248
+ this._checkAutocomplete(ta);
249
+ }
250
+ _onExprKeyDown(e) {
251
+ if (!this._showAutocomplete)
252
+ return;
253
+ const items = this._getFilteredAutocomplete();
254
+ if (e.key === 'ArrowDown') {
255
+ e.preventDefault();
256
+ this._selectedAutoIdx = Math.min(this._selectedAutoIdx + 1, items.length - 1);
257
+ }
258
+ else if (e.key === 'ArrowUp') {
259
+ e.preventDefault();
260
+ this._selectedAutoIdx = Math.max(this._selectedAutoIdx - 1, 0);
261
+ }
262
+ else if (e.key === 'Enter' || e.key === 'Tab') {
263
+ if (items[this._selectedAutoIdx]) {
264
+ e.preventDefault();
265
+ this._insertAutocomplete(items[this._selectedAutoIdx].insert);
266
+ }
267
+ }
268
+ else if (e.key === 'Escape') {
269
+ this._showAutocomplete = false;
270
+ }
271
+ }
272
+ _checkAutocomplete(ta) {
273
+ const pos = ta.selectionStart || 0;
274
+ const textBefore = this._expression.substring(0, pos);
275
+ // Check if we're after a { (field reference)
276
+ const lastBrace = textBefore.lastIndexOf('{');
277
+ const lastClose = textBefore.lastIndexOf('}');
278
+ if (lastBrace > lastClose) {
279
+ this._autocompleteFilter = textBefore.substring(lastBrace + 1).toLowerCase();
280
+ this._buildFieldAutocomplete();
281
+ this._showAutocomplete = true;
282
+ this._selectedAutoIdx = 0;
283
+ return;
284
+ }
285
+ // Check if we're typing a function name (after = or operator)
286
+ const wordMatch = textBefore.match(/([A-Z_]+)$/i);
287
+ if (wordMatch && wordMatch[1].length >= 2) {
288
+ this._autocompleteFilter = wordMatch[1].toLowerCase();
289
+ this._buildFunctionAutocomplete();
290
+ this._showAutocomplete = true;
291
+ this._selectedAutoIdx = 0;
292
+ return;
293
+ }
294
+ this._showAutocomplete = false;
295
+ }
296
+ _buildFieldAutocomplete() {
297
+ const items = [];
298
+ // Data source fields
299
+ for (const ds of this.dataSources) {
300
+ for (const f of (ds.fields || [])) {
301
+ items.push({
302
+ label: `${ds.name}.${f.label || f.name}`,
303
+ insert: `${ds.id}.${f.name}}`,
304
+ type: f.type,
305
+ detail: ds.name,
306
+ });
307
+ // Also without ds prefix for the active dataSource
308
+ if (ds.id === this.dataSourceId) {
309
+ items.push({
310
+ label: f.label || f.name,
311
+ insert: `${f.name}}`,
312
+ type: f.type,
313
+ });
314
+ }
315
+ }
316
+ }
317
+ // Existing formulas
318
+ for (const formula of this.existingFormulas) {
319
+ items.push({
320
+ label: `@${formula.name}`,
321
+ insert: `@${formula.name}}`,
322
+ type: 'formula',
323
+ detail: formula.expression,
324
+ });
325
+ }
326
+ this._autocompleteItems = items;
327
+ }
328
+ _buildFunctionAutocomplete() {
329
+ this._autocompleteItems = FUNCTIONS.map(fn => ({
330
+ label: fn.name,
331
+ insert: `${fn.name}(`,
332
+ type: 'function',
333
+ detail: `${fn.args} — ${fn.desc}`,
334
+ }));
335
+ }
336
+ _getFilteredAutocomplete() {
337
+ if (!this._autocompleteFilter)
338
+ return this._autocompleteItems.slice(0, 15);
339
+ return this._autocompleteItems
340
+ .filter(i => i.label.toLowerCase().includes(this._autocompleteFilter))
341
+ .slice(0, 15);
342
+ }
343
+ _insertAutocomplete(text) {
344
+ const ta = this.renderRoot.querySelector('.expr-textarea');
345
+ if (!ta)
346
+ return;
347
+ const pos = ta.selectionStart || 0;
348
+ const before = this._expression.substring(0, pos);
349
+ // Find start of current token
350
+ const lastBrace = before.lastIndexOf('{');
351
+ const lastClose = before.lastIndexOf('}');
352
+ let replaceFrom = pos;
353
+ if (lastBrace > lastClose) {
354
+ replaceFrom = lastBrace + 1;
355
+ }
356
+ else {
357
+ const wordMatch = before.match(/([A-Z_]+)$/i);
358
+ if (wordMatch)
359
+ replaceFrom = pos - wordMatch[1].length;
360
+ }
361
+ this._expression = this._expression.substring(0, replaceFrom) + text + this._expression.substring(pos);
362
+ this._showAutocomplete = false;
363
+ requestAnimationFrame(() => {
364
+ ta.focus();
365
+ const newPos = replaceFrom + text.length;
366
+ ta.setSelectionRange(newPos, newPos);
367
+ });
368
+ }
369
+ // ─── Insert from panels ───────────────────────────────────────
370
+ _insertFieldRef(dsId, fieldName) {
371
+ this._insertAtCursor(`{${dsId}.${fieldName}}`);
372
+ }
373
+ _insertFunction(fnName) {
374
+ this._insertAtCursor(`${fnName}(`);
375
+ }
376
+ _insertOperator(op) {
377
+ this._insertAtCursor(` ${op} `);
378
+ }
379
+ _insertFormulaRef(name) {
380
+ this._insertAtCursor(`{@${name}}`);
381
+ }
382
+ _insertAtCursor(text) {
383
+ const ta = this.renderRoot.querySelector('.expr-textarea');
384
+ if (!ta) {
385
+ this._expression += text;
386
+ return;
387
+ }
388
+ const pos = ta.selectionStart || this._expression.length;
389
+ this._expression = this._expression.substring(0, pos) + text + this._expression.substring(pos);
390
+ requestAnimationFrame(() => {
391
+ ta.focus();
392
+ const newPos = pos + text.length;
393
+ ta.setSelectionRange(newPos, newPos);
394
+ });
395
+ }
396
+ // ─── Validate ─────────────────────────────────────────────────
397
+ _validate() {
398
+ if (!this._expression || this._expression === '=') {
399
+ this._error = 'Expression is empty';
400
+ this._preview = '';
401
+ return;
402
+ }
403
+ // Basic validation: check balanced braces and parens
404
+ let braces = 0, parens = 0;
405
+ for (const ch of this._expression) {
406
+ if (ch === '{')
407
+ braces++;
408
+ if (ch === '}')
409
+ braces--;
410
+ if (ch === '(')
411
+ parens++;
412
+ if (ch === ')')
413
+ parens--;
414
+ if (braces < 0 || parens < 0)
415
+ break;
416
+ }
417
+ if (braces !== 0) {
418
+ this._error = 'Unbalanced { } braces';
419
+ this._preview = '';
420
+ return;
421
+ }
422
+ if (parens !== 0) {
423
+ this._error = 'Unbalanced ( ) parentheses';
424
+ this._preview = '';
425
+ return;
426
+ }
427
+ this._error = '';
428
+ this._preview = `Formula valid: ${this._expression}`;
429
+ }
430
+ // ─── Events ───────────────────────────────────────────────────
431
+ _onSave() {
432
+ this._validate();
433
+ if (this._error)
434
+ return;
435
+ this.dispatchEvent(new CustomEvent('formula-save', {
436
+ detail: { name: this._name, expression: this._expression, dataSourceId: this.dataSourceId },
437
+ bubbles: true, composed: true,
438
+ }));
439
+ }
440
+ _onCancel() {
441
+ this.dispatchEvent(new CustomEvent('formula-cancel', { bubbles: true, composed: true }));
442
+ }
443
+ _toggleDs(dsId) {
444
+ const s = new Set(this._expandedDs);
445
+ if (s.has(dsId))
446
+ s.delete(dsId);
447
+ else
448
+ s.add(dsId);
449
+ this._expandedDs = s;
450
+ }
451
+ // ─── Render ───────────────────────────────────────────────────
452
+ render() {
453
+ if (!this.open)
454
+ return nothing;
455
+ const autoItems = this._getFilteredAutocomplete();
456
+ return html `
457
+ <div class="overlay" @click=${this._onCancel}>
458
+ <div class="dialog" @click=${(e) => e.stopPropagation()}>
459
+ <div class="dialog-header">
460
+ <span class="dialog-title">\u{1D465} Formula Editor</span>
461
+ <button class="close-btn" @click=${this._onCancel}>\u2715</button>
462
+ </div>
463
+
464
+ <div class="dialog-body">
465
+ <!-- Name -->
466
+ <div class="name-row">
467
+ <span class="name-label">Name:</span>
468
+ <input class="name-input" .value=${this._name}
469
+ @input=${(e) => { this._name = e.target.value; }} />
470
+ </div>
471
+
472
+ <!-- Expression -->
473
+ <div class="expr-container">
474
+ <textarea class="expr-textarea"
475
+ .value=${this._expression}
476
+ @input=${(e) => this._onExprInput(e)}
477
+ @keydown=${(e) => this._onExprKeyDown(e)}
478
+ @blur=${() => { setTimeout(() => { this._showAutocomplete = false; }, 200); }}
479
+ placeholder="={ds.field} * {ds.field2}"
480
+ spellcheck="false"
481
+ ></textarea>
482
+ ${this._showAutocomplete && autoItems.length > 0 ? html `
483
+ <div class="autocomplete">
484
+ ${autoItems.map((item, i) => html `
485
+ <div class="ac-item ${i === this._selectedAutoIdx ? 'selected' : ''}"
486
+ @mousedown=${(e) => { e.preventDefault(); this._insertAutocomplete(item.insert); }}>
487
+ <span class="ac-label">${item.label}</span>
488
+ <span class="ac-type">${item.type}</span>
489
+ ${item.detail ? html `<span class="ac-detail">${item.detail}</span>` : nothing}
490
+ </div>
491
+ `)}
492
+ </div>
493
+ ` : nothing}
494
+ </div>
495
+
496
+ <!-- Preview / Error -->
497
+ ${this._error ? html `<div class="preview-bar err">\u26A0 ${this._error}</div>` : nothing}
498
+ ${this._preview && !this._error ? html `<div class="preview-bar ok">\u2713 ${this._preview}</div>` : nothing}
499
+
500
+ <!-- Bottom Panel: Fields / Functions / Operators -->
501
+ <div class="panel-tabs">
502
+ <div class="panel-tab ${this._activeTab === 'fields' ? 'active' : ''}"
503
+ @click=${() => { this._activeTab = 'fields'; }}>Fields</div>
504
+ <div class="panel-tab ${this._activeTab === 'functions' ? 'active' : ''}"
505
+ @click=${() => { this._activeTab = 'functions'; }}>Functions</div>
506
+ <div class="panel-tab ${this._activeTab === 'operators' ? 'active' : ''}"
507
+ @click=${() => { this._activeTab = 'operators'; }}>Operators</div>
508
+ </div>
509
+ <div class="panel-content">
510
+ ${this._activeTab === 'fields' ? this._renderFieldsPanel() : nothing}
511
+ ${this._activeTab === 'functions' ? this._renderFunctionsPanel() : nothing}
512
+ ${this._activeTab === 'operators' ? this._renderOperatorsPanel() : nothing}
513
+ </div>
514
+ </div>
515
+
516
+ <div class="dialog-footer">
517
+ <button class="btn" @click=${this._onCancel}>Cancel</button>
518
+ <button class="btn" @click=${this._validate}>Validate</button>
519
+ <button class="btn btn-primary"
520
+ ?disabled=${!this._name || !this._expression || this._expression === '='}
521
+ @click=${this._onSave}>
522
+ Save Formula
523
+ </button>
524
+ </div>
525
+ </div>
526
+ </div>
527
+ `;
528
+ }
529
+ _renderFieldsPanel() {
530
+ return html `
531
+ <!-- Existing formulas -->
532
+ ${this.existingFormulas.length > 0 ? html `
533
+ <div class="ds-header" style="color:#e65100">
534
+ \u{1D465} Formulas (${this.existingFormulas.length})
535
+ </div>
536
+ ${this.existingFormulas.map(f => html `
537
+ <div class="field-item" @click=${() => this._insertFormulaRef(f.name)}>
538
+ <span class="field-item-icon" style="color:#e65100">@</span>
539
+ <span class="field-item-name">${f.name}</span>
540
+ <span class="field-item-type">${f.expression}</span>
541
+ </div>
542
+ `)}
543
+ ` : nothing}
544
+
545
+ <!-- Data source fields -->
546
+ ${this.dataSources.map(ds => {
547
+ const isOpen = this._expandedDs.has(ds.id);
548
+ return html `
549
+ <div class="ds-header" @click=${() => this._toggleDs(ds.id)}>
550
+ <span style="font-size:8px;transition:transform 0.15s;${isOpen ? 'transform:rotate(90deg)' : ''}">\u25B6</span>
551
+ \u{1F4CB} ${ds.name} (${(ds.fields || []).length})
552
+ </div>
553
+ ${isOpen ? (ds.fields || []).map(f => html `
554
+ <div class="field-item" @click=${() => this._insertFieldRef(ds.id, f.name)}>
555
+ <span class="field-item-icon">\u{25C6}</span>
556
+ <span class="field-item-name">${f.label || f.name}</span>
557
+ <span class="field-item-type">${f.type}</span>
558
+ </div>
559
+ `) : nothing}
560
+ `;
561
+ })}
562
+ `;
563
+ }
564
+ _renderFunctionsPanel() {
565
+ const categories = [...new Set(FUNCTIONS.map(f => f.category))];
566
+ const filtered = this._fnCategoryFilter
567
+ ? FUNCTIONS.filter(f => f.category === this._fnCategoryFilter)
568
+ : FUNCTIONS;
569
+ return html `
570
+ <div class="fn-categories">
571
+ <button class="fn-cat-btn ${!this._fnCategoryFilter ? 'active' : ''}"
572
+ @click=${() => { this._fnCategoryFilter = ''; }}>All</button>
573
+ ${categories.map(cat => html `
574
+ <button class="fn-cat-btn ${this._fnCategoryFilter === cat ? 'active' : ''}"
575
+ @click=${() => { this._fnCategoryFilter = cat; }}>${cat}</button>
576
+ `)}
577
+ </div>
578
+ ${filtered.map(fn => html `
579
+ <div class="fn-item" @click=${() => this._insertFunction(fn.name)}>
580
+ <span class="fn-name">${fn.name}</span>
581
+ <span class="fn-args">(${fn.args})</span>
582
+ <span class="fn-desc">${fn.desc}</span>
583
+ </div>
584
+ `)}
585
+ `;
586
+ }
587
+ _renderOperatorsPanel() {
588
+ return html `
589
+ <div class="op-grid">
590
+ ${OPERATORS.map(op => html `
591
+ <button class="op-btn" title=${op.desc}
592
+ @click=${() => this._insertOperator(op.op)}>${op.op}</button>
593
+ `)}
594
+ </div>
595
+ `;
596
+ }
597
+ };
598
+ __decorate([
599
+ property({ type: Boolean })
600
+ ], FormulaEditor.prototype, "open", void 0);
601
+ __decorate([
602
+ property({ type: Array })
603
+ ], FormulaEditor.prototype, "dataSources", void 0);
604
+ __decorate([
605
+ property({ type: Array })
606
+ ], FormulaEditor.prototype, "existingFormulas", void 0);
607
+ __decorate([
608
+ property({ type: String })
609
+ ], FormulaEditor.prototype, "initialName", void 0);
610
+ __decorate([
611
+ property({ type: String })
612
+ ], FormulaEditor.prototype, "initialExpression", void 0);
613
+ __decorate([
614
+ property({ type: String })
615
+ ], FormulaEditor.prototype, "dataSourceId", void 0);
616
+ __decorate([
617
+ state()
618
+ ], FormulaEditor.prototype, "_name", void 0);
619
+ __decorate([
620
+ state()
621
+ ], FormulaEditor.prototype, "_expression", void 0);
622
+ __decorate([
623
+ state()
624
+ ], FormulaEditor.prototype, "_cursorPos", void 0);
625
+ __decorate([
626
+ state()
627
+ ], FormulaEditor.prototype, "_showAutocomplete", void 0);
628
+ __decorate([
629
+ state()
630
+ ], FormulaEditor.prototype, "_autocompleteItems", void 0);
631
+ __decorate([
632
+ state()
633
+ ], FormulaEditor.prototype, "_autocompleteFilter", void 0);
634
+ __decorate([
635
+ state()
636
+ ], FormulaEditor.prototype, "_selectedAutoIdx", void 0);
637
+ __decorate([
638
+ state()
639
+ ], FormulaEditor.prototype, "_activeTab", void 0);
640
+ __decorate([
641
+ state()
642
+ ], FormulaEditor.prototype, "_fnCategoryFilter", void 0);
643
+ __decorate([
644
+ state()
645
+ ], FormulaEditor.prototype, "_preview", void 0);
646
+ __decorate([
647
+ state()
648
+ ], FormulaEditor.prototype, "_error", void 0);
649
+ __decorate([
650
+ state()
651
+ ], FormulaEditor.prototype, "_expandedDs", void 0);
652
+ FormulaEditor = __decorate([
653
+ customElement('zrd-formula-editor')
654
+ ], FormulaEditor);
655
+ export { FormulaEditor };
656
+ //# sourceMappingURL=formula-editor.js.map