@kubex/zinc 1.0.105 → 1.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -89,6 +89,31 @@ The group scales well with multiple translation inputs under a single toggle.
89
89
  </zn-translation-group>
90
90
  ```
91
91
 
92
+ ### Language Overflow
93
+
94
+ When there are more languages than will fit across the header, the extras collapse into a chevron dropdown beside the
95
+ visible buttons. The dropdown sits before the `+` add-language dropdown and selects the same way — picking a language
96
+ switches every child translation input in the group.
97
+
98
+ ```html:preview
99
+ <div style="max-width: 480px;">
100
+ <zn-translation-group
101
+ label="Release Notes"
102
+ languages='{"en":"English","ar":"Arabic","de":"German","es":"Spanish","fr":"French","it":"Italian","ja":"Japanese","ko":"Korean","pt":"Portuguese","ru":"Russian","tr":"Turkish","zh-hans":"Simplified Chinese"}'>
103
+ <zn-translations
104
+ label="Headline"
105
+ name="headline"
106
+ values='{"en":"Now shipping","ar":"متاح الآن","de":"Jetzt verfügbar","es":"Ya disponible","fr":"Disponible dès maintenant","it":"Disponibile ora","ja":"発売開始","ko":"출시됨","pt":"Disponível agora","ru":"Уже в продаже","tr":"Şimdi mevcut","zh-hans":"现已发布"}'
107
+ ></zn-translations>
108
+ <zn-translations
109
+ label="Summary"
110
+ name="summary"
111
+ values='{"en":"Tap through the release highlights.","ar":"تصفح أبرز ميزات الإصدار.","de":"Highlights der Version ansehen.","es":"Consulta lo más destacado.","fr":"Découvrez les nouveautés.","it":"Scopri le novità.","ja":"リリースのハイライトをご覧ください。","ko":"업데이트 주요 내용 보기.","pt":"Veja os destaques.","ru":"Ознакомьтесь с обновлениями.","tr":"Sürüm önemli noktaları.","zh-hans":"浏览版本亮点。"}'
112
+ ></zn-translations>
113
+ </zn-translation-group>
114
+ </div>
115
+ ```
116
+
92
117
  ### Flush Layout
93
118
 
94
119
  Remove body padding for a more compact appearance using the `flush` attribute.
@@ -141,7 +141,8 @@ Use the `expand` slot to add custom buttons or actions.
141
141
 
142
142
  ### Many Languages
143
143
 
144
- The component handles many languages gracefully with scrollable tabs.
144
+ When the language buttons don't all fit on one line, the overflow collapses into a chevron dropdown so the row stays a
145
+ single line. Resizing the container re-measures and expands the buttons back out when space allows.
145
146
 
146
147
  ```html:preview
147
148
  <zn-translations
@@ -151,6 +152,21 @@ The component handles many languages gracefully with scrollable tabs.
151
152
  ></zn-translations>
152
153
  ```
153
154
 
155
+ ### Constrained Width
156
+
157
+ Placing the component in a narrow container forces the chevron overflow to kick in. Selecting a language from the
158
+ dropdown switches the active language just like clicking a visible button.
159
+
160
+ ```html:preview
161
+ <div style="max-width: 320px;">
162
+ <zn-translations
163
+ label="Narrow Container"
164
+ languages='{"en":"English","fr":"French","de":"German","es":"Spanish","it":"Italian","pt":"Portuguese","ja":"Japanese","ko":"Korean","zh-hans":"Simplified Chinese"}'
165
+ values='{"en":"Hello","fr":"Bonjour","de":"Hallo","es":"Hola","it":"Ciao","pt":"Olá","ja":"こんにちは","ko":"안녕하세요","zh-hans":"你好"}'
166
+ ></zn-translations>
167
+ </div>
168
+ ```
169
+
154
170
  ### RTL Language Support
155
171
 
156
172
  The component automatically detects and applies right-to-left text direction for Arabic and Hebrew languages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.105",
3
+ "version": "1.0.106",
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",
@@ -79,6 +79,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
79
79
  @property() step: number | 'any';
80
80
 
81
81
  @property({ attribute: "input-type" }) inputType: 'select' | 'text' | 'data-select' | 'number' | 'textarea' = 'text';
82
+ @property({ attribute: "textarea-rows", type: Number }) textareaRows: 1;
82
83
 
83
84
  @property({ type: Object }) options: { [key: string]: string } = {};
84
85
 
@@ -387,7 +388,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
387
388
  name="${this.name}"
388
389
  size="${this.size}"
389
390
  resize="auto"
390
- rows="1"
391
+ rows="${this.textareaRows}"
391
392
  .value="${this.value}"
392
393
  placeholder="${this.placeholder}"
393
394
  pattern=${ifDefined(this.pattern)}
@@ -14,7 +14,7 @@
14
14
  gap: 10px;
15
15
 
16
16
  &:not(.ai--editing) {
17
- .ai__input::part(base), .ai__input::part(combobox) {
17
+ .ai__input::part(base), .ai__input::part(combobox), .ai__input::part(textarea) {
18
18
  background-color: transparent;
19
19
  border-color: transparent;
20
20
  box-shadow: none;
@@ -30,7 +30,7 @@
30
30
  color: rgb(var(--zn-text));
31
31
  }
32
32
 
33
- .ai__input::part(input), .ai__input::part(combobox) {
33
+ .ai__input::part(input), .ai__input::part(combobox), .ai__input::part(textarea) {
34
34
  padding: 0;
35
35
  color: rgb(var(--zn-text));
36
36
  text-decoration: underline;
@@ -39,6 +39,12 @@
39
39
  text-underline-offset: 3px;
40
40
  }
41
41
 
42
+ .ai__input::part(textarea) {
43
+ padding-top: 8px;
44
+ box-shadow: none;
45
+ text-decoration:none;
46
+ }
47
+
42
48
  .ai__input::part(expand-icon) {
43
49
  display: none;
44
50
  }
@@ -150,7 +156,7 @@
150
156
  }
151
157
 
152
158
  &--disabled:not(.ai--editing) {
153
- .ai__input::part(input), .ai__input::part(combobox) {
159
+ .ai__input::part(input), .ai__input::part(combobox), .ai__input::part(textarea) {
154
160
  text-decoration: none;
155
161
  caret-color: transparent;
156
162
  border-radius: 0;
@@ -170,12 +176,4 @@ zn-textarea::part(textarea) {
170
176
  resize: none;
171
177
  }
172
178
 
173
- .ai:not(.ai--editing) {
174
- zn-textarea::part(textarea) {
175
- box-shadow: none;
176
- padding-left: 0;
177
- padding-right: 0;
178
- min-height: 35px;
179
- }
180
- }
181
179
 
@@ -78,7 +78,7 @@ export default class ZnTextarea extends ZincElement implements ZincFormControl {
78
78
  @property() placeholder = '';
79
79
 
80
80
  /** The number of rows to display by default. */
81
- @property({type: Number}) rows = 4;
81
+ @property({attribute: 'rows',type: Number}) rows = 4;
82
82
 
83
83
  /** Controls how the textarea can be resized. */
84
84
  @property() resize: 'none' | 'vertical' | 'auto' = 'vertical';
@@ -54,9 +54,39 @@ export default class ZnTranslationGroup extends ZnPanel {
54
54
  /** Tracks all language codes that have been activated across children. */
55
55
  @state() private _activatedLanguages: string[] = ['en'];
56
56
 
57
+ @state() private _overflowIndex = -1;
58
+
59
+ private _resizeObserver?: ResizeObserver;
60
+ private _lastObservedWidth = 0;
61
+ private _measureRafId = 0;
62
+
63
+ connectedCallback() {
64
+ super.connectedCallback();
65
+ this._resizeObserver = new ResizeObserver(entries => {
66
+ const width = entries[0]?.contentRect.width ?? 0;
67
+ if (Math.abs(width - this._lastObservedWidth) < 1) return;
68
+ this._lastObservedWidth = width;
69
+ if (this._overflowIndex !== -1) {
70
+ this._overflowIndex = -1;
71
+ } else {
72
+ this._scheduleLangOverflow();
73
+ }
74
+ });
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ super.disconnectedCallback();
79
+ this._resizeObserver?.disconnect();
80
+ if (this._measureRafId) {
81
+ cancelAnimationFrame(this._measureRafId);
82
+ this._measureRafId = 0;
83
+ }
84
+ }
85
+
57
86
  protected firstUpdated(_changedProperties: PropertyValues) {
58
87
  super.firstUpdated(_changedProperties);
59
88
  this.syncChildren();
89
+ this._resizeObserver?.observe(this);
60
90
  }
61
91
 
62
92
  protected updated(changedProperties: PropertyValues) {
@@ -64,6 +94,52 @@ export default class ZnTranslationGroup extends ZnPanel {
64
94
  if (changedProperties.has('languages')) {
65
95
  this.syncChildLanguages();
66
96
  }
97
+ this._scheduleLangOverflow();
98
+ }
99
+
100
+ private _scheduleLangOverflow() {
101
+ if (this._measureRafId) return;
102
+ this._measureRafId = requestAnimationFrame(() => {
103
+ this._measureRafId = 0;
104
+ this._computeLangOverflow();
105
+ });
106
+ }
107
+
108
+ private _computeLangOverflow() {
109
+ const group = this.shadowRoot?.querySelector<HTMLElement>('.translation-group__languages zn-button-group');
110
+ if (!group) return;
111
+
112
+ const buttons = Array.from(group.querySelectorAll<HTMLElement>('zn-button[data-lang-btn]'));
113
+ if (buttons.length < 2) {
114
+ if (this._overflowIndex !== -1) this._overflowIndex = -1;
115
+ return;
116
+ }
117
+
118
+ const firstTop = buttons[0].getBoundingClientRect().top;
119
+ let wrapAt = -1;
120
+ for (let i = 1; i < buttons.length; i++) {
121
+ if (buttons[i].getBoundingClientRect().top > firstTop + 1) {
122
+ wrapAt = i;
123
+ break;
124
+ }
125
+ }
126
+
127
+ if (wrapAt === -1) {
128
+ const trailing = group.querySelector<HTMLElement>('[data-lang-overflow], [data-lang-add]');
129
+ if (trailing && trailing.getBoundingClientRect().top > firstTop + 1) {
130
+ wrapAt = buttons.length;
131
+ }
132
+ }
133
+
134
+ if (wrapAt === -1) return;
135
+
136
+ const newIndex = this._overflowIndex === -1
137
+ ? wrapAt
138
+ : Math.max(1, Math.min(wrapAt, this._overflowIndex - 1));
139
+
140
+ if (newIndex !== this._overflowIndex) {
141
+ this._overflowIndex = newIndex;
142
+ }
67
143
  }
68
144
 
69
145
  private getAllTranslations(): ZnTranslations[] {
@@ -124,6 +200,15 @@ export default class ZnTranslationGroup extends ZnPanel {
124
200
  }
125
201
  };
126
202
 
203
+ private handleOverflowSelect = (e: ZnMenuSelectEvent) => {
204
+ e.stopPropagation();
205
+ const element = e.detail.element as HTMLElement;
206
+ const languageCode = element.getAttribute('data-path');
207
+ if (languageCode) {
208
+ this.switchLanguage(languageCode);
209
+ }
210
+ };
211
+
127
212
  render() {
128
213
  const hasActionSlot = this._slotController.test('actions');
129
214
  const hasFooterSlot = this._slotController.test('footer');
@@ -142,6 +227,16 @@ export default class ZnTranslationGroup extends ZnPanel {
142
227
  visibleTabs.unshift('en');
143
228
  }
144
229
 
230
+ const overflowCutoff = this._overflowIndex === -1 ? visibleTabs.length : this._overflowIndex;
231
+ const visibleLangTabs = visibleTabs.slice(0, overflowCutoff);
232
+ const overflowLangTabs = visibleTabs.slice(overflowCutoff);
233
+ const overflowActions = overflowLangTabs.map(code => ({
234
+ title: code.toUpperCase(),
235
+ type: 'dropdown',
236
+ path: code,
237
+ icon: code === this._activeLanguage ? 'check' : ''
238
+ }));
239
+
145
240
  const hasMultipleLanguages = Object.keys(this.languages).length > 1;
146
241
  const hasHeader = headerCaption || hasMultipleLanguages || hasActionSlot;
147
242
 
@@ -163,8 +258,9 @@ export default class ZnTranslationGroup extends ZnPanel {
163
258
  ${hasMultipleLanguages ? html`
164
259
  <div slot="actions" class="translation-group__languages">
165
260
  <zn-button-group>
166
- ${visibleTabs.map(code => html`
261
+ ${visibleLangTabs.map(code => html`
167
262
  <zn-button
263
+ data-lang-btn
168
264
  size="x-small"
169
265
  color="default"
170
266
  ?outline="${code !== this._activeLanguage}"
@@ -172,8 +268,23 @@ export default class ZnTranslationGroup extends ZnPanel {
172
268
  >${code.toUpperCase()}
173
269
  </zn-button>
174
270
  `)}
271
+ ${overflowActions.length > 0 ? html`
272
+ <zn-dropdown placement="bottom-end" data-lang-overflow>
273
+ <zn-button
274
+ slot="trigger"
275
+ size="x-small"
276
+ color="default"
277
+ icon="keyboard_arrow_down"
278
+ ?outline="${!overflowLangTabs.includes(this._activeLanguage)}"
279
+ ></zn-button>
280
+ <zn-menu
281
+ .actions=${overflowActions}
282
+ @zn-menu-select="${this.handleOverflowSelect}"
283
+ ></zn-menu>
284
+ </zn-dropdown>
285
+ ` : nothing}
175
286
  ${availableLanguages.length > 0 ? html`
176
- <zn-dropdown placement="bottom-end">
287
+ <zn-dropdown placement="bottom-end" data-lang-add>
177
288
  <zn-button
178
289
  slot="trigger"
179
290
  size="x-small"
@@ -2,6 +2,7 @@ import {classMap} from 'lit/directives/class-map.js';
2
2
  import {FormControlController, validValidityState} from '../../internal/form';
3
3
  import {HasSlotController} from '../../internal/slot';
4
4
  import {html, nothing, unsafeCSS} from 'lit';
5
+ import {ifDefined} from 'lit/directives/if-defined.js';
5
6
  import {keyed} from 'lit/directives/keyed.js';
6
7
  import {live} from 'lit/directives/live.js';
7
8
  import {property, state} from 'lit/decorators.js';
@@ -39,6 +40,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
39
40
  @property({type: Boolean, reflect: true}) required = false;
40
41
  @property({type: Boolean, reflect: true}) flush = false;
41
42
  @property({attribute: "input-type"}) inputType: 'select' | 'text' | 'number' | 'textarea' = 'text';
43
+ @property({attribute: "textarea-rows", type: Number}) textareaRows: number | undefined;
42
44
 
43
45
  /** When true, hides the individual language navbar and defers language control to a parent zn-translation-group. */
44
46
  @property({type: Boolean, reflect: true}) grouped = false;
@@ -49,6 +51,11 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
49
51
  @property({type: Object}) values: Record<string, string> = {};
50
52
 
51
53
  @state() private _activeLanguage = 'en';
54
+ @state() private _overflowIndex = -1;
55
+
56
+ private _resizeObserver?: ResizeObserver;
57
+ private _lastObservedWidth = 0;
58
+ private _measureRafId = 0;
52
59
 
53
60
  get validity(): ValidityState {
54
61
  return validValidityState;
@@ -99,8 +106,82 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
99
106
  return Object.keys(this.values);
100
107
  }
101
108
 
109
+ connectedCallback() {
110
+ super.connectedCallback();
111
+ this._resizeObserver = new ResizeObserver(entries => {
112
+ const width = entries[0]?.contentRect.width ?? 0;
113
+ if (Math.abs(width - this._lastObservedWidth) < 1) return;
114
+ this._lastObservedWidth = width;
115
+ if (this._overflowIndex !== -1) {
116
+ this._overflowIndex = -1;
117
+ } else {
118
+ this._scheduleLangOverflow();
119
+ }
120
+ });
121
+ }
122
+
123
+ disconnectedCallback() {
124
+ super.disconnectedCallback();
125
+ this._resizeObserver?.disconnect();
126
+ if (this._measureRafId) {
127
+ cancelAnimationFrame(this._measureRafId);
128
+ this._measureRafId = 0;
129
+ }
130
+ }
131
+
102
132
  protected firstUpdated() {
103
133
  this.formControlController.updateValidity();
134
+ this._resizeObserver?.observe(this);
135
+ }
136
+
137
+ protected updated(changedProperties: PropertyValues) {
138
+ super.updated(changedProperties);
139
+ this._scheduleLangOverflow();
140
+ }
141
+
142
+ private _scheduleLangOverflow() {
143
+ if (this._measureRafId) return;
144
+ this._measureRafId = requestAnimationFrame(() => {
145
+ this._measureRafId = 0;
146
+ this._computeLangOverflow();
147
+ });
148
+ }
149
+
150
+ private _computeLangOverflow() {
151
+ const group = this.shadowRoot?.querySelector<HTMLElement>('.translations__language-group');
152
+ if (!group) return;
153
+
154
+ const buttons = Array.from(group.querySelectorAll<HTMLElement>('zn-button[data-lang-btn]'));
155
+ if (buttons.length < 2) {
156
+ if (this._overflowIndex !== -1) this._overflowIndex = -1;
157
+ return;
158
+ }
159
+
160
+ const firstTop = buttons[0].getBoundingClientRect().top;
161
+ let wrapAt = -1;
162
+ for (let i = 1; i < buttons.length; i++) {
163
+ if (buttons[i].getBoundingClientRect().top > firstTop + 1) {
164
+ wrapAt = i;
165
+ break;
166
+ }
167
+ }
168
+
169
+ if (wrapAt === -1) {
170
+ const trailing = group.querySelector<HTMLElement>('[data-lang-overflow], [data-lang-add]');
171
+ if (trailing && trailing.getBoundingClientRect().top > firstTop + 1) {
172
+ wrapAt = buttons.length;
173
+ }
174
+ }
175
+
176
+ if (wrapAt === -1) return;
177
+
178
+ const newIndex = this._overflowIndex === -1
179
+ ? wrapAt
180
+ : Math.max(1, Math.min(wrapAt, this._overflowIndex - 1));
181
+
182
+ if (newIndex !== this._overflowIndex) {
183
+ this._overflowIndex = newIndex;
184
+ }
104
185
  }
105
186
 
106
187
  willUpdate(changedProperties: PropertyValues) {
@@ -164,6 +245,15 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
164
245
  }
165
246
  };
166
247
 
248
+ private handleOverflowSelect = (e: ZnMenuSelectEvent) => {
249
+ e.stopPropagation();
250
+ const element = e.detail.element as HTMLElement;
251
+ const languageCode = element.getAttribute('data-path');
252
+ if (languageCode) {
253
+ this.switchLanguage(languageCode);
254
+ }
255
+ };
256
+
167
257
  private switchLanguage = (lang: string) => {
168
258
  this._activeLanguage = lang;
169
259
  this.requestUpdate();
@@ -223,6 +313,16 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
223
313
  visibleTabs.unshift('en');
224
314
  }
225
315
 
316
+ const overflowCutoff = this._overflowIndex === -1 ? visibleTabs.length : this._overflowIndex;
317
+ const visibleLangTabs = visibleTabs.slice(0, overflowCutoff);
318
+ const overflowLangTabs = visibleTabs.slice(overflowCutoff);
319
+ const overflowActions = overflowLangTabs.map(code => ({
320
+ title: code.toUpperCase(),
321
+ type: 'dropdown',
322
+ path: code,
323
+ icon: code === this._activeLanguage ? 'check' : ''
324
+ }));
325
+
226
326
  const currentTranslation = this.values[this._activeLanguage] ?? '';
227
327
  const isRTL = this.isRTLLanguage(this._activeLanguage);
228
328
 
@@ -252,9 +352,10 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
252
352
  <div class="translations__actions">
253
353
  ${hasExpandSlot ? html`<slot name="expand"></slot>` : nothing}
254
354
  ${hasMultipleLanguages ? html`
255
- <zn-button-group>
256
- ${visibleTabs.map(code => html`
355
+ <zn-button-group class="translations__language-group">
356
+ ${visibleLangTabs.map(code => html`
257
357
  <zn-button
358
+ data-lang-btn
258
359
  size="x-small"
259
360
  color="default"
260
361
  ?outline="${code !== this._activeLanguage}"
@@ -262,8 +363,23 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
262
363
  >${code.toUpperCase()}
263
364
  </zn-button>
264
365
  `)}
366
+ ${overflowActions.length > 0 ? html`
367
+ <zn-dropdown placement="bottom-end" data-lang-overflow>
368
+ <zn-button
369
+ slot="trigger"
370
+ size="x-small"
371
+ color="default"
372
+ icon="keyboard_arrow_down"
373
+ ?outline="${!overflowLangTabs.includes(this._activeLanguage)}"
374
+ ></zn-button>
375
+ <zn-menu
376
+ .actions=${overflowActions}
377
+ @zn-menu-select="${this.handleOverflowSelect}"
378
+ ></zn-menu>
379
+ </zn-dropdown>
380
+ ` : nothing}
265
381
  ${availableLanguages.length > 0 ? html`
266
- <zn-dropdown placement="bottom-end">
382
+ <zn-dropdown placement="bottom-end" data-lang-add>
267
383
  <zn-button
268
384
  slot="trigger"
269
385
  size="x-small"
@@ -287,6 +403,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
287
403
  ${keyed(this._activeLanguage, html`
288
404
  <zn-inline-edit
289
405
  input-type=${this.inputType}
406
+ textarea-rows=${ifDefined(this.textareaRows)}
290
407
  .value=${live(currentTranslation)}
291
408
  name="${this.name}"
292
409
  placeholder="Enter translation..."