@kubex/zinc 1.0.24 → 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 +1407 -887
  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 -18
  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 +82 -23
  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 +328 -5
  137. package/src/components/input/input.scss +100 -4
  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 -8
  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 -42
  186. package/src/components/translations/translations.scss +5 -1
  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,12 +570,32 @@ 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;
327
592
  }
328
593
 
329
594
  const target = event.target as HTMLElement;
595
+ if (target.tagName === 'ZN-CHECKBOX' && (target.slot === 'prefix' || target.closest('[slot="prefix"]'))) {
596
+ return;
597
+ }
598
+
330
599
  if (target.classList.contains('input__prefix') || target.slot === 'prefix' || (target.hasAttribute('part') && target.getAttribute('part') === 'base')) {
331
600
  this.handleFocus();
332
601
  this.input.focus();
@@ -350,7 +619,44 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
350
619
  @watch('value', {waitUntilFirstUpdate: true})
351
620
  async handleValueChange() {
352
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
+
353
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
+ }
354
660
  }
355
661
 
356
662
  /** Sets focus on the input. */
@@ -507,7 +813,23 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
507
813
  })}>
508
814
 
509
815
  <span part="prefix" class="input__prefix">
510
- ${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'
511
833
  ? html`
512
834
  <zn-icon class="input__prefix-default" src="payments"></zn-icon>`
513
835
  : this.type === 'email' && hasOptionalIcon
@@ -524,7 +846,8 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
524
846
  part="input"
525
847
  id="input"
526
848
  class="input__control"
527
- 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'}
528
851
  title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
529
852
  name=${ifDefined(this.name)}
530
853
  ?disabled=${this.disabled}
@@ -535,7 +858,7 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
535
858
  maxlength=${ifDefined(this.maxlength)}
536
859
  min=${ifDefined(this.min)}
537
860
  max=${ifDefined(this.max)}
538
- step=${ifDefined(this.step)}
861
+ step=${ifDefined(this.type === 'currency' ? 0.01 : this.step)}
539
862
  .value=${live(this.value)}
540
863
  autocapitalize=${ifDefined(this.autocapitalize)}
541
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,
@@ -128,6 +134,8 @@
128
134
  display: inline-flex;
129
135
  flex: 0 0 auto;
130
136
  align-items: center;
137
+ align-self: stretch;
138
+ height: 100%;
131
139
  cursor: default;
132
140
  color: var(--zn-input-icon-color);
133
141
  font-size: var(--zn-font-size-small);
@@ -177,7 +185,9 @@
177
185
  }
178
186
 
179
187
  .input--x-small .input__suffix ::slotted(zn-button),
180
- .input--x-small .input__prefix ::slotted(zn-button) {
188
+ .input--x-small .input__prefix ::slotted(zn-button),
189
+ .input--x-small .input__suffix ::slotted(zn-checkbox),
190
+ .input--x-small .input__prefix ::slotted(zn-checkbox), {
181
191
  margin-inline-end: 0;
182
192
  margin-inline-start: 0;
183
193
  }
@@ -209,7 +219,9 @@
209
219
  }
210
220
 
211
221
  .input--small .input__suffix ::slotted(zn-button),
212
- .input--small .input__prefix ::slotted(zn-button) {
222
+ .input--small .input__prefix ::slotted(zn-button),
223
+ .input--small .input__suffix ::slotted(zn-checkbox),
224
+ .input--small .input__prefix ::slotted(zn-checkbox) {
213
225
  margin-inline-end: 0;
214
226
  margin-inline-start: 0;
215
227
  }
@@ -241,7 +253,9 @@
241
253
  }
242
254
 
243
255
  .input--medium .input__suffix ::slotted(zn-button),
244
- .input--medium .input__prefix ::slotted(zn-button) {
256
+ .input--medium .input__prefix ::slotted(zn-button),
257
+ .input--medium .input__suffix ::slotted(zn-checkbox),
258
+ .input--medium .input__prefix ::slotted(zn-checkbox) {
245
259
  margin-inline-end: 0;
246
260
  margin-inline-start: 0;
247
261
  }
@@ -264,7 +278,8 @@
264
278
 
265
279
  .input--large .input__suffix ::slotted(zn-button),
266
280
  .input--large .input__prefix ::slotted(zn-button),
267
- {
281
+ .input--large .input__suffix ::slotted(zn-checkbox),
282
+ .input--large .input__prefix ::slotted(zn-checkbox) {
268
283
  margin-inline-end: 0;
269
284
  margin-inline-start: 0;
270
285
  }
@@ -353,3 +368,84 @@
353
368
  input[type='date']::-webkit-calendar-picker-indicator {
354
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>');
355
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>`;