@kubex/zinc 1.1.97 → 1.1.98

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 (39) hide show
  1. package/dist/chunks/zn.R2EAU6OS.js +1 -0
  2. package/dist/custom-elements.json +1422 -61
  3. package/dist/vscode.html-custom-data.json +188 -2
  4. package/dist/web-types.json +462 -5
  5. package/dist/zn.d.ts +326 -2
  6. package/dist/zn.min.js +582 -457
  7. package/docs/pages/components/button.md +11 -1
  8. package/docs/pages/components/chart.md +165 -3
  9. package/docs/pages/components/icon-picker.md +8 -0
  10. package/docs/pages/components/thumbnail-group.md +216 -0
  11. package/docs/pages/components/thumbnail.md +349 -0
  12. package/docs/pages/components/well.md +34 -0
  13. package/package.json +1 -1
  14. package/src/components/button/button.component.ts +8 -0
  15. package/src/components/chart/builders.test.ts +190 -1
  16. package/src/components/chart/builders.ts +246 -4
  17. package/src/components/chart/chart.component.ts +33 -1
  18. package/src/components/chart/chart.test.ts +66 -1
  19. package/src/components/chart/echarts-loader.ts +10 -0
  20. package/src/components/collapsible/collapsible.component.ts +49 -4
  21. package/src/components/collapsible/collapsible.scss +33 -14
  22. package/src/components/header/header.scss +1 -1
  23. package/src/components/icon-picker/icon-picker.component.ts +3 -0
  24. package/src/components/icon-picker/lucide-icons.ts +1756 -0
  25. package/src/components/page/page.scss +1 -1
  26. package/src/components/remarkd-editor/remarkd-editor.test.ts +39 -0
  27. package/src/components/split-pane/split-pane.scss +21 -15
  28. package/src/components/thumbnail/index.ts +12 -0
  29. package/src/components/thumbnail/thumbnail.component.ts +460 -0
  30. package/src/components/thumbnail/thumbnail.scss +360 -0
  31. package/src/components/thumbnail/thumbnail.test.ts +379 -0
  32. package/src/components/thumbnail-group/index.ts +12 -0
  33. package/src/components/thumbnail-group/thumbnail-group.component.ts +268 -0
  34. package/src/components/thumbnail-group/thumbnail-group.scss +87 -0
  35. package/src/components/thumbnail-group/thumbnail-group.test.ts +151 -0
  36. package/src/components/well/well.component.ts +22 -4
  37. package/src/components/well/well.scss +24 -0
  38. package/src/components/well/well.test.ts +43 -0
  39. package/src/zinc.ts +2 -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;
@@ -52,6 +52,10 @@ export default class ZnCollapsible extends ZincElement {
52
52
 
53
53
  @state() numberOfItems: number = 0;
54
54
 
55
+ @state() private animating: boolean = false;
56
+
57
+ private animatingTimer?: ReturnType<typeof setTimeout>;
58
+
55
59
  protected _store: Store;
56
60
 
57
61
  private readonly hasSlotController = new HasSlotController(this, '[default]', 'header', 'caption', 'label', 'description');
@@ -94,6 +98,11 @@ export default class ZnCollapsible extends ZincElement {
94
98
  }
95
99
  }
96
100
 
101
+ disconnectedCallback() {
102
+ super.disconnectedCallback();
103
+ clearTimeout(this.animatingTimer);
104
+ }
105
+
97
106
  // this is for handling global toggles
98
107
  public handleCaptionToggle = (e: ZnInputEvent) => {
99
108
  const toggle = e.target as ZnToggle;
@@ -104,8 +113,38 @@ export default class ZnCollapsible extends ZincElement {
104
113
 
105
114
  protected updated(changedProperties: PropertyValues) {
106
115
  super.updated(changedProperties);
107
- if (changedProperties.has('expanded') && this.storeKey) {
108
- this._store.set(this.storeKey, this.expanded.toString());
116
+ if (changedProperties.has('expanded')) {
117
+ if (changedProperties.get('expanded') !== undefined) {
118
+ this.startAnimating();
119
+ }
120
+
121
+ if (this.storeKey) {
122
+ this._store.set(this.storeKey, this.expanded.toString());
123
+ }
124
+ }
125
+ }
126
+
127
+ // Content is only clipped while the height transition runs, so expanded
128
+ // content can overflow the host. The timer covers a transitionend that never
129
+ // arrives (reduced motion, hidden host).
130
+ private startAnimating() {
131
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
132
+ return;
133
+ }
134
+
135
+ this.animating = true;
136
+ clearTimeout(this.animatingTimer);
137
+ this.animatingTimer = setTimeout(() => (this.animating = false), 600);
138
+ }
139
+
140
+ private stopAnimating() {
141
+ clearTimeout(this.animatingTimer);
142
+ this.animating = false;
143
+ }
144
+
145
+ private handleTransitionEnd = (e: TransitionEvent) => {
146
+ if (e.target === e.currentTarget && e.propertyName === 'grid-template-rows') {
147
+ this.stopAnimating();
109
148
  }
110
149
  }
111
150
 
@@ -160,8 +199,14 @@ export default class ZnCollapsible extends ZincElement {
160
199
  <zn-icon library="material-outlined" src="expand_more" class="expand"></zn-icon>` : ''}
161
200
  </div>
162
201
  </slot>
163
- <div class=${classMap({ 'content': true, 'content--flush': this.flush, })} part="content">
164
- <slot @slotchange="${this.requestUpdate}"></slot>
202
+ <div class=${classMap({
203
+ 'content': true,
204
+ 'content--flush': this.flush,
205
+ 'content--animating': this.animating,
206
+ })} part="content" @transitionend="${this.handleTransitionEnd}">
207
+ <div class="content__inner">
208
+ <slot @slotchange="${this.requestUpdate}"></slot>
209
+ </div>
165
210
  </div>
166
211
  </div>`;
167
212
  }
@@ -76,29 +76,48 @@
76
76
  }
77
77
  }
78
78
 
79
- :host(:not([flush])).content {
80
- @include wc.padding();
79
+ // Height animates via grid-template-rows 0fr -> 1fr; the inline padding is
80
+ // constant so the content never reflows (and re-wraps) mid-transition.
81
+ .content {
82
+ display: grid;
83
+ grid-template-rows: 0fr;
84
+ opacity: 0;
85
+ transition: grid-template-rows var(--zn-transition-medium) cubic-bezier(0.32, 0.72, 0, 1),
86
+ opacity var(--zn-transition-fast) ease-out;
87
+
88
+ &__inner {
89
+ min-height: 0;
90
+ padding: 0 var(--zn-spacing-medium) var(--zn-spacing-medium);
91
+ transform: translateY(-4px);
92
+ transition: transform var(--zn-transition-medium) cubic-bezier(0.32, 0.72, 0, 1);
93
+ }
94
+
95
+ &--flush &__inner {
96
+ padding: 0;
97
+ }
81
98
  }
82
99
 
83
- .content {
84
- transition: .1s all ease-out;
100
+ // Clipped while collapsed or mid-transition only, so expanded content can still
101
+ // overflow the host (dropdowns, tooltips).
102
+ :host(:not([expanded])) .content,
103
+ .content--animating {
104
+ overflow: hidden;
85
105
  }
86
106
 
87
107
  :host([expanded]) .content {
88
- margin: 0 var(--zn-spacing-medium);
89
- padding-bottom: var(--zn-spacing-medium);
108
+ grid-template-rows: 1fr;
109
+ opacity: 1;
90
110
 
91
- &--flush {
92
- margin: 0;
93
- padding: 0;
111
+ .content__inner {
112
+ transform: none;
94
113
  }
95
114
  }
96
115
 
97
- :host(:not([expanded])) .content {
98
- max-height: 0;
99
- opacity: 0;
100
- overflow: hidden;
101
- padding: 0;
116
+ @media (prefers-reduced-motion: reduce) {
117
+ .content,
118
+ .content__inner {
119
+ transition-duration: 0s;
120
+ }
102
121
  }
103
122
 
104
123
  .expand {
@@ -24,7 +24,7 @@
24
24
  white-space: nowrap;
25
25
  text-overflow: ellipsis;
26
26
  display: flex;
27
- gap: var(--zn-spacing-x-small);
27
+ gap: var(--zn-spacing-2x-small);
28
28
  align-items: center;
29
29
  text-wrap: auto;
30
30
  }
@@ -131,6 +131,8 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
131
131
  return (await import('./brand-icons')).brandIcons;
132
132
  case 'line':
133
133
  return (await import('./line-icons')).lineIcons;
134
+ case 'lucide':
135
+ return (await import('./lucide-icons')).lucideIcons;
134
136
  default: {
135
137
  const lists = await import('./material-icons');
136
138
  switch (library) {
@@ -438,6 +440,7 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
438
440
  <zn-option value="material-symbols-outlined">Material Symbols Outlined</zn-option>
439
441
  <zn-option value="brands">Brands</zn-option>
440
442
  <zn-option value="line">Line</zn-option>
443
+ <zn-option value="lucide">Lucide</zn-option>
441
444
  <zn-option value="gravatar">Gravatar</zn-option>
442
445
  <zn-option value="libravatar">Libravatar</zn-option>
443
446
  <zn-option value="avatar">Avatar</zn-option>