@kgdiff/viewer 0.2.0 → 0.4.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.
- package/README.md +48 -6
- package/dist/controller.d.ts +63 -14
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +308 -54
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +17 -4
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +199 -26
- package/dist/element.js.map +1 -1
- package/dist/framing.d.ts +20 -1
- package/dist/framing.d.ts.map +1 -1
- package/dist/framing.js +49 -0
- package/dist/framing.js.map +1 -1
- package/dist/hash-sync.d.ts +4 -2
- package/dist/hash-sync.d.ts.map +1 -1
- package/dist/hash-sync.js +21 -5
- package/dist/hash-sync.js.map +1 -1
- package/dist/i18n.d.ts +7 -7
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +14 -14
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +66 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts +6 -4
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +6 -5
- package/dist/keyboard.js.map +1 -1
- package/dist/prefs.d.ts +13 -1
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +30 -3
- package/dist/prefs.js.map +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +84 -61
- package/dist/styles.js.map +1 -1
- package/dist/ui/context.d.ts +2 -0
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/dock.d.ts +17 -0
- package/dist/ui/dock.d.ts.map +1 -0
- package/dist/ui/dock.js +151 -0
- package/dist/ui/dock.js.map +1 -0
- package/dist/ui/header.d.ts +7 -2
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +24 -133
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/popovers.d.ts.map +1 -1
- package/dist/ui/popovers.js +3 -2
- package/dist/ui/popovers.js.map +1 -1
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +2 -1
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/stage.d.ts +8 -0
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +66 -93
- package/dist/ui/stage.js.map +1 -1
- package/dist/ui/swatch.d.ts +7 -0
- package/dist/ui/swatch.d.ts.map +1 -0
- package/dist/ui/swatch.js +26 -0
- package/dist/ui/swatch.js.map +1 -0
- package/package.json +6 -6
- package/src/controller.ts +332 -56
- package/src/element.ts +220 -29
- package/src/framing.ts +65 -1
- package/src/hash-sync.ts +20 -6
- package/src/i18n.ts +15 -14
- package/src/index.ts +65 -7
- package/src/keyboard.ts +12 -6
- package/src/prefs.ts +45 -5
- package/src/styles.ts +84 -61
- package/src/ui/context.ts +2 -0
- package/src/ui/dock.ts +198 -0
- package/src/ui/header.ts +38 -168
- package/src/ui/popovers.ts +3 -2
- package/src/ui/sidebar.ts +2 -1
- package/src/ui/stage.ts +88 -112
- package/src/ui/swatch.ts +31 -0
package/src/ui/stage.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The canvas area and its floating chrome: replay caption and transport
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The canvas area and its floating chrome: the view dock (with replay's caption and transport
|
|
3
|
+
* stacked above it), swipe divider, legend (a card, or one line of dots for chrome-less embeds),
|
|
4
|
+
* zoom controls + minimap (shown only while part of the content is out of view), hover tooltip
|
|
5
|
+
* and the loading / identical / error / empty states. Overlays never cover the current hunk:
|
|
6
|
+
* fits and flights frame it in the free part of the canvas.
|
|
6
7
|
*/
|
|
7
8
|
import { KgdiffError, type Rect } from '@kgdiff/core';
|
|
8
9
|
import type { ChangeFamily, Family } from '@kgdiff/scene';
|
|
@@ -21,35 +22,13 @@ import type { ChromeLayout } from '../framing.js';
|
|
|
21
22
|
import type { Translator } from '../i18n.js';
|
|
22
23
|
import { FAMILIES } from '../styles.js';
|
|
23
24
|
import { sceneHunkGlyph, type UiContext } from './context.js';
|
|
25
|
+
import { Dock } from './dock.js';
|
|
26
|
+
import { swatch } from './swatch.js';
|
|
24
27
|
|
|
25
28
|
const TICK_W = 1000;
|
|
26
29
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
const v = (name: string) => `var(--kg-${name})`;
|
|
30
|
-
const c = family === 'neutral' ? v('neutral') : v(family);
|
|
31
|
-
let body: string;
|
|
32
|
-
switch (family) {
|
|
33
|
-
case 'added':
|
|
34
|
-
body = `<circle cx="11" cy="11" r="8.5" fill="none" stroke="${c}" stroke-opacity=".35" stroke-width="2"/><circle cx="11" cy="11" r="5" fill="${c}"/>`;
|
|
35
|
-
break;
|
|
36
|
-
case 'removed':
|
|
37
|
-
body = `<circle cx="11" cy="11" r="5.25" fill="none" stroke="${c}" stroke-width="1.5" stroke-dasharray="3 2.5"/>`;
|
|
38
|
-
break;
|
|
39
|
-
case 'identity':
|
|
40
|
-
body = `<circle cx="11" cy="11" r="9.5" fill="none" stroke="${c}" stroke-opacity=".3" stroke-width="1.5"/><circle cx="11" cy="11" r="7.25" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.25" fill="${c}"/>`;
|
|
41
|
-
break;
|
|
42
|
-
case 'content':
|
|
43
|
-
case 'structure':
|
|
44
|
-
body = `<circle cx="11" cy="11" r="7.75" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.5" fill="${c}"/>`;
|
|
45
|
-
break;
|
|
46
|
-
default:
|
|
47
|
-
body = `<circle cx="11" cy="11" r="3.5" fill="${c}"/>`;
|
|
48
|
-
}
|
|
49
|
-
const tpl = document.createElement('template');
|
|
50
|
-
tpl.innerHTML = `<svg class="swatch" viewBox="0 0 22 22" aria-hidden="true" focusable="false">${body}</svg>`;
|
|
51
|
-
return tpl.content.firstElementChild as SVGSVGElement;
|
|
52
|
-
}
|
|
30
|
+
/** How the legend shows: the card, one line of dots, or not at all. */
|
|
31
|
+
export type LegendStyle = 'full' | 'mini' | 'none';
|
|
53
32
|
|
|
54
33
|
export class Stage {
|
|
55
34
|
readonly el: HTMLElement;
|
|
@@ -76,24 +55,25 @@ export class Stage {
|
|
|
76
55
|
readonly #divider: HTMLElement;
|
|
77
56
|
readonly #chipBefore: HTMLElement;
|
|
78
57
|
readonly #chipAfter: HTMLElement;
|
|
79
|
-
//
|
|
80
|
-
readonly
|
|
81
|
-
readonly #
|
|
82
|
-
readonly #expandButton: HTMLButtonElement;
|
|
58
|
+
// Dock
|
|
59
|
+
readonly dock: Dock;
|
|
60
|
+
readonly #dockWrap: HTMLElement;
|
|
83
61
|
// Chrome
|
|
84
62
|
readonly #legend: HTMLElement;
|
|
85
63
|
readonly #legendHead: HTMLButtonElement;
|
|
86
64
|
readonly #legendBody: HTMLElement;
|
|
65
|
+
readonly #miniLegend: HTMLElement;
|
|
87
66
|
readonly #zoomIn: HTMLButtonElement;
|
|
88
67
|
readonly #zoomOut: HTMLButtonElement;
|
|
89
68
|
readonly #fitButton: HTMLButtonElement;
|
|
90
69
|
readonly #mapCluster: HTMLElement;
|
|
70
|
+
readonly #zoomBar: HTMLElement;
|
|
91
71
|
readonly tooltip: HTMLElement;
|
|
92
72
|
readonly #state: HTMLElement;
|
|
93
73
|
#pointer: { x: number; y: number } | null = null;
|
|
94
74
|
#scrubbing: number | null = null;
|
|
95
75
|
#dragging: number | null = null;
|
|
96
|
-
#legendOpen
|
|
76
|
+
#legendOpen = false;
|
|
97
77
|
#stateKey = '';
|
|
98
78
|
|
|
99
79
|
readonly #ctx: UiContext;
|
|
@@ -182,39 +162,15 @@ export class Stage {
|
|
|
182
162
|
this.#divider,
|
|
183
163
|
);
|
|
184
164
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const b = h('button', {
|
|
189
|
-
type: 'button',
|
|
190
|
-
class: 'opt',
|
|
191
|
-
role: 'radio',
|
|
192
|
-
text: String(n),
|
|
193
|
-
onclick: () => c.setHops(n),
|
|
194
|
-
});
|
|
195
|
-
this.#hopButtons.push(b);
|
|
196
|
-
opts.appendChild(b);
|
|
197
|
-
}
|
|
198
|
-
opts.addEventListener('keydown', (e) => {
|
|
199
|
-
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
200
|
-
e.preventDefault();
|
|
201
|
-
e.stopPropagation();
|
|
202
|
-
const next = Math.max(0, Math.min(2, c.hops + (e.key === 'ArrowRight' ? 1 : -1)));
|
|
203
|
-
c.setHops(next);
|
|
204
|
-
this.#hopButtons[next]?.focus();
|
|
205
|
-
});
|
|
206
|
-
this.#expandButton = h(
|
|
207
|
-
'button',
|
|
208
|
-
{ type: 'button', class: 'expand', onclick: () => c.expandNearest() },
|
|
209
|
-
icon('plus'),
|
|
210
|
-
h('span', { class: 'n' }),
|
|
211
|
-
);
|
|
212
|
-
this.#stepper = h(
|
|
165
|
+
// View dock, with replay's finished pill, caption and transport stacked above it.
|
|
166
|
+
this.dock = new Dock(ctx);
|
|
167
|
+
this.#dockWrap = h(
|
|
213
168
|
'div',
|
|
214
|
-
{ class: '
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this.#
|
|
169
|
+
{ class: 'dock' },
|
|
170
|
+
this.#finished,
|
|
171
|
+
this.#caption,
|
|
172
|
+
this.#transport,
|
|
173
|
+
this.dock.el,
|
|
218
174
|
);
|
|
219
175
|
|
|
220
176
|
// Legend
|
|
@@ -223,7 +179,8 @@ export class Stage {
|
|
|
223
179
|
{
|
|
224
180
|
type: 'button',
|
|
225
181
|
class: 'legend-head',
|
|
226
|
-
|
|
182
|
+
'aria-expanded': 'false',
|
|
183
|
+
onclick: () => this.#setLegend(!this.#legendOpen),
|
|
227
184
|
},
|
|
228
185
|
h('span', { class: 'lbl' }),
|
|
229
186
|
icon('chevron-down'),
|
|
@@ -232,26 +189,28 @@ export class Stage {
|
|
|
232
189
|
attr(this.#legendHead, 'aria-controls', 'kg-legend');
|
|
233
190
|
this.#legend = h(
|
|
234
191
|
'div',
|
|
235
|
-
{ class: 'legend card float bottom-left', 'data-open': '
|
|
192
|
+
{ class: 'legend card float bottom-left', 'data-open': 'false' },
|
|
236
193
|
this.#legendHead,
|
|
237
194
|
this.#legendBody,
|
|
238
195
|
);
|
|
196
|
+
this.#miniLegend = h('div', { class: 'mini-legend float bottom-left', role: 'list' });
|
|
239
197
|
|
|
240
198
|
// Zoom + minimap
|
|
241
199
|
this.#zoomIn = iconButton('plus', '', () => c.zoom(1));
|
|
242
200
|
this.#zoomOut = iconButton('minus', '', () => c.zoom(-1));
|
|
243
201
|
this.#fitButton = iconButton('fit', '', () => void c.fit());
|
|
244
202
|
this.minimapHost = h('div', { class: 'minimap-host' });
|
|
203
|
+
this.#zoomBar = h(
|
|
204
|
+
'div',
|
|
205
|
+
{ class: 'zoom-bar card', role: 'toolbar' },
|
|
206
|
+
this.#zoomIn,
|
|
207
|
+
this.#zoomOut,
|
|
208
|
+
this.#fitButton,
|
|
209
|
+
);
|
|
245
210
|
this.#mapCluster = h(
|
|
246
211
|
'div',
|
|
247
212
|
{ class: 'map-cluster float bottom-right' },
|
|
248
|
-
|
|
249
|
-
'div',
|
|
250
|
-
{ class: 'zoom-bar card', role: 'toolbar' },
|
|
251
|
-
this.#zoomIn,
|
|
252
|
-
this.#zoomOut,
|
|
253
|
-
this.#fitButton,
|
|
254
|
-
),
|
|
213
|
+
this.#zoomBar,
|
|
255
214
|
this.minimapHost,
|
|
256
215
|
);
|
|
257
216
|
|
|
@@ -261,12 +220,10 @@ export class Stage {
|
|
|
261
220
|
'div',
|
|
262
221
|
{ class: 'chrome' },
|
|
263
222
|
this.#swipe,
|
|
264
|
-
this.#caption,
|
|
265
|
-
this.#stepper,
|
|
266
223
|
this.#legend,
|
|
224
|
+
this.#miniLegend,
|
|
267
225
|
this.#mapCluster,
|
|
268
|
-
this.#
|
|
269
|
-
this.#transport,
|
|
226
|
+
this.#dockWrap,
|
|
270
227
|
this.tooltip,
|
|
271
228
|
);
|
|
272
229
|
this.el = h(
|
|
@@ -297,16 +254,20 @@ export class Stage {
|
|
|
297
254
|
const obstacles: Rect[] = [];
|
|
298
255
|
const parts = [
|
|
299
256
|
this.#legend,
|
|
300
|
-
this.#
|
|
257
|
+
this.#miniLegend,
|
|
258
|
+
this.#zoomBar,
|
|
259
|
+
this.dock.el,
|
|
301
260
|
this.#transport,
|
|
302
261
|
this.#finished,
|
|
303
|
-
this.#stepper,
|
|
304
262
|
this.#chipBefore,
|
|
305
263
|
this.#chipAfter,
|
|
306
264
|
];
|
|
307
265
|
if (this.#caption.dataset.empty !== 'true') parts.push(this.#caption);
|
|
266
|
+
if (this.minimapHost.dataset.hidden !== 'true') parts.push(this.minimapHost);
|
|
267
|
+
// The HUD (title, actions) floats over the canvas too.
|
|
268
|
+
for (const el of this.el.querySelectorAll<HTMLElement>('.hud .card')) parts.push(el);
|
|
308
269
|
for (const el of parts) {
|
|
309
|
-
if (el.hidden) continue;
|
|
270
|
+
if (el.hidden || el.closest('[hidden]')) continue;
|
|
310
271
|
const b = el.getBoundingClientRect();
|
|
311
272
|
if (b.width <= 0 || b.height <= 0) continue;
|
|
312
273
|
obstacles.push({ x: b.left - r.left, y: b.top - r.top, width: b.width, height: b.height });
|
|
@@ -314,10 +275,9 @@ export class Stage {
|
|
|
314
275
|
return { width: r.width, height: r.height, obstacles };
|
|
315
276
|
}
|
|
316
277
|
|
|
317
|
-
/**
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return hgt === 0 || hgt >= 560;
|
|
278
|
+
/** Re-measure what depends on rendered sizes (fonts, resizes). */
|
|
279
|
+
layout(): void {
|
|
280
|
+
this.dock.layout();
|
|
321
281
|
}
|
|
322
282
|
|
|
323
283
|
#setLegend(open: boolean): void {
|
|
@@ -326,19 +286,27 @@ export class Stage {
|
|
|
326
286
|
attr(this.#legendHead, 'aria-expanded', String(open));
|
|
327
287
|
}
|
|
328
288
|
|
|
289
|
+
/** Legend style, and whether clicks select (the tooltip only hints at clicking then). */
|
|
290
|
+
setChrome(legend: LegendStyle, selectable: boolean): void {
|
|
291
|
+
this.el.dataset.legend = legend;
|
|
292
|
+
this.el.dataset.selectable = String(selectable);
|
|
293
|
+
}
|
|
294
|
+
|
|
329
295
|
update(topics: ReadonlySet<Topic>): void {
|
|
330
296
|
const c = this.#ctx.controller;
|
|
331
297
|
const tr = this.#ctx.tr();
|
|
332
298
|
const all = topics.has('status') || topics.has('data') || topics.has('locale');
|
|
333
299
|
if (all) {
|
|
334
300
|
this.#renderLabels(tr);
|
|
301
|
+
this.#renderMiniLegend(tr);
|
|
335
302
|
this.#renderState(tr);
|
|
336
303
|
this.#renderTicks();
|
|
337
304
|
this.#renderCanvasLabel(tr);
|
|
338
|
-
if (this.#legendOpen === null) this.#setLegend(this.#defaultLegendOpen());
|
|
339
305
|
}
|
|
340
|
-
|
|
341
|
-
|
|
306
|
+
this.dock.update(topics);
|
|
307
|
+
if (all || topics.has('mode')) this.#renderMode();
|
|
308
|
+
if (all || topics.has('overview')) {
|
|
309
|
+
attr(this.minimapHost, 'data-hidden', String(c.overview));
|
|
342
310
|
}
|
|
343
311
|
if (all || topics.has('mode') || topics.has('replay')) this.#renderReplay(tr);
|
|
344
312
|
if (all || topics.has('mode') || topics.has('swipe')) this.#renderSwipe();
|
|
@@ -358,8 +326,6 @@ export class Stage {
|
|
|
358
326
|
setText(this.#chipBefore, tr.t('detail.before'));
|
|
359
327
|
setText(this.#chipAfter, tr.t('detail.after'));
|
|
360
328
|
attr(this.#divider, 'aria-label', tr.t('swipe.divider'));
|
|
361
|
-
setText(this.#stepper.querySelector('.label') as Element, tr.t('focus.context'));
|
|
362
|
-
attr(this.#stepper.querySelector('.opts') as Element, 'aria-label', tr.t('focus.hops'));
|
|
363
329
|
setText(this.#legendHead.querySelector('.lbl') as Element, tr.t('legend.title'));
|
|
364
330
|
attr(this.#legendHead, 'aria-label', tr.t('legend.toggle'));
|
|
365
331
|
relabel(this.#zoomIn, `${tr.t('action.zoomIn')} (+)`);
|
|
@@ -381,6 +347,32 @@ export class Stage {
|
|
|
381
347
|
);
|
|
382
348
|
}
|
|
383
349
|
|
|
350
|
+
/** One line: the families this diff has, then the unchanged context. */
|
|
351
|
+
#renderMiniLegend(tr: Translator): void {
|
|
352
|
+
const scene = this.#ctx.controller.scene;
|
|
353
|
+
const present = new Set<Family>(scene?.hunks.map((x) => x.family));
|
|
354
|
+
if (scene?.nodes.some((n) => n.status === 'unchanged')) present.add('neutral');
|
|
355
|
+
attr(this.#miniLegend, 'aria-label', tr.t('legend.title'));
|
|
356
|
+
replace(
|
|
357
|
+
this.#miniLegend,
|
|
358
|
+
[...FAMILIES, 'neutral' as const]
|
|
359
|
+
.filter((f) => present.has(f))
|
|
360
|
+
.map((f) =>
|
|
361
|
+
h(
|
|
362
|
+
'span',
|
|
363
|
+
{
|
|
364
|
+
class: 'mini-item',
|
|
365
|
+
role: 'listitem',
|
|
366
|
+
'data-family': f,
|
|
367
|
+
title: tr.t(`familyHint.${f}`),
|
|
368
|
+
},
|
|
369
|
+
h('span', { class: 'dot', 'aria-hidden': 'true' }),
|
|
370
|
+
tr.t(`family.${f}`),
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
384
376
|
#renderCanvasLabel(tr: Translator): void {
|
|
385
377
|
const stats = this.#ctx.controller.diff?.stats;
|
|
386
378
|
const canvas = this.canvasHost.querySelector('canvas');
|
|
@@ -396,26 +388,8 @@ export class Stage {
|
|
|
396
388
|
setText(this.#liveCanvasLabel, '');
|
|
397
389
|
}
|
|
398
390
|
|
|
399
|
-
#renderMode(
|
|
400
|
-
const
|
|
401
|
-
const mode = c.mode;
|
|
402
|
-
this.#stepper.hidden = mode !== 'changes';
|
|
403
|
-
this.#hopButtons.forEach((b, n) => {
|
|
404
|
-
attr(b, 'aria-checked', String(n === c.hops));
|
|
405
|
-
b.tabIndex = n === c.hops ? 0 : -1;
|
|
406
|
-
attr(b, 'aria-label', `${tr.t('focus.hops')}: ${n}`);
|
|
407
|
-
});
|
|
408
|
-
const group = c.focus?.collapsed[c.nearestGroup()];
|
|
409
|
-
this.#expandButton.hidden = !group;
|
|
410
|
-
if (group) {
|
|
411
|
-
const usable = c.canExpand(c.nearestGroup()) || group.nodes.length > 0;
|
|
412
|
-
this.#expandButton.disabled = !usable;
|
|
413
|
-
setText(this.#expandButton.querySelector('.n') as Element, tr.num(group.count));
|
|
414
|
-
relabel(
|
|
415
|
-
this.#expandButton,
|
|
416
|
-
`${usable ? tr.t('focus.expandNearest') : tr.t('focus.expandUnavailable')}: ${group.label} (E)`,
|
|
417
|
-
);
|
|
418
|
-
}
|
|
391
|
+
#renderMode(): void {
|
|
392
|
+
const mode = this.#ctx.controller.mode;
|
|
419
393
|
this.#transport.hidden = mode !== 'replay';
|
|
420
394
|
this.#caption.hidden = mode !== 'replay';
|
|
421
395
|
this.#swipe.hidden = mode !== 'swipe';
|
|
@@ -491,6 +465,8 @@ export class Stage {
|
|
|
491
465
|
}
|
|
492
466
|
const finished = clock.finished;
|
|
493
467
|
this.#finished.hidden = !finished;
|
|
468
|
+
// The finished pill takes the caption's place above the transport.
|
|
469
|
+
this.#caption.hidden = finished;
|
|
494
470
|
if (finished && this.#finished.childElementCount === 0) {
|
|
495
471
|
replace(
|
|
496
472
|
this.#finished,
|
package/src/ui/swatch.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family swatches: the node cue of each family (docs/visual-language.md §Meaning families), drawn
|
|
3
|
+
* the same in the legend and on the sidebar's filter chips so the chips double as a legend.
|
|
4
|
+
*/
|
|
5
|
+
import type { Family } from '@kgdiff/scene';
|
|
6
|
+
|
|
7
|
+
export function swatch(family: Family): SVGSVGElement {
|
|
8
|
+
const v = (name: string) => `var(--kg-${name})`;
|
|
9
|
+
const c = family === 'neutral' ? v('neutral') : v(family);
|
|
10
|
+
let body: string;
|
|
11
|
+
switch (family) {
|
|
12
|
+
case 'added':
|
|
13
|
+
body = `<circle cx="11" cy="11" r="8.5" fill="none" stroke="${c}" stroke-opacity=".35" stroke-width="2"/><circle cx="11" cy="11" r="5" fill="${c}"/>`;
|
|
14
|
+
break;
|
|
15
|
+
case 'removed':
|
|
16
|
+
body = `<circle cx="11" cy="11" r="5.25" fill="none" stroke="${c}" stroke-width="1.5" stroke-dasharray="3 2.5"/>`;
|
|
17
|
+
break;
|
|
18
|
+
case 'identity':
|
|
19
|
+
body = `<circle cx="11" cy="11" r="9.5" fill="none" stroke="${c}" stroke-opacity=".3" stroke-width="1.5"/><circle cx="11" cy="11" r="7.25" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.25" fill="${c}"/>`;
|
|
20
|
+
break;
|
|
21
|
+
case 'content':
|
|
22
|
+
case 'structure':
|
|
23
|
+
body = `<circle cx="11" cy="11" r="7.75" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.5" fill="${c}"/>`;
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
body = `<circle cx="11" cy="11" r="3.5" fill="${c}"/>`;
|
|
27
|
+
}
|
|
28
|
+
const tpl = document.createElement('template');
|
|
29
|
+
tpl.innerHTML = `<svg class="swatch" viewBox="0 0 22 22" aria-hidden="true" focusable="false">${body}</svg>`;
|
|
30
|
+
return tpl.content.firstElementChild as SVGSVGElement;
|
|
31
|
+
}
|