@kubex/zinc 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
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",
@@ -1,8 +1,15 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
3
  import {FormControlController} from "../../internal/form";
4
+ import {litToHTML} from "../../utilities/lit-to-html";
4
5
  import {property, query} from 'lit/decorators.js';
5
6
  import ZincElement from '../../internal/zinc-element';
7
+ import ZnButton from "../button";
8
+ import ZnInput from "../input";
9
+ import ZnOption from "../option";
10
+ import ZnSelect from "../select";
11
+ import type {ZnChangeEvent} from "../../events/zn-change";
12
+ import type {ZnInputEvent} from "../../events/zn-input";
6
13
 
7
14
  import styles from './bulk-actions.scss';
8
15
 
@@ -29,7 +36,10 @@ export interface BulkActionItem {
29
36
  * @status experimental
30
37
  * @since 1.0
31
38
  *
32
- * @dependency zn-example
39
+ * @dependency zn-button
40
+ * @dependency zn-input
41
+ * @dependency zn-option
42
+ * @dependency zn-select
33
43
  *
34
44
  * @event zn-event-name - Emitted as an example.
35
45
  *
@@ -42,9 +52,15 @@ export interface BulkActionItem {
42
52
  */
43
53
  export default class ZnBulkActions extends ZincElement {
44
54
  static styles: CSSResultGroup = unsafeCSS(styles);
55
+ static dependencies = {
56
+ 'zn-button': ZnButton,
57
+ 'zn-input': ZnInput,
58
+ 'zn-option': ZnOption,
59
+ 'zn-select': ZnSelect,
60
+ };
45
61
 
46
62
  @query('.bulk-actions') container: HTMLDivElement;
47
- @query('.add-rule') addRule: HTMLSelectElement;
63
+ @query('.add-rule') addRule: ZnSelect;
48
64
  @query('input#main-input') input: HTMLInputElement;
49
65
 
50
66
  @property() name: string;
@@ -73,12 +89,13 @@ export default class ZnBulkActions extends ZincElement {
73
89
  <div class="${classMap({
74
90
  'bulk-actions': true
75
91
  })}">
76
- <select class="add-rule" @change="${this._addRule}">
77
- <option value="">Select Filter</option>
92
+ <zn-select class="add-rule" placeholder="Select Filter" @zn-change="${this._addRule}">
78
93
  ${this.actions.map((item: BulkActionItem) => html`
79
- <option value="${item.id}">${item.name.charAt(0).toUpperCase() + item.name.slice(1)}</option>`)}
80
- </select>
81
- <input id="main-input" name="${this.name}" value="${this.value}" type="hidden">
94
+ <zn-option value="${item.id}">
95
+ ${item.name.charAt(0).toUpperCase() + item.name.slice(1)}
96
+ </zn-option>`)}
97
+ </zn-select>
98
+ <input id="main-input" name="${this.name}" value="${this.value}" hidden>
82
99
  </div>
83
100
  `;
84
101
  }
@@ -95,8 +112,9 @@ export default class ZnBulkActions extends ZincElement {
95
112
  this.value = btoa(JSON.stringify(data));
96
113
  }
97
114
 
98
- private _addRule(event: Event | null, value: string = "") {
99
- const id = value ? value : event ? (event.target as HTMLSelectElement).value : '';
115
+ private _addRule(event: Event | null, value: string = "", pos?: number) {
116
+ const target = event?.target as ZnSelect;
117
+ const id = value ? value : target?.value || '';
100
118
  if (id === '') return;
101
119
 
102
120
  const filter = this.actions.find(item => item.id === id);
@@ -109,104 +127,154 @@ export default class ZnBulkActions extends ZincElement {
109
127
  value: ''
110
128
  });
111
129
 
112
- const row = document.createElement('div');
113
- row.classList.add('query-builder__row');
114
- row.id = uniqueId;
115
-
116
- const select = document.createElement('select');
117
- this.actions.forEach(item => {
118
- const option = document.createElement('option');
119
- option.value = item.id;
120
- option.text = item.name.charAt(0).toUpperCase() + item.name.slice(1);
121
- option.selected = item.id === filter.id;
122
- select.appendChild(option);
123
- });
124
- select.addEventListener('change', (e: Event) => this._changeRule(uniqueId, e));
125
- select.classList.add('query-builder__key');
130
+ const keySelect = html`
131
+ <zn-select class="query-builder__key"
132
+ @zn-change="${(e: ZnChangeEvent) => this._changeRule(uniqueId, e)}"
133
+ value="${filter.id}">
134
+ ${this.actions.map((item: BulkActionItem) => html`
135
+ <zn-option value="${item.id}">
136
+ ${item.name.charAt(0).toUpperCase() + item.name.slice(1)}
137
+ </zn-option>
138
+ `)}
139
+ </zn-select>
140
+ `;
126
141
 
127
- row.appendChild(select);
142
+ const input = this._createInput(uniqueId, filter);
143
+
144
+ const remove = html`
145
+ <zn-button class="query-builder__remove"
146
+ icon="delete"
147
+ icon-size="24"
148
+ color="transparent"
149
+ size="square"
150
+ @click="${(e: Event) => this._removeRule(uniqueId, e)}">
151
+ </zn-button>`;
152
+
153
+ const wrapper = html`
154
+ <div class="query-builder__wrapper">
155
+ ${input}
156
+ ${remove}
157
+ </div>
158
+ `;
128
159
 
129
- let input: HTMLSelectElement | HTMLInputElement | HTMLDivElement;
130
- if (filter.options) {
131
- input = document.createElement('select');
132
- const options = Object.keys(filter.options);
133
- options.forEach((item: string) => {
134
- const option = document.createElement('option');
135
- option.value = item;
136
- option.text = filter.options ? (filter.options[item]) : '';
137
- input.appendChild(option);
138
- });
139
- this._updateValue(uniqueId, {target: input});
140
- input.addEventListener('change', (e: Event) => this._updateValue(uniqueId, e));
141
- } else if (filter.type === 'bool' || filter.type === 'boolean') {
142
- input = document.createElement('select');
143
- const option1 = document.createElement('option');
144
- option1.value = '1';
145
- option1.text = 'True';
146
- input.appendChild(option1);
147
- const option2 = document.createElement('option');
148
- option2.value = '0';
149
- option2.text = 'False';
150
- input.appendChild(option2);
151
- input.addEventListener('change', (e: Event) => this._updateValue(uniqueId, e));
152
- } else if (filter.type === 'number') {
153
- input = document.createElement('input');
154
- input.setAttribute('type', 'number');
155
- input.addEventListener('input', (e: Event) => this._updateValue(uniqueId, e));
156
- } else if (filter.type === 'date') {
157
- input = document.createElement('input');
158
- input.setAttribute('type', 'date');
159
- input.addEventListener('input', (e: Event) => this._updateValue(uniqueId, e));
160
+ const rowElement = html`
161
+ <div id="${uniqueId}" class="query-builder__row">
162
+ ${keySelect}
163
+ ${wrapper}
164
+ </div>
165
+ `;
166
+
167
+ const row = litToHTML<HTMLDivElement>(rowElement);
168
+ if (!row) return;
169
+
170
+ if (pos !== undefined) {
171
+ this.container.insertBefore(row, this.container.children[pos]);
160
172
  } else {
161
- input = document.createElement('input');
162
- input.setAttribute('type', 'text');
163
- input.addEventListener('input', (e: Event) => this._updateValue(uniqueId, e));
173
+ this.container.insertBefore(row, this.addRule);
164
174
  }
165
175
 
166
- input.classList.add('query-builder__value');
167
- const wrapper = document.createElement('div');
168
- wrapper.classList.add('query-builder__wrapper');
169
- wrapper.appendChild(input);
170
- row.appendChild(wrapper);
171
-
172
- const remove = document.createElement('zn-button');
173
- remove.setAttribute('icon', 'delete');
174
- remove.setAttribute('icon-size', '24');
175
- remove.setAttribute('color', 'transparent');
176
- remove.setAttribute('size', 'square');
177
- remove.addEventListener('click', (e: Event) => this._removeRule(uniqueId, e));
178
- remove.classList.add('query-builder__remove');
179
- row.appendChild(remove);
180
-
181
- this.container.insertBefore(row, this.addRule);
176
+ // Reset back to default placeholder
182
177
  this.addRule.value = '';
178
+ this.addRule.displayLabel = '';
179
+ if (this.addRule.selectedOptions?.length) {
180
+ this.addRule.selectedOptions[0].selected = false;
181
+ }
182
+
183
183
  this._handleChange();
184
184
  }
185
185
 
186
- private _updateValue(id: string, event: Event | { target: HTMLSelectElement | HTMLInputElement | HTMLDivElement }) {
186
+ private _createInput(uniqueId: string, filter: BulkActionItem): ZnSelect | ZnInput | null {
187
+ if (filter.options) {
188
+ const input = html`
189
+ <zn-select class="query-builder__value"
190
+ @zn-change="${(e: ZnChangeEvent) => this._updateValue(uniqueId, e)}">
191
+ ${Object.keys(filter.options).map(key => html`
192
+ <zn-option value="${key}">
193
+ ${filter.options ? filter.options[key] : ''}
194
+ </zn-option>
195
+ `)}
196
+ </zn-select>`;
197
+ const el = litToHTML<ZnSelect>(input);
198
+ if (el) {
199
+ // initialize with current selection
200
+ this._updateValue(uniqueId, {target: el} as unknown as Event);
201
+ }
202
+ return el;
203
+ }
204
+
205
+ if (filter.type === 'bool' || filter.type === 'boolean') {
206
+ const input = html`
207
+ <zn-select class="query-builder__value"
208
+ @zn-change="${(e: ZnChangeEvent) => this._updateValue(uniqueId, e)}">
209
+ <zn-option value="1">True</zn-option>
210
+ <zn-option value="0">False</zn-option>
211
+ </zn-select>`;
212
+ return litToHTML<ZnSelect>(input);
213
+ }
214
+
215
+ if (filter.type === 'number') {
216
+ const input = html`
217
+ <zn-input type="number"
218
+ class="query-builder__value"
219
+ @zn-input="${(e: ZnInputEvent) => this._updateValue(uniqueId, e)}">
220
+ </zn-input>`;
221
+ return litToHTML<ZnInput>(input);
222
+ }
223
+
224
+ if (filter.type === 'date') {
225
+ const input = html`
226
+ <zn-input type="date"
227
+ class="query-builder__value"
228
+ @zn-input="${(e: ZnInputEvent) => this._updateValue(uniqueId, e)}">
229
+ </zn-input>`;
230
+ return litToHTML<ZnInput>(input);
231
+ }
232
+
233
+ const input = html`
234
+ <zn-input type="text"
235
+ class="query-builder__value"
236
+ @zn-input="${(e: ZnInputEvent) => this._updateValue(uniqueId, e)}">
237
+ </zn-input>`;
238
+ return litToHTML<ZnInput>(input);
239
+ }
240
+
241
+ private _updateValue(id: string, event: Event | { target: ZnSelect | ZnInput | HTMLDivElement }) {
187
242
  const filter = this._selectedRules.get(id);
188
243
  if (!filter) return;
189
244
 
190
- const input = event.target as HTMLSelectElement | HTMLInputElement;
191
- filter.value = input.value;
245
+ const input = event.target as ZnSelect | ZnInput;
246
+ filter.value = input.value as string;
192
247
 
193
248
  this._selectedRules.set(id, filter);
194
249
  this._handleChange();
195
250
  }
196
251
 
197
- private _changeRule(id: string, event: Event) {
252
+ private _getRulePosition(id: string): number {
253
+ const rules = this.container.querySelectorAll('.query-builder__row');
254
+ let position: number = -1;
255
+ rules.forEach((item, index) => {
256
+ if ((item as HTMLDivElement).id === id) {
257
+ position = index;
258
+ }
259
+ });
260
+ return position;
261
+ }
262
+
263
+ private _changeRule(id: string, event: ZnChangeEvent) {
198
264
  // remove the element from the dom
199
- const button = event.target as HTMLSelectElement;
200
- button.parentElement?.remove();
265
+ const pos: number = this._getRulePosition(id);
266
+ const select = event.target as ZnSelect;
267
+ select.popup.active = false;
268
+ select?.parentElement?.remove();
201
269
  // recreate the element based on the selected value;
202
- this._removeRule(id, event);
203
- this._addRule(event);
270
+ this._removeRule(id, event as unknown as Event);
271
+ this._addRule(event as unknown as Event, '', pos);
204
272
  }
205
273
 
206
274
  private _removeRule(id: string, event: Event) {
207
275
  this._selectedRules.delete(id);
208
- const button = event.target as HTMLButtonElement;
209
- button.parentElement?.remove();
276
+ const button = event.target as ZnButton;
277
+ button?.closest('.query-builder__row')?.remove();
210
278
  this._handleChange();
211
279
  }
212
280
 
@@ -40,7 +40,9 @@
40
40
  }
41
41
 
42
42
  &__wrapper {
43
+ display: flex;
43
44
  width: 100%;
45
+ gap: 10px;
44
46
  }
45
47
 
46
48
  &__date {
@@ -1,5 +1,5 @@
1
- import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
2
- import { property } from 'lit/decorators.js';
1
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
2
+ import {property} from 'lit/decorators.js';
3
3
  import ApexCharts from "apexcharts";
4
4
  import ZincElement from '../../internal/zinc-element';
5
5
 
@@ -26,21 +26,22 @@ export default class ZnChart extends ZincElement {
26
26
  static styles: CSSResultGroup = unsafeCSS(styles);
27
27
 
28
28
  @property() type: 'area' | 'bar' | 'line' = 'bar';
29
- @property({ type: Array }) data: any[] = [];
30
- @property({ type: Array }) categories: string | string[] = '';
29
+ @property({type: Array}) data: any[] = [];
30
+ @property({type: Array}) categories: string | string[] = '';
31
31
 
32
32
  @property() xAxis: string;
33
- @property({ type: Number, attribute: 'd-size' }) datapointSize: number = 1;
33
+ @property({type: Number, attribute: 'd-size'}) datapointSize: number = 1;
34
+ @property({type: Boolean}) stacked = false;
34
35
 
35
36
  // Live
36
- @property({ type: Boolean }) live = false;
37
- @property({ attribute: 'data-url' }) dataUrl = '';
38
- @property({ attribute: 'live-interval', type: Number }) liveInterval = 1000;
39
- @property({ type: Number, reflect: true }) height = 300;
37
+ @property({type: Boolean}) live = false;
38
+ @property({attribute: 'data-url'}) dataUrl = '';
39
+ @property({attribute: 'live-interval', type: Number}) liveInterval = 1000;
40
+ @property({type: Number, reflect: true}) height = 300;
40
41
 
41
- @property({ attribute: 'enable-animations', type: Boolean }) enableAnimations = false;
42
+ @property({attribute: 'enable-animations', type: Boolean}) enableAnimations = false;
42
43
 
43
- @property({ attribute: 'y-axis-append' }) yAxisAppend: string;
44
+ @property({attribute: 'y-axis-append'}) yAxisAppend: string;
44
45
 
45
46
  private chart: ApexCharts;
46
47
 
@@ -54,10 +55,11 @@ export default class ZnChart extends ZincElement {
54
55
  animations: this.enableAnimations,
55
56
  chart: {
56
57
  type: this.type,
57
- toolbar: { show: false },
58
+ toolbar: {show: false},
58
59
  foreColor: theme == 'dark' ? "rgb(149, 163, 184)" : "rgb(123, 112, 151)",
59
60
  height: this.height + 'px',
60
61
  fontFamily: '"Inter", sans',
62
+ stacked: this.stacked,
61
63
  zoom: {
62
64
  enabled: false
63
65
  },
@@ -25,7 +25,6 @@ import styles from './cols.scss';
25
25
  export default class ZnCols extends ZincElement {
26
26
  static styles: CSSResultGroup = unsafeCSS(styles);
27
27
 
28
-
29
28
  @property({reflect: true, attribute: 'layout'}) layout: string = '';
30
29
 
31
30
  @property({attribute: 'mc', type: Number, reflect: true}) maxColumns: number = 0;
@@ -35,7 +34,11 @@ export default class ZnCols extends ZincElement {
35
34
  @property({type: Boolean}) border: boolean = false;
36
35
 
37
36
  @property({type: Boolean}) pad: boolean;
37
+
38
+ @property({type: Boolean}) divide: boolean = false;
39
+
38
40
  @property({attribute: 'pad-x', type: Boolean}) padX: boolean;
41
+
39
42
  @property({attribute: 'pad-y', type: Boolean}) padY: boolean;
40
43
 
41
44
  render() {
@@ -47,7 +50,7 @@ export default class ZnCols extends ZincElement {
47
50
 
48
51
  this.layout = layout.join(',');
49
52
 
50
- this.maxColumns = Math.min(5, layout.reduce((a, b) => a + b, 0));
53
+ this.maxColumns = layout.reduce((a, b) => a + b, 0);
51
54
 
52
55
  const prefix = 'zn-col-';
53
56
  this.querySelectorAll(':scope > *').forEach((element: HTMLElement, index) => {
@@ -66,7 +69,8 @@ export default class ZnCols extends ZincElement {
66
69
  'cols--pad': this.pad,
67
70
  'cols--pad-x': this.padX,
68
71
  'cols--pad-y': this.padY,
69
- [`cols--layout-${this.layout.replace(',', '')}`]: !!this.layout,
72
+ 'cols--divide': this.divide,
73
+ [`cols--layout-${this.layout.replaceAll(',', '')}`]: !!this.layout,
70
74
  [`cols--mc-${this.maxColumns}`]: !!this.maxColumns,
71
75
  })}">
72
76
  <slot></slot>
@@ -26,20 +26,10 @@
26
26
  }
27
27
  }
28
28
 
29
- &--mc-2 {
30
- --col-width: 40%;
31
- }
32
-
33
- &--mc-3 {
34
- --col-width: 30%;
35
- }
36
-
37
- &--mc-4 {
38
- --col-width: 23%;
39
- }
40
-
41
- &--mc-5 {
42
- --col-width: 18%;
29
+ @for $i from 1 through 12 {
30
+ &--mc-#{$i} {
31
+ --col-width: calc(100% / #{$i + 1});
32
+ }
43
33
  }
44
34
 
45
35
  &--layout-121 {
@@ -54,6 +44,24 @@
54
44
  }
55
45
  }
56
46
 
47
+ &--divide {
48
+ ::slotted(*:not(:last-child)) {
49
+ position: relative;
50
+
51
+ &:after {
52
+ content: "";
53
+ position: absolute;
54
+ display: block;
55
+ width: 1px;
56
+ top: 0;
57
+ height: 100%;
58
+ max-height: 100%;
59
+ right: calc((var(--zn-spacing-small) / 2) * -1);
60
+ background-color: rgb(var(--zn-border-color));
61
+ }
62
+ }
63
+ }
64
+
57
65
  &--no-gap {
58
66
  gap: 0;
59
67
  }
@@ -28,6 +28,7 @@ export default class ContentBlock extends ZincElement {
28
28
 
29
29
  @property({type: Boolean, reflect: true}) outbound = false;
30
30
  @property({type: Boolean, reflect: true}) short = false;
31
+ @property({attribute: 'default-display', reflect: true}) defaultDisplay: 'text' | 'html' = 'text';
31
32
 
32
33
  @queryAssignedNodes({slot: 'html', flatten: true}) htmlNodes!: Node[];
33
34
 
@@ -37,49 +38,82 @@ export default class ContentBlock extends ZincElement {
37
38
 
38
39
  private _textRows: TextRow[] = [];
39
40
 
41
+ private _themeChangeObserver?: MutationObserver;
42
+
40
43
  connectedCallback() {
41
44
  super.connectedCallback();
42
45
  this.iframe.then((iframe) => {
43
- if (iframe.contentDocument) {
46
+ const rebuild = () => {
44
47
  const div = this.htmlNodes[0] as HTMLDivElement;
45
- if (div) {
46
- let convertedHtml = div?.innerHTML ?? '';
47
- convertedHtml = convertedHtml.replace(/&nbsp;/g, ' ');
48
- convertedHtml = convertedHtml.replace(/&lt;/g, '<');
49
- convertedHtml = convertedHtml.replace(/&gt;/g, '>');
50
- convertedHtml = convertedHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
51
-
52
- const baseStyles = "<style>body,html{padding: 0; margin: 0; font-size: 13px; font-family: Arial;}img{max-width:100%; max-height: 500px;}</style>";
53
- iframe.srcdoc = baseStyles + convertedHtml;
54
- }
55
- }
48
+ if (!div) return;
49
+
50
+ let convertedHtml = div?.innerHTML ?? '';
51
+ convertedHtml = convertedHtml.replace(/&nbsp;/g, ' ');
52
+ convertedHtml = convertedHtml.replace(/&lt;/g, '<');
53
+ convertedHtml = convertedHtml.replace(/&gt;/g, '>');
54
+ convertedHtml = convertedHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
55
+
56
+ const themeAttr = document.documentElement.getAttribute('t') || document.body.getAttribute('t') || '';
57
+
58
+ // Pull stylesheet links from host to inherit Zinc styles
59
+ const linkTags = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]'))
60
+ .map((element: Element) => {
61
+ const href = (element as HTMLLinkElement).getAttribute('href') || (element as HTMLLinkElement).href || '';
62
+ return href ? `<link rel="stylesheet" href="${href}">` : '';
63
+ })
64
+ .join('');
65
+
66
+ const baseStyles = `<style>
67
+ html, body {
68
+ background-color: var(--zn-panel-background-color);
69
+ container-type: size;
70
+ margin: 0;
71
+ }
56
72
 
57
- iframe.addEventListener("load", () => {
58
- // Save original styles
59
- const originalDisplay = iframe.style.display;
60
- const originalVisibility = iframe.style.visibility;
61
- const originalPosition = iframe.style.position;
62
-
63
- // Make iframe render but stay hidden
64
- iframe.style.display = 'block';
65
- iframe.style.visibility = 'hidden';
66
- iframe.style.position = 'absolute';
67
-
68
- setTimeout(() => {
69
- const iframeBody = iframe.contentDocument?.body;
70
- if (iframeBody) {
71
- const height = iframeBody.scrollHeight;
72
- iframe.style.height = `${height + 30}px`;
73
+ hr {
74
+ height: 0;
75
+ border: none;
76
+ border-top: 1px solid rgb(var(--zn-border-color));
73
77
  }
74
- // Restore original styles
75
- iframe.style.display = originalDisplay;
76
- iframe.style.visibility = originalVisibility;
77
- iframe.style.position = originalPosition;
78
- }, 50);
79
- });
78
+
79
+ code, kbd, samp, pre, .mono {
80
+ font-family: var(--zn-font-family-mono),sans-serif;
81
+ font-size: 1em;
82
+ }
83
+
84
+ small {
85
+ font-size: 80%;
86
+ }
87
+ </style>`;
88
+
89
+ const head = `${linkTags}${baseStyles}`;
90
+ const htmlAttrs = `${themeAttr ? ` t="${themeAttr}"` : ''}`;
91
+ const baseHref = document.baseURI || window.location.href;
92
+
93
+ iframe.srcdoc = `<!doctype html><html${htmlAttrs}><head><base href="${baseHref}" target="_blank">${head}</head><body>${convertedHtml}</body></html>`;
94
+ };
95
+
96
+ rebuild();
97
+
98
+ // Observe theme changes on both attribute and theme classes on html and body
99
+ this._themeChangeObserver = new MutationObserver(() => rebuild());
100
+ this._themeChangeObserver.observe(document.documentElement, {attributes: true, attributeFilter: ['t', 'class']});
101
+ this._themeChangeObserver.observe(document.body, {attributes: true, attributeFilter: ['t', 'class']});
102
+
103
+ const doc = iframe.contentDocument;
104
+ if (!doc) return;
105
+
106
+ iframe.style.height = (75 + doc.body.scrollHeight) + 'px';
107
+ iframe.style.width = '100%';
108
+ iframe.style.border = '0';
80
109
  });
81
110
  }
82
111
 
112
+ disconnectedCallback() {
113
+ super.disconnectedCallback();
114
+ this._themeChangeObserver?.disconnect();
115
+ }
116
+
83
117
  private _collapseContent(e: Event) {
84
118
  const target = e.target as HTMLElement;
85
119
  const headerClick = target.classList.contains('content-block-header') ||
@@ -130,6 +164,7 @@ export default class ContentBlock extends ZincElement {
130
164
  const hasTextSlot = this.hasSlotController.test('text');
131
165
  const hasHtmlSlot = this.hasSlotController.test('html');
132
166
  const showActions = hasTextSlot && hasHtmlSlot;
167
+ const initialShowHtml = hasHtmlSlot && (!hasTextSlot || this.defaultDisplay === 'html');
133
168
 
134
169
  return html`
135
170
  <zn-panel flush tabbed class="${classMap({
@@ -167,10 +202,10 @@ export default class ContentBlock extends ZincElement {
167
202
  </zn-header>
168
203
 
169
204
  <zn-sp no-gap>
170
- <iframe class="hidden" title="Content block HTML preview"></iframe>
205
+ <iframe class="${classMap({'hidden': !initialShowHtml})}" title="Content block HTML preview"></iframe>
171
206
  <slot class="html-content" name="html" style="display: none;"></slot>
172
207
  <slot name="text" style="display: none;"></slot>
173
- <div class="text-content">
208
+ <div class="${classMap({'text-content': true, 'hidden': initialShowHtml})}">
174
209
  ${text.map((section) => html`
175
210
  ${section.type === 'reply' ? html`
176
211
  <div class="toggle-reply" @click="${this.showReply}">...</div>` : ''}
@@ -220,6 +255,7 @@ export default class ContentBlock extends ZincElement {
220
255
  if (row.startsWith('Sent from')) type = 'reply';
221
256
  if (row.trim() === '' && previousType === 'reply') type = 'reply';
222
257
  if (type === 'reply' && row.startsWith('>')) row = row.substring(1).trim();
258
+ row = this.transformLineForImages(row);
223
259
 
224
260
  if (previousType === type) {
225
261
  if (textRows.length === 0) {
@@ -239,6 +275,36 @@ export default class ContentBlock extends ZincElement {
239
275
  return this._textRows;
240
276
  }
241
277
 
278
+ private transformLineForImages(line: string): string {
279
+ let out = line;
280
+
281
+ // Replace Markdown image syntax ![alt](url) for http(s)
282
+ const mdHttpImg = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+?\.(?:png|jpe?g|gif|webp|bmp|svg))\)/gi;
283
+ out = out.replace(mdHttpImg, (_match, alt, url) => {
284
+ return `<img src="${url}" alt="${alt || 'image'}" loading="lazy" style="max-width:100%;max-height:500px;">`;
285
+ });
286
+
287
+ // Replace Markdown image syntax with data URI (base64)
288
+ const mdDataImg = /!\[([^\]]*)\]\((data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+)\)/gi;
289
+ out = out.replace(mdDataImg, (_match, alt, url) => {
290
+ return `<img src="${url}" alt="${alt || 'image'}" loading="lazy" style="max-width:100%;max-height:500px;">`;
291
+ });
292
+
293
+ // Replace bare http(s) image URLs with <img>
294
+ const httpImg = /(https?:\/\/[^\s]+?\.(?:png|jpe?g|gif|webp|bmp|svg))(?!\S)/gi;
295
+ out = out.replace(httpImg, (_m, url) => {
296
+ return `<img src="${url}" alt="image" loading="lazy" style="max-width:100%;max-height:500px;">`;
297
+ });
298
+
299
+ // Replace bare data URI images with <img>
300
+ const dataImg = /(data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+)(?!\S)/gi;
301
+ out = out.replace(dataImg, (_m, url) => {
302
+ return `<img src="${url}" alt="image" loading="lazy" style="max-width:100%;max-height:500px;">`;
303
+ });
304
+
305
+ return out;
306
+ }
307
+
242
308
  showReply(e: MouseEvent) {
243
309
  const target = e.target as HTMLElement;
244
310
  const nextElement = target.nextElementSibling;