@kubex/zinc 1.1.14 → 1.1.16

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.1.14",
3
+ "version": "1.1.16",
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,6 +1,6 @@
1
1
  import {cleanHTML} from "./clean-message";
2
2
  import {type CSSResultGroup, html, nothing, unsafeCSS} from 'lit';
3
- import {property} from "lit/decorators.js";
3
+ import {property, state} from "lit/decorators.js";
4
4
  import {unsafeHTML} from "lit/directives/unsafe-html.js";
5
5
  import ZincElement from '../../internal/zinc-element';
6
6
  import ZnIcon from "../icon";
@@ -80,6 +80,9 @@ export default class ZnChatMessage extends ZincElement {
80
80
  /** Marks the message as initiated by an agent (affects styling and grouping). */
81
81
  @property({type: Boolean, reflect: true, attribute: 'agent-initiated'}) agentInitiated = false;
82
82
 
83
+ /** Whether any element is assigned to the `attachments` slot — drives the attachments row visibility. */
84
+ @state() private hasAttachments = false;
85
+
83
86
  connectedCallback() {
84
87
  const isContent = !this.actionType || this.actionType === 'internal';
85
88
  const agentInitiated = isContent && this.agentInitiated;
@@ -122,7 +125,12 @@ export default class ZnChatMessage extends ZincElement {
122
125
  <div part="bubble" class="message__bubble ${sending ? 'message__bubble--sending' : ''}">
123
126
  <div class="message__main">
124
127
  <div part="content" class="message__content">${this.renderContent()}</div>
125
- <slot name="attachments" part="attachments" class="message__attachments"></slot>
128
+ <slot
129
+ name="attachments"
130
+ part="attachments"
131
+ class="message__attachments ${this.hasAttachments ? 'message__attachments--visible' : ''}"
132
+ @slotchange=${this.handleAttachmentsSlotChange}
133
+ ></slot>
126
134
  </div>
127
135
  ${sending ? html`
128
136
  <div class="message__meta">Sending...</div>` : html`
@@ -136,6 +144,20 @@ export default class ZnChatMessage extends ZincElement {
136
144
  `;
137
145
  }
138
146
 
147
+ protected firstUpdated() {
148
+ this.syncHasAttachments();
149
+ Promise.resolve().then(() => this.syncHasAttachments());
150
+ }
151
+
152
+ private handleAttachmentsSlotChange() {
153
+ this.syncHasAttachments();
154
+ }
155
+
156
+ private syncHasAttachments() {
157
+ const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="attachments"]');
158
+ this.hasAttachments = !!slot && slot.assignedElements().length > 0;
159
+ }
160
+
139
161
  private isSystemMessage() {
140
162
  return ZnChatMessage.SYSTEM_ACTION_TYPES.includes(this.actionType);
141
163
  }
@@ -65,15 +65,16 @@
65
65
  }
66
66
 
67
67
  &__attachments {
68
- // Always rendered so projection is reliable regardless of when children are
69
- // parsed/added, but kept out of flow (no phantom gap below the bubble) until
70
- // an attachment actually exists — toggled on via :host(:has(...)) below.
71
68
  display: none;
72
69
  flex-direction: row;
73
70
  flex-wrap: wrap;
74
71
  align-items: center;
75
72
  gap: var(--zn-spacing-small);
76
73
 
74
+ &--visible {
75
+ display: flex;
76
+ }
77
+
77
78
  ::slotted(zn-chat-message-attachment) {
78
79
  min-width: 0;
79
80
  }
@@ -168,13 +169,6 @@
168
169
  }
169
170
  }
170
171
 
171
- // Reveal the attachments row only when an attachment is actually present. Using
172
- // :has on the host keeps this live and parse-order independent — no reliance on
173
- // conditional rendering that could miss a child added during parsing.
174
- :host(:has([slot="attachments"])) .message__attachments {
175
- display: flex;
176
- }
177
-
178
172
  // Bubble tints by message origin / type.
179
173
  :host([agent-initiated]) .message__bubble {
180
174
  background: var(--message-background, rgba(var(--zn-color-info), 0.1));
@@ -35,7 +35,7 @@ export default class ZnChatMessageAttachment extends ZincElement {
35
35
  @property() name: string = '';
36
36
 
37
37
  /** The leading icon name. */
38
- @property() icon: string = 'attach_file';
38
+ @property() icon: string = 'paperclip@lu';
39
39
 
40
40
  /** Where to open the link. Defaults to a new tab. */
41
41
  @property() target: string = '_blank';
@@ -46,13 +46,11 @@ export default class ZnChatMessageAttachment extends ZincElement {
46
46
  connectedCallback() {
47
47
  super.connectedCallback();
48
48
 
49
- // This component is only meaningful inside the attachments row of a chat message
50
- // slot it there automatically so consumers don't have to.
51
- if (!this.slot) {
52
- this.slot = 'attachments';
53
- }
54
-
55
- if (!this.closest('zn-chat-message')) {
49
+ if (this.closest('zn-chat-message')) {
50
+ if (!this.slot) {
51
+ this.slot = 'attachments';
52
+ }
53
+ } else {
56
54
  console.warn('<zn-chat-message-attachment> can only be used inside a <zn-chat-message>.', this);
57
55
  }
58
56
  }
@@ -22,6 +22,17 @@ describe('<zn-chat-message-attachment>', () => {
22
22
  expect(attachment?.getAttribute('slot')).to.equal('attachments');
23
23
  });
24
24
 
25
+ it('should reveal the attachments row when an attachment is slotted in', async () => {
26
+ const el = await fixture<HTMLElement>(html`
27
+ <zn-chat-message message="Test">
28
+ <zn-chat-message-attachment href="#" name="file.pdf"></zn-chat-message-attachment>
29
+ </zn-chat-message>`);
30
+
31
+ const row = el.shadowRoot!.querySelector('.message__attachments') as HTMLElement;
32
+ // The row is display:none until something is slotted in; a slotted attachment must reveal it.
33
+ expect(getComputedStyle(row).display).to.not.equal('none');
34
+ });
35
+
25
36
  it('should render the name in the link', async () => {
26
37
  const el = await fixture<HTMLElement>(html`
27
38
  <zn-chat-message>
@@ -261,6 +261,13 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
261
261
 
262
262
  this.quillElement = quill;
263
263
 
264
+ this.getForm()?.addEventListener('submit', () => {
265
+ setTimeout(() => {
266
+ const attachmentModule = this.quillElement.getModule('attachment') as Attachment;
267
+ attachmentModule?.reset();
268
+ }, 0);
269
+ });
270
+
264
271
  // @ts-expect-error getSelection is available it lies.
265
272
  const hasShadowRootSelection = !!(document.createElement('div').attachShadow({mode: 'open'}).getSelection);
266
273
  // Each browser engine has a different implementation for retrieving the Range
@@ -400,7 +407,10 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
400
407
 
401
408
  if (this.interactionType === 'chat') {
402
409
  const form = this.closest('form');
403
- if (form && this.value && this.value.trim().length > 0 && !empty(this.value)) {
410
+ const hasText = !!this.value && this.value.trim().length > 0 && !empty(this.value);
411
+ const attachmentInput = form?.querySelector('input[name="attachments"]') as HTMLInputElement | null;
412
+ const hasAttachments = !!attachmentInput?.value && attachmentInput.value !== '[]';
413
+ if (form && (hasText || hasAttachments)) {
404
414
  this.emit('zn-submit', {detail: {value: this.value, element: this}});
405
415
  form.requestSubmit();
406
416
  this.quillElement.setText('');
@@ -57,7 +57,7 @@ export default class Attachment {
57
57
  public addAttachment(file: File, dataUrl?: string) {
58
58
  const attachmentId = generateId();
59
59
  const insertWithDataUrl = (base64: string) => {
60
- this._insertAttachment({ dataUrl: base64, file, id: attachmentId });
60
+ this._insertAttachment({dataUrl: base64, file, id: attachmentId});
61
61
  };
62
62
 
63
63
  if (dataUrl) {
@@ -72,8 +72,8 @@ export default class Attachment {
72
72
  }
73
73
 
74
74
  if (typeof this._options.upload === 'function') {
75
- this._options.upload(file).then(({ path, url }: { path: string; url: string }) => {
76
- this._uploadAttachment(file, url);
75
+ this._options.upload(file).then(async ({path, url}: { path: string; url: string }) => {
76
+ await this._uploadAttachment(file, url);
77
77
  this._updateAttachment(attachmentId, url, path);
78
78
  }).catch((err: { message: string }) => {
79
79
  console.warn(err.message);
@@ -163,6 +163,15 @@ export default class Attachment {
163
163
  return attachment;
164
164
  }
165
165
 
166
+ /** Clear every staged attachment preview and reset the backing form input. */
167
+ public reset() {
168
+ this._attachmentContainer.replaceChildren();
169
+ const attachments = this._options.attachmentInput;
170
+ if (attachments) {
171
+ attachments.value = '';
172
+ }
173
+ }
174
+
166
175
  private _removeAttachment(id: string) {
167
176
  const attachment = this._quill.container.querySelector(`#${id}`);
168
177
  if (attachment) {
@@ -184,24 +193,26 @@ export default class Attachment {
184
193
  }
185
194
  }
186
195
 
187
- private _uploadAttachment(file: File, url: string) {
188
- const xhr = new XMLHttpRequest();
196
+ private _uploadAttachment(file: File, url: string): Promise<void> {
197
+ return new Promise<void>((resolve, reject) => {
198
+ const xhr = new XMLHttpRequest();
189
199
 
190
- xhr.open('PUT', url, true);
191
- xhr.setRequestHeader('Content-Type', file.type);
200
+ xhr.open('PUT', url, true);
201
+ xhr.setRequestHeader('Content-Type', file.type);
192
202
 
193
- xhr.onload = () => {
194
- if (xhr.status >= 200 && xhr.status < 300) {
195
- console.log(`[Quill Attachment Module] File uploaded successfully to ${url}`);
196
- } else {
197
- console.error(`[Quill Attachment Module] Failed to upload file. Status: ${xhr.status}`);
198
- }
199
- };
203
+ xhr.onload = () => {
204
+ if (xhr.status >= 200 && xhr.status < 300) {
205
+ resolve();
206
+ } else {
207
+ reject(new Error(`[Quill Attachment Module] Failed to upload file. Status: ${xhr.status}`));
208
+ }
209
+ };
200
210
 
201
- xhr.onerror = () => {
202
- console.error(`[Quill Attachment Module] Error occurred while uploading file.`);
203
- };
211
+ xhr.onerror = () => {
212
+ reject(new Error(`[Quill Attachment Module] Error occurred while uploading file.`));
213
+ };
204
214
 
205
- xhr.send(file);
215
+ xhr.send(file);
216
+ });
206
217
  }
207
218
  }
@@ -167,7 +167,7 @@ input[type='text'].input__control[data-color-input] {
167
167
 
168
168
  .input--x-small .input__control {
169
169
  height: calc(var(--zn-input-height-x-small) - var(--zn-input-border-width) * 2);
170
- padding: 0 var(--zn-input-spacing-small);
170
+ padding: 0 var(--zn-input-spacing-small) 0 var(--zn-spacing-small);
171
171
  }
172
172
 
173
173
  .input--x-small .input__clear,
@@ -201,7 +201,7 @@ input[type='text'].input__control[data-color-input] {
201
201
 
202
202
  .input--small .input__control {
203
203
  height: calc(var(--zn-input-height-small) - var(--zn-input-border-width) * 2);
204
- padding: 0 var(--zn-input-spacing-small);
204
+ padding: 0 var(--zn-input-spacing-small) 0 var(--zn-spacing-small);
205
205
  }
206
206
 
207
207
  .input--small .input__clear,
@@ -235,7 +235,7 @@ input[type='text'].input__control[data-color-input] {
235
235
 
236
236
  .input--medium .input__control {
237
237
  height: calc(var(--zn-input-height-medium) - var(--zn-input-border-width) * 2);
238
- padding: 0 var(--zn-input-spacing-small);
238
+ padding: 0 var(--zn-input-spacing-small) 0 var(--zn-spacing-small);
239
239
  }
240
240
 
241
241
  .input--medium .input__clear,
@@ -283,7 +283,7 @@ input[type='text'].input__control[data-color-input] {
283
283
 
284
284
  .input--large .input__control {
285
285
  height: calc(var(--zn-input-height-large) - var(--zn-input-border-width) * 2);
286
- padding: 0 var(--zn-input-spacing-medium);
286
+ padding: 0 var(--zn-input-spacing-medium) 0 var(--zn-spacing-small);
287
287
  }
288
288
 
289
289
  .input--large .input__clear,
@@ -208,7 +208,7 @@
208
208
  font-size: var(--zn-input-font-size-small);
209
209
  min-height: var(--zn-input-height-small);
210
210
  padding-block: 0;
211
- padding-inline: var(--zn-input-spacing-small);
211
+ padding-inline: var(--zn-spacing-small);
212
212
  }
213
213
 
214
214
  .select--small .select__clear {
@@ -233,7 +233,7 @@
233
233
  font-size: var(--zn-input-font-size-medium);
234
234
  min-height: var(--zn-input-height-medium);
235
235
  padding-block: 0;
236
- padding-inline: var(--zn-input-spacing-medium);
236
+ padding-inline: var(--zn-spacing-small);
237
237
  }
238
238
 
239
239
  .select--has-input-prefix.select--medium .select__combobox {
@@ -262,7 +262,7 @@
262
262
  font-size: var(--zn-input-font-size-large);
263
263
  min-height: var(--zn-input-height-large);
264
264
  padding-block: 0;
265
- padding-inline: var(--zn-input-spacing-large);
265
+ padding-inline: var(--zn-spacing-small);
266
266
  }
267
267
 
268
268
  .select--has-input-prefix.select--large .select__combobox {
@@ -9,17 +9,18 @@
9
9
  }
10
10
 
11
11
  .switch__input-wrapper {
12
+ box-sizing: border-box;
12
13
  text-align: left;
13
14
  position: relative;
14
15
  display: inline-flex;
15
- width: 50px;
16
- height: 25px;
16
+ width: 42px;
17
+ height: 24px;
17
18
  flex-shrink: 0;
18
19
  cursor: pointer;
19
- border-radius: 20px;
20
+ border-radius: 12px;
20
21
  align-items: center;
21
- background: rgba(0, 0, 0, 0.15);
22
- border-width: 1px !important;
22
+ background-color: rgba(0, 0, 0, 0.15);
23
+ border: 0.92px solid transparent;
23
24
  margin: var(--zn-toggle-margin);
24
25
  transition: color .2s ease-in-out, background-color .4s ease-in-out, border-color .2s ease-in-out;
25
26
  }
@@ -29,12 +30,11 @@
29
30
  }
30
31
 
31
32
  :host([checked]) .switch__input-wrapper {
32
- background: rgba(var(--zn-primary), 0.8);
33
+ background-color: rgb(var(--zn-color-primary));
34
+ border-color: rgb(var(--zn-color-primary));
33
35
 
34
36
  .switch__control {
35
- transform: translateX(calc(100% - 1px)) scale(0.8);
36
- background: rgba(255, 255, 255, 1);
37
- width: 25px;
37
+ transform: translate(16px, -50%);
38
38
  }
39
39
  }
40
40
 
@@ -52,19 +52,18 @@
52
52
 
53
53
  .switch__control {
54
54
  pointer-events: none;
55
- display: inline-block;
56
- width: 100%;
57
- height: 25px;
58
- border-radius: 100px;
59
- background: rgba(255, 255, 255, 0.9);
60
- box-shadow: rgb(255, 255, 255) 0 0 0 0, rgba(59, 130, 246, 0.5) 0 0 0 0, rgba(0, 0, 0, 0.1) 0 1px 3px 0, rgba(0, 0, 0, 0.1) 0 1px 2px -1px;
55
+ position: absolute;
56
+ top: 50%;
57
+ left: 3px;
58
+ width: 20px;
59
+ height: 20px;
60
+ border-radius: 50%;
61
+ background: #fff;
62
+ box-shadow: rgba(0, 0, 0, 0.1) 0 1px 3px 0, rgba(0, 0, 0, 0.1) 0 1px 2px -1px;
61
63
 
62
- position: relative;
63
- top: 0;
64
- transform: scale(0.8);
65
- transition: transform .3s ease-in-out, background-color .4s ease-in-out, opacity .2s ease-in-out, max-width .2s, width .4s;
64
+ transform: translateY(-50%);
65
+ transition: transform .3s ease-in-out, background-color .4s ease-in-out;
66
66
  outline: none;
67
- max-width: 25px;
68
67
  }
69
68
 
70
69
  .switch__input {
@@ -81,17 +80,17 @@
81
80
  width: 35px;
82
81
  }
83
82
 
83
+ .switch__wrapper--small .switch__control {
84
+ width: 14px;
85
+ height: 14px;
86
+ }
87
+
84
88
  :host([checked]) .switch__wrapper--small .switch__input-wrapper {
85
89
  .switch__control {
86
- transform: translateX(calc(100% - 7px)) scale(0.8);
90
+ transform: translate(13px, -50%);
87
91
  }
88
92
  }
89
93
 
90
- .switch__wrapper--small .switch__control {
91
- max-width: 20px;
92
- height: 20px;
93
- }
94
-
95
94
  // Support inline label
96
95
  .switch__wrapper--inline label {
97
96
  align-items: center;