@kubex/zinc 1.0.105 → 1.0.107

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.
@@ -89,6 +89,31 @@ The group scales well with multiple translation inputs under a single toggle.
89
89
  </zn-translation-group>
90
90
  ```
91
91
 
92
+ ### Language Overflow
93
+
94
+ When there are more languages than will fit across the header, the extras collapse into a chevron dropdown beside the
95
+ visible buttons. The dropdown sits before the `+` add-language dropdown and selects the same way — picking a language
96
+ switches every child translation input in the group.
97
+
98
+ ```html:preview
99
+ <div style="max-width: 480px;">
100
+ <zn-translation-group
101
+ label="Release Notes"
102
+ languages='{"en":"English","ar":"Arabic","de":"German","es":"Spanish","fr":"French","it":"Italian","ja":"Japanese","ko":"Korean","pt":"Portuguese","ru":"Russian","tr":"Turkish","zh-hans":"Simplified Chinese"}'>
103
+ <zn-translations
104
+ label="Headline"
105
+ name="headline"
106
+ values='{"en":"Now shipping","ar":"متاح الآن","de":"Jetzt verfügbar","es":"Ya disponible","fr":"Disponible dès maintenant","it":"Disponibile ora","ja":"発売開始","ko":"출시됨","pt":"Disponível agora","ru":"Уже в продаже","tr":"Şimdi mevcut","zh-hans":"现已发布"}'
107
+ ></zn-translations>
108
+ <zn-translations
109
+ label="Summary"
110
+ name="summary"
111
+ values='{"en":"Tap through the release highlights.","ar":"تصفح أبرز ميزات الإصدار.","de":"Highlights der Version ansehen.","es":"Consulta lo más destacado.","fr":"Découvrez les nouveautés.","it":"Scopri le novità.","ja":"リリースのハイライトをご覧ください。","ko":"업데이트 주요 내용 보기.","pt":"Veja os destaques.","ru":"Ознакомьтесь с обновлениями.","tr":"Sürüm önemli noktaları.","zh-hans":"浏览版本亮点。"}'
112
+ ></zn-translations>
113
+ </zn-translation-group>
114
+ </div>
115
+ ```
116
+
92
117
  ### Flush Layout
93
118
 
94
119
  Remove body padding for a more compact appearance using the `flush` attribute.
@@ -141,7 +141,8 @@ Use the `expand` slot to add custom buttons or actions.
141
141
 
142
142
  ### Many Languages
143
143
 
144
- The component handles many languages gracefully with scrollable tabs.
144
+ When the language buttons don't all fit on one line, the overflow collapses into a chevron dropdown so the row stays a
145
+ single line. Resizing the container re-measures and expands the buttons back out when space allows.
145
146
 
146
147
  ```html:preview
147
148
  <zn-translations
@@ -151,6 +152,21 @@ The component handles many languages gracefully with scrollable tabs.
151
152
  ></zn-translations>
152
153
  ```
153
154
 
155
+ ### Constrained Width
156
+
157
+ Placing the component in a narrow container forces the chevron overflow to kick in. Selecting a language from the
158
+ dropdown switches the active language just like clicking a visible button.
159
+
160
+ ```html:preview
161
+ <div style="max-width: 320px;">
162
+ <zn-translations
163
+ label="Narrow Container"
164
+ languages='{"en":"English","fr":"French","de":"German","es":"Spanish","it":"Italian","pt":"Portuguese","ja":"Japanese","ko":"Korean","zh-hans":"Simplified Chinese"}'
165
+ values='{"en":"Hello","fr":"Bonjour","de":"Hallo","es":"Hola","it":"Ciao","pt":"Olá","ja":"こんにちは","ko":"안녕하세요","zh-hans":"你好"}'
166
+ ></zn-translations>
167
+ </div>
168
+ ```
169
+
154
170
  ### RTL Language Support
155
171
 
156
172
  The component automatically detects and applies right-to-left text direction for Arabic and Hebrew languages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.105",
3
+ "version": "1.0.107",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -46,9 +46,8 @@
46
46
  "@lit/task": "^1.0.1",
47
47
  "@shoelace-style/localize": "^3.2.1",
48
48
  "air-datepicker": "^3.5.3",
49
- "apexcharts": "^4.5.0",
50
- "chart.js": "^4.4.8",
51
49
  "composed-offset-position": "^0.0.6",
50
+ "echarts": "^6.0.0",
52
51
  "emoji-mart": "^5.6.0",
53
52
  "lit": "^2.7.6",
54
53
  "marked": "^18.0.2",
@@ -77,9 +76,9 @@
77
76
  "custom-element-vs-code-integration": "^1.5.0",
78
77
  "custom-element-vuejs-integration": "^1.4.0",
79
78
  "del": "^8.0.0",
80
- "esbuild": "^0.25.0",
79
+ "esbuild": "^0.27.7",
81
80
  "esbuild-plugin-replace": "^1.4.0",
82
- "esbuild-sass-plugin": "^3.3.1",
81
+ "esbuild-sass-plugin": "^3.7.0",
83
82
  "eslint": "^8.57.1",
84
83
  "eslint-plugin-chai-expect": "^3.1.0",
85
84
  "eslint-plugin-chai-friendly": "^0.7.2",
@@ -94,7 +93,7 @@
94
93
  "jsdom": "^26.0.0",
95
94
  "lodash": "^4.17.21",
96
95
  "lunr": "^2.3.9",
97
- "markdown-it": "^14.1.0",
96
+ "markdown-it": "^14.1.1",
98
97
  "markdown-it-container": "^4.0.0",
99
98
  "markdown-it-ins": "^4.0.0",
100
99
  "markdown-it-kbd": "^2.2.2",
@@ -0,0 +1,211 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call */
2
+ import {
3
+ buildAreaOption,
4
+ buildBarOption,
5
+ type BuilderProps,
6
+ buildLineOption,
7
+ buildSankeyOption,
8
+ } from './builders';
9
+ import { expect } from '@open-wc/testing';
10
+
11
+ const baseProps: BuilderProps = {
12
+ type: 'bar',
13
+ data: [{ name: 'Sales', data: [10, 20, 30] }],
14
+ categories: ['Q1', 'Q2', 'Q3'],
15
+ stacked: false,
16
+ enableAnimations: false,
17
+ datapointSize: 1,
18
+ theme: 'light',
19
+ };
20
+
21
+ describe('buildBarOption', () => {
22
+ it('maps a single series into bar type with categories', () => {
23
+ const opt = buildBarOption(baseProps);
24
+ expect(opt.series).to.have.lengthOf(1);
25
+ expect((opt.series as any[])[0].type).to.equal('bar');
26
+ expect((opt.series as any[])[0].name).to.equal('Sales');
27
+ expect((opt.series as any[])[0].data).to.deep.equal([10, 20, 30]);
28
+ expect((opt.xAxis as any).type).to.equal('category');
29
+ expect((opt.xAxis as any).data).to.deep.equal(['Q1', 'Q2', 'Q3']);
30
+ });
31
+
32
+ it('applies stacking when stacked=true', () => {
33
+ const opt = buildBarOption({
34
+ ...baseProps,
35
+ stacked: true,
36
+ data: [
37
+ { name: 'A', data: [1, 2] },
38
+ { name: 'B', data: [3, 4] },
39
+ ],
40
+ });
41
+ expect((opt.series as any[])[0].stack).to.equal('total');
42
+ expect((opt.series as any[])[1].stack).to.equal('total');
43
+ });
44
+
45
+ it('does not stack when stacked=false', () => {
46
+ const opt = buildBarOption({
47
+ ...baseProps,
48
+ data: [
49
+ { name: 'A', data: [1, 2] },
50
+ { name: 'B', data: [3, 4] },
51
+ ],
52
+ });
53
+ expect((opt.series as any[])[0].stack).to.be.undefined;
54
+ });
55
+
56
+ it('appends y-axis suffix via formatter', () => {
57
+ const opt = buildBarOption({ ...baseProps, yAxisAppend: '%' });
58
+ const formatter = ((opt.yAxis as any).axisLabel.formatter) as (v: number) => string;
59
+ expect(formatter(42)).to.equal('42%');
60
+ });
61
+
62
+ it('maps per-series color to itemStyle.color', () => {
63
+ const opt = buildBarOption({
64
+ ...baseProps,
65
+ data: [{ name: 'Sales', data: [1, 2, 3], color: '#ff0000' }],
66
+ });
67
+ expect((opt.series as any[])[0].itemStyle.color).to.equal('#ff0000');
68
+ });
69
+
70
+ it('maps colors array to option.color', () => {
71
+ const opt = buildBarOption({ ...baseProps, colors: ['#abc', '#def'] });
72
+ expect(opt.color).to.deep.equal(['#abc', '#def']);
73
+ });
74
+
75
+ it('disables animation by default', () => {
76
+ const opt = buildBarOption(baseProps);
77
+ expect(opt.animation).to.equal(false);
78
+ });
79
+
80
+ it('enables animation when enableAnimations=true', () => {
81
+ const opt = buildBarOption({ ...baseProps, enableAnimations: true });
82
+ expect(opt.animation).to.equal(true);
83
+ });
84
+ });
85
+
86
+ describe('buildLineOption', () => {
87
+ const baseLineProps: BuilderProps = {
88
+ type: 'line',
89
+ data: [{ name: 'Revenue', data: [10, 20, 30] }],
90
+ categories: ['Jan', 'Feb', 'Mar'],
91
+ stacked: false,
92
+ enableAnimations: false,
93
+ datapointSize: 3,
94
+ theme: 'light',
95
+ };
96
+
97
+ it('maps a series to line type', () => {
98
+ const opt = buildLineOption(baseLineProps);
99
+ expect((opt.series as any[])[0].type).to.equal('line');
100
+ });
101
+
102
+ it('applies symbolSize from datapointSize', () => {
103
+ const opt = buildLineOption(baseLineProps);
104
+ expect((opt.series as any[])[0].symbolSize).to.equal(3);
105
+ });
106
+
107
+ it('uses datetime x-axis when xAxisType=datetime', () => {
108
+ const opt = buildLineOption({ ...baseLineProps, xAxisType: 'datetime' });
109
+ expect((opt.xAxis as any).type).to.equal('time');
110
+ });
111
+
112
+ it('does not apply areaStyle for plain line', () => {
113
+ const opt = buildLineOption(baseLineProps);
114
+ expect((opt.series as any[])[0].areaStyle).to.be.undefined;
115
+ });
116
+ });
117
+
118
+ describe('buildAreaOption', () => {
119
+ const baseAreaProps: BuilderProps = {
120
+ type: 'area',
121
+ data: [{ name: 'Traffic', data: [100, 200, 300] }],
122
+ categories: ['Mon', 'Tue', 'Wed'],
123
+ stacked: false,
124
+ enableAnimations: false,
125
+ datapointSize: 1,
126
+ theme: 'light',
127
+ };
128
+
129
+ it('maps a series to line type with areaStyle set', () => {
130
+ const opt = buildAreaOption(baseAreaProps);
131
+ expect((opt.series as any[])[0].type).to.equal('line');
132
+ expect((opt.series as any[])[0].areaStyle).to.be.an('object');
133
+ });
134
+
135
+ it('applies stacking when stacked=true', () => {
136
+ const opt = buildAreaOption({
137
+ ...baseAreaProps,
138
+ stacked: true,
139
+ data: [
140
+ { name: 'A', data: [1, 2] },
141
+ { name: 'B', data: [3, 4] },
142
+ ],
143
+ });
144
+ expect((opt.series as any[])[0].stack).to.equal('total');
145
+ });
146
+ });
147
+
148
+ describe('buildSankeyOption', () => {
149
+ const sankeyProps: BuilderProps = {
150
+ type: 'sankey',
151
+ data: [{
152
+ name: 'Flow',
153
+ data: [
154
+ { source: 'A', target: 'B', value: 10 },
155
+ { source: 'A', target: 'C', value: 5 },
156
+ { source: 'B', target: 'D', value: 7 },
157
+ ],
158
+ }],
159
+ categories: [],
160
+ stacked: false,
161
+ enableAnimations: false,
162
+ datapointSize: 1,
163
+ theme: 'light',
164
+ };
165
+
166
+ it('builds one sankey series with links mapped from edges', () => {
167
+ const opt = buildSankeyOption(sankeyProps);
168
+ const series = (opt.series as any[])[0];
169
+ expect(series.type).to.equal('sankey');
170
+ expect(series.links).to.deep.equal([
171
+ { source: 'A', target: 'B', value: 10 },
172
+ { source: 'A', target: 'C', value: 5 },
173
+ { source: 'B', target: 'D', value: 7 },
174
+ ]);
175
+ });
176
+
177
+ it('derives unique nodes from edge source/target values', () => {
178
+ const opt = buildSankeyOption(sankeyProps);
179
+ const series = (opt.series as any[])[0];
180
+ const names = series.data.map((n: any) => n.name).sort();
181
+ expect(names).to.deep.equal(['A', 'B', 'C', 'D']);
182
+ });
183
+
184
+ it('omits xAxis and yAxis (sankey has no cartesian axes)', () => {
185
+ const opt = buildSankeyOption(sankeyProps);
186
+ expect(opt.xAxis).to.be.undefined;
187
+ expect(opt.yAxis).to.be.undefined;
188
+ });
189
+
190
+ it('passes through explicit nodes when provided on the series object', () => {
191
+ const withNodes = {
192
+ ...sankeyProps,
193
+ data: [{
194
+ name: 'Flow',
195
+ nodes: [
196
+ { name: 'A', itemStyle: { color: '#f00' } },
197
+ { name: 'B' }, { name: 'C' }, { name: 'D' },
198
+ ],
199
+ data: sankeyProps.data[0].data,
200
+ }] as any,
201
+ };
202
+ const opt = buildSankeyOption(withNodes);
203
+ const series = (opt.series as any[])[0];
204
+ expect(series.data[0]).to.deep.equal({ name: 'A', itemStyle: { color: '#f00' } });
205
+ });
206
+
207
+ it('uses tooltip trigger=item (not axis) for sankey', () => {
208
+ const opt = buildSankeyOption(sankeyProps);
209
+ expect((opt.tooltip as any).trigger).to.equal('item');
210
+ });
211
+ });
@@ -0,0 +1,221 @@
1
+ // src/components/chart/builders.ts
2
+ import type { EChartsOption } from 'echarts';
3
+
4
+ export type ChartType = 'area' | 'bar' | 'line' | 'sankey';
5
+
6
+ export interface SeriesItem {
7
+ name: string;
8
+ data: any[];
9
+ color?: string;
10
+ }
11
+
12
+ export interface SankeyEdge {
13
+ source: string;
14
+ target: string;
15
+ value: number;
16
+ }
17
+
18
+ export interface BuilderProps {
19
+ type: ChartType;
20
+ data: SeriesItem[];
21
+ categories: string[];
22
+ xAxisType?: 'datetime' | 'category' | 'numeric';
23
+ yAxisAppend?: string;
24
+ stacked: boolean;
25
+ enableAnimations: boolean | number;
26
+ datapointSize: number;
27
+ colors?: string[];
28
+ theme: 'light' | 'dark';
29
+ smooth?: boolean;
30
+ scale?: boolean | number;
31
+ textColor?: string;
32
+ borderColor?: string;
33
+ }
34
+
35
+ function commonOption(props: BuilderProps): EChartsOption {
36
+ const fallback = props.theme === 'dark' ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)';
37
+ const textColor = props.textColor ?? fallback;
38
+ const animEnabled = props.enableAnimations !== false && props.enableAnimations !== 0;
39
+ const animDuration = typeof props.enableAnimations === 'number' ? props.enableAnimations : 1500;
40
+ return {
41
+ animation: animEnabled,
42
+ animationDuration: animDuration,
43
+ animationEasing: 'cubicOut',
44
+ ...(props.colors ? { color: props.colors } : {}),
45
+ textStyle: { color: textColor },
46
+ tooltip: {
47
+ trigger: 'axis',
48
+ valueFormatter: props.yAxisAppend
49
+ ? (v: number | string) => `${v}${props.yAxisAppend}`
50
+ : undefined,
51
+ },
52
+ legend: {
53
+ top: 0,
54
+ right: 0,
55
+ icon: 'circle',
56
+ textStyle: { color: textColor },
57
+ },
58
+ grid: {
59
+ left: 40,
60
+ right: 20,
61
+ top: 40,
62
+ bottom: 30,
63
+ },
64
+ };
65
+ }
66
+
67
+ function hasNoData(data: SeriesItem[]): boolean {
68
+ return data.length === 0 || data.every((s) => !s.data || s.data.length === 0);
69
+ }
70
+
71
+ function buildYAxis(props: BuilderProps) {
72
+ const emptyOpts = hasNoData(props.data) ? { min: 0, max: 6 } : {};
73
+ const scaleOpts = props.scale
74
+ ? {
75
+ scale: true,
76
+ ...(typeof props.scale === 'number'
77
+ ? { boundaryGap: [`${props.scale}%`, `${props.scale}%`] as [string, string] }
78
+ : {}),
79
+ }
80
+ : {};
81
+ const splitLineOpts = props.borderColor
82
+ ? { splitLine: { lineStyle: { color: props.borderColor } } }
83
+ : {};
84
+ return {
85
+ type: 'value' as const,
86
+ ...splitLineOpts,
87
+ ...emptyOpts,
88
+ ...scaleOpts,
89
+ axisLabel: props.yAxisAppend
90
+ ? { formatter: (v: number) => `${v}${props.yAxisAppend}` }
91
+ : {},
92
+ };
93
+ }
94
+
95
+ function buildXAxis(props: BuilderProps, edgeToEdge = false) {
96
+ const axisLineOpts = props.borderColor
97
+ ? { axisLine: { lineStyle: { color: props.borderColor } } }
98
+ : {};
99
+ if (props.xAxisType === 'datetime') return { type: 'time' as const, ...axisLineOpts };
100
+ if (props.xAxisType === 'numeric') return { type: 'value' as const, ...axisLineOpts };
101
+ return {
102
+ type: 'category' as const,
103
+ data: props.categories,
104
+ ...(edgeToEdge ? { boundaryGap: false } : {}),
105
+ ...axisLineOpts,
106
+ };
107
+ }
108
+
109
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */
110
+ function normalizeData(data: any[]): any[] {
111
+ return data.map((d: any) => {
112
+ if (d && typeof d === 'object' && !Array.isArray(d) && 'x' in d && 'y' in d) {
113
+ return [d.x, d.y];
114
+ }
115
+ return d;
116
+ });
117
+ }
118
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */
119
+
120
+ function seriesFromProps(
121
+ props: BuilderProps,
122
+ seriesType: 'bar' | 'line',
123
+ extra: (s: SeriesItem) => Record<string, unknown> = () => ({}),
124
+ ) {
125
+ return props.data.map((s) => ({
126
+ type: seriesType,
127
+ name: s.name,
128
+ data: normalizeData(s.data),
129
+ ...(s.color ? { itemStyle: { color: s.color } } : {}),
130
+ ...(props.stacked ? { stack: 'total' } : {}),
131
+ ...(props.enableAnimations !== false && props.enableAnimations !== 0 && seriesType === 'bar'
132
+ ? { animationDelay: (idx: number) => idx * 50 }
133
+ : {}),
134
+ ...extra(s),
135
+ }));
136
+ }
137
+
138
+ export function buildBarOption(props: BuilderProps): EChartsOption {
139
+ return {
140
+ ...commonOption(props),
141
+ xAxis: buildXAxis(props),
142
+ yAxis: buildYAxis(props),
143
+ series: seriesFromProps(props, 'bar'),
144
+ };
145
+ }
146
+
147
+ export function buildLineOption(props: BuilderProps): EChartsOption {
148
+ return {
149
+ ...commonOption(props),
150
+ xAxis: buildXAxis(props, true),
151
+ yAxis: buildYAxis(props),
152
+ series: seriesFromProps(props, 'line', () => ({
153
+ symbolSize: props.datapointSize,
154
+ smooth: props.smooth,
155
+ })),
156
+ };
157
+ }
158
+
159
+ export function buildAreaOption(props: BuilderProps): EChartsOption {
160
+ return {
161
+ ...commonOption(props),
162
+ xAxis: buildXAxis(props, true),
163
+ yAxis: buildYAxis(props),
164
+ series: seriesFromProps(props, 'line', () => ({
165
+ symbolSize: props.datapointSize,
166
+ smooth: props.smooth,
167
+ areaStyle: { opacity: 0.1 },
168
+ })),
169
+ };
170
+ }
171
+
172
+ function deriveNodes(edges: SankeyEdge[]): { name: string }[] {
173
+ const seen = new Set<string>();
174
+ const nodes: { name: string }[] = [];
175
+ for (const edge of edges) {
176
+ if (!seen.has(edge.source)) {
177
+ seen.add(edge.source);
178
+ nodes.push({ name: edge.source });
179
+ }
180
+ if (!seen.has(edge.target)) {
181
+ seen.add(edge.target);
182
+ nodes.push({ name: edge.target });
183
+ }
184
+ }
185
+ return nodes;
186
+ }
187
+
188
+ export function buildSankeyOption(props: BuilderProps): EChartsOption {
189
+ const first = props.data[0] ?? { name: '', data: [] };
190
+ const edges = (first.data ?? []) as SankeyEdge[];
191
+ const explicitNodes = (first as { nodes?: { name: string }[] }).nodes;
192
+ const nodes = explicitNodes ?? deriveNodes(edges);
193
+ const fallback = props.theme === 'dark' ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)';
194
+ const textColor = props.textColor ?? fallback;
195
+
196
+ const animEnabled = props.enableAnimations !== false && props.enableAnimations !== 0;
197
+ const animDuration = typeof props.enableAnimations === 'number' ? props.enableAnimations : 1500;
198
+ return {
199
+ animation: animEnabled,
200
+ animationDuration: animDuration,
201
+ animationEasing: 'cubicOut',
202
+ ...(props.colors ? { color: props.colors } : {}),
203
+ textStyle: { color: textColor },
204
+ tooltip: { trigger: 'item' },
205
+ series: [{
206
+ type: 'sankey',
207
+ name: first.name,
208
+ data: nodes,
209
+ links: edges,
210
+ label: {
211
+ color: textColor,
212
+ textBorderWidth: 0,
213
+ },
214
+ lineStyle: {
215
+ color: 'source',
216
+ opacity: props.theme === 'dark' ? 0.4 : 0.3,
217
+ },
218
+ emphasis: { focus: 'adjacency' },
219
+ }],
220
+ };
221
+ }