@kubex/zinc 1.0.25 → 1.0.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 (198) hide show
  1. package/.claude/project.md +556 -0
  2. package/.github/workflows/js_build_and_deploy.yaml +9 -10
  3. package/dist/custom-elements.json +13431 -8728
  4. package/dist/vscode.html-custom-data.json +571 -37
  5. package/dist/web-types.json +1439 -181
  6. package/dist/zn.d.ts +1642 -661
  7. package/dist/zn.min.css +1 -1
  8. package/dist/zn.min.js +1406 -891
  9. package/docs/_utilities/code-previews.cjs +10 -1
  10. package/docs/assets/scripts/docs.js +42 -18
  11. package/docs/data/data-table.json +229 -66
  12. package/docs/data/empty.json +6 -0
  13. package/docs/data/products-table.json +41 -193
  14. package/docs/pages/components/action-bar.md +121 -6
  15. package/docs/pages/components/alert.md +118 -15
  16. package/docs/pages/components/animated-button.md +299 -0
  17. package/docs/pages/components/audio-select.md +534 -0
  18. package/docs/pages/components/bulk-actions.md +79 -5
  19. package/docs/pages/components/button-group.md +235 -27
  20. package/docs/pages/components/button.md +139 -1
  21. package/docs/pages/components/chart.md +365 -9
  22. package/docs/pages/components/checkbox.md +387 -58
  23. package/docs/pages/components/chip.md +253 -33
  24. package/docs/pages/components/collapsible.md +393 -46
  25. package/docs/pages/components/cols.md +152 -30
  26. package/docs/pages/components/confirm.md +182 -16
  27. package/docs/pages/components/copy-button.md +169 -6
  28. package/docs/pages/components/data-select.md +194 -15
  29. package/docs/pages/components/data-table-filter.md +96 -5
  30. package/docs/pages/components/data-table-search.md +451 -0
  31. package/docs/pages/components/data-table-sort.md +66 -5
  32. package/docs/pages/components/data-table.md +727 -66
  33. package/docs/pages/components/datepicker.md +402 -7
  34. package/docs/pages/components/defined-label.md +120 -5
  35. package/docs/pages/components/dialog.md +339 -59
  36. package/docs/pages/components/dropdown.md +366 -11
  37. package/docs/pages/components/editor.md +604 -7
  38. package/docs/pages/components/empty-state.md +315 -16
  39. package/docs/pages/components/expanding-action.md +212 -28
  40. package/docs/pages/components/file.md +421 -12
  41. package/docs/pages/components/filter-container.md +158 -5
  42. package/docs/pages/components/filter-wrapper.md +167 -6
  43. package/docs/pages/components/form-group.md +383 -6
  44. package/docs/pages/components/hover-container.md +237 -6
  45. package/docs/pages/components/icon-picker.md +121 -0
  46. package/docs/pages/components/icon.md +454 -48
  47. package/docs/pages/components/inline-edit.md +787 -19
  48. package/docs/pages/components/input-group.md +207 -13
  49. package/docs/pages/components/input.md +194 -0
  50. package/docs/pages/components/key-container.md +115 -0
  51. package/docs/pages/components/key.md +101 -0
  52. package/docs/pages/components/menu.md +512 -6
  53. package/docs/pages/components/note.md +239 -36
  54. package/docs/pages/components/opt-group.md +107 -0
  55. package/docs/pages/components/pagination.md +332 -6
  56. package/docs/pages/components/pane.md +222 -6
  57. package/docs/pages/components/panel.md +213 -26
  58. package/docs/pages/components/popup.md +1354 -6
  59. package/docs/pages/components/priority-list.md +233 -0
  60. package/docs/pages/components/progress-bar.md +387 -6
  61. package/docs/pages/components/radio-group.md +242 -5
  62. package/docs/pages/components/radio.md +474 -6
  63. package/docs/pages/components/rating.md +331 -7
  64. package/docs/pages/components/reveal.md +524 -6
  65. package/docs/pages/components/scroll-container.md +552 -6
  66. package/docs/pages/components/select.md +762 -160
  67. package/docs/pages/components/skeleton.md +473 -30
  68. package/docs/pages/components/slideout.md +261 -5
  69. package/docs/pages/components/sp.md +99 -41
  70. package/docs/pages/components/split-button.md +347 -9
  71. package/docs/pages/components/split-pane.md +445 -6
  72. package/docs/pages/components/stat.md +549 -9
  73. package/docs/pages/components/status-indicator.md +230 -1
  74. package/docs/pages/components/stepper.md +563 -6
  75. package/docs/pages/components/tabs.md +439 -13
  76. package/docs/pages/components/textarea.md +415 -17
  77. package/docs/pages/components/tile.md +385 -26
  78. package/docs/pages/components/toggle.md +424 -6
  79. package/docs/pages/components/tooltip.md +367 -0
  80. package/docs/pages/components/translation-group.md +210 -0
  81. package/docs/pages/components/translations.md +319 -19
  82. package/docs/pages/components/vertical-stepper.md +401 -8
  83. package/docs/pages/components/well.md +300 -3
  84. package/docs/pages/getting-started/form-controls.md +162 -0
  85. package/docs/pages/index.md +116 -15
  86. package/package.json +5 -6
  87. package/scss/_global-spacing.scss +4 -0
  88. package/scss/_root.scss +2 -2
  89. package/scss/boot.scss +19 -0
  90. package/scss/shared/layout.scss +5 -0
  91. package/scss/themes/_light.scss +3 -3
  92. package/src/components/alert/alert.scss +1 -0
  93. package/src/components/animated-button/README.md +306 -0
  94. package/src/components/animated-button/animated-button.component.ts +229 -0
  95. package/src/components/animated-button/animated-button.scss +228 -0
  96. package/src/components/animated-button/animated-button.test.ts +143 -0
  97. package/src/components/animated-button/index.ts +12 -0
  98. package/src/components/button/button.component.ts +5 -0
  99. package/src/components/button-group/button-group.component.ts +25 -13
  100. package/src/components/button-group/button-group.scss +6 -0
  101. package/src/components/checkbox/checkbox.component.ts +4 -0
  102. package/src/components/checkbox/checkbox.scss +7 -4
  103. package/src/components/checkbox-group/checkbox-group.scss +7 -0
  104. package/src/components/chip/chip.component.ts +7 -2
  105. package/src/components/chip/chip.scss +16 -2
  106. package/src/components/collapsible/collapsible.component.ts +27 -20
  107. package/src/components/cols/cols.component.ts +14 -2
  108. package/src/components/cols/cols.scss +6 -12
  109. package/src/components/confirm/confirm.component.ts +7 -0
  110. package/src/components/copy-button/copy-button.component.ts +12 -11
  111. package/src/components/data-select/data-select.component.ts +215 -44
  112. package/src/components/data-select/data-select.scss +66 -6
  113. package/src/components/data-select/providers/currency-data-provider.ts +20 -6
  114. package/src/components/data-select/providers/provider.ts +1 -0
  115. package/src/components/data-select/providers/us-state-data-provider.ts +68 -0
  116. package/src/components/data-table/data-table.component.ts +81 -22
  117. package/src/components/data-table-search/data-table-search.component.ts +184 -0
  118. package/src/components/data-table-search/data-table-search.scss +17 -0
  119. package/src/components/data-table-search/data-table-search.test.ts +11 -0
  120. package/src/components/data-table-search/index.ts +12 -0
  121. package/src/components/datepicker/datepicker.component.ts +347 -13
  122. package/src/components/dialog/dialog.component.ts +1 -1
  123. package/src/components/empty-state/empty-state.scss +6 -1
  124. package/src/components/header/header.scss +3 -3
  125. package/src/components/icon/icon.component.ts +23 -1
  126. package/src/components/icon/icon.scss +23 -0
  127. package/src/components/icon-picker/brand-icons.ts +24 -0
  128. package/src/components/icon-picker/icon-picker.component.ts +407 -0
  129. package/src/components/icon-picker/icon-picker.scss +205 -0
  130. package/src/components/icon-picker/index.ts +12 -0
  131. package/src/components/icon-picker/line-icons.ts +612 -0
  132. package/src/components/icon-picker/material-icons.ts +14543 -0
  133. package/src/components/inline-edit/inline-edit.component.ts +190 -46
  134. package/src/components/inline-edit/inline-edit.scss +29 -0
  135. package/src/components/inline-edit/inline-edit.test.ts +314 -1
  136. package/src/components/input/input.component.ts +324 -5
  137. package/src/components/input/input.scss +87 -0
  138. package/src/components/input-group/input-group.component.ts +28 -7
  139. package/src/components/input-group/input-group.scss +48 -10
  140. package/src/components/item/item.component.ts +11 -2
  141. package/src/components/item/item.scss +7 -0
  142. package/src/components/key/index.ts +12 -0
  143. package/src/components/key/key.component.ts +47 -0
  144. package/src/components/key/key.scss +38 -0
  145. package/src/components/key-container/index.ts +12 -0
  146. package/src/components/key-container/key-container.component.ts +142 -0
  147. package/src/components/key-container/key-container.scss +33 -0
  148. package/src/components/menu-item/menu-item.component.ts +20 -0
  149. package/src/components/menu-item/menu-item.scss +69 -2
  150. package/src/components/navbar/navbar.component.ts +103 -11
  151. package/src/components/navbar/navbar.scss +6 -1
  152. package/src/components/opt-group/index.ts +12 -0
  153. package/src/components/opt-group/opt-group.component.ts +74 -0
  154. package/src/components/opt-group/opt-group.scss +37 -0
  155. package/src/components/opt-group/opt-group.test.ts +27 -0
  156. package/src/components/option/option.component.ts +1 -16
  157. package/src/components/option/option.scss +4 -0
  158. package/src/components/panel/panel.component.ts +10 -8
  159. package/src/components/panel/panel.scss +20 -0
  160. package/src/components/priority-list/index.ts +12 -0
  161. package/src/components/priority-list/priority-list.component.ts +611 -0
  162. package/src/components/priority-list/priority-list.scss +185 -0
  163. package/src/components/priority-list/priority-list.test.ts +139 -0
  164. package/src/components/progress-bar/progress-bar.component.ts +22 -16
  165. package/src/components/radio/radio.scss +10 -3
  166. package/src/components/select/select.component.ts +812 -72
  167. package/src/components/select/select.scss +108 -2
  168. package/src/components/select/select.test.ts +147 -2
  169. package/src/components/settings-container/settings-container.component.ts +44 -19
  170. package/src/components/settings-container/settings-container.scss +1 -1
  171. package/src/components/skeleton/skeleton.component.ts +6 -1
  172. package/src/components/slideout/slideout.scss +8 -3
  173. package/src/components/sp/sp.component.ts +3 -1
  174. package/src/components/sp/sp.scss +19 -0
  175. package/src/components/stepper/stepper.component.ts +16 -10
  176. package/src/components/textarea/textarea.scss +0 -4
  177. package/src/components/tile/tile.component.ts +12 -3
  178. package/src/components/tile/tile.scss +1 -1
  179. package/src/components/timer/timer.component.ts +15 -2
  180. package/src/components/toggle/toggle.component.ts +29 -26
  181. package/src/components/toggle/toggle.scss +13 -1
  182. package/src/components/translation-group/index.ts +12 -0
  183. package/src/components/translation-group/translation-group.component.ts +207 -0
  184. package/src/components/translation-group/translation-group.scss +8 -0
  185. package/src/components/translations/translations.component.ts +90 -44
  186. package/src/components/translations/translations.scss +4 -0
  187. package/src/components/vertical-stepper/vertical-stepper.component.ts +14 -9
  188. package/src/events/events.ts +12 -7
  189. package/src/events/zn-language-change.ts +7 -0
  190. package/src/events/zn-purchase.ts +11 -0
  191. package/src/events/zn-redirect.ts +7 -0
  192. package/src/events/zn-reorder.ts +7 -0
  193. package/src/events/zn-search-change.ts +11 -0
  194. package/src/events/zn-submit.ts +1 -1
  195. package/src/form-control.scss +4 -0
  196. package/src/internal/form-navigation.ts +510 -0
  197. package/src/internal/form.ts +6 -0
  198. package/src/zinc.ts +91 -83
@@ -66,8 +66,10 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
66
66
  private readonly localize = new LocalizeController(this);
67
67
 
68
68
  @query('.input__control') input: HTMLInputElement;
69
+ @query('.input__color-picker') colorPicker: HTMLInputElement;
69
70
 
70
71
  @state() private hasFocus = false;
72
+ @state() private isUserTyping = false;
71
73
  @property() title = "" // make reactive pass through
72
74
 
73
75
  private __numberInput = Object.assign(document.createElement('input'), {type: 'number'});
@@ -77,7 +79,7 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
77
79
  * The type of input. Works the same as native `<input>` element. But only a subset of types is supported. Defaults
78
80
  * to `text`
79
81
  */
80
- @property({reflect: true}) type: 'currency' | 'date' | 'datetime-local' | 'email' | 'number' | 'password' |
82
+ @property({reflect: true}) type: 'color' | 'currency' | 'date' | 'datetime-local' | 'email' | 'number' | 'password' |
81
83
  'search' | 'tel' | 'text' | 'time' | 'url' = 'text';
82
84
 
83
85
  /** The name of the input, submitted as a name/value pair with form data. */
@@ -141,6 +143,9 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
141
143
  /** Hides the browsers built-in increment/decrement spin buttons for number inputs **/
142
144
  @property({attribute: 'no-spin-buttons', type: Boolean}) noSpinButtons: boolean = false;
143
145
 
146
+ /** The color format to display for color inputs. Only applies when type is 'color'. **/
147
+ @property({attribute: 'color-format'}) colorFormat: 'hex' | 'rgb' | 'hsl' | 'oklch' = 'hex';
148
+
144
149
  /**
145
150
  * By default, form-controls are associated with the nearest containing `<form>` element. This attribute allows you
146
151
  * to place the form control outside a form and associate it with the form that has this `id`. The form must be
@@ -261,15 +266,247 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
261
266
  }
262
267
  }
263
268
 
269
+ // Color format conversion utilities
270
+ private hexToRgb(hex: string): string {
271
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
272
+ if (!result) return '';
273
+ const r = parseInt(result[1], 16);
274
+ const g = parseInt(result[2], 16);
275
+ const b = parseInt(result[3], 16);
276
+ return `rgb(${r}, ${g}, ${b})`;
277
+ }
278
+
279
+ private rgbToHex(rgb: string): string {
280
+ const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
281
+ if (!match) return '';
282
+ const r = parseInt(match[1]).toString(16).padStart(2, '0');
283
+ const g = parseInt(match[2]).toString(16).padStart(2, '0');
284
+ const b = parseInt(match[3]).toString(16).padStart(2, '0');
285
+ return `#${r}${g}${b}`;
286
+ }
287
+
288
+ private hexToOklch(hex: string): string {
289
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
290
+ if (!result) return '';
291
+
292
+ // Convert hex to RGB (0-1 range)
293
+ let r = parseInt(result[1], 16) / 255;
294
+ let g = parseInt(result[2], 16) / 255;
295
+ let b = parseInt(result[3], 16) / 255;
296
+
297
+ // Apply gamma correction
298
+ r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
299
+ g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
300
+ b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
301
+
302
+ // Convert to XYZ
303
+ const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
304
+ const y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
305
+ const z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041;
306
+
307
+ // Convert XYZ to Lab
308
+ const xn = x / 0.95047;
309
+ const yn = y / 1.00000;
310
+ const zn = z / 1.08883;
311
+
312
+ const fx = xn > 0.008856 ? Math.pow(xn, 1/3) : (7.787 * xn + 16/116);
313
+ const fy = yn > 0.008856 ? Math.pow(yn, 1/3) : (7.787 * yn + 16/116);
314
+ const fz = zn > 0.008856 ? Math.pow(zn, 1/3) : (7.787 * zn + 16/116);
315
+
316
+ const L = 116 * fy - 16;
317
+ const a = 500 * (fx - fy);
318
+ const b_lab = 200 * (fy - fz);
319
+
320
+ // Convert Lab to LCH (which is close to OKLCH)
321
+ const C = Math.sqrt(a * a + b_lab * b_lab);
322
+ let H = Math.atan2(b_lab, a) * 180 / Math.PI;
323
+ if (H < 0) H += 360;
324
+
325
+ // Normalize to OKLCH ranges
326
+ const l = (L / 100).toFixed(3);
327
+ const c = (C / 150).toFixed(3);
328
+ const h = H.toFixed(1);
329
+
330
+ return `oklch(${l} ${c} ${h})`;
331
+ }
332
+
333
+ private oklchToHex(oklch: string): string {
334
+ const match = oklch.match(/^oklch\(([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\)$/);
335
+ if (!match) return '';
336
+
337
+ const L = parseFloat(match[1]) * 100;
338
+ const C = parseFloat(match[2]) * 150;
339
+ const H = parseFloat(match[3]);
340
+
341
+ // Convert LCH to Lab
342
+ const a = C * Math.cos(H * Math.PI / 180);
343
+ const b_lab = C * Math.sin(H * Math.PI / 180);
344
+
345
+ // Convert Lab to XYZ
346
+ const fy = (L + 16) / 116;
347
+ const fx = a / 500 + fy;
348
+ const fz = fy - b_lab / 200;
349
+
350
+ const xn = fx > 0.206897 ? Math.pow(fx, 3) : (fx - 16/116) / 7.787;
351
+ const yn = fy > 0.206897 ? Math.pow(fy, 3) : (fy - 16/116) / 7.787;
352
+ const zn = fz > 0.206897 ? Math.pow(fz, 3) : (fz - 16/116) / 7.787;
353
+
354
+ const x = xn * 0.95047;
355
+ const y = yn * 1.00000;
356
+ const z = zn * 1.08883;
357
+
358
+ // Convert XYZ to RGB
359
+ let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
360
+ let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560;
361
+ let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
362
+
363
+ // Apply gamma correction
364
+ r = r > 0.0031308 ? 1.055 * Math.pow(r, 1/2.4) - 0.055 : 12.92 * r;
365
+ g = g > 0.0031308 ? 1.055 * Math.pow(g, 1/2.4) - 0.055 : 12.92 * g;
366
+ b = b > 0.0031308 ? 1.055 * Math.pow(b, 1/2.4) - 0.055 : 12.92 * b;
367
+
368
+ // Clamp and convert to hex
369
+ r = Math.max(0, Math.min(1, r));
370
+ g = Math.max(0, Math.min(1, g));
371
+ b = Math.max(0, Math.min(1, b));
372
+
373
+ const rHex = Math.round(r * 255).toString(16).padStart(2, '0');
374
+ const gHex = Math.round(g * 255).toString(16).padStart(2, '0');
375
+ const bHex = Math.round(b * 255).toString(16).padStart(2, '0');
376
+
377
+ return `#${rHex}${gHex}${bHex}`;
378
+ }
379
+
380
+ private hexToHsl(hex: string): string {
381
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
382
+ if (!result) return '';
383
+ let r = parseInt(result[1], 16) / 255;
384
+ let g = parseInt(result[2], 16) / 255;
385
+ let b = parseInt(result[3], 16) / 255;
386
+
387
+ const max = Math.max(r, g, b);
388
+ const min = Math.min(r, g, b);
389
+ const l = (max + min) / 2;
390
+ let h = 0;
391
+ let s = 0;
392
+
393
+ if (max !== min) {
394
+ const d = max - min;
395
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
396
+ switch (max) {
397
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
398
+ case g: h = ((b - r) / d + 2) / 6; break;
399
+ case b: h = ((r - g) / d + 4) / 6; break;
400
+ }
401
+ }
402
+
403
+ return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
404
+ }
405
+
406
+ private hslToHex(hsl: string): string {
407
+ const match = hsl.match(/^hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$/);
408
+ if (!match) return '';
409
+ const h = parseInt(match[1]) / 360;
410
+ const s = parseInt(match[2]) / 100;
411
+ const l = parseInt(match[3]) / 100;
412
+
413
+ let r: number, g: number, b: number;
414
+ if (s === 0) {
415
+ r = g = b = l;
416
+ } else {
417
+ const hue2rgb = (p: number, q: number, t: number) => {
418
+ if (t < 0) t += 1;
419
+ if (t > 1) t -= 1;
420
+ if (t < 1/6) return p + (q - p) * 6 * t;
421
+ if (t < 1/2) return q;
422
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
423
+ return p;
424
+ };
425
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
426
+ const p = 2 * l - q;
427
+ r = hue2rgb(p, q, h + 1/3);
428
+ g = hue2rgb(p, q, h);
429
+ b = hue2rgb(p, q, h - 1/3);
430
+ }
431
+
432
+ const rHex = Math.round(r * 255).toString(16).padStart(2, '0');
433
+ const gHex = Math.round(g * 255).toString(16).padStart(2, '0');
434
+ const bHex = Math.round(b * 255).toString(16).padStart(2, '0');
435
+ return `#${rHex}${gHex}${bHex}`;
436
+ }
437
+
438
+ private convertToHex(value: string): string {
439
+ if (!value) return '#000000';
440
+ if (value.startsWith('#')) return value;
441
+ if (value.startsWith('rgb')) return this.rgbToHex(value);
442
+ if (value.startsWith('hsl')) return this.hslToHex(value);
443
+ if (value.startsWith('oklch')) return this.oklchToHex(value);
444
+ return value;
445
+ }
446
+
447
+ private convertFromHex(hex: string): string {
448
+ if (!hex) return '';
449
+ if (this.colorFormat === 'hex') return hex;
450
+ if (this.colorFormat === 'rgb') return this.hexToRgb(hex);
451
+ if (this.colorFormat === 'hsl') return this.hexToHsl(hex);
452
+ if (this.colorFormat === 'oklch') return this.hexToOklch(hex);
453
+ return hex;
454
+ }
455
+
264
456
  private handleBlur() {
265
457
  this.hasFocus = false;
458
+ this.isUserTyping = false;
266
459
  this.validateMinMax();
460
+
461
+ // Normalize color value to the correct format on blur
462
+ if (this.type === 'color' && this.value) {
463
+ const hexValue = this.convertToHex(this.value);
464
+ if (hexValue) {
465
+ const normalizedValue = this.convertFromHex(hexValue);
466
+ if (normalizedValue !== this.value) {
467
+ this.value = normalizedValue;
468
+ }
469
+ }
470
+ }
471
+
267
472
  this.emit('zn-blur');
268
473
  }
269
474
 
270
475
  private handleChange() {
271
476
  if (this.type === 'currency' && this.input.value) {
272
477
  this.value = parseFloat(this.input.value as string).toFixed(2)
478
+ } else if (this.type === 'color') {
479
+ const colorValue = this.input.value.trim();
480
+
481
+ // Validate based on format
482
+ if (colorValue === '') {
483
+ this.value = '';
484
+ } else if (this.colorFormat === 'hex') {
485
+ // Validate hex format
486
+ let hexValue = colorValue;
487
+ if (hexValue && !hexValue.startsWith('#')) {
488
+ hexValue = '#' + hexValue;
489
+ }
490
+ if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hexValue)) {
491
+ this.value = hexValue.toLowerCase();
492
+ }
493
+ } else if (this.colorFormat === 'rgb') {
494
+ // Validate rgb format
495
+ if (/^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/.test(colorValue)) {
496
+ this.value = colorValue;
497
+ }
498
+ } else if (this.colorFormat === 'hsl') {
499
+ // Validate hsl format
500
+ if (/^hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\)$/.test(colorValue)) {
501
+ this.value = colorValue;
502
+ }
503
+ } else if (this.colorFormat === 'oklch') {
504
+ // Validate oklch format
505
+ if (/^oklch\(\s*[0-9.]+\s+[0-9.]+\s+[0-9.]+\s*\)$/.test(colorValue)) {
506
+ this.value = colorValue;
507
+ }
508
+ }
509
+ // If invalid, keep the previous value
273
510
  } else {
274
511
  this.value = this.input.value;
275
512
  }
@@ -293,7 +530,13 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
293
530
  }
294
531
 
295
532
  private handleInput() {
296
- this.value = this.input.value;
533
+ this.isUserTyping = true;
534
+ if (this.type === 'color') {
535
+ // For color inputs, update value on input but don't validate format yet
536
+ this.value = this.input.value;
537
+ } else {
538
+ this.value = this.input.value;
539
+ }
297
540
  this.formControlController.updateValidity();
298
541
  this.emit('zn-input');
299
542
  }
@@ -312,6 +555,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
312
555
  // When using a Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
313
556
  // to check for this is to look at event.isComposing, which will be true when the IME is open.
314
557
  if (!event.defaultPrevented && !event.isComposing) {
558
+ // Check if form has navigation enabled - if so, let FormNavigationController handle it
559
+ const form = this.formControlController.getForm();
560
+ if (form?.hasAttribute('enter-navigation') || form?.hasAttribute('data-enter-navigation')) {
561
+ return; // Let FormNavigationController handle it
562
+ }
563
+
315
564
  this.formControlController.submit();
316
565
  }
317
566
  }
@@ -321,6 +570,22 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
321
570
  this.passwordVisible = !this.passwordVisible;
322
571
  }
323
572
 
573
+ private handleColorSwatchClick() {
574
+ if (this.type === 'color' && this.colorPicker) {
575
+ this.colorPicker.click();
576
+ }
577
+ }
578
+
579
+ private handleColorPickerChange() {
580
+ if (this.type === 'color' && this.colorPicker) {
581
+ // Convert hex from native picker to the specified format
582
+ const hexValue = this.colorPicker.value;
583
+ this.value = this.convertFromHex(hexValue);
584
+ this.emit('zn-change');
585
+ this.emit('zn-input');
586
+ }
587
+ }
588
+
324
589
  private focusInput(event: MouseEvent) {
325
590
  if (this.hasFocus) {
326
591
  return;
@@ -354,7 +619,44 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
354
619
  @watch('value', {waitUntilFirstUpdate: true})
355
620
  async handleValueChange() {
356
621
  await this.updateComplete;
622
+
623
+ // Normalize color value to match colorFormat, but NOT while user is typing
624
+ // Only normalize on initial load or programmatic changes
625
+ if (this.type === 'color' && this.value && !this.isUserTyping) {
626
+ const hexValue = this.convertToHex(this.value);
627
+ if (hexValue) {
628
+ const normalizedValue = this.convertFromHex(hexValue);
629
+ // Only update if the conversion actually changed something
630
+ // This prevents infinite loops because once normalized, it won't change again
631
+ if (normalizedValue !== this.value) {
632
+ this.value = normalizedValue;
633
+ this.emit('zn-change');
634
+ return; // Will re-trigger, but next time values will match
635
+ }
636
+ }
637
+ }
638
+
357
639
  this.formControlController.updateValidity();
640
+
641
+ // Update color picker if this is a color input (needs hex)
642
+ if (this.type === 'color' && this.colorPicker) {
643
+ this.colorPicker.value = this.convertToHex(this.value) || '#000000';
644
+ }
645
+ }
646
+
647
+ @watch('colorFormat', {waitUntilFirstUpdate: true})
648
+ async handleColorFormatChange() {
649
+ // When color format changes, convert the current value to the new format
650
+ if (this.type === 'color' && this.value) {
651
+ const hexValue = this.convertToHex(this.value);
652
+ if (hexValue) {
653
+ const normalizedValue = this.convertFromHex(hexValue);
654
+ if (normalizedValue !== this.value) {
655
+ this.value = normalizedValue;
656
+ this.emit('zn-change');
657
+ }
658
+ }
659
+ }
358
660
  }
359
661
 
360
662
  /** Sets focus on the input. */
@@ -511,7 +813,23 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
511
813
  })}>
512
814
 
513
815
  <span part="prefix" class="input__prefix">
514
- ${this.type === 'currency'
816
+ ${this.type === 'color'
817
+ ? html`
818
+ <div
819
+ class="input__color-swatch"
820
+ @click=${this.handleColorSwatchClick}
821
+ style="--color-value: ${this.convertToHex(this.value) || '#000000'}">
822
+ <div class="input__color-swatch-inner"></div>
823
+ </div>
824
+ <input
825
+ class="input__color-picker"
826
+ type="color"
827
+ .value=${this.convertToHex(this.value) || '#000000'}
828
+ @change=${this.handleColorPickerChange}
829
+ @input=${this.handleColorPickerChange}
830
+ tabindex="-1"
831
+ />`
832
+ : this.type === 'currency'
515
833
  ? html`
516
834
  <zn-icon class="input__prefix-default" src="payments"></zn-icon>`
517
835
  : this.type === 'email' && hasOptionalIcon
@@ -528,7 +846,8 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
528
846
  part="input"
529
847
  id="input"
530
848
  class="input__control"
531
- type=${this.type === 'password' && this.passwordVisible ? 'text' : this.type === 'currency' ? 'number' : this.type}
849
+ type=${this.type === 'password' && this.passwordVisible ? 'text' : this.type === 'currency' ? 'number' : this.type === 'color' ? 'text' : this.type}
850
+ ?data-color-input=${this.type === 'color'}
532
851
  title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
533
852
  name=${ifDefined(this.name)}
534
853
  ?disabled=${this.disabled}
@@ -539,7 +858,7 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
539
858
  maxlength=${ifDefined(this.maxlength)}
540
859
  min=${ifDefined(this.min)}
541
860
  max=${ifDefined(this.max)}
542
- step=${ifDefined(this.step)}
861
+ step=${ifDefined(this.type === 'currency' ? 0.01 : this.step)}
543
862
  .value=${live(this.value)}
544
863
  autocapitalize=${ifDefined(this.autocapitalize)}
545
864
  autocomplete=${ifDefined(this.autocomplete)}
@@ -93,6 +93,12 @@
93
93
  -webkit-appearance: none;
94
94
  }
95
95
 
96
+ /* Color input text should use monospace font */
97
+ input[type='text'].input__control[data-color-input] {
98
+ font-family: var(--zn-font-mono, 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace);
99
+ font-size: 0.9em;
100
+ }
101
+
96
102
  .input__control::-webkit-search-decoration,
97
103
  .input__control::-webkit-search-cancel-button,
98
104
  .input__control::-webkit-search-results-button,
@@ -362,3 +368,84 @@
362
368
  input[type='date']::-webkit-calendar-picker-indicator {
363
369
  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="14" width="12.25" viewBox="0 0 448 512"><path fill="%236d7176" d="M128 0c13.3 0 24 10.7 24 24V64H296V24c0-13.3 10.7-24 24-24s24 10.7 24 24V64h40c35.3 0 64 28.7 64 64v16 48V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192 144 128C0 92.7 28.7 64 64 64h40V24c0-13.3 10.7-24 24-24zM400 192H48V448c0 8.8 7.2 16 16 16H384c8.8 0 16-7.2 16-16V192zM112 256h96c8.8 0 16 7.2 16 16v96c0 8.8-7.2 16-16 16H112c-8.8 0-16-7.2-16-16V272c0-8.8 7.2-16 16-16z"/></svg>');
364
370
  }
371
+
372
+ /* Color input styling */
373
+ .input__color-swatch {
374
+ display: inline-flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ width: 1.75rem;
378
+ height: 1.75rem;
379
+ border-radius: var(--zn-border-radius-medium);
380
+ border: solid 1px rgba(0, 0, 0, 0.15);
381
+ background-color: var(--zn-color-neutral-0);
382
+ cursor: pointer;
383
+ transition: var(--zn-transition-fast) border-color, var(--zn-transition-fast) box-shadow;
384
+ overflow: hidden;
385
+ position: relative;
386
+ flex-shrink: 0;
387
+ margin-inline-start: var(--zn-input-spacing-small);
388
+
389
+ &:hover {
390
+ border-color: rgba(0, 0, 0, 0.25);
391
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
392
+ }
393
+ }
394
+
395
+ /* Size-specific spacing adjustments for color swatch */
396
+ .input--x-small .input__color-swatch {
397
+ margin-inline-start: var(--zn-input-spacing-x-small);
398
+ }
399
+
400
+ .input--small .input__color-swatch {
401
+ margin-inline-start: var(--zn-input-spacing-small);
402
+ }
403
+
404
+ .input--medium .input__color-swatch {
405
+ margin-inline-start: var(--zn-input-spacing-medium);
406
+ }
407
+
408
+ .input--large .input__color-swatch {
409
+ margin-inline-start: var(--zn-input-spacing-medium);
410
+ }
411
+
412
+ .input__color-swatch-inner {
413
+ width: 100%;
414
+ height: 100%;
415
+ border-radius: calc(var(--zn-border-radius-medium) - 2px);
416
+ background-color: var(--color-value, #000000);
417
+ }
418
+
419
+ /* Hide the native color input but keep it functional */
420
+ .input__color-picker {
421
+ position: absolute;
422
+ opacity: 0;
423
+ width: 0;
424
+ height: 0;
425
+ padding: 0;
426
+ margin: 0;
427
+ pointer-events: none;
428
+ border: none;
429
+ visibility: hidden;
430
+ }
431
+
432
+ /* Size adjustments for color inputs */
433
+ .input--x-small .input__color-swatch {
434
+ width: 1.25rem;
435
+ height: 1.25rem;
436
+ }
437
+
438
+ .input--small .input__color-swatch {
439
+ width: 1.5rem;
440
+ height: 1.5rem;
441
+ }
442
+
443
+ .input--medium .input__color-swatch {
444
+ width: 1.75rem;
445
+ height: 1.75rem;
446
+ }
447
+
448
+ .input--large .input__color-swatch {
449
+ width: 2rem;
450
+ height: 2rem;
451
+ }
@@ -30,20 +30,32 @@ export default class ZnInputGroup extends ZincElement {
30
30
  /** The input group's label. If you need to display HTML, use the `label` slot. */
31
31
  @property() label = '';
32
32
 
33
+ /** Adds a gap between the grouped elements, preserving individual border radii. Use `sm`, `md`, or `lg`. */
34
+ @property() gap: 'sm' | 'md' | 'lg';
35
+
33
36
  private handleSlotChange() {
34
37
  const slottedElements = [...this.defaultSlot.assignedElements({flatten: true})] as HTMLElement[];
35
38
 
36
- // Filter for inputs and selects
39
+ // Filter for inputs, selects, and buttons
37
40
  const supportedTags = ['ZN-INPUT', 'ZN-SELECT', 'ZN-BUTTON'];
38
41
  const controls = slottedElements.filter(el => supportedTags.includes(el.tagName));
39
42
 
40
- controls.forEach(el => {
41
- const index = controls.indexOf(el);
43
+ // Buttons are styled independently — only inputs/selects participate in joined positioning
44
+ const joinedControls = controls.filter(el => el.tagName !== 'ZN-BUTTON');
42
45
 
46
+ controls.forEach(el => {
43
47
  el.toggleAttribute('data-zn-input-group__input', true);
44
- el.toggleAttribute('data-zn-input-group__input--first', index === 0);
45
- el.toggleAttribute('data-zn-input-group__input--inner', index > 0 && index < controls.length - 1);
46
- el.toggleAttribute('data-zn-input-group__input--last', index === controls.length - 1);
48
+
49
+ if (el.tagName === 'ZN-BUTTON') {
50
+ el.toggleAttribute('data-zn-input-group__input--first', false);
51
+ el.toggleAttribute('data-zn-input-group__input--inner', false);
52
+ el.toggleAttribute('data-zn-input-group__input--last', false);
53
+ } else {
54
+ const index = joinedControls.indexOf(el);
55
+ el.toggleAttribute('data-zn-input-group__input--first', index === 0);
56
+ el.toggleAttribute('data-zn-input-group__input--inner', index > 0 && index < joinedControls.length - 1);
57
+ el.toggleAttribute('data-zn-input-group__input--last', index === joinedControls.length - 1);
58
+ }
47
59
  });
48
60
  }
49
61
 
@@ -65,7 +77,16 @@ export default class ZnInputGroup extends ZincElement {
65
77
  <slot name="label">${this.label}</slot>
66
78
  </label>
67
79
 
68
- <div part="base form-control-input" class="input-group" role="group" aria-labelledby="label">
80
+ <div part="base form-control-input"
81
+ class="${classMap({
82
+ 'input-group': true,
83
+ 'input-group--gap': !!this.gap,
84
+ 'input-group--gap-sm': this.gap === 'sm',
85
+ 'input-group--gap-md': this.gap === 'md',
86
+ 'input-group--gap-lg': this.gap === 'lg',
87
+ })}"
88
+ role="group"
89
+ aria-labelledby="label">
69
90
  <slot @slotchange=${this.handleSlotChange}></slot>
70
91
  </div>
71
92
  </div>`;
@@ -12,6 +12,33 @@
12
12
  width: 100%;
13
13
  }
14
14
 
15
+ /* Gap variants */
16
+ .input-group--gap-sm {
17
+ gap: var(--zn-spacing-x-small);
18
+ }
19
+
20
+ .input-group--gap-md {
21
+ gap: var(--zn-spacing-small);
22
+ }
23
+
24
+ .input-group--gap-lg {
25
+ gap: var(--zn-spacing-medium);
26
+ }
27
+
28
+ /* When gap is set, restore individual border radii */
29
+ .input-group--gap ::slotted([data-zn-input-group__input]) {
30
+ margin-inline-end: 0;
31
+ }
32
+
33
+ .input-group--gap ::slotted([data-zn-input-group__input--first]),
34
+ .input-group--gap ::slotted([data-zn-input-group__input--inner]),
35
+ .input-group--gap ::slotted([data-zn-input-group__input--last]) {
36
+ --zn-input-border-radius-x-small: var(--zn-border-radius-medium);
37
+ --zn-input-border-radius-small: var(--zn-border-radius-medium);
38
+ --zn-input-border-radius-medium: var(--zn-border-radius-medium);
39
+ --zn-input-border-radius-large: var(--zn-border-radius-medium);
40
+ }
41
+
15
42
  /* Common styles for slotted inputs */
16
43
  ::slotted([data-zn-input-group__input]) {
17
44
  /* Overlap borders */
@@ -21,6 +48,15 @@
21
48
  position: relative;
22
49
  z-index: 1;
23
50
  flex: 1;
51
+ min-width: 0;
52
+ }
53
+
54
+ @for $i from 1 through 12 {
55
+ ::slotted([data-zn-input-group__input][span="#{$i}"]) {
56
+ @include wc.media-query(md) {
57
+ flex: #{$i};
58
+ }
59
+ }
24
60
  }
25
61
 
26
62
  ::slotted([data-zn-input-group__input]:focus-within) {
@@ -50,17 +86,19 @@
50
86
  margin-inline-end: 0;
51
87
  }
52
88
 
53
- /* Button overrides */
54
- ::slotted(zn-button[data-zn-input-group__input])::part(base) {
55
- border-radius: 0;
89
+ /* Button overrides - don't join to other inputs */
90
+ ::slotted(zn-button[data-zn-input-group__input]) {
91
+ flex: 0 0 auto;
92
+ margin-inline-start: var(--zn-spacing-x-small);
93
+ margin-inline-end: 0;
56
94
  }
57
95
 
58
- ::slotted(zn-button[data-zn-input-group__input--first])::part(base) {
59
- border-start-start-radius: var(--zn-border-radius-medium);
60
- border-end-start-radius: var(--zn-border-radius-medium);
96
+ ::slotted(zn-button[data-zn-input-group__input--first]),
97
+ ::slotted(zn-button[data-zn-input-group__input--inner]),
98
+ ::slotted(zn-button[data-zn-input-group__input--last]) {
99
+ --zn-input-border-radius-x-small: var(--zn-border-radius-medium);
100
+ --zn-input-border-radius-small: var(--zn-border-radius-medium);
101
+ --zn-input-border-radius-medium: var(--zn-border-radius-medium);
102
+ --zn-input-border-radius-large: var(--zn-border-radius-medium);
61
103
  }
62
104
 
63
- ::slotted(zn-button[data-zn-input-group__input--last])::part(base) {
64
- border-start-end-radius: var(--zn-border-radius-medium);
65
- border-end-end-radius: var(--zn-border-radius-medium);
66
- }
@@ -52,7 +52,7 @@ export default class ZnItem extends ZincElement {
52
52
  @property({type: Boolean, attribute: 'align-end'}) alignEnd: boolean;
53
53
 
54
54
  // align items in the center
55
- @property({type: Boolean, attribute: 'align-center'}) alignCenter: boolean;
55
+ @property({type: Boolean, attribute: 'align-center'}) alignCenter: boolean;
56
56
 
57
57
  connectedCallback() {
58
58
  super.connectedCallback();
@@ -74,12 +74,21 @@ export default class ZnItem extends ZincElement {
74
74
  return slotContent > 0 || this.innerText.length > 0 || !(this.value === undefined || this.value === null)
75
75
  }
76
76
 
77
+ protected _hasRequiredSlot(): boolean {
78
+ const slotElements = this.querySelectorAll(":scope > *:not([slot])");
79
+ return Array.from(slotElements).some(el => el.hasAttribute && el.hasAttribute('required'));
80
+ }
81
+
77
82
  render() {
78
83
  const hasIcon = this.icon && this.icon.length > 0;
79
84
 
80
85
  const headings = html`
81
86
  <div class="item__headings">
82
- <div class="item__caption" part="caption">${this.caption}</div>
87
+ <div class=${classMap({
88
+ 'item__caption': true,
89
+ 'item__caption--required': this._hasRequiredSlot()
90
+ })} part="caption">${this.caption}
91
+ </div>
83
92
  ${this.description ? html`
84
93
  <div class="item__description">${this.description}</div>` : ''}
85
94
  </div>`;