@opendata-ai/openchart-vanilla 6.4.1 → 6.5.1
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.
- package/dist/index.d.ts +9 -2
- package/dist/index.js +327 -174
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -764
- package/package.json +3 -3
- package/src/__tests__/animation.test.ts +358 -0
- package/src/__tests__/edit-events.test.ts +35 -35
- package/src/__tests__/events.test.ts +7 -7
- package/src/__tests__/export.test.ts +1 -1
- package/src/__tests__/mount.test.ts +10 -10
- package/src/__tests__/selection-events.test.ts +14 -14
- package/src/__tests__/svg-renderer.test.ts +67 -67
- package/src/__tests__/table-keyboard.test.ts +18 -18
- package/src/__tests__/table-mount.test.ts +138 -17
- package/src/__tests__/tooltip.test.ts +12 -12
- package/src/animation.ts +75 -0
- package/src/graph/__tests__/graph-mount.test.ts +16 -16
- package/src/graph-mount.ts +18 -18
- package/src/mount.ts +71 -37
- package/src/renderers/table-cells.ts +11 -9
- package/src/svg-renderer.ts +161 -54
- package/src/table-keyboard.ts +5 -5
- package/src/table-mount.ts +34 -11
- package/src/table-renderer.ts +70 -39
- package/src/tooltip.ts +8 -8
package/src/table-mount.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from '@opendata-ai/openchart-core';
|
|
21
21
|
import { getBreakpoint } from '@opendata-ai/openchart-core';
|
|
22
22
|
import { compileTable } from '@opendata-ai/openchart-engine';
|
|
23
|
+
import { setupTableAnimationCleanup } from './animation';
|
|
23
24
|
import { observeResize } from './resize-observer';
|
|
24
25
|
import { attachKeyboardNav } from './table-keyboard';
|
|
25
26
|
import { renderTable } from './table-renderer';
|
|
@@ -117,6 +118,8 @@ export function createTable(
|
|
|
117
118
|
let wrapperElement: HTMLElement | null = null;
|
|
118
119
|
let disconnectResize: (() => void) | null = null;
|
|
119
120
|
let cleanupKeyboard: (() => void) | null = null;
|
|
121
|
+
let cleanupAnimations: (() => void) | null = null;
|
|
122
|
+
let isFirstRender = true;
|
|
120
123
|
let destroyed = false;
|
|
121
124
|
|
|
122
125
|
// Internal state (used in uncontrolled mode)
|
|
@@ -193,7 +196,7 @@ export function createTable(
|
|
|
193
196
|
*/
|
|
194
197
|
function announce(message: string): void {
|
|
195
198
|
if (!wrapperElement) return;
|
|
196
|
-
const liveRegion = wrapperElement.querySelector('.
|
|
199
|
+
const liveRegion = wrapperElement.querySelector('.oc-table-live-region');
|
|
197
200
|
if (liveRegion) {
|
|
198
201
|
liveRegion.textContent = message;
|
|
199
202
|
}
|
|
@@ -210,10 +213,10 @@ export function createTable(
|
|
|
210
213
|
const bp = getBreakpoint(width);
|
|
211
214
|
|
|
212
215
|
if (bp === 'compact' || bp === 'medium') {
|
|
213
|
-
wrapperElement.classList.add('
|
|
216
|
+
wrapperElement.classList.add('oc-table--compact');
|
|
214
217
|
} else if (!currentLayout?.compact) {
|
|
215
218
|
// Only remove compact if the spec didn't explicitly request it
|
|
216
|
-
wrapperElement.classList.remove('
|
|
219
|
+
wrapperElement.classList.remove('oc-table--compact');
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
222
|
|
|
@@ -221,6 +224,12 @@ export function createTable(
|
|
|
221
224
|
if (destroyed) return;
|
|
222
225
|
|
|
223
226
|
try {
|
|
227
|
+
// Cancel any in-flight animations before re-rendering
|
|
228
|
+
if (cleanupAnimations) {
|
|
229
|
+
cleanupAnimations();
|
|
230
|
+
cleanupAnimations = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
224
233
|
// Clean up previous keyboard nav
|
|
225
234
|
if (cleanupKeyboard) {
|
|
226
235
|
cleanupKeyboard();
|
|
@@ -234,14 +243,23 @@ export function createTable(
|
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
currentLayout = compile();
|
|
237
|
-
|
|
246
|
+
const shouldAnimate = isFirstRender && !!currentLayout.animation?.enabled;
|
|
247
|
+
wrapperElement = renderTable(currentLayout, container, { animate: shouldAnimate });
|
|
248
|
+
|
|
249
|
+
// Set up animation cleanup on first animated render
|
|
250
|
+
if (shouldAnimate && wrapperElement) {
|
|
251
|
+
cleanupAnimations = setupTableAnimationCleanup(wrapperElement);
|
|
252
|
+
}
|
|
253
|
+
if (isFirstRender) {
|
|
254
|
+
isFirstRender = false;
|
|
255
|
+
}
|
|
238
256
|
|
|
239
257
|
// Apply dark mode class
|
|
240
258
|
const isDark = resolveDarkMode(options?.darkMode);
|
|
241
259
|
if (isDark) {
|
|
242
|
-
container.classList.add('
|
|
260
|
+
container.classList.add('oc-dark');
|
|
243
261
|
} else {
|
|
244
|
-
container.classList.remove('
|
|
262
|
+
container.classList.remove('oc-dark');
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
// Apply responsive breakpoint
|
|
@@ -249,7 +267,7 @@ export function createTable(
|
|
|
249
267
|
|
|
250
268
|
// Add clickable class if onRowClick is provided
|
|
251
269
|
if (options?.onRowClick) {
|
|
252
|
-
wrapperElement.classList.add('
|
|
270
|
+
wrapperElement.classList.add('oc-table--clickable');
|
|
253
271
|
}
|
|
254
272
|
|
|
255
273
|
// Wire up event handlers
|
|
@@ -301,7 +319,7 @@ export function createTable(
|
|
|
301
319
|
|
|
302
320
|
// Search input
|
|
303
321
|
const searchInput = wrapperElement.querySelector(
|
|
304
|
-
'.
|
|
322
|
+
'.oc-table-search input',
|
|
305
323
|
) as HTMLInputElement | null;
|
|
306
324
|
if (searchInput) {
|
|
307
325
|
searchInput.addEventListener('input', handleSearchInput);
|
|
@@ -403,7 +421,7 @@ export function createTable(
|
|
|
403
421
|
|
|
404
422
|
// Capture current search input state before re-render
|
|
405
423
|
const searchInput = wrapperElement?.querySelector(
|
|
406
|
-
'.
|
|
424
|
+
'.oc-table-search input',
|
|
407
425
|
) as HTMLInputElement | null;
|
|
408
426
|
const hadFocus = searchInput && document.activeElement === searchInput;
|
|
409
427
|
const selectionStart = searchInput?.selectionStart ?? 0;
|
|
@@ -414,7 +432,7 @@ export function createTable(
|
|
|
414
432
|
// Restore search focus after re-render
|
|
415
433
|
if (hadFocus) {
|
|
416
434
|
const newInput = wrapperElement?.querySelector(
|
|
417
|
-
'.
|
|
435
|
+
'.oc-table-search input',
|
|
418
436
|
) as HTMLInputElement | null;
|
|
419
437
|
if (newInput) {
|
|
420
438
|
newInput.focus();
|
|
@@ -431,6 +449,7 @@ export function createTable(
|
|
|
431
449
|
|
|
432
450
|
function resize(): void {
|
|
433
451
|
if (destroyed) return;
|
|
452
|
+
if (cleanupAnimations) return; // Skip resize during entrance animation
|
|
434
453
|
render();
|
|
435
454
|
}
|
|
436
455
|
|
|
@@ -480,6 +499,10 @@ export function createTable(
|
|
|
480
499
|
if (destroyed) return;
|
|
481
500
|
destroyed = true;
|
|
482
501
|
|
|
502
|
+
if (cleanupAnimations) {
|
|
503
|
+
cleanupAnimations();
|
|
504
|
+
cleanupAnimations = null;
|
|
505
|
+
}
|
|
483
506
|
if (cleanupKeyboard) {
|
|
484
507
|
cleanupKeyboard();
|
|
485
508
|
cleanupKeyboard = null;
|
|
@@ -500,7 +523,7 @@ export function createTable(
|
|
|
500
523
|
wrapperElement.parentNode.removeChild(wrapperElement);
|
|
501
524
|
wrapperElement = null;
|
|
502
525
|
}
|
|
503
|
-
container.classList.remove('
|
|
526
|
+
container.classList.remove('oc-dark');
|
|
504
527
|
}
|
|
505
528
|
|
|
506
529
|
// Initial render
|
package/src/table-renderer.ts
CHANGED
|
@@ -8,8 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ResolvedColumn, TableLayout, TableRow } from '@opendata-ai/openchart-core';
|
|
10
10
|
import { BRAND_FONT_SIZE } from '@opendata-ai/openchart-core';
|
|
11
|
+
import { clampStaggerDelay } from '@opendata-ai/openchart-engine';
|
|
11
12
|
import { renderCell } from './renderers/table-cells';
|
|
12
13
|
|
|
14
|
+
/** Options for renderTable(). */
|
|
15
|
+
export interface TableRenderOptions {
|
|
16
|
+
/** Whether to apply entrance animation on this render. */
|
|
17
|
+
animate?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** CSS easing preset map for animation custom properties. */
|
|
21
|
+
const EASE_VAR_MAP: Record<string, string> = {
|
|
22
|
+
smooth: 'var(--oc-ease-smooth)',
|
|
23
|
+
snappy: 'var(--oc-ease-snappy)',
|
|
24
|
+
};
|
|
25
|
+
|
|
13
26
|
// ---------------------------------------------------------------------------
|
|
14
27
|
// Constants
|
|
15
28
|
// ---------------------------------------------------------------------------
|
|
@@ -31,17 +44,17 @@ function renderChromeBlock(
|
|
|
31
44
|
if (!chrome.title && !chrome.subtitle) return null;
|
|
32
45
|
|
|
33
46
|
const div = document.createElement('div');
|
|
34
|
-
div.className = '
|
|
47
|
+
div.className = 'oc-chrome';
|
|
35
48
|
|
|
36
49
|
if (chrome.title) {
|
|
37
50
|
const h = document.createElement('div');
|
|
38
|
-
h.className = '
|
|
51
|
+
h.className = 'oc-table-title';
|
|
39
52
|
h.textContent = chrome.title.text;
|
|
40
53
|
div.appendChild(h);
|
|
41
54
|
}
|
|
42
55
|
if (chrome.subtitle) {
|
|
43
56
|
const sub = document.createElement('div');
|
|
44
|
-
sub.className = '
|
|
57
|
+
sub.className = 'oc-table-subtitle';
|
|
45
58
|
sub.textContent = chrome.subtitle.text;
|
|
46
59
|
div.appendChild(sub);
|
|
47
60
|
}
|
|
@@ -53,17 +66,17 @@ function renderChromeBlock(
|
|
|
53
66
|
if (!chrome.source && !chrome.footer) return null;
|
|
54
67
|
|
|
55
68
|
const div = document.createElement('div');
|
|
56
|
-
div.className = '
|
|
69
|
+
div.className = 'oc-chrome oc-chrome-footer';
|
|
57
70
|
|
|
58
71
|
if (chrome.source) {
|
|
59
72
|
const src = document.createElement('div');
|
|
60
|
-
src.className = '
|
|
73
|
+
src.className = 'oc-table-source';
|
|
61
74
|
src.textContent = chrome.source.text;
|
|
62
75
|
div.appendChild(src);
|
|
63
76
|
}
|
|
64
77
|
if (chrome.footer) {
|
|
65
78
|
const foot = document.createElement('div');
|
|
66
|
-
foot.className = '
|
|
79
|
+
foot.className = 'oc-table-footer-text';
|
|
67
80
|
foot.textContent = chrome.footer.text;
|
|
68
81
|
div.appendChild(foot);
|
|
69
82
|
}
|
|
@@ -105,7 +118,7 @@ function renderThead(
|
|
|
105
118
|
// Sort button
|
|
106
119
|
if (col.sortable) {
|
|
107
120
|
const btn = document.createElement('button');
|
|
108
|
-
btn.className = '
|
|
121
|
+
btn.className = 'oc-table-sort-btn';
|
|
109
122
|
btn.setAttribute('aria-label', `Sort by ${col.label}`);
|
|
110
123
|
btn.setAttribute('data-sort-column', col.key);
|
|
111
124
|
btn.type = 'button';
|
|
@@ -131,6 +144,7 @@ function renderTbody(rows: TableRow[], columns: ResolvedColumn[]): HTMLTableSect
|
|
|
131
144
|
const tr = document.createElement('tr');
|
|
132
145
|
tr.setAttribute('role', 'row');
|
|
133
146
|
tr.setAttribute('data-row-id', row.id);
|
|
147
|
+
tr.style.setProperty('--oc-row-index', String(r));
|
|
134
148
|
|
|
135
149
|
for (let c = 0; c < columns.length; c++) {
|
|
136
150
|
const cell = row.cells[c];
|
|
@@ -156,7 +170,7 @@ function renderSearchBar(layout: TableLayout): HTMLDivElement | null {
|
|
|
156
170
|
if (!layout.search.enabled) return null;
|
|
157
171
|
|
|
158
172
|
const div = document.createElement('div');
|
|
159
|
-
div.className = '
|
|
173
|
+
div.className = 'oc-table-search';
|
|
160
174
|
|
|
161
175
|
const input = document.createElement('input');
|
|
162
176
|
input.type = 'search';
|
|
@@ -178,10 +192,10 @@ function renderPagination(layout: TableLayout): HTMLDivElement | null {
|
|
|
178
192
|
const { page, pageSize, totalRows, totalPages } = layout.pagination;
|
|
179
193
|
|
|
180
194
|
const div = document.createElement('div');
|
|
181
|
-
div.className = '
|
|
195
|
+
div.className = 'oc-table-pagination';
|
|
182
196
|
|
|
183
197
|
const info = document.createElement('span');
|
|
184
|
-
info.className = '
|
|
198
|
+
info.className = 'oc-table-pagination-info';
|
|
185
199
|
|
|
186
200
|
if (totalRows === 0) {
|
|
187
201
|
info.textContent = 'No results';
|
|
@@ -194,7 +208,7 @@ function renderPagination(layout: TableLayout): HTMLDivElement | null {
|
|
|
194
208
|
div.appendChild(info);
|
|
195
209
|
|
|
196
210
|
const btnGroup = document.createElement('span');
|
|
197
|
-
btnGroup.className = '
|
|
211
|
+
btnGroup.className = 'oc-table-pagination-btns';
|
|
198
212
|
|
|
199
213
|
const prevBtn = document.createElement('button');
|
|
200
214
|
prevBtn.setAttribute('aria-label', 'Previous page');
|
|
@@ -220,7 +234,7 @@ function renderPagination(layout: TableLayout): HTMLDivElement | null {
|
|
|
220
234
|
|
|
221
235
|
function renderEmptyState(message: string): HTMLDivElement {
|
|
222
236
|
const div = document.createElement('div');
|
|
223
|
-
div.className = '
|
|
237
|
+
div.className = 'oc-table-empty';
|
|
224
238
|
div.setAttribute('aria-live', 'polite');
|
|
225
239
|
div.textContent = message;
|
|
226
240
|
return div;
|
|
@@ -237,53 +251,57 @@ function renderEmptyState(message: string): HTMLDivElement {
|
|
|
237
251
|
* @param container - The container element to render into.
|
|
238
252
|
* @returns The wrapper element that was created.
|
|
239
253
|
*/
|
|
240
|
-
export function renderTable(
|
|
254
|
+
export function renderTable(
|
|
255
|
+
layout: TableLayout,
|
|
256
|
+
container: HTMLElement,
|
|
257
|
+
opts?: TableRenderOptions,
|
|
258
|
+
): HTMLElement {
|
|
241
259
|
const wrapper = document.createElement('div');
|
|
242
|
-
wrapper.className = '
|
|
260
|
+
wrapper.className = 'oc-table-wrapper';
|
|
243
261
|
|
|
244
262
|
// Apply theme colors as CSS custom properties so table CSS picks them up.
|
|
245
263
|
// Without this, dark-background themes show invisible text since the
|
|
246
|
-
// CSS defaults (--
|
|
264
|
+
// CSS defaults (--oc-text etc.) are light-mode values.
|
|
247
265
|
const { theme, chrome } = layout;
|
|
248
266
|
if (theme) {
|
|
249
267
|
const s = wrapper.style;
|
|
250
|
-
s.setProperty('--
|
|
251
|
-
s.setProperty('--
|
|
252
|
-
s.setProperty('--
|
|
253
|
-
s.setProperty('--
|
|
254
|
-
s.setProperty('--
|
|
255
|
-
s.setProperty('--
|
|
256
|
-
s.setProperty('--
|
|
268
|
+
s.setProperty('--oc-bg', theme.colors.background);
|
|
269
|
+
s.setProperty('--oc-text', theme.colors.text);
|
|
270
|
+
s.setProperty('--oc-text-secondary', theme.colors.axis ?? theme.colors.text);
|
|
271
|
+
s.setProperty('--oc-text-muted', theme.colors.axis ?? theme.colors.text);
|
|
272
|
+
s.setProperty('--oc-gridline', theme.colors.gridline);
|
|
273
|
+
s.setProperty('--oc-border', theme.colors.gridline);
|
|
274
|
+
s.setProperty('--oc-font-family', theme.fonts.family);
|
|
257
275
|
s.fontFamily = theme.fonts.family;
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
// Set computed chrome CSS custom properties so chrome elements pick up
|
|
261
|
-
// theme-resolved values via CSS fallbacks (e.g. --
|
|
279
|
+
// theme-resolved values via CSS fallbacks (e.g. --oc-title-computed-size).
|
|
262
280
|
{
|
|
263
281
|
const s = wrapper.style;
|
|
264
282
|
if (chrome.title) {
|
|
265
|
-
s.setProperty('--
|
|
266
|
-
s.setProperty('--
|
|
267
|
-
s.setProperty('--
|
|
283
|
+
s.setProperty('--oc-title-computed-size', `${chrome.title.style.fontSize}px`);
|
|
284
|
+
s.setProperty('--oc-title-computed-weight', String(chrome.title.style.fontWeight));
|
|
285
|
+
s.setProperty('--oc-title-computed-color', chrome.title.style.fill);
|
|
268
286
|
}
|
|
269
287
|
if (chrome.subtitle) {
|
|
270
|
-
s.setProperty('--
|
|
271
|
-
s.setProperty('--
|
|
272
|
-
s.setProperty('--
|
|
288
|
+
s.setProperty('--oc-subtitle-computed-size', `${chrome.subtitle.style.fontSize}px`);
|
|
289
|
+
s.setProperty('--oc-subtitle-computed-weight', String(chrome.subtitle.style.fontWeight));
|
|
290
|
+
s.setProperty('--oc-subtitle-computed-color', chrome.subtitle.style.fill);
|
|
273
291
|
}
|
|
274
292
|
if (chrome.source) {
|
|
275
|
-
s.setProperty('--
|
|
276
|
-
s.setProperty('--
|
|
293
|
+
s.setProperty('--oc-source-computed-size', `${chrome.source.style.fontSize}px`);
|
|
294
|
+
s.setProperty('--oc-source-computed-color', chrome.source.style.fill);
|
|
277
295
|
}
|
|
278
296
|
if (chrome.footer) {
|
|
279
|
-
s.setProperty('--
|
|
280
|
-
s.setProperty('--
|
|
297
|
+
s.setProperty('--oc-footer-computed-size', `${chrome.footer.style.fontSize}px`);
|
|
298
|
+
s.setProperty('--oc-footer-computed-color', chrome.footer.style.fill);
|
|
281
299
|
}
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
// Apply class modifiers
|
|
285
303
|
if (layout.compact) {
|
|
286
|
-
wrapper.classList.add('
|
|
304
|
+
wrapper.classList.add('oc-table--compact');
|
|
287
305
|
}
|
|
288
306
|
|
|
289
307
|
// Header chrome
|
|
@@ -305,7 +323,7 @@ export function renderTable(layout: TableLayout, container: HTMLElement): HTMLEl
|
|
|
305
323
|
} else {
|
|
306
324
|
// Scroll container
|
|
307
325
|
const scroll = document.createElement('div');
|
|
308
|
-
scroll.className = '
|
|
326
|
+
scroll.className = 'oc-table-scroll';
|
|
309
327
|
|
|
310
328
|
// Table
|
|
311
329
|
const table = document.createElement('table');
|
|
@@ -313,13 +331,13 @@ export function renderTable(layout: TableLayout, container: HTMLElement): HTMLEl
|
|
|
313
331
|
table.setAttribute('aria-label', layout.a11y.caption);
|
|
314
332
|
|
|
315
333
|
if (layout.stickyFirstColumn) {
|
|
316
|
-
table.classList.add('
|
|
334
|
+
table.classList.add('oc-table--sticky');
|
|
317
335
|
}
|
|
318
336
|
|
|
319
337
|
// Caption (screen reader only – inline styles ensure hiding even without
|
|
320
338
|
// the external stylesheet, e.g. CDN / esm.sh usage)
|
|
321
339
|
const caption = document.createElement('caption');
|
|
322
|
-
caption.className = '
|
|
340
|
+
caption.className = 'oc-sr-only';
|
|
323
341
|
caption.style.position = 'absolute';
|
|
324
342
|
caption.style.width = '1px';
|
|
325
343
|
caption.style.height = '1px';
|
|
@@ -356,7 +374,7 @@ export function renderTable(layout: TableLayout, container: HTMLElement): HTMLEl
|
|
|
356
374
|
|
|
357
375
|
// Live region for screen reader announcements (sort changes, search results)
|
|
358
376
|
const liveRegion = document.createElement('div');
|
|
359
|
-
liveRegion.className = '
|
|
377
|
+
liveRegion.className = 'oc-table-live-region oc-sr-only';
|
|
360
378
|
liveRegion.style.position = 'absolute';
|
|
361
379
|
liveRegion.style.width = '1px';
|
|
362
380
|
liveRegion.style.height = '1px';
|
|
@@ -374,7 +392,7 @@ export function renderTable(layout: TableLayout, container: HTMLElement): HTMLEl
|
|
|
374
392
|
// Brand watermark
|
|
375
393
|
const brandColor = theme ? theme.colors.axis : '#999999';
|
|
376
394
|
const brand = document.createElement('div');
|
|
377
|
-
brand.className = '
|
|
395
|
+
brand.className = 'oc-table-ref';
|
|
378
396
|
brand.style.cssText = 'text-align: right; padding: 4px 8px;';
|
|
379
397
|
const brandLink = document.createElement('a');
|
|
380
398
|
brandLink.href = BRAND_URL;
|
|
@@ -385,6 +403,19 @@ export function renderTable(layout: TableLayout, container: HTMLElement): HTMLEl
|
|
|
385
403
|
brand.appendChild(brandLink);
|
|
386
404
|
wrapper.appendChild(brand);
|
|
387
405
|
|
|
406
|
+
// Animation: stamp CSS custom properties and add oc-animate class BEFORE
|
|
407
|
+
// DOM insertion to avoid a flash of final state.
|
|
408
|
+
if (opts?.animate && layout.animation?.enabled) {
|
|
409
|
+
const anim = layout.animation;
|
|
410
|
+
const rowCount = layout.rows.length;
|
|
411
|
+
const stagger = clampStaggerDelay(anim.staggerDelay, rowCount);
|
|
412
|
+
const s = wrapper.style;
|
|
413
|
+
s.setProperty('--oc-animation-duration', `${anim.duration}ms`);
|
|
414
|
+
s.setProperty('--oc-animation-stagger', `${stagger}ms`);
|
|
415
|
+
s.setProperty('--oc-animation-ease', EASE_VAR_MAP[anim.ease] || EASE_VAR_MAP.smooth);
|
|
416
|
+
wrapper.classList.add('oc-animate');
|
|
417
|
+
}
|
|
418
|
+
|
|
388
419
|
container.appendChild(wrapper);
|
|
389
420
|
return wrapper;
|
|
390
421
|
}
|
package/src/tooltip.ts
CHANGED
|
@@ -32,7 +32,7 @@ const TOOLTIP_OFFSET = 12;
|
|
|
32
32
|
*/
|
|
33
33
|
export function createTooltipManager(container: HTMLElement): TooltipManager {
|
|
34
34
|
const tooltip = document.createElement('div');
|
|
35
|
-
tooltip.className = '
|
|
35
|
+
tooltip.className = 'oc-tooltip';
|
|
36
36
|
tooltip.setAttribute('role', 'tooltip');
|
|
37
37
|
|
|
38
38
|
container.style.position = container.style.position || 'relative';
|
|
@@ -63,21 +63,21 @@ export function createTooltipManager(container: HTMLElement): TooltipManager {
|
|
|
63
63
|
// Title row: optional color dot + title text
|
|
64
64
|
if (content.title) {
|
|
65
65
|
const titleColor = content.fields.find((f) => f.color)?.color;
|
|
66
|
-
html += '<div class="
|
|
66
|
+
html += '<div class="oc-tooltip-header">';
|
|
67
67
|
if (titleColor) {
|
|
68
|
-
html += `<span class="
|
|
68
|
+
html += `<span class="oc-tooltip-dot" style="background:${esc(titleColor)}"></span>`;
|
|
69
69
|
}
|
|
70
|
-
html += `<span class="
|
|
70
|
+
html += `<span class="oc-tooltip-title">${esc(content.title)}</span>`;
|
|
71
71
|
html += '</div>';
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// Field rows
|
|
75
75
|
if (content.fields.length > 0) {
|
|
76
|
-
html += '<div class="
|
|
76
|
+
html += '<div class="oc-tooltip-body">';
|
|
77
77
|
for (const field of content.fields) {
|
|
78
|
-
html += '<div class="
|
|
79
|
-
html += `<span class="
|
|
80
|
-
html += `<span class="
|
|
78
|
+
html += '<div class="oc-tooltip-row">';
|
|
79
|
+
html += `<span class="oc-tooltip-label">${esc(field.label)}</span>`;
|
|
80
|
+
html += `<span class="oc-tooltip-value">${esc(field.value)}</span>`;
|
|
81
81
|
html += '</div>';
|
|
82
82
|
}
|
|
83
83
|
html += '</div>';
|