@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
@@ -1,7 +1,20 @@
1
1
  // src/components/chart/builders.ts
2
2
  import type { EChartsOption } from 'echarts';
3
3
 
4
- export type ChartType = 'area' | 'bar' | 'line' | 'sankey';
4
+ export type ChartType =
5
+ | 'area'
6
+ | 'bar'
7
+ | 'donut'
8
+ | 'funnel'
9
+ | 'gauge'
10
+ | 'heatmap'
11
+ | 'line'
12
+ | 'pie'
13
+ | 'radar'
14
+ | 'sankey'
15
+ | 'scatter'
16
+ | 'sunburst'
17
+ | 'treemap';
5
18
 
6
19
  export interface SeriesItem {
7
20
  name: string;
@@ -15,6 +28,20 @@ export interface SankeyEdge {
15
28
  value: number;
16
29
  }
17
30
 
31
+ export interface PieDataItem {
32
+ name?: string;
33
+ value: number;
34
+ color?: string;
35
+ itemStyle?: Record<string, unknown>;
36
+ }
37
+
38
+ export interface RadarIndicator {
39
+ name: string;
40
+ max?: number;
41
+ min?: number;
42
+ color?: string;
43
+ }
44
+
18
45
  export interface BuilderProps {
19
46
  type: ChartType;
20
47
  data: SeriesItem[];
@@ -30,9 +57,16 @@ export interface BuilderProps {
30
57
  scale?: boolean | number;
31
58
  textColor?: string;
32
59
  borderColor?: string;
60
+ innerRadius?: number | string;
61
+ outerRadius?: number | string;
62
+ showLabels?: boolean;
63
+ yCategories?: string[];
64
+ indicators?: RadarIndicator[];
65
+ minValue?: number;
66
+ maxValue?: number;
33
67
  }
34
68
 
35
- function commonOption(props: BuilderProps): EChartsOption {
69
+ function baseOption(props: BuilderProps, tooltipTrigger: 'axis' | 'item'): EChartsOption {
36
70
  const fallback = props.theme === 'dark' ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)';
37
71
  const textColor = props.textColor ?? fallback;
38
72
  const animEnabled = props.enableAnimations !== false && props.enableAnimations !== 0;
@@ -44,7 +78,7 @@ function commonOption(props: BuilderProps): EChartsOption {
44
78
  ...(props.colors ? { color: props.colors } : {}),
45
79
  textStyle: { color: textColor },
46
80
  tooltip: {
47
- trigger: 'axis',
81
+ trigger: tooltipTrigger,
48
82
  appendTo: 'body',
49
83
  valueFormatter: props.yAxisAppend
50
84
  ? (v: number | string) => `${v}${props.yAxisAppend}`
@@ -56,6 +90,12 @@ function commonOption(props: BuilderProps): EChartsOption {
56
90
  icon: 'circle',
57
91
  textStyle: { color: textColor },
58
92
  },
93
+ };
94
+ }
95
+
96
+ function commonOption(props: BuilderProps): EChartsOption {
97
+ return {
98
+ ...baseOption(props, 'axis'),
59
99
  grid: {
60
100
  left: 40,
61
101
  right: 20,
@@ -120,7 +160,7 @@ function normalizeData(data: any[]): any[] {
120
160
 
121
161
  function seriesFromProps(
122
162
  props: BuilderProps,
123
- seriesType: 'bar' | 'line',
163
+ seriesType: 'bar' | 'line' | 'scatter',
124
164
  extra: (s: SeriesItem) => Record<string, unknown> = () => ({}),
125
165
  ) {
126
166
  return props.data.map((s) => ({
@@ -170,6 +210,208 @@ export function buildAreaOption(props: BuilderProps): EChartsOption {
170
210
  };
171
211
  }
172
212
 
213
+ export function buildScatterOption(props: BuilderProps): EChartsOption {
214
+ return {
215
+ ...commonOption(props),
216
+ xAxis: buildXAxis({ ...props, xAxisType: props.xAxisType ?? 'numeric' }),
217
+ yAxis: buildYAxis(props),
218
+ series: seriesFromProps(props, 'scatter', () => ({
219
+ symbolSize: props.datapointSize,
220
+ })),
221
+ };
222
+ }
223
+
224
+ function normalizePieData(data: any[], categories: string[]): PieDataItem[] {
225
+ return data.map((item: number | PieDataItem, index) => {
226
+ if (typeof item === 'number') {
227
+ return { name: categories[index] ?? `${index + 1}`, value: item };
228
+ }
229
+
230
+ const { color, itemStyle, ...rest } = item;
231
+ return {
232
+ ...rest,
233
+ name: item.name ?? categories[index] ?? `${index + 1}`,
234
+ ...(color ? { itemStyle: { ...itemStyle, color } } : itemStyle ? { itemStyle } : {}),
235
+ };
236
+ });
237
+ }
238
+
239
+ export function buildPieOption(props: BuilderProps): EChartsOption {
240
+ const first = props.data[0] ?? { name: '', data: [] };
241
+ const textColor = props.textColor
242
+ ?? (props.theme === 'dark' ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)');
243
+ const innerRadius = props.type === 'donut' ? (props.innerRadius ?? '50%') : 0;
244
+ const outerRadius = props.outerRadius ?? '70%';
245
+
246
+ return {
247
+ ...baseOption(props, 'item'),
248
+ series: [{
249
+ type: 'pie',
250
+ name: first.name,
251
+ data: normalizePieData(first.data ?? [], props.categories),
252
+ radius: [innerRadius, outerRadius],
253
+ center: ['50%', '55%'],
254
+ avoidLabelOverlap: true,
255
+ label: {
256
+ show: props.showLabels ?? false,
257
+ color: textColor,
258
+ },
259
+ labelLine: {
260
+ show: props.showLabels ?? false,
261
+ ...(props.borderColor ? { lineStyle: { color: props.borderColor } } : {}),
262
+ },
263
+ emphasis: {
264
+ label: { show: props.showLabels ?? false },
265
+ },
266
+ }],
267
+ };
268
+ }
269
+
270
+ export function buildRadarOption(props: BuilderProps): EChartsOption {
271
+ const indicators = props.indicators?.length
272
+ ? props.indicators
273
+ : props.categories.map((name) => ({ name }));
274
+
275
+ return {
276
+ ...baseOption(props, 'item'),
277
+ radar: { indicator: indicators },
278
+ series: [{
279
+ type: 'radar',
280
+ data: props.data.map((series) => ({
281
+ name: series.name,
282
+ value: normalizeData(series.data),
283
+ ...(series.color ? {
284
+ itemStyle: { color: series.color },
285
+ lineStyle: { color: series.color },
286
+ } : {}),
287
+ })),
288
+ }],
289
+ };
290
+ }
291
+
292
+ export function buildGaugeOption(props: BuilderProps): EChartsOption {
293
+ const first = props.data[0] ?? { name: '', data: [] };
294
+ const [firstValue] = first.data as unknown[];
295
+ const data = typeof firstValue === 'number'
296
+ ? [{ name: first.name, value: firstValue }]
297
+ : normalizePieData(first.data ?? [], props.categories);
298
+
299
+ return {
300
+ ...baseOption(props, 'item'),
301
+ series: [{
302
+ type: 'gauge',
303
+ name: first.name,
304
+ min: props.minValue ?? 0,
305
+ max: props.maxValue ?? 100,
306
+ data,
307
+ progress: { show: true },
308
+ detail: {
309
+ valueAnimation: props.enableAnimations !== false && props.enableAnimations !== 0,
310
+ formatter: props.yAxisAppend
311
+ ? (value: number) => `${value}${props.yAxisAppend}`
312
+ : '{value}',
313
+ },
314
+ }],
315
+ };
316
+ }
317
+
318
+ export function buildFunnelOption(props: BuilderProps): EChartsOption {
319
+ const first = props.data[0] ?? { name: '', data: [] };
320
+ return {
321
+ ...baseOption(props, 'item'),
322
+ series: [{
323
+ type: 'funnel',
324
+ name: first.name,
325
+ min: props.minValue,
326
+ max: props.maxValue,
327
+ data: normalizePieData(first.data ?? [], props.categories),
328
+ label: { show: props.showLabels ?? true },
329
+ emphasis: { label: { fontWeight: 'bold' } },
330
+ }],
331
+ };
332
+ }
333
+
334
+ function isHeatmapPoint(item: unknown): item is [unknown, unknown, number] {
335
+ if (!Array.isArray(item)) return false;
336
+ const point = item as unknown[];
337
+ return typeof point[2] === 'number';
338
+ }
339
+
340
+ function heatmapExtent(data: unknown[], operation: 'max' | 'min', fallback: number): number {
341
+ const values = data
342
+ .filter(isHeatmapPoint)
343
+ .map((item) => item[2]);
344
+ return values.length ? Math[operation](...values) : fallback;
345
+ }
346
+
347
+ export function buildHeatmapOption(props: BuilderProps): EChartsOption {
348
+ const first = props.data[0] ?? { name: '', data: [] };
349
+ const data = first.data ?? [];
350
+ return {
351
+ ...baseOption(props, 'item'),
352
+ grid: { left: 60, right: 20, top: 40, bottom: 70 },
353
+ xAxis: {
354
+ type: 'category',
355
+ data: props.categories,
356
+ splitArea: { show: true },
357
+ },
358
+ yAxis: {
359
+ type: 'category',
360
+ data: props.yCategories ?? [],
361
+ splitArea: { show: true },
362
+ },
363
+ visualMap: {
364
+ min: props.minValue ?? heatmapExtent(data, 'min', 0),
365
+ max: props.maxValue ?? heatmapExtent(data, 'max', 100),
366
+ calculable: true,
367
+ orient: 'horizontal',
368
+ left: 'center',
369
+ bottom: 0,
370
+ },
371
+ series: [{
372
+ type: 'heatmap',
373
+ name: first.name,
374
+ data,
375
+ label: { show: props.showLabels ?? false },
376
+ emphasis: {
377
+ itemStyle: {
378
+ shadowBlur: 10,
379
+ shadowColor: 'rgba(0, 0, 0, 0.35)',
380
+ },
381
+ },
382
+ }],
383
+ };
384
+ }
385
+
386
+ export function buildTreemapOption(props: BuilderProps): EChartsOption {
387
+ const first = props.data[0] ?? { name: '', data: [] };
388
+ return {
389
+ ...baseOption(props, 'item'),
390
+ series: [{
391
+ type: 'treemap',
392
+ name: first.name,
393
+ data: first.data ?? [],
394
+ label: { show: props.showLabels ?? true },
395
+ upperLabel: { show: props.showLabels ?? true },
396
+ }],
397
+ };
398
+ }
399
+
400
+ export function buildSunburstOption(props: BuilderProps): EChartsOption {
401
+ const first = props.data[0] ?? { name: '', data: [] };
402
+ return {
403
+ ...baseOption(props, 'item'),
404
+ series: [{
405
+ type: 'sunburst',
406
+ name: first.name,
407
+ data: first.data ?? [],
408
+ radius: [props.innerRadius ?? 0, props.outerRadius ?? '90%'],
409
+ label: { show: props.showLabels ?? true },
410
+ emphasis: { focus: 'ancestor' },
411
+ }],
412
+ };
413
+ }
414
+
173
415
  function deriveNodes(edges: SankeyEdge[]): { name: string }[] {
174
416
  const seen = new Set<string>();
175
417
  const nodes: { name: string }[] = [];
@@ -2,9 +2,18 @@ import {
2
2
  buildAreaOption,
3
3
  buildBarOption,
4
4
  type BuilderProps,
5
+ buildFunnelOption,
6
+ buildGaugeOption,
7
+ buildHeatmapOption,
5
8
  buildLineOption,
9
+ buildPieOption,
10
+ buildRadarOption,
6
11
  buildSankeyOption,
12
+ buildScatterOption,
13
+ buildSunburstOption,
14
+ buildTreemapOption,
7
15
  type ChartType,
16
+ type RadarIndicator,
8
17
  type SeriesItem,
9
18
  } from './builders';
10
19
  import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
@@ -29,6 +38,8 @@ export default class ZnChart extends ZincElement {
29
38
  @property() type: ChartType = 'bar';
30
39
  @property({ type: Array }) data: SeriesItem[] = [];
31
40
  @property({ type: Array }) categories: string[] = [];
41
+ @property({ attribute: 'y-categories', type: Array }) yCategories: string[] = [];
42
+ @property({ type: Array }) indicators: RadarIndicator[] = [];
32
43
 
33
44
  @property({ attribute: 'xaxis' }) xAxis: 'datetime' | 'category' | 'numeric';
34
45
  @property({ type: Number, attribute: 'd-size' }) datapointSize: number = 1;
@@ -55,6 +66,11 @@ export default class ZnChart extends ZincElement {
55
66
  @property({ type: Array }) colors?: string[];
56
67
  @property({ attribute: 'sync-group' }) syncGroup?: string;
57
68
  @property({ type: Boolean }) smooth = false;
69
+ @property({ attribute: 'inner-radius' }) innerRadius?: number | string;
70
+ @property({ attribute: 'outer-radius' }) outerRadius?: number | string;
71
+ @property({ attribute: 'show-labels', type: Boolean }) showLabels?: boolean;
72
+ @property({ attribute: 'min-value', type: Number }) minValue?: number;
73
+ @property({ attribute: 'max-value', type: Number }) maxValue?: number;
58
74
  @property({
59
75
  converter: {
60
76
  fromAttribute: (value: string | null) => {
@@ -122,12 +138,28 @@ export default class ZnChart extends ZincElement {
122
138
  scale: this.scale,
123
139
  textColor: this.getTextColor(),
124
140
  borderColor: this.getBorderColor(),
141
+ innerRadius: this.innerRadius,
142
+ outerRadius: this.outerRadius,
143
+ showLabels: this.showLabels,
144
+ yCategories: Array.isArray(this.yCategories) ? this.yCategories : [],
145
+ indicators: Array.isArray(this.indicators) ? this.indicators : [],
146
+ minValue: this.minValue,
147
+ maxValue: this.maxValue,
125
148
  };
126
149
  switch (this.type) {
127
150
  case 'bar': return buildBarOption(props);
128
151
  case 'line': return buildLineOption(props);
129
152
  case 'area': return buildAreaOption(props);
153
+ case 'funnel': return buildFunnelOption(props);
154
+ case 'gauge': return buildGaugeOption(props);
155
+ case 'heatmap': return buildHeatmapOption(props);
156
+ case 'donut':
157
+ case 'pie': return buildPieOption(props);
158
+ case 'radar': return buildRadarOption(props);
130
159
  case 'sankey': return buildSankeyOption(props);
160
+ case 'scatter': return buildScatterOption(props);
161
+ case 'sunburst': return buildSunburstOption(props);
162
+ case 'treemap': return buildTreemapOption(props);
131
163
  default: return buildBarOption(props);
132
164
  }
133
165
  }
@@ -207,4 +239,4 @@ export default class ZnChart extends ZincElement {
207
239
  protected render(): unknown {
208
240
  return html`<div class="chart"><div id="chart"></div></div>`;
209
241
  }
210
- }
242
+ }
@@ -41,6 +41,71 @@ describe('<zn-chart>', () => {
41
41
  expect(canvas).to.exist;
42
42
  });
43
43
 
44
+ it('renders pie and donut charts', async () => {
45
+ for (const type of ['pie', 'donut']) {
46
+ const el: any = await fixture(html`
47
+ <zn-chart
48
+ type=${type}
49
+ .data=${[{ name: 'Insights', data: [8, 1] }]}
50
+ .categories=${['Configuration suggestion', 'Insecure configuration']}
51
+ ></zn-chart>
52
+ `);
53
+ await el.updateComplete;
54
+ await new Promise((r) => setTimeout(r, 50));
55
+ const canvas = el.shadowRoot.querySelector('canvas');
56
+ expect(canvas, `${type} chart canvas`).to.exist;
57
+ }
58
+ });
59
+
60
+ it('renders the additional native chart types', async () => {
61
+ const cases = [
62
+ { type: 'scatter', data: [{ name: 'Points', data: [[1, 2], [2, 4]] }] },
63
+ {
64
+ type: 'radar',
65
+ data: [{ name: 'Current', data: [80, 65, 90] }],
66
+ categories: ['Security', 'Performance', 'Reliability'],
67
+ },
68
+ { type: 'gauge', data: [{ name: 'Health', data: [82] }] },
69
+ {
70
+ type: 'funnel',
71
+ data: [{ name: 'Conversion', data: [1000, 320, 85] }],
72
+ categories: ['Visitors', 'Trials', 'Customers'],
73
+ },
74
+ {
75
+ type: 'heatmap',
76
+ data: [{ name: 'Requests', data: [[0, 0, 5], [1, 0, 12]] }],
77
+ categories: ['Mon', 'Tue'],
78
+ yCategories: ['API'],
79
+ },
80
+ {
81
+ type: 'treemap',
82
+ data: [{ name: 'Insights', data: [{ name: 'Configuration', value: 8 }] }],
83
+ },
84
+ {
85
+ type: 'sunburst',
86
+ data: [{
87
+ name: 'Insights',
88
+ data: [{ name: 'Security', children: [{ name: 'Configuration', value: 8 }] }],
89
+ }],
90
+ },
91
+ ];
92
+
93
+ for (const chartCase of cases) {
94
+ const el: any = await fixture(html`
95
+ <zn-chart
96
+ type=${chartCase.type}
97
+ .data=${chartCase.data}
98
+ .categories=${chartCase.categories ?? []}
99
+ .yCategories=${chartCase.yCategories ?? []}
100
+ ></zn-chart>
101
+ `);
102
+ await el.updateComplete;
103
+ await new Promise((r) => setTimeout(r, 50));
104
+ const canvas = el.shadowRoot.querySelector('canvas');
105
+ expect(canvas, `${chartCase.type} chart canvas`).to.exist;
106
+ }
107
+ });
108
+
44
109
  it('joins a sync-group when the attribute is set', async () => {
45
110
  const a: any = await fixture(html`
46
111
  <zn-chart sync-group="g1" type="bar"
@@ -51,4 +116,4 @@ describe('<zn-chart>', () => {
51
116
  await new Promise((r) => setTimeout(r, 50));
52
117
  expect(a.chart?.group).to.equal('g1');
53
118
  });
54
- });
119
+ });
@@ -15,13 +15,23 @@ export function loadECharts(): Promise<EChartsModule> {
15
15
  ]);
16
16
  core.use([
17
17
  charts.BarChart,
18
+ charts.FunnelChart,
19
+ charts.GaugeChart,
20
+ charts.HeatmapChart,
18
21
  charts.LineChart,
22
+ charts.PieChart,
23
+ charts.RadarChart,
19
24
  charts.SankeyChart,
25
+ charts.ScatterChart,
26
+ charts.SunburstChart,
27
+ charts.TreemapChart,
20
28
  components.GridComponent,
21
29
  components.TooltipComponent,
22
30
  components.LegendComponent,
23
31
  components.TitleComponent,
24
32
  components.DataZoomComponent,
33
+ components.RadarComponent,
34
+ components.VisualMapComponent,
25
35
  renderers.CanvasRenderer,
26
36
  ]);
27
37
  return core;
@@ -9,6 +9,7 @@ import {property, query, state} from 'lit/decorators.js';
9
9
  import {watch} from '../../internal/watch';
10
10
  import ZincElement from '../../internal/zinc-element';
11
11
  import ZnIcon from "../icon";
12
+ import type {SelectionCardControlPosition, SelectionCardImagePosition} from '../../internal/selection-card';
12
13
  import type {ZincFormControl} from '../../internal/zinc-element';
13
14
 
14
15
  import styles from './checkbox.scss';
@@ -24,6 +25,7 @@ type ColorOption = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'suc
24
25
  * @dependency zn-icon
25
26
  *
26
27
  * @slot - The checkbox's label.
28
+ * @slot image - Replaces the built-in image used by contained checkboxes.
27
29
  * @slot description - A description of the checkbox's label. Serves as help text for a checkbox item. Alternatively, you can use the `description` attribute.
28
30
  * @slot selected-content - Use to nest rich content (like an input) inside a selected checkbox item. Use only with the contained style.
29
31
  *
@@ -40,9 +42,22 @@ type ColorOption = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'suc
40
42
  * @csspart checked-icon - The checked icon, an `<zn-icon>` element.
41
43
  * @csspart unchecked-icon - The unchecked icon, an `<zn-icon>` element.
42
44
  * @csspart indeterminate-icon - The indeterminate icon, an `<zn-icon>` element.
45
+ * @csspart image-container - The wrapper around the built-in image or image slot.
46
+ * @csspart image - The built-in image.
47
+ * @csspart card-title - The title inside a selection card.
43
48
  * @csspart label - The container that wraps the checkbox's label.
44
49
  * @csspart description - The container that wraps the checkbox's description.
45
50
  * @csspart selected-content - The container that wraps optional content that appears when a checkbox is checked.
51
+ *
52
+ * @cssproperty --zn-selection-card-image-width - Width of the built-in card image.
53
+ * @cssproperty --zn-selection-card-image-height - Height of the built-in card image.
54
+ * @cssproperty --zn-selection-card-min-height - Minimum height of a contained checkbox with an image.
55
+ * @cssproperty --zn-selection-card-content-min-width - Width the card text keeps before it wraps below the image.
56
+ * @cssproperty --zn-selection-card-title-font-size - Font size of a selection card title.
57
+ * @cssproperty --zn-selection-card-padding - Equal inset around the contents of a selection card.
58
+ * @cssproperty --zn-selection-card-gap - Space between the image, title, and indicator gutter.
59
+ * @cssproperty --zn-selection-card-control-offset - Distance between a positioned control and the card edge.
60
+ * @cssproperty --zn-selection-card-border-radius - Corner radius of a contained container.
46
61
  */
47
62
  export default class ZnCheckbox extends ZincElement implements ZincFormControl {
48
63
  static styles: CSSResultGroup = unsafeCSS(styles);
@@ -53,7 +68,7 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
53
68
  defaultValue: (control: ZnCheckbox) => control.defaultChecked,
54
69
  setValue: (control: ZnCheckbox, checked: boolean) => (control.checked = checked)
55
70
  });
56
- private readonly hasSlotController = new HasSlotController(this, 'description');
71
+ private readonly hasSlotController = new HasSlotController(this, '[default]', 'description', 'image');
57
72
 
58
73
  @query('input[type="checkbox"]') input: HTMLInputElement;
59
74
 
@@ -67,6 +82,9 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
67
82
  /** The current value of the checkbox, submitted as a name/value pair with form data. */
68
83
  @property() value: string;
69
84
 
85
+ /** Title rendered inside the card. The default slot takes precedence when provided. */
86
+ @property({attribute: 'card-title'}) cardTitle = '';
87
+
70
88
  /** The unchecked value of the checkbox, submitted as a name/value pair with form data. */
71
89
  @property({attribute: 'unchecked-value', reflect: true}) uncheckedValue: string;
72
90
 
@@ -88,6 +106,24 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
88
106
  /** Draws a container around the checkbox. */
89
107
  @property({type: Boolean, reflect: true}) contained = false;
90
108
 
109
+ /** Squares off the corners of the container drawn by `contained`, which is rounded by default. */
110
+ @property({type: Boolean, reflect: true}) square = false;
111
+
112
+ /** URL for the image shown in a contained checkbox. */
113
+ @property() src = '';
114
+
115
+ /** Accessible text for the built-in image. Leave empty when the image is decorative. */
116
+ @property({attribute: 'image-alt'}) imageAlt = '';
117
+
118
+ /** Places the image above, beside, or below the checkbox's text. Requires `contained`. */
119
+ @property({attribute: 'image-position', reflect: true}) imagePosition: SelectionCardImagePosition = 'left';
120
+
121
+ /**
122
+ * Places the checkbox indicator within a contained card. Use `none` to hide the indicator so the card itself
123
+ * shows the selected state. Requires `contained`.
124
+ */
125
+ @property({attribute: 'control-position', reflect: true}) controlPosition: SelectionCardControlPosition = 'start';
126
+
91
127
  /** Removes a container around the checkbox. */
92
128
  @property({type: Boolean, reflect: true}) borderless = false;
93
129
 
@@ -244,6 +280,9 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
244
280
  render() {
245
281
  const hasDescriptionSlot = this.hasSlotController.test('description');
246
282
  const hasDescription = this.description ? true : hasDescriptionSlot;
283
+ const hasImage = this.contained && (Boolean(this.src) || this.hasSlotController.test('image'));
284
+ const hasDefaultSlot = this.hasSlotController.test('[default]');
285
+ const isCard = this.contained && (hasImage || Boolean(this.cardTitle) || this.controlPosition !== 'start');
247
286
  const hasLabelSlot = this.hasSlotController.test('label');
248
287
  const hasLabelTooltip = this.hasSlotController.test('label-tooltip');
249
288
  const hasLabel = this.label || hasLabelSlot;
@@ -309,7 +348,11 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
309
348
  'checkbox--warning': effectiveColor === 'warning',
310
349
  'checkbox--transparent': effectiveColor === 'transparent',
311
350
  'checkbox--has-description': hasDescription,
312
- 'checkbox--has-selected-content': this.hasSlotController.test('selected-content')
351
+ 'checkbox--has-selected-content': this.hasSlotController.test('selected-content'),
352
+ 'selection-card': isCard,
353
+ 'selection-card--has-image': hasImage,
354
+ [`selection-card--image-${this.imagePosition}`]: hasImage,
355
+ [`selection-card--control-${this.controlPosition}`]: isCard && this.controlPosition !== 'start'
313
356
  })}>
314
357
 
315
358
  <input
@@ -323,7 +366,7 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
323
366
  .disabled=${this.disabled}
324
367
  .required=${this.required}
325
368
  aria-checked=${this.checked ? 'true' : 'false'}
326
- aria-describedby=${hasDescription ? '' : 'description'}
369
+ aria-describedby=${hasDescription ? 'description' : ''}
327
370
  @click=${this.handleClick}
328
371
  @input=${this.handleInput}
329
372
  @invalid=${this.handleInvalid}
@@ -334,7 +377,7 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
334
377
  part="control${this.checked ? ' control--checked' : ''}${this.indeterminate
335
378
  ? ' control--indeterminate'
336
379
  : ''}"
337
- class="checkbox__control">
380
+ class="checkbox__control selection-card__control">
338
381
 
339
382
  ${this.checked && !this.indeterminate
340
383
  ? this.checkedIcon
@@ -367,8 +410,21 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
367
410
  : ''}
368
411
  </span>
369
412
 
370
- <div part="label" class="checkbox__label">
371
- <slot></slot>
413
+ ${hasImage
414
+ ? html`
415
+ <span part="image-container" class="selection-card__image-container">
416
+ <slot name="image">
417
+ ${this.src
418
+ ? html`<img part="image" class="selection-card__image" src=${this.src} alt=${this.imageAlt}>`
419
+ : ''}
420
+ </slot>
421
+ </span>`
422
+ : ''}
423
+
424
+ <div part="label" class="checkbox__label selection-card__content">
425
+ <span part="card-title" class="selection-card__title">
426
+ <slot></slot>${hasDefaultSlot ? '' : this.cardTitle}
427
+ </span>
372
428
  <div
373
429
  aria-hidden=${hasDescription ? 'false' : 'true'}
374
430
  class="checkbox__description"
@@ -1,5 +1,6 @@
1
1
  @use "../../wc";
2
2
  @use "../../form-control";
3
+ @use "../../selection-card";
3
4
 
4
5
  :host {
5
6
  display: block;
@@ -7,4 +7,60 @@ describe('<zn-checkbox>', () => {
7
7
 
8
8
  expect(el).to.exist;
9
9
  });
10
+
11
+ it('renders card content and a built-in image', async () => {
12
+ const el = await fixture(html`
13
+ <zn-checkbox
14
+ contained
15
+ card-title="Payroll"
16
+ description="Salary and pay schedule tools."
17
+ src="payroll.svg"
18
+ image-alt="Payroll illustration">
19
+ </zn-checkbox>
20
+ `);
21
+
22
+ const image = el.shadowRoot!.querySelector('[part="image"]')!;
23
+ expect(image.getAttribute('src')).to.equal('payroll.svg');
24
+ expect(image.getAttribute('alt')).to.equal('Payroll illustration');
25
+ expect(el.shadowRoot!.querySelector('[part="label"]')!.textContent).to.include('Payroll');
26
+ });
27
+
28
+ it('renders card-title when formatting whitespace is present in the default slot', async () => {
29
+ const el = await fixture(html`
30
+ <zn-checkbox contained card-title="Benefits">
31
+ <zn-icon slot="image" src="featured_seasonal_and_gifts"></zn-icon>
32
+ </zn-checkbox>
33
+ `);
34
+
35
+ expect(el.shadowRoot!.querySelector('[part="card-title"]')!.textContent).to.include('Benefits');
36
+ });
37
+
38
+ it('positions card images and controls', async () => {
39
+ const el = await fixture(html`
40
+ <zn-checkbox contained image-position="right" control-position="top-left">
41
+ Benefits
42
+ <zn-icon slot="image" src="featured_seasonal_and_gifts"></zn-icon>
43
+ </zn-checkbox>
44
+ `);
45
+ const base = el.shadowRoot!.querySelector('[part="base"]')!;
46
+
47
+ expect(base).to.have.class('selection-card--image-right');
48
+ expect(base).to.have.class('selection-card--control-top-left');
49
+ expect(el.shadowRoot!.querySelector('slot[name="image"]')).to.exist;
50
+ });
51
+
52
+ it('leaves a plain contained checkbox without card styles', async () => {
53
+ const el = await fixture(html`<zn-checkbox contained description="Supporting text">Payroll</zn-checkbox>`);
54
+
55
+ expect(el.shadowRoot!.querySelector('[part="base"]')).not.to.have.class('selection-card');
56
+ expect(el.shadowRoot!.querySelector('[part="image-container"]')).to.be.null;
57
+ });
58
+
59
+ it('can hide the visual control without removing the native input', async () => {
60
+ const el = await fixture(html`<zn-checkbox contained control-position="none">Payroll</zn-checkbox>`);
61
+
62
+ expect(el.shadowRoot!.querySelector('input[type="checkbox"]')).to.exist;
63
+ expect(el.shadowRoot!.querySelector('[part~="control"]')).to.exist;
64
+ expect(el.shadowRoot!.querySelector('[part="base"]')).to.have.class('selection-card--control-none');
65
+ });
10
66
  });