@kubex/zinc 1.1.97 → 1.1.99

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 (85) hide show
  1. package/AGENTS.md +5 -0
  2. package/CLAUDE.md +5 -0
  3. package/dist/chunks/zn.R2EAU6OS.js +1 -0
  4. package/dist/custom-elements.json +14747 -12528
  5. package/dist/vscode.html-custom-data.json +386 -61
  6. package/dist/web-types.json +1232 -481
  7. package/dist/zn.d.ts +529 -11
  8. package/dist/zn.min.js +649 -478
  9. package/docs/pages/components/background.md +143 -0
  10. package/docs/pages/components/button.md +11 -1
  11. package/docs/pages/components/chart.md +165 -3
  12. package/docs/pages/components/checkbox-group.md +13 -1
  13. package/docs/pages/components/checkbox.md +59 -1
  14. package/docs/pages/components/icon-picker.md +8 -0
  15. package/docs/pages/components/radio-group.md +12 -0
  16. package/docs/pages/components/radio.md +188 -0
  17. package/docs/pages/components/remarkd-editor.md +22 -0
  18. package/docs/pages/components/scroll-container.md +14 -15
  19. package/docs/pages/components/thumbnail-group.md +216 -0
  20. package/docs/pages/components/thumbnail.md +349 -0
  21. package/docs/pages/components/well.md +34 -0
  22. package/package.json +1 -1
  23. package/src/components/background/background.component.ts +111 -0
  24. package/src/components/background/background.scss +236 -0
  25. package/src/components/background/background.test.ts +120 -0
  26. package/src/components/background/index.ts +12 -0
  27. package/src/components/button/button.component.ts +8 -0
  28. package/src/components/chart/builders.test.ts +190 -1
  29. package/src/components/chart/builders.ts +246 -4
  30. package/src/components/chart/chart.component.ts +33 -1
  31. package/src/components/chart/chart.test.ts +66 -1
  32. package/src/components/chart/echarts-loader.ts +10 -0
  33. package/src/components/checkbox/checkbox.component.ts +62 -6
  34. package/src/components/checkbox/checkbox.scss +1 -0
  35. package/src/components/checkbox/checkbox.test.ts +56 -0
  36. package/src/components/checkbox-group/checkbox-group.component.ts +15 -0
  37. package/src/components/checkbox-group/checkbox-group.scss +23 -5
  38. package/src/components/checkbox-group/checkbox-group.test.ts +15 -0
  39. package/src/components/collapsible/collapsible.component.ts +49 -4
  40. package/src/components/collapsible/collapsible.scss +33 -14
  41. package/src/components/confirm/confirm.component.ts +3 -3
  42. package/src/components/confirm/confirm.test.ts +15 -0
  43. package/src/components/dialog/dialog.component.ts +3 -2
  44. package/src/components/editor/editor.component.ts +7 -6
  45. package/src/components/header/header.scss +2 -2
  46. package/src/components/icon-picker/icon-picker.component.ts +3 -0
  47. package/src/components/icon-picker/lucide-icons.ts +1756 -0
  48. package/src/components/navbar/navbar.scss +11 -0
  49. package/src/components/page/page.scss +1 -1
  50. package/src/components/radio/radio.component.ts +63 -7
  51. package/src/components/radio/radio.scss +1 -0
  52. package/src/components/radio/radio.test.ts +56 -0
  53. package/src/components/radio-group/radio-group.component.ts +16 -3
  54. package/src/components/radio-group/radio-group.scss +23 -4
  55. package/src/components/radio-group/radio-group.test.ts +15 -0
  56. package/src/components/remarkd-editor/remarkd-editor.component.ts +118 -5
  57. package/src/components/remarkd-editor/remarkd-editor.scss +8 -1
  58. package/src/components/remarkd-editor/remarkd-editor.test.ts +135 -0
  59. package/src/components/scroll-container/scroll-container.component.ts +15 -1
  60. package/src/components/scroll-container/scroll-container.scss +4 -2
  61. package/src/components/scroll-container/scroll-container.test.ts +13 -0
  62. package/src/components/slideout/slideout.component.ts +3 -2
  63. package/src/components/split-pane/split-pane.scss +21 -15
  64. package/src/components/thumbnail/index.ts +12 -0
  65. package/src/components/thumbnail/thumbnail.component.ts +460 -0
  66. package/src/components/thumbnail/thumbnail.scss +360 -0
  67. package/src/components/thumbnail/thumbnail.test.ts +379 -0
  68. package/src/components/thumbnail-group/index.ts +12 -0
  69. package/src/components/thumbnail-group/thumbnail-group.component.ts +268 -0
  70. package/src/components/thumbnail-group/thumbnail-group.scss +86 -0
  71. package/src/components/thumbnail-group/thumbnail-group.test.ts +151 -0
  72. package/src/components/tile/tile.scss +4 -2
  73. package/src/components/translation-group/translation-group.test.ts +22 -0
  74. package/src/components/translations/translations.component.ts +18 -3
  75. package/src/components/well/well.component.ts +22 -4
  76. package/src/components/well/well.scss +24 -0
  77. package/src/components/well/well.test.ts +43 -0
  78. package/src/form-control.scss +3 -1
  79. package/src/internal/conditional.ts +2 -2
  80. package/src/internal/form.ts +2 -1
  81. package/src/internal/selection-card.ts +19 -0
  82. package/src/selection-card.scss +187 -0
  83. package/src/utilities/query.test.ts +18 -1
  84. package/src/utilities/query.ts +8 -0
  85. package/src/zinc.ts +3 -0
@@ -0,0 +1,236 @@
1
+ @use "../../wc";
2
+
3
+ :host {
4
+ display: block;
5
+ position: relative;
6
+ min-width: 0;
7
+ min-height: 0;
8
+ overflow: hidden;
9
+ isolation: isolate;
10
+ }
11
+
12
+ .background {
13
+ position: relative;
14
+ box-sizing: border-box;
15
+ width: 100%;
16
+ height: 100%;
17
+ min-height: inherit;
18
+ overflow: hidden;
19
+ background-color: var(--_zn-background-color, var(--zn-background-color, transparent));
20
+ }
21
+
22
+ .background__image,
23
+ .background__overlay,
24
+ .background__floating-icons {
25
+ position: absolute;
26
+ inset: 0;
27
+ width: 100%;
28
+ height: 100%;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .background__image {
33
+ width: 108%;
34
+ height: 108%;
35
+ max-width: none;
36
+ inset: -4%;
37
+ object-fit: cover;
38
+ object-position: var(--zn-background-image-position, center);
39
+ opacity: 1;
40
+ }
41
+
42
+ :host([image-strength="soft"]) .background__image {
43
+ opacity: 0.3;
44
+ }
45
+
46
+ :host([image-strength="medium"]) .background__image {
47
+ opacity: 0.62;
48
+ }
49
+
50
+ .background__overlay {
51
+ background: linear-gradient(
52
+ var(--zn-background-overlay-angle, 110deg),
53
+ rgb(255 255 255 / 18%),
54
+ rgb(255 255 255 / 4%)
55
+ );
56
+ }
57
+
58
+ :host([overlay="none"]) .background__overlay {
59
+ display: none;
60
+ }
61
+
62
+ :host([overlay="strong"]) .background__overlay {
63
+ background: linear-gradient(
64
+ var(--zn-background-overlay-angle, 110deg),
65
+ rgb(255 255 255 / 48%),
66
+ rgb(255 255 255 / 18%)
67
+ );
68
+ }
69
+
70
+ :host([overlay-tone="dark"]) .background__overlay {
71
+ background: linear-gradient(
72
+ var(--zn-background-overlay-angle, 110deg),
73
+ rgb(4 12 32 / 28%),
74
+ rgb(4 12 32 / 6%)
75
+ );
76
+ }
77
+
78
+ :host([overlay-tone="dark"][overlay="strong"]) .background__overlay {
79
+ background: linear-gradient(
80
+ var(--zn-background-overlay-angle, 110deg),
81
+ rgb(4 12 32 / 62%),
82
+ rgb(4 12 32 / 28%)
83
+ );
84
+ }
85
+
86
+ :host([motion="drift"]) .background__image {
87
+ animation: zn-background-drift 28s ease-in-out infinite alternate;
88
+ }
89
+
90
+ :host([motion="breathe"]) .background__image {
91
+ animation: zn-background-breathe 18s ease-in-out infinite;
92
+ }
93
+
94
+ :host([paused]) .background__image {
95
+ animation-play-state: paused;
96
+ }
97
+
98
+ .background__floating-icons {
99
+ overflow: hidden;
100
+ color: var(--zn-background-floating-icon-color, currentColor);
101
+ }
102
+
103
+ .background__floating-icon {
104
+ --_zn-background-icon-opacity-min: 0.01;
105
+ --_zn-background-icon-opacity-max: 0.18;
106
+ position: absolute;
107
+ left: 8%;
108
+ top: 13%;
109
+ opacity: var(--_zn-background-icon-opacity-min);
110
+ animation: zn-background-icon-float 11s ease-in-out infinite alternate both;
111
+ }
112
+
113
+ .background__floating-icon:nth-child(2) {
114
+ --_zn-background-icon-opacity-min: 0.08;
115
+ --_zn-background-icon-opacity-max: 0.34;
116
+ left: auto;
117
+ right: 9%;
118
+ top: 9%;
119
+ animation-delay: -4s;
120
+ animation-duration: 14s;
121
+ }
122
+
123
+ .background__floating-icon:nth-child(3) {
124
+ --_zn-background-icon-opacity-min: 0.03;
125
+ --_zn-background-icon-opacity-max: 0.5;
126
+ left: 2%;
127
+ top: 47%;
128
+ animation-delay: -7s;
129
+ animation-duration: 13s;
130
+ }
131
+
132
+ .background__floating-icon:nth-child(4) {
133
+ --_zn-background-icon-opacity-min: 0.12;
134
+ --_zn-background-icon-opacity-max: 0.38;
135
+ left: auto;
136
+ right: 12%;
137
+ top: 45%;
138
+ animation-delay: -2s;
139
+ animation-duration: 10s;
140
+ }
141
+
142
+ .background__floating-icon:nth-child(5) {
143
+ --_zn-background-icon-opacity-min: 0.05;
144
+ --_zn-background-icon-opacity-max: 0.29;
145
+ left: 21%;
146
+ top: auto;
147
+ bottom: 4%;
148
+ animation-delay: -5s;
149
+ animation-duration: 15s;
150
+ }
151
+
152
+ .background__floating-icon:nth-child(6) {
153
+ --_zn-background-icon-opacity-min: 0.01;
154
+ --_zn-background-icon-opacity-max: 0.42;
155
+ left: auto;
156
+ right: 4%;
157
+ top: auto;
158
+ bottom: 3%;
159
+ animation-delay: -9s;
160
+ animation-duration: 12s;
161
+ }
162
+
163
+ .background__floating-icon:nth-child(7) {
164
+ --_zn-background-icon-opacity-min: 0.09;
165
+ --_zn-background-icon-opacity-max: 0.31;
166
+ left: 42%;
167
+ top: 7%;
168
+ animation-delay: -6s;
169
+ animation-duration: 16s;
170
+ }
171
+
172
+ .background__floating-icon:nth-child(8) {
173
+ --_zn-background-icon-opacity-min: 0.04;
174
+ --_zn-background-icon-opacity-max: 0.46;
175
+ left: 54%;
176
+ top: auto;
177
+ bottom: 8%;
178
+ animation-delay: -11s;
179
+ animation-duration: 17s;
180
+ }
181
+
182
+ :host([paused]) .background__floating-icon {
183
+ animation-play-state: paused;
184
+ }
185
+
186
+ .background__content {
187
+ position: relative;
188
+ z-index: 1;
189
+ box-sizing: border-box;
190
+ width: 100%;
191
+ height: 100%;
192
+ min-height: inherit;
193
+ }
194
+
195
+ @keyframes zn-background-drift {
196
+ from {
197
+ transform: scale(1.04) translate3d(-1.2%, -0.6%, 0);
198
+ }
199
+
200
+ to {
201
+ transform: scale(1.04) translate3d(1.2%, 0.6%, 0);
202
+ }
203
+ }
204
+
205
+ @keyframes zn-background-breathe {
206
+ 0%, 100% {
207
+ transform: scale(1.02);
208
+ }
209
+
210
+ 50% {
211
+ transform: scale(1.07);
212
+ }
213
+ }
214
+
215
+ @keyframes zn-background-icon-float {
216
+ from {
217
+ opacity: var(--_zn-background-icon-opacity-min);
218
+ transform: translate3d(-8%, -12%, 0) scale(0.88) rotate(-8deg);
219
+ }
220
+
221
+ to {
222
+ opacity: var(--_zn-background-icon-opacity-max);
223
+ transform: translate3d(8%, 12%, 0) scale(1.06) rotate(8deg);
224
+ }
225
+ }
226
+
227
+ @media (prefers-reduced-motion: reduce) {
228
+ .background__image,
229
+ .background__floating-icon {
230
+ animation: none;
231
+ }
232
+
233
+ .background__floating-icon {
234
+ opacity: var(--_zn-background-icon-opacity-max);
235
+ }
236
+ }
@@ -0,0 +1,120 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {expect, fixture, html, waitUntil} from '@open-wc/testing';
3
+ import type ZnBackground from './background.component';
4
+
5
+ const transparentImage = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
6
+
7
+ describe('<zn-background>', () => {
8
+ it('renders slotted content above the background layers', async () => {
9
+ const el = await fixture<ZnBackground>(html`
10
+ <zn-background><p>Lesson content</p></zn-background>`);
11
+
12
+ const content = el.shadowRoot!.querySelector<HTMLElement>('.background__content')!;
13
+ const slot = content.querySelector<HTMLSlotElement>('slot')!;
14
+
15
+ expect(slot.assignedElements()[0].textContent).to.equal('Lesson content');
16
+ expect(getComputedStyle(content).zIndex).to.equal('1');
17
+ });
18
+
19
+ it('composes the configured colour and decorative image', async () => {
20
+ const el = await fixture<ZnBackground>(html`
21
+ <zn-background color="#123456" image=${transparentImage}></zn-background>`);
22
+
23
+ const base = el.shadowRoot!.querySelector<HTMLElement>('.background')!;
24
+ const image = el.shadowRoot!.querySelector<HTMLImageElement>('.background__image')!;
25
+
26
+ expect(getComputedStyle(base).backgroundColor).to.equal('rgb(18, 52, 86)');
27
+ expect(image.src).to.equal(transparentImage);
28
+ expect(image.alt).to.equal('');
29
+ expect(image.getAttribute('part')).to.equal('image');
30
+ });
31
+
32
+ it('does not render an image element without an image URL', async () => {
33
+ const el = await fixture<ZnBackground>(html`<zn-background></zn-background>`);
34
+
35
+ expect(el.shadowRoot!.querySelector('.background__image')).to.not.exist;
36
+ });
37
+
38
+ it('applies soft, medium and full image strengths', async () => {
39
+ const el = await fixture<ZnBackground>(html`
40
+ <zn-background image=${transparentImage} image-strength="soft"></zn-background>`);
41
+ const image = el.shadowRoot!.querySelector<HTMLImageElement>('.background__image')!;
42
+
43
+ expect(getComputedStyle(image).opacity).to.equal('0.3');
44
+ el.imageStrength = 'medium';
45
+ await el.updateComplete;
46
+ expect(getComputedStyle(image).opacity).to.equal('0.62');
47
+ el.imageStrength = 'full';
48
+ await el.updateComplete;
49
+ expect(getComputedStyle(image).opacity).to.equal('1');
50
+ });
51
+
52
+ it('supports drift and breathe motion and can pause it', async () => {
53
+ const el = await fixture<ZnBackground>(html`
54
+ <zn-background image=${transparentImage} motion="drift"></zn-background>`);
55
+ const image = el.shadowRoot!.querySelector<HTMLImageElement>('.background__image')!;
56
+
57
+ expect(getComputedStyle(image).animationName).to.equal('zn-background-drift');
58
+ el.motion = 'breathe';
59
+ el.paused = true;
60
+ await el.updateComplete;
61
+ expect(getComputedStyle(image).animationName).to.equal('zn-background-breathe');
62
+ expect(getComputedStyle(image).animationPlayState).to.equal('paused');
63
+ });
64
+
65
+ it('supports none, soft and strong light or dark overlays', async () => {
66
+ const el = await fixture<ZnBackground>(html`
67
+ <zn-background overlay="none"></zn-background>`);
68
+ const overlay = el.shadowRoot!.querySelector<HTMLElement>('.background__overlay')!;
69
+
70
+ expect(getComputedStyle(overlay).display).to.equal('none');
71
+ el.overlay = 'soft';
72
+ await el.updateComplete;
73
+ const lightGradient = getComputedStyle(overlay).backgroundImage;
74
+ expect(lightGradient).to.contain('255, 255, 255');
75
+
76
+ el.overlay = 'strong';
77
+ el.overlayTone = 'dark';
78
+ await el.updateComplete;
79
+ const darkGradient = getComputedStyle(overlay).backgroundImage;
80
+ expect(darkGradient).to.contain('4, 12, 32');
81
+ expect(darkGradient).to.not.equal(lightGradient);
82
+ });
83
+
84
+ it('renders up to eight comma-separated floating Zinc icons', async () => {
85
+ const el = await fixture<ZnBackground>(html`
86
+ <zn-background
87
+ floating-icons="school, lightbulb, quiz, psychology, star, rocket, science, public, hidden">
88
+ </zn-background>`);
89
+ const icons = el.shadowRoot!.querySelectorAll<HTMLElement>('.background__floating-icon');
90
+
91
+ expect(icons).to.have.lengthOf(8);
92
+ expect(icons[0].getAttribute('src')).to.equal('school');
93
+ expect(icons[7].getAttribute('src')).to.equal('public');
94
+ expect(el.shadowRoot!.querySelector('.background__floating-icons')!.getAttribute('aria-hidden')).to.equal('true');
95
+ });
96
+
97
+ it('sizes floating icons to five percent of the background width', async () => {
98
+ const el = await fixture<ZnBackground>(html`
99
+ <zn-background floating-icons="school" style="width: 1000px;"></zn-background>`);
100
+ const icon = el.shadowRoot!.querySelector<HTMLElement & {size: number}>('.background__floating-icon')!;
101
+
102
+ await waitUntil(() => icon.size === 50);
103
+ expect(icon.size).to.equal(50);
104
+ });
105
+
106
+ it('caps floating icons at 150 pixels and keeps opacity within one to fifty percent', async () => {
107
+ const el = await fixture<ZnBackground>(html`
108
+ <zn-background floating-icons="school,lightbulb" style="width: 4000px;"></zn-background>`);
109
+ const icons = [...el.shadowRoot!.querySelectorAll<HTMLElement & {size: number}>('.background__floating-icon')];
110
+
111
+ await waitUntil(() => icons[0].size === 150);
112
+ expect(icons[0].size).to.equal(150);
113
+ icons.forEach(icon => {
114
+ const styles = getComputedStyle(icon);
115
+ expect(Number(styles.getPropertyValue('--_zn-background-icon-opacity-min'))).to.be.at.least(0.01);
116
+ expect(Number(styles.getPropertyValue('--_zn-background-icon-opacity-max'))).to.be.at.most(0.5);
117
+ expect(styles.animationName).to.equal('zn-background-icon-float');
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,12 @@
1
+ import ZnBackground from './background.component';
2
+
3
+ export * from './background.component';
4
+ export default ZnBackground;
5
+
6
+ ZnBackground.define('zn-background');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-background': ZnBackground;
11
+ }
12
+ }
@@ -121,6 +121,9 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
121
121
  // Tooltip Specific
122
122
  @property() tooltip: string;
123
123
 
124
+ /** Accessible name for the button. Icon-only buttons fall back to `tooltip`. */
125
+ @property() label: string;
126
+
124
127
  // Auto Click Specific
125
128
  @property({type: Boolean, attribute: 'auto-click'}) autoClick = false;
126
129
  @property({type: Number, attribute: 'auto-click-delay'}) autoClickDelay = 2000;
@@ -369,6 +372,10 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
369
372
  library="${ifDefined(this.iconLibrary)}"></zn-icon>`
370
373
  : '';
371
374
  const tag = isLink ? literal`a` : literal`button`;
375
+ // An icon-only button has no text to name it, so the tooltip doubles as the
376
+ // accessible name. Buttons with content keep their text as the name.
377
+ const hasContent = this.hasSlotController.test('[default]') || !!this.content;
378
+ const ariaLabel = this.label || (hasContent ? undefined : this.tooltip);
372
379
  const iconButtonStyles = this.iconButton ? {
373
380
  '--icon-button-color': this.getIconButtonColor(this.color),
374
381
  '--icon-button-hover-color': this.getIconButtonColor(this.hoverColor || 'primary')
@@ -412,6 +419,7 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
412
419
  data-target="${ifDefined(isLink ? this.dataTarget : undefined)}"
413
420
  rel="${ifDefined(isLink ? this.rel : undefined)}"
414
421
  gaid="${ifDefined(this.gaid)}"
422
+ aria-label="${ifDefined(ariaLabel)}"
415
423
  data-notification="${ifDefined(this.notification)}"
416
424
  disabled="${this.disabled || nothing}"
417
425
  style=${styleMap(iconButtonStyles)}
@@ -3,8 +3,16 @@ import {
3
3
  buildAreaOption,
4
4
  buildBarOption,
5
5
  type BuilderProps,
6
+ buildFunnelOption,
7
+ buildGaugeOption,
8
+ buildHeatmapOption,
6
9
  buildLineOption,
10
+ buildPieOption,
11
+ buildRadarOption,
7
12
  buildSankeyOption,
13
+ buildScatterOption,
14
+ buildSunburstOption,
15
+ buildTreemapOption,
8
16
  } from './builders';
9
17
  import { expect } from '@open-wc/testing';
10
18
 
@@ -145,6 +153,187 @@ describe('buildAreaOption', () => {
145
153
  });
146
154
  });
147
155
 
156
+ describe('buildPieOption', () => {
157
+ const pieProps: BuilderProps = {
158
+ type: 'pie',
159
+ data: [{ name: 'Insights', data: [8, 1] }],
160
+ categories: ['Configuration suggestion', 'Insecure configuration'],
161
+ stacked: false,
162
+ enableAnimations: false,
163
+ datapointSize: 1,
164
+ theme: 'light',
165
+ };
166
+
167
+ it('maps categories and numeric values into pie slices', () => {
168
+ const opt = buildPieOption(pieProps);
169
+ const series = (opt.series as any[])[0];
170
+ expect(series.type).to.equal('pie');
171
+ expect(series.name).to.equal('Insights');
172
+ expect(series.data).to.deep.equal([
173
+ { name: 'Configuration suggestion', value: 8 },
174
+ { name: 'Insecure configuration', value: 1 },
175
+ ]);
176
+ expect(series.radius).to.deep.equal([0, '70%']);
177
+ });
178
+
179
+ it('uses inner and outer radii for a donut', () => {
180
+ const opt = buildPieOption({
181
+ ...pieProps,
182
+ type: 'donut',
183
+ innerRadius: '45%',
184
+ outerRadius: '80%',
185
+ });
186
+ expect((opt.series as any[])[0].radius).to.deep.equal(['45%', '80%']);
187
+ });
188
+
189
+ it('accepts named ECharts-style slices and maps their colors', () => {
190
+ const opt = buildPieOption({
191
+ ...pieProps,
192
+ data: [{
193
+ name: 'Insights',
194
+ data: [
195
+ { name: 'Moderate', value: 4, color: '#f5a623' },
196
+ { name: 'Low', value: 5 },
197
+ ],
198
+ }],
199
+ });
200
+ expect((opt.series as any[])[0].data).to.deep.equal([
201
+ { name: 'Moderate', value: 4, itemStyle: { color: '#f5a623' } },
202
+ { name: 'Low', value: 5 },
203
+ ]);
204
+ });
205
+
206
+ it('uses item tooltips, optional labels, and value suffixes', () => {
207
+ const opt = buildPieOption({ ...pieProps, showLabels: true, yAxisAppend: '%' });
208
+ const formatter = (opt.tooltip as any).valueFormatter as (value: number) => string;
209
+ expect((opt.tooltip as any).trigger).to.equal('item');
210
+ expect((opt.series as any[])[0].label.show).to.equal(true);
211
+ expect(formatter(42)).to.equal('42%');
212
+ });
213
+
214
+ it('has no cartesian axes or grid', () => {
215
+ const opt = buildPieOption(pieProps);
216
+ expect(opt.xAxis).to.be.undefined;
217
+ expect(opt.yAxis).to.be.undefined;
218
+ expect(opt.grid).to.be.undefined;
219
+ });
220
+ });
221
+
222
+ describe('additional native chart builders', () => {
223
+ it('builds a scatter chart with numeric axes and coordinate normalization', () => {
224
+ const opt = buildScatterOption({
225
+ ...baseProps,
226
+ type: 'scatter',
227
+ datapointSize: 12,
228
+ data: [{ name: 'Latency', data: [[1, 20], { x: 2, y: 35 }] }],
229
+ });
230
+ expect((opt.series as any[])[0]).to.include({ type: 'scatter', symbolSize: 12 });
231
+ expect((opt.series as any[])[0].data).to.deep.equal([[1, 20], [2, 35]]);
232
+ expect((opt.xAxis as any).type).to.equal('value');
233
+ expect((opt.yAxis as any).type).to.equal('value');
234
+ });
235
+
236
+ it('builds radar indicators and values from categories and series', () => {
237
+ const opt = buildRadarOption({
238
+ ...baseProps,
239
+ type: 'radar',
240
+ categories: ['Security', 'Performance', 'Reliability'],
241
+ data: [
242
+ { name: 'Current', data: [80, 65, 90] },
243
+ { name: 'Target', data: [90, 90, 95] },
244
+ ],
245
+ });
246
+ expect((opt.radar as any).indicator).to.deep.equal([
247
+ { name: 'Security' },
248
+ { name: 'Performance' },
249
+ { name: 'Reliability' },
250
+ ]);
251
+ expect((opt.series as any[])[0].data[0]).to.deep.equal({
252
+ name: 'Current',
253
+ value: [80, 65, 90],
254
+ });
255
+ });
256
+
257
+ it('uses explicit radar indicators when supplied', () => {
258
+ const indicators = [
259
+ { name: 'Security', max: 100 },
260
+ { name: 'Latency', max: 500, min: 0 },
261
+ ];
262
+ const opt = buildRadarOption({ ...baseProps, type: 'radar', indicators });
263
+ expect((opt.radar as any).indicator).to.deep.equal(indicators);
264
+ });
265
+
266
+ it('builds a gauge with bounds, progress, and a value suffix', () => {
267
+ const opt = buildGaugeOption({
268
+ ...baseProps,
269
+ type: 'gauge',
270
+ data: [{ name: 'Health', data: [82] }],
271
+ minValue: 20,
272
+ maxValue: 120,
273
+ yAxisAppend: '%',
274
+ });
275
+ const series = (opt.series as any[])[0];
276
+ expect(series).to.include({ type: 'gauge', min: 20, max: 120 });
277
+ expect(series.data).to.deep.equal([{ name: 'Health', value: 82 }]);
278
+ expect(series.detail.formatter(82)).to.equal('82%');
279
+ });
280
+
281
+ it('builds funnel stages from categories and numeric values', () => {
282
+ const opt = buildFunnelOption({
283
+ ...baseProps,
284
+ type: 'funnel',
285
+ categories: ['Visitors', 'Trials', 'Customers'],
286
+ data: [{ name: 'Conversion', data: [1000, 320, 85] }],
287
+ });
288
+ const series = (opt.series as any[])[0];
289
+ expect(series.type).to.equal('funnel');
290
+ expect(series.data).to.deep.equal([
291
+ { name: 'Visitors', value: 1000 },
292
+ { name: 'Trials', value: 320 },
293
+ { name: 'Customers', value: 85 },
294
+ ]);
295
+ expect(series.label.show).to.equal(true);
296
+ });
297
+
298
+ it('builds a categorical heatmap and derives its visual range', () => {
299
+ const data = [[0, 0, 5], [1, 0, 12], [0, 1, 3], [1, 1, 9]];
300
+ const opt = buildHeatmapOption({
301
+ ...baseProps,
302
+ type: 'heatmap',
303
+ categories: ['Mon', 'Tue'],
304
+ yCategories: ['API', 'Web'],
305
+ data: [{ name: 'Requests', data }],
306
+ });
307
+ expect((opt.xAxis as any).data).to.deep.equal(['Mon', 'Tue']);
308
+ expect((opt.yAxis as any).data).to.deep.equal(['API', 'Web']);
309
+ expect((opt.visualMap as any)).to.include({ min: 3, max: 12 });
310
+ expect((opt.series as any[])[0].data).to.equal(data);
311
+ });
312
+
313
+ it('passes hierarchical data to treemap and sunburst charts', () => {
314
+ const hierarchy = [{
315
+ name: 'Security',
316
+ children: [
317
+ { name: 'Configuration', value: 8 },
318
+ { name: 'Insecure', value: 1 },
319
+ ],
320
+ }];
321
+ const data = [{ name: 'Insights', data: hierarchy }];
322
+ const treemap = buildTreemapOption({ ...baseProps, type: 'treemap', data });
323
+ const sunburst = buildSunburstOption({
324
+ ...baseProps,
325
+ type: 'sunburst',
326
+ data,
327
+ innerRadius: '10%',
328
+ outerRadius: '85%',
329
+ });
330
+ expect((treemap.series as any[])[0].data).to.equal(hierarchy);
331
+ expect((treemap.series as any[])[0].label.show).to.equal(true);
332
+ expect((sunburst.series as any[])[0].data).to.equal(hierarchy);
333
+ expect((sunburst.series as any[])[0].radius).to.deep.equal(['10%', '85%']);
334
+ });
335
+ });
336
+
148
337
  describe('buildSankeyOption', () => {
149
338
  const sankeyProps: BuilderProps = {
150
339
  type: 'sankey',
@@ -208,4 +397,4 @@ describe('buildSankeyOption', () => {
208
397
  const opt = buildSankeyOption(sankeyProps);
209
398
  expect((opt.tooltip as any).trigger).to.equal('item');
210
399
  });
211
- });
400
+ });