@trustquery/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ // StyleManager - Handles all inline styling for textarea, wrapper, and overlay
2
+ // Makes TrustQuery completely self-contained without requiring external CSS
3
+
4
+ export default class StyleManager {
5
+ /**
6
+ * Create style manager
7
+ * @param {Object} options - Theme and style options
8
+ */
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ // Theme colors
12
+ backgroundColor: options.backgroundColor || '#fff',
13
+ textColor: options.textColor || '#333',
14
+ caretColor: options.caretColor || '#000',
15
+ borderColor: options.borderColor || '#ddd',
16
+ borderColorFocus: options.borderColorFocus || '#4a90e2',
17
+
18
+ // Match colors (can be overridden)
19
+ matchBackgroundColor: options.matchBackgroundColor || 'rgba(74, 144, 226, 0.15)',
20
+ matchTextColor: options.matchTextColor || '#2b6cb0',
21
+ matchHoverBackgroundColor: options.matchHoverBackgroundColor || 'rgba(74, 144, 226, 0.25)',
22
+
23
+ // Font settings (optional, will use textarea's if not specified)
24
+ fontFamily: options.fontFamily || null,
25
+ fontSize: options.fontSize || null,
26
+ lineHeight: options.lineHeight || null,
27
+
28
+ ...options
29
+ };
30
+
31
+ console.log('[StyleManager] Initialized with theme:', this.options);
32
+ }
33
+
34
+ /**
35
+ * Apply all styles to wrapper, textarea, and overlay
36
+ * @param {HTMLElement} wrapper - Wrapper element
37
+ * @param {HTMLElement} textarea - Textarea element
38
+ * @param {HTMLElement} overlay - Overlay element
39
+ */
40
+ applyAllStyles(wrapper, textarea, overlay) {
41
+ // Get computed styles from original textarea
42
+ const computedStyle = window.getComputedStyle(textarea);
43
+
44
+ // Apply styles to each element
45
+ this.applyWrapperStyles(wrapper, computedStyle);
46
+ this.applyTextareaStyles(textarea, computedStyle);
47
+ this.applyOverlayStyles(overlay, computedStyle);
48
+
49
+ // Apply focus handlers
50
+ this.setupFocusStyles(wrapper, textarea);
51
+
52
+ console.log('[StyleManager] All styles applied');
53
+ }
54
+
55
+ /**
56
+ * Apply wrapper styles (container for both textarea and overlay)
57
+ * @param {HTMLElement} wrapper - Wrapper element
58
+ * @param {CSSStyleDeclaration} computedStyle - Computed styles from textarea
59
+ */
60
+ applyWrapperStyles(wrapper, computedStyle) {
61
+ Object.assign(wrapper.style, {
62
+ position: 'relative',
63
+ display: 'block',
64
+ width: '100%',
65
+ background: this.options.backgroundColor,
66
+ border: `1px solid ${this.options.borderColor}`,
67
+ borderRadius: '4px',
68
+ boxSizing: 'border-box',
69
+ transition: 'border-color 0.15s ease, box-shadow 0.15s ease'
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Apply textarea styles (transparent text, visible caret)
75
+ * @param {HTMLElement} textarea - Textarea element
76
+ * @param {CSSStyleDeclaration} computedStyle - Original computed styles
77
+ */
78
+ applyTextareaStyles(textarea, computedStyle) {
79
+ // Use provided font settings or fall back to computed/defaults
80
+ const fontFamily = this.options.fontFamily || computedStyle.fontFamily || "'Courier New', monospace";
81
+ const fontSize = this.options.fontSize || computedStyle.fontSize || '14px';
82
+ const lineHeight = this.options.lineHeight || computedStyle.lineHeight || '1.5';
83
+ const padding = computedStyle.padding || '12px';
84
+
85
+ // Store existing transition if any (for FOUC prevention)
86
+ const existingTransition = textarea.style.transition;
87
+
88
+ Object.assign(textarea.style, {
89
+ fontFamily,
90
+ fontSize,
91
+ lineHeight,
92
+ padding,
93
+ border: 'none',
94
+ borderRadius: '0',
95
+ background: 'transparent',
96
+ color: this.options.textColor, // Set color for caret visibility
97
+ WebkitTextFillColor: 'transparent', // Make text transparent but keep caret
98
+ caretColor: this.options.caretColor,
99
+ resize: 'none',
100
+ width: '100%',
101
+ boxSizing: 'border-box',
102
+ position: 'relative',
103
+ zIndex: '0', // Below overlay so hover events reach overlay matches
104
+ whiteSpace: 'pre-wrap',
105
+ wordWrap: 'break-word',
106
+ overflowWrap: 'break-word',
107
+ outline: 'none',
108
+ margin: '0',
109
+ transition: existingTransition // Preserve opacity transition from HTML
110
+ });
111
+
112
+ // Add CSS to make placeholder visible (not affected by -webkit-text-fill-color)
113
+ this.ensurePlaceholderStyles();
114
+
115
+ // Store computed values for overlay to use
116
+ this._textareaStyles = {
117
+ fontFamily,
118
+ fontSize,
119
+ lineHeight,
120
+ padding
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Apply overlay styles (must match textarea exactly for alignment)
126
+ * @param {HTMLElement} overlay - Overlay element
127
+ * @param {CSSStyleDeclaration} computedStyle - Computed styles from textarea
128
+ */
129
+ applyOverlayStyles(overlay, computedStyle) {
130
+ // Use the stored textarea styles to ensure perfect alignment
131
+ const { fontFamily, fontSize, lineHeight, padding } = this._textareaStyles;
132
+
133
+ Object.assign(overlay.style, {
134
+ position: 'absolute',
135
+ top: '0',
136
+ left: '0',
137
+ right: '0',
138
+ bottom: '0',
139
+ fontFamily,
140
+ fontSize,
141
+ lineHeight,
142
+ padding,
143
+ color: this.options.textColor,
144
+ pointerEvents: 'none', // Let clicks pass through to textarea (except on match spans with pointer-events: auto)
145
+ overflow: 'hidden',
146
+ whiteSpace: 'pre-wrap',
147
+ wordWrap: 'break-word',
148
+ overflowWrap: 'break-word',
149
+ zIndex: '1', // Above textarea so match spans can receive hover/click events
150
+ boxSizing: 'border-box',
151
+ margin: '0'
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Setup focus styles (apply on focus, remove on blur)
157
+ * @param {HTMLElement} wrapper - Wrapper element
158
+ * @param {HTMLElement} textarea - Textarea element
159
+ */
160
+ setupFocusStyles(wrapper, textarea) {
161
+ textarea.addEventListener('focus', () => {
162
+ wrapper.style.borderColor = this.options.borderColorFocus;
163
+ wrapper.style.boxShadow = `0 0 0 3px ${this.options.borderColorFocus}1a`; // 1a = 10% opacity
164
+ });
165
+
166
+ textarea.addEventListener('blur', () => {
167
+ wrapper.style.borderColor = this.options.borderColor;
168
+ wrapper.style.boxShadow = 'none';
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Apply match (highlighted word) styles
174
+ * @param {HTMLElement} matchElement - Span element for matched word
175
+ * @param {string} matchType - Type of match (keyword, mention, command, etc.)
176
+ */
177
+ applyMatchStyles(matchElement, matchType = 'default') {
178
+ // Base match styles
179
+ Object.assign(matchElement.style, {
180
+ pointerEvents: 'auto', // Enable interactions
181
+ cursor: 'pointer',
182
+ padding: '2px 4px',
183
+ margin: '-2px -4px',
184
+ borderRadius: '3px',
185
+ transition: 'background-color 0.15s ease',
186
+ backgroundColor: this.options.matchBackgroundColor,
187
+ color: this.options.matchTextColor
188
+ });
189
+
190
+ // Hover styles (on mouseover)
191
+ matchElement.addEventListener('mouseenter', () => {
192
+ matchElement.style.backgroundColor = this.options.matchHoverBackgroundColor;
193
+ });
194
+
195
+ matchElement.addEventListener('mouseleave', () => {
196
+ matchElement.style.backgroundColor = this.options.matchBackgroundColor;
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Apply bubble (tooltip) styles
202
+ * @param {HTMLElement} bubble - Bubble element
203
+ */
204
+ applyBubbleStyles(bubble) {
205
+ Object.assign(bubble.style, {
206
+ position: 'absolute',
207
+ background: '#ffffff',
208
+ border: `1px solid ${this.options.borderColor}`,
209
+ borderRadius: '6px',
210
+ padding: '0',
211
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
212
+ zIndex: '10000',
213
+ maxWidth: '300px',
214
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
215
+ fontSize: '13px',
216
+ lineHeight: '1.4',
217
+ color: this.options.textColor,
218
+ pointerEvents: 'auto',
219
+ opacity: '1',
220
+ overflow: 'hidden',
221
+ animation: 'tq-bubble-appear 0.15s ease-out'
222
+ });
223
+
224
+ // Add animation keyframes if not already added
225
+ this.ensureAnimationStyles();
226
+
227
+ // Style the content container
228
+ const contentContainer = bubble.querySelector('.tq-bubble-content');
229
+ if (contentContainer) {
230
+ Object.assign(contentContainer.style, {
231
+ padding: '8px 12px',
232
+ fontSize: '12px',
233
+ lineHeight: '1.4'
234
+ });
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Apply bubble header styles
240
+ * @param {HTMLElement} header - Header container element
241
+ * @param {string} messageState - Message state (error, warning, info)
242
+ */
243
+ applyBubbleHeaderStyles(header, messageState) {
244
+ const colorMap = {
245
+ 'error': '#991b1b',
246
+ 'warning': '#92400e',
247
+ 'info': '#065f46'
248
+ };
249
+
250
+ const bgColorMap = {
251
+ 'error': '#fee2e2',
252
+ 'warning': '#fef3c7',
253
+ 'info': '#d1fae5'
254
+ };
255
+
256
+ const color = colorMap[messageState] || '#2b6cb0';
257
+ const bgColor = bgColorMap[messageState] || '#e0f2fe';
258
+
259
+ Object.assign(header.style, {
260
+ display: 'flex',
261
+ alignItems: 'center',
262
+ gap: '8px',
263
+ padding: '10px 12px',
264
+ backgroundColor: bgColor,
265
+ color: color,
266
+ fontWeight: '600',
267
+ fontSize: '11px',
268
+ borderBottom: `1px solid ${this.options.borderColor}`
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Apply dropdown (menu) styles
274
+ * @param {HTMLElement} dropdown - Dropdown element
275
+ */
276
+ applyDropdownStyles(dropdown) {
277
+ Object.assign(dropdown.style, {
278
+ position: 'absolute',
279
+ background: '#ffffff',
280
+ border: `1px solid ${this.options.borderColor}`,
281
+ borderRadius: '6px',
282
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
283
+ zIndex: '10000',
284
+ minWidth: '150px',
285
+ maxWidth: '300px',
286
+ overflow: 'hidden',
287
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
288
+ fontSize: '14px',
289
+ opacity: '1',
290
+ animation: 'tq-dropdown-appear 0.15s ease-out'
291
+ });
292
+
293
+ this.ensureAnimationStyles();
294
+ }
295
+
296
+ /**
297
+ * Apply dropdown header styles
298
+ * @param {HTMLElement} header - Header container element
299
+ * @param {string} messageState - Message state (error, warning, info)
300
+ */
301
+ applyDropdownHeaderStyles(header, messageState) {
302
+ const colorMap = {
303
+ 'error': '#991b1b',
304
+ 'warning': '#92400e',
305
+ 'info': '#065f46'
306
+ };
307
+
308
+ const bgColorMap = {
309
+ 'error': '#fee2e2',
310
+ 'warning': '#fef3c7',
311
+ 'info': '#d1fae5'
312
+ };
313
+
314
+ const color = colorMap[messageState] || '#2b6cb0';
315
+ const bgColor = bgColorMap[messageState] || '#e0f2fe';
316
+
317
+ Object.assign(header.style, {
318
+ display: 'flex',
319
+ alignItems: 'center',
320
+ gap: '8px',
321
+ padding: '10px 12px',
322
+ backgroundColor: bgColor,
323
+ color: color,
324
+ fontWeight: '600',
325
+ fontSize: '11px',
326
+ borderBottom: `1px solid ${this.options.borderColor}`
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Apply dropdown description styles
332
+ * @param {HTMLElement} description - Description element
333
+ * @param {string} messageState - Message state (error, warning, info)
334
+ */
335
+ applyDropdownDescriptionStyles(description, messageState) {
336
+ Object.assign(description.style, {
337
+ padding: '8px 12px',
338
+ fontSize: '12px',
339
+ lineHeight: '1.5',
340
+ color: '#4a5568',
341
+ backgroundColor: '#f7fafc',
342
+ borderBottom: `1px solid ${this.options.borderColor}`
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Apply dropdown item styles
348
+ * @param {HTMLElement} item - Dropdown item element
349
+ */
350
+ applyDropdownItemStyles(item) {
351
+ Object.assign(item.style, {
352
+ padding: '8px 12px',
353
+ cursor: 'pointer',
354
+ color: this.options.textColor,
355
+ transition: 'background-color 0.1s ease'
356
+ });
357
+
358
+ item.addEventListener('mouseenter', () => {
359
+ item.style.backgroundColor = '#f0f4f8';
360
+ });
361
+
362
+ item.addEventListener('mouseleave', () => {
363
+ item.style.backgroundColor = 'transparent';
364
+ });
365
+
366
+ item.addEventListener('mousedown', () => {
367
+ item.style.backgroundColor = '#e2e8f0';
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Apply user input styles
373
+ * @param {HTMLElement} container - User input container
374
+ * @param {HTMLElement} input - Input element
375
+ */
376
+ applyUserInputStyles(container, input) {
377
+ // Container is now a tq-dropdown-item, so don't add padding here
378
+ // Just set the border and background
379
+ Object.assign(container.style, {
380
+ backgroundColor: 'transparent',
381
+ borderTop: `1px solid ${this.options.borderColor}`,
382
+ // Override cursor from dropdown-item styles
383
+ cursor: 'text'
384
+ });
385
+
386
+ Object.assign(input.style, {
387
+ width: '100%',
388
+ padding: '8px 12px',
389
+ border: 'none',
390
+ borderBottom: '1px solid #ccc',
391
+ borderRadius: '0',
392
+ fontSize: '14px',
393
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
394
+ color: this.options.textColor,
395
+ backgroundColor: 'transparent',
396
+ boxSizing: 'border-box',
397
+ outline: 'none'
398
+ });
399
+
400
+ input.addEventListener('focus', () => {
401
+ input.style.borderBottom = '1px solid #ccc';
402
+ });
403
+
404
+ input.addEventListener('blur', () => {
405
+ input.style.borderBottom = '1px solid #ccc';
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Apply dropdown filter input styles
411
+ * @param {HTMLElement} filterInput - Filter input element
412
+ */
413
+ applyDropdownFilterStyles(filterInput) {
414
+ Object.assign(filterInput.style, {
415
+ width: '100%',
416
+ padding: '8px 12px',
417
+ border: 'none',
418
+ borderBottom: `1px solid ${this.options.borderColor}`,
419
+ outline: 'none',
420
+ fontSize: '14px',
421
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
422
+ color: this.options.textColor,
423
+ backgroundColor: '#f7fafc',
424
+ boxSizing: 'border-box'
425
+ });
426
+
427
+ // Focus styles
428
+ filterInput.addEventListener('focus', () => {
429
+ filterInput.style.backgroundColor = '#fff';
430
+ filterInput.style.borderBottomColor = this.options.borderColorFocus;
431
+ });
432
+
433
+ filterInput.addEventListener('blur', () => {
434
+ filterInput.style.backgroundColor = '#f7fafc';
435
+ filterInput.style.borderBottomColor = this.options.borderColor;
436
+ });
437
+ }
438
+
439
+ /**
440
+ * Ensure animation keyframes are added to document (only once)
441
+ */
442
+ ensureAnimationStyles() {
443
+ if (document.getElementById('tq-animations')) {
444
+ return; // Already added
445
+ }
446
+
447
+ const style = document.createElement('style');
448
+ style.id = 'tq-animations';
449
+ style.textContent = `
450
+ @keyframes tq-bubble-appear {
451
+ from {
452
+ opacity: 0;
453
+ transform: translateY(-4px);
454
+ }
455
+ to {
456
+ opacity: 1;
457
+ transform: translateY(0);
458
+ }
459
+ }
460
+
461
+ @keyframes tq-dropdown-appear {
462
+ from {
463
+ opacity: 0;
464
+ transform: scale(0.95);
465
+ }
466
+ to {
467
+ opacity: 1;
468
+ transform: scale(1);
469
+ }
470
+ }
471
+
472
+ .tq-dropdown-item-selected {
473
+ background-color: #e2e8f0 !important;
474
+ }
475
+ `;
476
+ document.head.appendChild(style);
477
+ }
478
+
479
+ /**
480
+ * Ensure placeholder styles are added to document (only once)
481
+ */
482
+ ensurePlaceholderStyles() {
483
+ if (document.getElementById('tq-placeholder-styles')) {
484
+ return; // Already added
485
+ }
486
+
487
+ const style = document.createElement('style');
488
+ style.id = 'tq-placeholder-styles';
489
+ style.textContent = `
490
+ .tq-textarea::placeholder {
491
+ color: #a0aec0 !important;
492
+ opacity: 1 !important;
493
+ -webkit-text-fill-color: #a0aec0 !important;
494
+ }
495
+
496
+ .tq-textarea::-webkit-input-placeholder {
497
+ color: #a0aec0 !important;
498
+ opacity: 1 !important;
499
+ -webkit-text-fill-color: #a0aec0 !important;
500
+ }
501
+
502
+ .tq-textarea::-moz-placeholder {
503
+ color: #a0aec0 !important;
504
+ opacity: 1 !important;
505
+ }
506
+
507
+ .tq-textarea:-ms-input-placeholder {
508
+ color: #a0aec0 !important;
509
+ opacity: 1 !important;
510
+ }
511
+ `;
512
+ document.head.appendChild(style);
513
+ }
514
+
515
+ /**
516
+ * Update theme colors dynamically
517
+ * @param {Object} newColors - New color options
518
+ */
519
+ updateTheme(newColors) {
520
+ Object.assign(this.options, newColors);
521
+ console.log('[StyleManager] Theme updated:', this.options);
522
+ }
523
+ }