@lifecorner/angular-ui-kit 3.28.0 → 3.30.0

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.
@@ -9,17 +9,25 @@ function decimalsOf(n) {
9
9
  const decimals = (mantissa.split(".")[1]?.length ?? 0) - (exponent ? Number(exponent) : 0);
10
10
  return Math.max(0, decimals);
11
11
  }
12
- function sanitizeNumericText(raw) {
12
+ function sanitizeNumericText(raw, separator) {
13
13
  let out = "";
14
14
  let seenPoint = false;
15
15
  for (const ch of raw) if (ch >= "0" && ch <= "9") out += ch;
16
16
  else if (ch === "-" && out.length === 0) out += ch;
17
17
  else if ((ch === "." || ch === ",") && !seenPoint) {
18
18
  seenPoint = true;
19
- out += ".";
19
+ out += separator;
20
20
  }
21
21
  return out;
22
22
  }
23
+ function decimalSeparatorOf(locale) {
24
+ if (!locale) return ".";
25
+ try {
26
+ return new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal")?.value === "," ? "," : ".";
27
+ } catch {
28
+ return ".";
29
+ }
30
+ }
23
31
  var NumberInputComponent = class NumberInputComponent {
24
32
  static nextId = 0;
25
33
  min = input(void 0, ...ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []);
@@ -33,6 +41,10 @@ var NumberInputComponent = class NumberInputComponent {
33
41
  disabled = input(false, ...ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []);
34
42
  decrementAriaLabel = input("Decrease value", ...ngDevMode ? [{ debugName: "decrementAriaLabel" }] : /* istanbul ignore next */ []);
35
43
  incrementAriaLabel = input("Increase value", ...ngDevMode ? [{ debugName: "incrementAriaLabel" }] : /* istanbul ignore next */ []);
44
+ locale = input("", ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
45
+ suffix = input("", ...ngDevMode ? [{ debugName: "suffix" }] : /* istanbul ignore next */ []);
46
+ suffixAriaLabel = input("", ...ngDevMode ? [{ debugName: "suffixAriaLabel" }] : /* istanbul ignore next */ []);
47
+ size = input("md", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
36
48
  valueChange = output();
37
49
  inputId = `lc-number-input-${++NumberInputComponent.nextId}`;
38
50
  value = signal(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
@@ -42,14 +54,20 @@ var NumberInputComponent = class NumberInputComponent {
42
54
  inputEl = viewChild("inputEl", ...ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []);
43
55
  stepPrecision = computed(() => decimalsOf(this.step()), ...ngDevMode ? [{ debugName: "stepPrecision" }] : /* istanbul ignore next */ []);
44
56
  inputMode = computed(() => this.stepPrecision() > 0 ? "decimal" : "numeric", ...ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []);
57
+ separator = computed(() => decimalSeparatorOf(this.locale()), ...ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []);
58
+ spokenSuffix = computed(() => this.suffixAriaLabel() || this.suffix(), ...ngDevMode ? [{ debugName: "spokenSuffix" }] : /* istanbul ignore next */ []);
59
+ iconSize = computed(() => this.size() === "xs" || this.size() === "sm" ? "xs" : "sm", ...ngDevMode ? [{ debugName: "iconSize" }] : /* istanbul ignore next */ []);
45
60
  describedBy = computed(() => {
46
61
  const ids = [];
62
+ if (this.spokenSuffix()) ids.push(`${this.inputId}-suffix`);
47
63
  if (this.error()) ids.push(`${this.inputId}-error`);
48
64
  else if (this.helperText()) ids.push(`${this.inputId}-helper`);
49
65
  return ids.length ? ids.join(" ") : null;
50
66
  }, ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
51
67
  containerClasses = computed(() => {
52
68
  const classes = ["number-input"];
69
+ const size = this.size();
70
+ if (size === "xs" || size === "sm" || size === "lg") classes.push(`number-input--${size}`);
53
71
  if (this.isDisabled()) classes.push("number-input--disabled");
54
72
  if (this.error()) classes.push("number-input--error");
55
73
  return classes.join(" ");
@@ -93,14 +111,14 @@ var NumberInputComponent = class NumberInputComponent {
93
111
  onInput(event) {
94
112
  const target = event.target;
95
113
  const raw = target.value;
96
- const text = sanitizeNumericText(raw);
114
+ const text = sanitizeNumericText(raw, this.separator());
97
115
  if (text !== raw) {
98
116
  const caret = Math.max(0, (target.selectionStart ?? text.length) - (raw.length - text.length));
99
117
  target.value = text;
100
118
  target.setSelectionRange(caret, caret);
101
119
  }
102
120
  this.text.set(text);
103
- const parsed = text === "" ? NaN : Number(text);
121
+ const parsed = text === "" ? NaN : Number(text.replace(",", "."));
104
122
  if (Number.isNaN(parsed)) {
105
123
  if (this.value() !== null) {
106
124
  this.value.set(null);
@@ -123,7 +141,13 @@ var NumberInputComponent = class NumberInputComponent {
123
141
  if (el) el.value = canonical;
124
142
  }
125
143
  format(val) {
126
- return val === null ? "" : String(val);
144
+ if (val === null) return "";
145
+ const text = String(val);
146
+ return this.separator() === "," ? text.replace(".", ",") : text;
147
+ }
148
+ focusField(event) {
149
+ event.preventDefault();
150
+ if (!this.isDisabled()) this.inputEl()?.nativeElement.focus();
127
151
  }
128
152
  clamp(val) {
129
153
  const minV = this.min();
@@ -242,6 +266,34 @@ var NumberInputComponent = class NumberInputComponent {
242
266
  isSignal: true,
243
267
  isRequired: false,
244
268
  transformFunction: null
269
+ },
270
+ locale: {
271
+ classPropertyName: "locale",
272
+ publicName: "locale",
273
+ isSignal: true,
274
+ isRequired: false,
275
+ transformFunction: null
276
+ },
277
+ suffix: {
278
+ classPropertyName: "suffix",
279
+ publicName: "suffix",
280
+ isSignal: true,
281
+ isRequired: false,
282
+ transformFunction: null
283
+ },
284
+ suffixAriaLabel: {
285
+ classPropertyName: "suffixAriaLabel",
286
+ publicName: "suffixAriaLabel",
287
+ isSignal: true,
288
+ isRequired: false,
289
+ transformFunction: null
290
+ },
291
+ size: {
292
+ classPropertyName: "size",
293
+ publicName: "size",
294
+ isSignal: true,
295
+ isRequired: false,
296
+ transformFunction: null
245
297
  }
246
298
  },
247
299
  outputs: { valueChange: "valueChange" },
@@ -258,8 +310,8 @@ var NumberInputComponent = class NumberInputComponent {
258
310
  isSignal: true
259
311
  }],
260
312
  ngImport: i0,
261
- template: "<div [class]=\"containerClasses()\">\n @if (label()) {\n <label class=\"number-input__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"number-input__control\">\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--decrement\"\n [disabled]=\"isDisabled() || !canDecrement()\"\n (click)=\"decrement()\"\n [attr.aria-label]=\"decrementAriaLabel()\"\n >\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <input\n #inputEl\n type=\"text\"\n [id]=\"inputId\"\n [attr.inputmode]=\"inputMode()\"\n class=\"number-input__input\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n [attr.aria-invalid]=\"error() ? true : null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--increment\"\n [disabled]=\"isDisabled() || !canIncrement()\"\n (click)=\"increment()\"\n [attr.aria-label]=\"incrementAriaLabel()\"\n >\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n @if (error()) {\n <p [id]=\"inputId + '-error'\" class=\"number-input__error\" role=\"alert\">{{ error() }}</p>\n } @else if (helperText()) {\n <p [id]=\"inputId + '-helper'\" class=\"number-input__helper\">{{ helperText() }}</p>\n }\n</div>\n",
262
- styles: [".number-input{display:inline-flex;flex-direction:column;gap:.375rem}.number-input__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.number-input__control{display:inline-flex;align-items:center;border:1px solid var(--color-field-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface-2);transition:border-color .15s ease}.number-input__control:hover{border-color:var(--color-text-secondary)}.number-input__control:focus-within{border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.number-input__button{display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border:none;background:transparent;color:var(--color-text-secondary);cursor:pointer;flex-shrink:0;transition:background-color .15s ease,color .15s ease}.number-input__button:hover:not(:disabled){background-color:var(--color-surface-hover);color:var(--color-text-primary)}.number-input__button:disabled{opacity:.4;cursor:not-allowed}.number-input__button:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}.number-input__button--decrement{border-right:1px solid var(--color-border)}.number-input__button--increment{border-left:1px solid var(--color-border)}.number-input__input{width:4rem;text-align:center;border:none;outline:none;background:transparent;font-size:var(--typography-font-size-base, 1rem);font-variant-numeric:tabular-nums;color:var(--color-text-primary);padding:.5rem .25rem}.number-input__input::placeholder{color:var(--color-text-tertiary)}.number-input__input:disabled{cursor:not-allowed}.number-input__helper,.number-input__error{margin:0;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.number-input__helper{color:var(--color-text-tertiary)}.number-input__error{color:var(--color-text-error)}.number-input--error .number-input__control{border-color:var(--color-error-600)}.number-input--error .number-input__control:focus-within{border-color:var(--color-error-600);outline:2px solid var(--color-error-600);outline-offset:0;box-shadow:none}.number-input--disabled{opacity:.5;pointer-events:none}@media(forced-colors:active){.number-input--error .number-input__control:focus-within,.number-input__button:focus-visible{outline-color:Highlight}}\n"],
313
+ template: "<div [class]=\"containerClasses()\">\n @if (label()) {\n <label class=\"number-input__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"number-input__control\">\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--decrement\"\n [disabled]=\"isDisabled() || !canDecrement()\"\n (click)=\"decrement()\"\n [attr.aria-label]=\"decrementAriaLabel()\"\n >\n <lc-icon name=\"minus\" [size]=\"iconSize()\" [decorative]=\"true\" />\n </button>\n <input\n #inputEl\n type=\"text\"\n [id]=\"inputId\"\n [attr.inputmode]=\"inputMode()\"\n class=\"number-input__input\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n [attr.aria-invalid]=\"error() ? true : null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n @if (suffix()) {\n <!-- The visible unit (3.29): not read; the field's description says it once. A press focuses the field. -->\n <span class=\"number-input__suffix\" aria-hidden=\"true\" (mousedown)=\"focusField($event)\">{{ suffix() }}</span>\n }\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--increment\"\n [disabled]=\"isDisabled() || !canIncrement()\"\n (click)=\"increment()\"\n [attr.aria-label]=\"incrementAriaLabel()\"\n >\n <lc-icon name=\"plus\" [size]=\"iconSize()\" [decorative]=\"true\" />\n </button>\n </div>\n @if (spokenSuffix()) {\n <span hidden [id]=\"inputId + '-suffix'\">{{ spokenSuffix() }}</span>\n }\n @if (error()) {\n <p [id]=\"inputId + '-error'\" class=\"number-input__error\" role=\"alert\">{{ error() }}</p>\n } @else if (helperText()) {\n <p [id]=\"inputId + '-helper'\" class=\"number-input__helper\">{{ helperText() }}</p>\n }\n</div>\n",
314
+ styles: [".number-input{display:inline-flex;flex-direction:column;gap:.375rem}.number-input__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.number-input__control{display:inline-flex;align-items:center;align-self:flex-start;border:1px solid var(--color-field-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface-2);transition:border-color .15s ease}.number-input__control:hover{border-color:var(--color-text-secondary)}.number-input__control:focus-within{border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.number-input__button{display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border:none;background:transparent;color:var(--color-text-secondary);cursor:pointer;flex-shrink:0;transition:background-color .15s ease,color .15s ease}.number-input__button:hover:not(:disabled){background-color:var(--color-surface-hover);color:var(--color-text-primary)}.number-input__button:disabled{opacity:.4;cursor:not-allowed}.number-input__button:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}.number-input__button--decrement{border-right:1px solid var(--color-border)}.number-input__button--increment{border-left:1px solid var(--color-border)}.number-input__input{width:4rem;text-align:center;border:none;outline:none;background:transparent;font-size:var(--typography-font-size-base, 1rem);font-variant-numeric:tabular-nums;color:var(--color-text-primary);padding:.5rem .25rem}.number-input__input::placeholder{color:var(--color-text-tertiary)}.number-input__input:disabled{cursor:not-allowed}.number-input__suffix{flex-shrink:0;padding-inline:.25em var(--spacing-2);font-size:var(--typography-font-size-base, 1rem);color:var(--color-text-secondary)}.number-input__control:has(.number-input__suffix) .number-input__input{text-align:end;padding-inline-end:0}.number-input--xs .number-input__input,.number-input--xs .number-input__suffix,.number-input--sm .number-input__input,.number-input--sm .number-input__suffix,.number-input--lg .number-input__input,.number-input--lg .number-input__suffix{align-self:stretch}.number-input--xs .number-input__input,.number-input--sm .number-input__input,.number-input--lg .number-input__input{padding-block:0}.number-input--xs .number-input__suffix,.number-input--sm .number-input__suffix,.number-input--lg .number-input__suffix{display:flex;align-items:center}.number-input--xs .number-input__button{width:var(--size-interactive-xs-height);height:var(--size-interactive-xs-height)}.number-input--xs .number-input__input{width:3rem}.number-input--xs .number-input__input,.number-input--xs .number-input__suffix{font-size:var(--size-interactive-xs-font-size)}.number-input--sm .number-input__button{width:var(--size-interactive-sm-height);height:var(--size-interactive-sm-height)}.number-input--sm .number-input__input{width:3.5rem}.number-input--sm .number-input__input,.number-input--sm .number-input__suffix{font-size:var(--size-interactive-sm-font-size)}.number-input--lg .number-input__button{width:var(--size-interactive-lg-height);height:var(--size-interactive-lg-height)}.number-input--lg .number-input__input{width:4.5rem}.number-input--lg .number-input__input,.number-input--lg .number-input__suffix{font-size:var(--size-interactive-lg-font-size)}.number-input__helper,.number-input__error{margin:0;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.number-input__helper{color:var(--color-text-tertiary)}.number-input__error{color:var(--color-text-error)}.number-input--error .number-input__control{border-color:var(--color-error-600)}.number-input--error .number-input__control:focus-within{border-color:var(--color-error-600);outline:2px solid var(--color-error-600);outline-offset:0;box-shadow:none}.number-input--disabled{opacity:.5;pointer-events:none}@media(forced-colors:active){.number-input--error .number-input__control:focus-within,.number-input__button:focus-visible{outline-color:Highlight}}\n"],
263
315
  dependencies: [{
264
316
  kind: "component",
265
317
  type: IconComponent,
@@ -296,8 +348,8 @@ i0.ɵɵngDeclareClassMetadata({
296
348
  useExisting: forwardRef(() => NumberInputComponent),
297
349
  multi: true
298
350
  }],
299
- template: "<div [class]=\"containerClasses()\">\n @if (label()) {\n <label class=\"number-input__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"number-input__control\">\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--decrement\"\n [disabled]=\"isDisabled() || !canDecrement()\"\n (click)=\"decrement()\"\n [attr.aria-label]=\"decrementAriaLabel()\"\n >\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <input\n #inputEl\n type=\"text\"\n [id]=\"inputId\"\n [attr.inputmode]=\"inputMode()\"\n class=\"number-input__input\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n [attr.aria-invalid]=\"error() ? true : null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--increment\"\n [disabled]=\"isDisabled() || !canIncrement()\"\n (click)=\"increment()\"\n [attr.aria-label]=\"incrementAriaLabel()\"\n >\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n @if (error()) {\n <p [id]=\"inputId + '-error'\" class=\"number-input__error\" role=\"alert\">{{ error() }}</p>\n } @else if (helperText()) {\n <p [id]=\"inputId + '-helper'\" class=\"number-input__helper\">{{ helperText() }}</p>\n }\n</div>\n",
300
- styles: [".number-input{display:inline-flex;flex-direction:column;gap:.375rem}.number-input__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.number-input__control{display:inline-flex;align-items:center;border:1px solid var(--color-field-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface-2);transition:border-color .15s ease}.number-input__control:hover{border-color:var(--color-text-secondary)}.number-input__control:focus-within{border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.number-input__button{display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border:none;background:transparent;color:var(--color-text-secondary);cursor:pointer;flex-shrink:0;transition:background-color .15s ease,color .15s ease}.number-input__button:hover:not(:disabled){background-color:var(--color-surface-hover);color:var(--color-text-primary)}.number-input__button:disabled{opacity:.4;cursor:not-allowed}.number-input__button:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}.number-input__button--decrement{border-right:1px solid var(--color-border)}.number-input__button--increment{border-left:1px solid var(--color-border)}.number-input__input{width:4rem;text-align:center;border:none;outline:none;background:transparent;font-size:var(--typography-font-size-base, 1rem);font-variant-numeric:tabular-nums;color:var(--color-text-primary);padding:.5rem .25rem}.number-input__input::placeholder{color:var(--color-text-tertiary)}.number-input__input:disabled{cursor:not-allowed}.number-input__helper,.number-input__error{margin:0;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.number-input__helper{color:var(--color-text-tertiary)}.number-input__error{color:var(--color-text-error)}.number-input--error .number-input__control{border-color:var(--color-error-600)}.number-input--error .number-input__control:focus-within{border-color:var(--color-error-600);outline:2px solid var(--color-error-600);outline-offset:0;box-shadow:none}.number-input--disabled{opacity:.5;pointer-events:none}@media(forced-colors:active){.number-input--error .number-input__control:focus-within,.number-input__button:focus-visible{outline-color:Highlight}}\n"]
351
+ template: "<div [class]=\"containerClasses()\">\n @if (label()) {\n <label class=\"number-input__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"number-input__control\">\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--decrement\"\n [disabled]=\"isDisabled() || !canDecrement()\"\n (click)=\"decrement()\"\n [attr.aria-label]=\"decrementAriaLabel()\"\n >\n <lc-icon name=\"minus\" [size]=\"iconSize()\" [decorative]=\"true\" />\n </button>\n <input\n #inputEl\n type=\"text\"\n [id]=\"inputId\"\n [attr.inputmode]=\"inputMode()\"\n class=\"number-input__input\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n [attr.aria-invalid]=\"error() ? true : null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n @if (suffix()) {\n <!-- The visible unit (3.29): not read; the field's description says it once. A press focuses the field. -->\n <span class=\"number-input__suffix\" aria-hidden=\"true\" (mousedown)=\"focusField($event)\">{{ suffix() }}</span>\n }\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--increment\"\n [disabled]=\"isDisabled() || !canIncrement()\"\n (click)=\"increment()\"\n [attr.aria-label]=\"incrementAriaLabel()\"\n >\n <lc-icon name=\"plus\" [size]=\"iconSize()\" [decorative]=\"true\" />\n </button>\n </div>\n @if (spokenSuffix()) {\n <span hidden [id]=\"inputId + '-suffix'\">{{ spokenSuffix() }}</span>\n }\n @if (error()) {\n <p [id]=\"inputId + '-error'\" class=\"number-input__error\" role=\"alert\">{{ error() }}</p>\n } @else if (helperText()) {\n <p [id]=\"inputId + '-helper'\" class=\"number-input__helper\">{{ helperText() }}</p>\n }\n</div>\n",
352
+ styles: [".number-input{display:inline-flex;flex-direction:column;gap:.375rem}.number-input__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.number-input__control{display:inline-flex;align-items:center;align-self:flex-start;border:1px solid var(--color-field-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface-2);transition:border-color .15s ease}.number-input__control:hover{border-color:var(--color-text-secondary)}.number-input__control:focus-within{border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.number-input__button{display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border:none;background:transparent;color:var(--color-text-secondary);cursor:pointer;flex-shrink:0;transition:background-color .15s ease,color .15s ease}.number-input__button:hover:not(:disabled){background-color:var(--color-surface-hover);color:var(--color-text-primary)}.number-input__button:disabled{opacity:.4;cursor:not-allowed}.number-input__button:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px}.number-input__button--decrement{border-right:1px solid var(--color-border)}.number-input__button--increment{border-left:1px solid var(--color-border)}.number-input__input{width:4rem;text-align:center;border:none;outline:none;background:transparent;font-size:var(--typography-font-size-base, 1rem);font-variant-numeric:tabular-nums;color:var(--color-text-primary);padding:.5rem .25rem}.number-input__input::placeholder{color:var(--color-text-tertiary)}.number-input__input:disabled{cursor:not-allowed}.number-input__suffix{flex-shrink:0;padding-inline:.25em var(--spacing-2);font-size:var(--typography-font-size-base, 1rem);color:var(--color-text-secondary)}.number-input__control:has(.number-input__suffix) .number-input__input{text-align:end;padding-inline-end:0}.number-input--xs .number-input__input,.number-input--xs .number-input__suffix,.number-input--sm .number-input__input,.number-input--sm .number-input__suffix,.number-input--lg .number-input__input,.number-input--lg .number-input__suffix{align-self:stretch}.number-input--xs .number-input__input,.number-input--sm .number-input__input,.number-input--lg .number-input__input{padding-block:0}.number-input--xs .number-input__suffix,.number-input--sm .number-input__suffix,.number-input--lg .number-input__suffix{display:flex;align-items:center}.number-input--xs .number-input__button{width:var(--size-interactive-xs-height);height:var(--size-interactive-xs-height)}.number-input--xs .number-input__input{width:3rem}.number-input--xs .number-input__input,.number-input--xs .number-input__suffix{font-size:var(--size-interactive-xs-font-size)}.number-input--sm .number-input__button{width:var(--size-interactive-sm-height);height:var(--size-interactive-sm-height)}.number-input--sm .number-input__input{width:3.5rem}.number-input--sm .number-input__input,.number-input--sm .number-input__suffix{font-size:var(--size-interactive-sm-font-size)}.number-input--lg .number-input__button{width:var(--size-interactive-lg-height);height:var(--size-interactive-lg-height)}.number-input--lg .number-input__input{width:4.5rem}.number-input--lg .number-input__input,.number-input--lg .number-input__suffix{font-size:var(--size-interactive-lg-font-size)}.number-input__helper,.number-input__error{margin:0;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.number-input__helper{color:var(--color-text-tertiary)}.number-input__error{color:var(--color-text-error)}.number-input--error .number-input__control{border-color:var(--color-error-600)}.number-input--error .number-input__control:focus-within{border-color:var(--color-error-600);outline:2px solid var(--color-error-600);outline-offset:0;box-shadow:none}.number-input--disabled{opacity:.5;pointer-events:none}@media(forced-colors:active){.number-input--error .number-input__control:focus-within,.number-input__button:focus-visible{outline-color:Highlight}}\n"]
301
353
  }]
302
354
  }],
303
355
  propDecorators: {
@@ -389,6 +441,38 @@ i0.ɵɵngDeclareClassMetadata({
389
441
  required: false
390
442
  }]
391
443
  }],
444
+ locale: [{
445
+ type: i0.Input,
446
+ args: [{
447
+ isSignal: true,
448
+ alias: "locale",
449
+ required: false
450
+ }]
451
+ }],
452
+ suffix: [{
453
+ type: i0.Input,
454
+ args: [{
455
+ isSignal: true,
456
+ alias: "suffix",
457
+ required: false
458
+ }]
459
+ }],
460
+ suffixAriaLabel: [{
461
+ type: i0.Input,
462
+ args: [{
463
+ isSignal: true,
464
+ alias: "suffixAriaLabel",
465
+ required: false
466
+ }]
467
+ }],
468
+ size: [{
469
+ type: i0.Input,
470
+ args: [{
471
+ isSignal: true,
472
+ alias: "size",
473
+ required: false
474
+ }]
475
+ }],
392
476
  valueChange: [{
393
477
  type: i0.Output,
394
478
  args: ["valueChange"]
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-number-input.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/number-input/number-input.component.ts","../../../../libs/angular-ui-kit/src/lib/number-input/number-input.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n input,\n output,\n computed,\n signal,\n forwardRef,\n viewChild,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_PLUS } from '@lifecorner/angular-ui-kit/icons';\n\n/** Number of decimals a number is written with (`0.001` → 3, `1e-7` → 7, `5` → 0). */\nfunction decimalsOf(n: number): number {\n if (!Number.isFinite(n)) return 0;\n const [mantissa, exponent] = String(n).split('e');\n const decimals = (mantissa.split('.')[1]?.length ?? 0) - (exponent ? Number(exponent) : 0);\n return Math.max(0, decimals);\n}\n\n/**\n * Reduces raw text to something `Number()` can parse: digits, one leading\n * minus and one decimal point. A comma is taken as a decimal separator so a\n * German keyboard layout does not silently lose the fraction.\n */\nfunction sanitizeNumericText(raw: string): string {\n let out = '';\n let seenPoint = false;\n for (const ch of raw) {\n if (ch >= '0' && ch <= '9') {\n out += ch;\n } else if (ch === '-' && out.length === 0) {\n out += ch;\n } else if ((ch === '.' || ch === ',') && !seenPoint) {\n seenPoint = true;\n out += '.';\n }\n }\n return out;\n}\n\n@Component({\n selector: 'lc-number-input',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS])],\n templateUrl: './number-input.component.html',\n styleUrls: ['./number-input.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NumberInputComponent),\n multi: true,\n },\n ],\n})\n/**\n * Number input component with increment/decrement controls.\n *\n * Features:\n * - Increment and decrement buttons (also ArrowUp / ArrowDown)\n * - Configurable min, max, and step values\n * - Value clamping within bounds; stepping is rounded to the step's precision\n * - Non-numeric characters are rejected as they are typed\n * - Disabled state support\n * - ControlValueAccessor integration for reactive forms\n *\n * @example\n * ```html\n * <lc-number-input label=\"Quantity\" [min]=\"0\" [max]=\"100\" [step]=\"1\" />\n * ```\n */\nexport class NumberInputComponent implements ControlValueAccessor {\n private static nextId = 0;\n\n /** Minimum value */\n min = input<number | undefined>(undefined);\n\n /** Maximum value */\n max = input<number | undefined>(undefined);\n\n /** Step increment */\n step = input<number>(1);\n\n /** Label text */\n label = input<string>();\n\n /** Accessible name for the field when no visible `label` is rendered. */\n ariaLabel = input<string>();\n\n /** Placeholder */\n placeholder = input<string>('0');\n\n /** Helper text shown below the control (linked via `aria-describedby`). */\n helperText = input<string>();\n\n /** Error message shown below the control; marks the field `aria-invalid`. */\n error = input<string>();\n\n /** Disabled state */\n disabled = input<boolean>(false);\n\n /** Accessible name of the minus button. */\n decrementAriaLabel = input('Decrease value');\n\n /** Accessible name of the plus button. */\n incrementAriaLabel = input('Increase value');\n\n /** Emits value on change */\n valueChange = output<number>();\n\n /** Per-instance id wiring the label, hint and error to the field. */\n readonly inputId = `lc-number-input-${++NumberInputComponent.nextId}`;\n\n protected value = signal<number | null>(null);\n\n /**\n * Text shown in the field. It is re-derived from the model on `writeValue`,\n * stepping and blur — never while typing — so an in-progress entry such as\n * \"1.\" or \"-\" is not snapped back to \"1\" under the user's cursor.\n */\n protected readonly text = signal('');\n\n /** Disabled through the form control (`setDisabledState`), as opposed to the input. */\n private readonly formDisabled = signal(false);\n\n /** Disabled by either route — the one flag the template consults. */\n protected readonly isDisabled = computed(() => this.disabled() || this.formDisabled());\n\n private readonly inputEl = viewChild<ElementRef<HTMLInputElement>>('inputEl');\n\n /** Decimals implied by `step` — stepped results are rounded to it so 0.1 + 0.2 does not drift. */\n private readonly stepPrecision = computed(() => decimalsOf(this.step()));\n\n /** Fractions need a decimal key on touch keyboards; integers do not. */\n protected readonly inputMode = computed(() => (this.stepPrecision() > 0 ? 'decimal' : 'numeric'));\n\n protected readonly describedBy = computed(() => {\n const ids: string[] = [];\n if (this.error()) ids.push(`${this.inputId}-error`);\n else if (this.helperText()) ids.push(`${this.inputId}-helper`);\n return ids.length ? ids.join(' ') : null;\n });\n\n protected containerClasses = computed(() => {\n const classes = ['number-input'];\n if (this.isDisabled()) classes.push('number-input--disabled');\n if (this.error()) classes.push('number-input--error');\n return classes.join(' ');\n });\n\n protected canDecrement = computed(() => {\n const val = this.value();\n const minV = this.min();\n if (val === null || minV === undefined) return true;\n return val > minV;\n });\n\n protected canIncrement = computed(() => {\n const val = this.value();\n const maxV = this.max();\n if (val === null || maxV === undefined) return true;\n return val < maxV;\n });\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onChange: (val: number | null) => void = () => {};\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onTouched: () => void = () => {};\n\n decrement(): void {\n this.stepBy(-1);\n }\n\n increment(): void {\n this.stepBy(1);\n }\n\n private stepBy(direction: 1 | -1): void {\n const cur = this.value() ?? 0;\n // Keep whatever precision the user typed (1.25 + 0.1 → 1.35), but never\n // less than the step's own — that is what removes the binary drift.\n const precision = Math.max(this.stepPrecision(), decimalsOf(cur));\n const next = this.clamp(Number((cur + direction * this.step()).toFixed(precision)));\n this.setValue(next);\n this.text.set(this.format(next));\n }\n\n onKeydown(event: KeyboardEvent): void {\n if (event.key === 'ArrowUp') {\n event.preventDefault();\n if (this.canIncrement()) this.increment();\n } else if (event.key === 'ArrowDown') {\n event.preventDefault();\n if (this.canDecrement()) this.decrement();\n }\n }\n\n onInput(event: Event): void {\n const target = event.target as HTMLInputElement;\n const raw = target.value;\n const text = sanitizeNumericText(raw);\n // Property binding alone would not repaint the field when the model does\n // not change (typing \"a\" after \"12\" leaves 12 as-is), so the stripped\n // text is written straight back to the element.\n if (text !== raw) {\n const caret = Math.max(0, (target.selectionStart ?? text.length) - (raw.length - text.length));\n target.value = text;\n target.setSelectionRange(caret, caret);\n }\n this.text.set(text);\n // \"\" would parse as 0; it and the other in-progress entries (\"-\", \".\")\n // mean the model has no number yet.\n const parsed = text === '' ? NaN : Number(text);\n if (Number.isNaN(parsed)) {\n if (this.value() !== null) {\n this.value.set(null);\n this.onChange(null);\n }\n return;\n }\n this.setValue(parsed);\n }\n\n onBlur(): void {\n this.onTouched();\n // Clamp on blur\n const val = this.value();\n if (val !== null) {\n const clamped = this.clamp(val);\n if (clamped !== val) this.setValue(clamped);\n }\n // Whatever half-typed text is left (\"1.\", \"-\", \"007\") is replaced by the\n // model's canonical form. The element is written directly as well, in\n // case the DOM drifted from the bound text without an input event.\n const canonical = this.format(this.value());\n this.text.set(canonical);\n const el = this.inputEl()?.nativeElement;\n if (el) el.value = canonical;\n }\n\n private format(val: number | null): string {\n return val === null ? '' : String(val);\n }\n\n private clamp(val: number): number {\n const minV = this.min();\n const maxV = this.max();\n if (minV !== undefined) val = Math.max(minV, val);\n if (maxV !== undefined) val = Math.min(maxV, val);\n return val;\n }\n\n private setValue(val: number): void {\n if (val === this.value()) return;\n this.value.set(val);\n this.valueChange.emit(val);\n this.onChange(val);\n }\n\n // ControlValueAccessor\n writeValue(val: number | null): void {\n this.value.set(val);\n this.text.set(this.format(val));\n }\n\n registerOnChange(fn: (val: number | null) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n}\n","<div [class]=\"containerClasses()\">\n @if (label()) {\n <label class=\"number-input__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"number-input__control\">\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--decrement\"\n [disabled]=\"isDisabled() || !canDecrement()\"\n (click)=\"decrement()\"\n [attr.aria-label]=\"decrementAriaLabel()\"\n >\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <input\n #inputEl\n type=\"text\"\n [id]=\"inputId\"\n [attr.inputmode]=\"inputMode()\"\n class=\"number-input__input\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n [attr.aria-invalid]=\"error() ? true : null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--increment\"\n [disabled]=\"isDisabled() || !canIncrement()\"\n (click)=\"increment()\"\n [attr.aria-label]=\"incrementAriaLabel()\"\n >\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n @if (error()) {\n <p [id]=\"inputId + '-error'\" class=\"number-input__error\" role=\"alert\">{{ error() }}</p>\n } @else if (helperText()) {\n <p [id]=\"inputId + '-helper'\" class=\"number-input__helper\">{{ helperText() }}</p>\n }\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-number-input.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/number-input/number-input.component.ts","../../../../libs/angular-ui-kit/src/lib/number-input/number-input.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n input,\n output,\n computed,\n signal,\n forwardRef,\n viewChild,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_PLUS } from '@lifecorner/angular-ui-kit/icons';\n\n/** Number of decimals a number is written with (`0.001` → 3, `1e-7` → 7, `5` → 0). */\nfunction decimalsOf(n: number): number {\n if (!Number.isFinite(n)) return 0;\n const [mantissa, exponent] = String(n).split('e');\n const decimals = (mantissa.split('.')[1]?.length ?? 0) - (exponent ? Number(exponent) : 0);\n return Math.max(0, decimals);\n}\n\n/**\n * Reduces raw text to digits, one leading minus and one decimal separator.\n * A comma and a point are both taken as the decimal separator, so a German\n * keyboard layout does not silently lose the fraction; the separator is\n * written as `separator` (the locale's, a point by default).\n */\nfunction sanitizeNumericText(raw: string, separator: '.' | ','): string {\n let out = '';\n let seenPoint = false;\n for (const ch of raw) {\n if (ch >= '0' && ch <= '9') {\n out += ch;\n } else if (ch === '-' && out.length === 0) {\n out += ch;\n } else if ((ch === '.' || ch === ',') && !seenPoint) {\n seenPoint = true;\n out += separator;\n }\n }\n return out;\n}\n\n/** The decimal separator of `locale`: `,` or `.`; any other separator, `''` and an invalid locale give `.`. */\nfunction decimalSeparatorOf(locale: string): '.' | ',' {\n if (!locale) return '.';\n try {\n const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === 'decimal')?.value;\n return part === ',' ? ',' : '.';\n } catch {\n return '.';\n }\n}\n\n/** Height of `lc-number-input`'s field and steppers: `xs` 24, `sm` 32, `md` 40 (the default), `lg` 44 px. */\nexport type NumberInputSize = 'xs' | 'sm' | 'md' | 'lg';\n\n@Component({\n selector: 'lc-number-input',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS])],\n templateUrl: './number-input.component.html',\n styleUrls: ['./number-input.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NumberInputComponent),\n multi: true,\n },\n ],\n})\n/**\n * Number input component with increment/decrement controls.\n *\n * Features:\n * - Increment and decrement buttons (also ArrowUp / ArrowDown)\n * - Configurable min, max, and step values\n * - Value clamping within bounds; stepping is rounded to the step's precision\n * - Non-numeric characters are rejected as they are typed; a comma and a\n * point are both accepted as the decimal separator, in every locale\n * - The value is shown in `locale` („7,5“ in `de-DE`), with an optional unit\n * after it (`suffix`, „7,5 %“), read once (`suffixAriaLabel`)\n * - Sizes `xs`, `sm`, `md` (default), `lg` (44 px touch targets)\n * - Disabled state support\n * - ControlValueAccessor integration for reactive forms; the model is\n * always a `number`, clamped to `min`/`max` on blur\n *\n * @example\n * ```html\n * <lc-number-input label=\"Quantity\" [min]=\"0\" [max]=\"100\" [step]=\"1\" />\n * <lc-number-input label=\"Anteil\" locale=\"de-DE\" suffix=\"%\" suffixAriaLabel=\"Prozent\"\n * size=\"lg\" [step]=\"0.5\" [(ngModel)]=\"share\" />\n * ```\n */\nexport class NumberInputComponent implements ControlValueAccessor {\n private static nextId = 0;\n\n /** Minimum value */\n min = input<number | undefined>(undefined);\n\n /** Maximum value */\n max = input<number | undefined>(undefined);\n\n /** Step increment */\n step = input<number>(1);\n\n /** Label text */\n label = input<string>();\n\n /** Accessible name for the field when no visible `label` is rendered. */\n ariaLabel = input<string>();\n\n /** Placeholder */\n placeholder = input<string>('0');\n\n /** Helper text shown below the control (linked via `aria-describedby`). */\n helperText = input<string>();\n\n /** Error message shown below the control; marks the field `aria-invalid`. */\n error = input<string>();\n\n /** Disabled state */\n disabled = input<boolean>(false);\n\n /** Accessible name of the minus button. */\n decrementAriaLabel = input('Decrease value');\n\n /** Accessible name of the plus button. */\n incrementAriaLabel = input('Increase value');\n\n /**\n * BCP 47 locale of the shown number (3.29, regeln row 3). Only the decimal\n * separator follows it: `de-DE` shows „7,5“, `en-US` \"7.5\"; a locale whose\n * separator is neither `,` nor `.` shows a point. No grouping. `''`: a\n * point, as before. Typing accepts `,` and `.` in every locale and shows\n * the locale's separator; the model stays a `number`; blur clamps to\n * `min`/`max`.\n * @default ''\n */\n locale = input<string>('');\n\n /**\n * Unit shown after the value inside the field („7,5 %“, 3.29). Not typed\n * and not part of the model; with a suffix the value aligns to the end.\n * Screen readers read `suffixAriaLabel` (or the suffix) once after the value.\n * @default ''\n */\n suffix = input<string>('');\n\n /**\n * Spoken unit („Prozent“), read once after the value as the field's\n * description (before the helper or error text). `''`: the `suffix`.\n * @default ''\n */\n suffixAriaLabel = input<string>('');\n\n /**\n * Height of the field and the steppers (3.29, regeln row 4): `xs` 24,\n * `sm` 32, `md` 40 (as before), `lg` 44 px (touch: field and both\n * steppers at least 44 × 44 px).\n * @default 'md'\n */\n size = input<NumberInputSize>('md');\n\n /** Emits value on change */\n valueChange = output<number>();\n\n /** Per-instance id wiring the label, hint and error to the field. */\n readonly inputId = `lc-number-input-${++NumberInputComponent.nextId}`;\n\n protected value = signal<number | null>(null);\n\n /**\n * Text shown in the field. It is re-derived from the model on `writeValue`,\n * stepping and blur — never while typing — so an in-progress entry such as\n * \"1.\" or \"-\" is not snapped back to \"1\" under the user's cursor.\n */\n protected readonly text = signal('');\n\n /** Disabled through the form control (`setDisabledState`), as opposed to the input. */\n private readonly formDisabled = signal(false);\n\n /** Disabled by either route — the one flag the template consults. */\n protected readonly isDisabled = computed(() => this.disabled() || this.formDisabled());\n\n private readonly inputEl = viewChild<ElementRef<HTMLInputElement>>('inputEl');\n\n /** Decimals implied by `step` — stepped results are rounded to it so 0.1 + 0.2 does not drift. */\n private readonly stepPrecision = computed(() => decimalsOf(this.step()));\n\n /** Fractions need a decimal key on touch keyboards; integers do not. */\n protected readonly inputMode = computed(() => (this.stepPrecision() > 0 ? 'decimal' : 'numeric'));\n\n /** The decimal separator shown in the field: the `locale`'s `,` or `.`. */\n private readonly separator = computed(() => decimalSeparatorOf(this.locale()));\n\n /** The spoken unit: `suffixAriaLabel`, else the `suffix`; `''` = none. */\n protected readonly spokenSuffix = computed(() => this.suffixAriaLabel() || this.suffix());\n\n /** Icon size of the steppers: `xs` (16 px) for the `xs`/`sm` fields, `sm` (20 px) as before. */\n protected readonly iconSize = computed(() => (this.size() === 'xs' || this.size() === 'sm' ? 'xs' : 'sm'));\n\n protected readonly describedBy = computed(() => {\n const ids: string[] = [];\n if (this.spokenSuffix()) ids.push(`${this.inputId}-suffix`);\n if (this.error()) ids.push(`${this.inputId}-error`);\n else if (this.helperText()) ids.push(`${this.inputId}-helper`);\n return ids.length ? ids.join(' ') : null;\n });\n\n protected containerClasses = computed(() => {\n const classes = ['number-input'];\n // `md` (the default) keeps the base rules: no class.\n const size = this.size();\n if (size === 'xs' || size === 'sm' || size === 'lg') classes.push(`number-input--${size}`);\n if (this.isDisabled()) classes.push('number-input--disabled');\n if (this.error()) classes.push('number-input--error');\n return classes.join(' ');\n });\n\n protected canDecrement = computed(() => {\n const val = this.value();\n const minV = this.min();\n if (val === null || minV === undefined) return true;\n return val > minV;\n });\n\n protected canIncrement = computed(() => {\n const val = this.value();\n const maxV = this.max();\n if (val === null || maxV === undefined) return true;\n return val < maxV;\n });\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onChange: (val: number | null) => void = () => {};\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onTouched: () => void = () => {};\n\n decrement(): void {\n this.stepBy(-1);\n }\n\n increment(): void {\n this.stepBy(1);\n }\n\n private stepBy(direction: 1 | -1): void {\n const cur = this.value() ?? 0;\n // Keep whatever precision the user typed (1.25 + 0.1 → 1.35), but never\n // less than the step's own — that is what removes the binary drift.\n const precision = Math.max(this.stepPrecision(), decimalsOf(cur));\n const next = this.clamp(Number((cur + direction * this.step()).toFixed(precision)));\n this.setValue(next);\n this.text.set(this.format(next));\n }\n\n onKeydown(event: KeyboardEvent): void {\n if (event.key === 'ArrowUp') {\n event.preventDefault();\n if (this.canIncrement()) this.increment();\n } else if (event.key === 'ArrowDown') {\n event.preventDefault();\n if (this.canDecrement()) this.decrement();\n }\n }\n\n onInput(event: Event): void {\n const target = event.target as HTMLInputElement;\n const raw = target.value;\n const text = sanitizeNumericText(raw, this.separator());\n // Property binding alone would not repaint the field when the model does\n // not change (typing \"a\" after \"12\" leaves 12 as-is), so the stripped\n // text is written straight back to the element.\n if (text !== raw) {\n const caret = Math.max(0, (target.selectionStart ?? text.length) - (raw.length - text.length));\n target.value = text;\n target.setSelectionRange(caret, caret);\n }\n this.text.set(text);\n // \"\" would parse as 0; it and the other in-progress entries (\"-\", \".\")\n // mean the model has no number yet.\n const parsed = text === '' ? NaN : Number(text.replace(',', '.'));\n if (Number.isNaN(parsed)) {\n if (this.value() !== null) {\n this.value.set(null);\n this.onChange(null);\n }\n return;\n }\n this.setValue(parsed);\n }\n\n onBlur(): void {\n this.onTouched();\n // Clamp on blur\n const val = this.value();\n if (val !== null) {\n const clamped = this.clamp(val);\n if (clamped !== val) this.setValue(clamped);\n }\n // Whatever half-typed text is left (\"1.\", \"-\", \"007\") is replaced by the\n // model's canonical form. The element is written directly as well, in\n // case the DOM drifted from the bound text without an input event.\n const canonical = this.format(this.value());\n this.text.set(canonical);\n const el = this.inputEl()?.nativeElement;\n if (el) el.value = canonical;\n }\n\n /** The model as text: `String(val)` with the locale's decimal separator, no grouping. */\n private format(val: number | null): string {\n if (val === null) return '';\n const text = String(val);\n return this.separator() === ',' ? text.replace('.', ',') : text;\n }\n\n /** A press on the visible unit puts the caret in the field, as a click on the field would. */\n protected focusField(event: MouseEvent): void {\n event.preventDefault();\n if (!this.isDisabled()) this.inputEl()?.nativeElement.focus();\n }\n\n private clamp(val: number): number {\n const minV = this.min();\n const maxV = this.max();\n if (minV !== undefined) val = Math.max(minV, val);\n if (maxV !== undefined) val = Math.min(maxV, val);\n return val;\n }\n\n private setValue(val: number): void {\n if (val === this.value()) return;\n this.value.set(val);\n this.valueChange.emit(val);\n this.onChange(val);\n }\n\n // ControlValueAccessor\n writeValue(val: number | null): void {\n this.value.set(val);\n this.text.set(this.format(val));\n }\n\n registerOnChange(fn: (val: number | null) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n}\n","<div [class]=\"containerClasses()\">\n @if (label()) {\n <label class=\"number-input__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"number-input__control\">\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--decrement\"\n [disabled]=\"isDisabled() || !canDecrement()\"\n (click)=\"decrement()\"\n [attr.aria-label]=\"decrementAriaLabel()\"\n >\n <lc-icon name=\"minus\" [size]=\"iconSize()\" [decorative]=\"true\" />\n </button>\n <input\n #inputEl\n type=\"text\"\n [id]=\"inputId\"\n [attr.inputmode]=\"inputMode()\"\n class=\"number-input__input\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n [attr.aria-invalid]=\"error() ? true : null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n @if (suffix()) {\n <!-- The visible unit (3.29): not read; the field's description says it once. A press focuses the field. -->\n <span class=\"number-input__suffix\" aria-hidden=\"true\" (mousedown)=\"focusField($event)\">{{ suffix() }}</span>\n }\n <button\n type=\"button\"\n class=\"number-input__button number-input__button--increment\"\n [disabled]=\"isDisabled() || !canIncrement()\"\n (click)=\"increment()\"\n [attr.aria-label]=\"incrementAriaLabel()\"\n >\n <lc-icon name=\"plus\" [size]=\"iconSize()\" [decorative]=\"true\" />\n </button>\n </div>\n @if (spokenSuffix()) {\n <span hidden [id]=\"inputId + '-suffix'\">{{ spokenSuffix() }}</span>\n }\n @if (error()) {\n <p [id]=\"inputId + '-error'\" class=\"number-input__error\" role=\"alert\">{{ error() }}</p>\n } @else if (helperText()) {\n <p [id]=\"inputId + '-helper'\" class=\"number-input__helper\">{{ helperText() }}</p>\n }\n</div>\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifecorner/angular-ui-kit",
3
- "version": "3.28.0",
3
+ "version": "3.30.0",
4
4
  "description": "LifeCorner Design System – Angular UI component library",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1892,3 +1892,61 @@ describe('Component pairings of 3.27.2 (request 044)', () => {
1892
1892
  });
1893
1893
  });
1894
1894
  });
1895
+
1896
+ /**
1897
+ * Request 047 (3.30, 046-f2, design 047 §5): the selected day carries the
1898
+ * month cell's inset --color-primary-500 ring, since its fill alone is
1899
+ * 2.78:1 on the dark panel (WCAG 1.4.11); the new combobox clear ring sits
1900
+ * on the field fill (--color-surface-2).
1901
+ */
1902
+ describe('Component pairings of 3.30 (request 047)', () => {
1903
+ const lib = (path: string) => resolve(__dirname, '../lib', path);
1904
+ const sheets = {
1905
+ datepicker: compile(lib('datepicker/datepicker.component.scss')).css,
1906
+ combobox: compile(lib('combobox/combobox.component.scss')).css,
1907
+ };
1908
+ const body = (sheet: keyof typeof sheets, selector: string) => {
1909
+ const css = sheets[sheet];
1910
+ const at = css.indexOf(`\n${selector} {`);
1911
+ expect([selector, at > -1]).toEqual([selector, true]);
1912
+ return css.slice(at, css.indexOf('}', at));
1913
+ };
1914
+ const floor = (n: number) => Math.floor(n * 100) / 100;
1915
+
1916
+ it('AC-2, AC-3: the rules pair these tokens', () => {
1917
+ expect(body('datepicker', '.lc-datepicker-calendar')).toContain('background-color: var(--color-surface)');
1918
+ const selected = body('datepicker', '.lc-datepicker-day.lc-datepicker-day--selected');
1919
+ expect(selected).toContain('box-shadow: inset 0 0 0 2px var(--color-primary-500)');
1920
+ expect(selected).toContain('background-color: var(--color-primary-fill)');
1921
+ expect(selected).toContain('color: var(--color-on-primary)');
1922
+ expect(body('combobox', '.lc-combobox__input-wrapper')).toContain('background-color: var(--color-surface-2)');
1923
+ expect(body('combobox', '.lc-combobox__clear:focus-visible')).toContain('outline: 2px solid var(--color-primary-500)');
1924
+ });
1925
+
1926
+ // [light, dark], floored; design 047 §5 quotes the browser's rounded
1927
+ // values (ring 4.37 / 9.13, clear ring 4.18 / 7.58).
1928
+ const expected = {
1929
+ ringOnPanel: [4.36, 9.12],
1930
+ textOnFill: [6.21, 6.21],
1931
+ textOnHoverFill: [9.13, 9.13],
1932
+ fillOnPanel: [6.21, 2.78],
1933
+ clearRingOnField: [4.17, 7.57],
1934
+ };
1935
+
1936
+ it.each(palettes.map(([name, tokens], i) => [name, tokens, i] as const))('AC-2, AC-3: %s palette: the design 047 §5 ratios', (_palette, tokens, i) => {
1937
+ const c = (name: string) => color(tokens, name);
1938
+ const on = (ink: string, base: string) => floor(ratio(over(c(ink), c(base)), c(base)));
1939
+ const got = {
1940
+ ringOnPanel: on('--color-primary-500', '--color-surface'),
1941
+ textOnFill: on('--color-on-primary', '--color-primary-fill'),
1942
+ textOnHoverFill: on('--color-on-primary', '--color-primary-fill-hover'),
1943
+ fillOnPanel: on('--color-primary-fill', '--color-surface'),
1944
+ clearRingOnField: on('--color-primary-500', '--color-surface-2'),
1945
+ };
1946
+ expect(got).toEqual(Object.fromEntries(Object.entries(expected).map(([key, pair]) => [key, pair[i]])));
1947
+ // The state is carried by the ring in both themes (the dark fill is not).
1948
+ expect(got.ringOnPanel).toBeGreaterThanOrEqual(3);
1949
+ expect(got.clearRingOnField).toBeGreaterThanOrEqual(3);
1950
+ expect(Math.min(got.textOnFill, got.textOnHoverFill)).toBeGreaterThanOrEqual(4.5);
1951
+ });
1952
+ });
@@ -4,6 +4,8 @@ import * as i0 from "@angular/core";
4
4
  export type CardVariant = 'elevated' | 'outlined' | 'flat' | 'raised' | 'dashed';
5
5
  /** Heading level of `lc-card`'s built-in title. */
6
6
  export type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;
7
+ /** Layout of `lc-card`'s `[card-footer]` slot: `default` (a flex row, end-aligned) or `equal` (children at equal width). */
8
+ export type CardFooterLayout = 'default' | 'equal';
7
9
  /**
8
10
  * Card component for content grouping and visual hierarchy.
9
11
  *
@@ -15,6 +17,8 @@ export type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;
15
17
  * - Content projection for flexible body content
16
18
  * - Link mode (3.24): `href` or `route` make the whole card a real link,
17
19
  * the title stays a heading
20
+ * - Equal footer (3.29): `footerLayout="equal"` gives two answers of equal
21
+ * weight in the `[card-footer]` slot the same width
18
22
  *
19
23
  * @example
20
24
  * ```html
@@ -32,6 +36,15 @@ export type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;
32
36
  * [route]="['/quellen', 'sap-ist']" [queryParams]="{ ansicht: 'laeufe' }">
33
37
  * ...
34
38
  * </lc-card>
39
+ *
40
+ * <!-- Two answers of equal weight (3.29): primary answer first -->
41
+ * <lc-card title="Netflix" variant="outlined" footerLayout="equal">
42
+ * ...
43
+ * <div card-footer>
44
+ * <lc-button variant="outline" size="lg" [fullWidth]="true">Stimmt</lc-button>
45
+ * <lc-button variant="outline" size="lg" [fullWidth]="true">Kein Vertrag</lc-button>
46
+ * </div>
47
+ * </lc-card>
35
48
  * ```
36
49
  */
37
50
  export declare class CardComponent {
@@ -152,6 +165,22 @@ export declare class CardComponent {
152
165
  * @default undefined
153
166
  */
154
167
  queryParams: import("@angular/core").InputSignal<Params | null | undefined>;
168
+ /**
169
+ * Layout of the `[card-footer]` slot (3.29, CC-2). `equal`: its children
170
+ * share the row at equal width once the card is at least 480 px (30rem)
171
+ * wide, and stack full width, 8 px apart, below that (the card's own width,
172
+ * at any viewport; long labels wrap inside the buttons, which get the
173
+ * row's height). For two answers of equal weight, put
174
+ * `<lc-button variant="outline" size="lg" [fullWidth]="true">` in it,
175
+ * primary answer first; the card adds layout only (gap, divider and
176
+ * spacing as `default`) and never restyles the buttons. The card becomes a
177
+ * size container and takes its parent's full width. Keep the default
178
+ * `padding` (with `none` the buttons' focus ring is clipped). Works in link
179
+ * cards (the buttons stay clickable above the link) and in prerendered
180
+ * HTML (CSS only).
181
+ * @default 'default'
182
+ */
183
+ footerLayout: import("@angular/core").InputSignal<CardFooterLayout>;
155
184
  /**
156
185
  * Emitted when a `clickable` card is clicked or activated with Enter or
157
186
  * Space. Not emitted on a link card (`href`/`route`): the link navigates by
@@ -275,6 +304,11 @@ export declare class CardComponent {
275
304
  "required": false;
276
305
  "isSignal": true;
277
306
  };
307
+ "footerLayout": {
308
+ "alias": "footerLayout";
309
+ "required": false;
310
+ "isSignal": true;
311
+ };
278
312
  }, {
279
313
  "cardClick": "cardClick";
280
314
  }, never, ["[card-badge]", "[card-header-action]", "[card-header]", "*", "[card-footer]"], true, never>;
@@ -19,12 +19,16 @@ export type ComboboxSize = 'xs' | 'sm' | 'md' | 'lg';
19
19
  * The suggestion list is rendered in a CDK overlay so it is never clipped
20
20
  * by `overflow: hidden` ancestors (cards, modals, table cells).
21
21
  *
22
+ * The selection is two-way bindable by an option's `value` string
23
+ * (`[(selectedValue)]`, single mode), next to the `valueChange` output with
24
+ * the option object and the forms binding.
25
+ *
22
26
  * @example
23
27
  * ```html
24
28
  * <lc-combobox
25
29
  * label="Assign to user"
26
30
  * [loadOptions]="searchUsers"
27
- * [(value)]="assignee"
31
+ * [(selectedValue)]="assigneeId"
28
32
  * allowCreate
29
33
  * />
30
34
  * ```
@@ -71,6 +75,19 @@ export declare class ComboboxComponent implements ControlValueAccessor {
71
75
  readonly createOptionLabel: import("@angular/core").InputSignal<string>;
72
76
  /** Size */
73
77
  readonly size: import("@angular/core").InputSignal<ComboboxSize>;
78
+ /**
79
+ * The `value` of the selected option, two-way (3.29, regeln row 2):
80
+ * `[(selectedValue)]="month"`. Shows the matching option's label (from
81
+ * `options`, or the current selection); an unknown value shows an empty
82
+ * field and is filled in when `options` with it arrive. Page writes
83
+ * (including back to `null`) select or clear silently: no
84
+ * `selectedValueChange`, no `valueChange`, no forms `onChange`. Every user
85
+ * choice or clear emits `selectedValueChange`, next to the unchanged
86
+ * `valueChange` (the option object). Single mode only: ignored with
87
+ * `multiple` (a dev warning). Use it or a forms binding, not both.
88
+ * @default null
89
+ */
90
+ readonly selectedValue: import("@angular/core").ModelSignal<string | null>;
74
91
  /** Value change event */
75
92
  readonly valueChange: import("@angular/core").OutputEmitterRef<ComboboxValue>;
76
93
  /** Query change event */
@@ -92,6 +109,10 @@ export declare class ComboboxComponent implements ControlValueAccessor {
92
109
  /** Selected values */
93
110
  protected selectedSingle: import("@angular/core").WritableSignal<ComboboxOption | null>;
94
111
  protected selectedMultiple: import("@angular/core").WritableSignal<ComboboxOption[]>;
112
+ /** The last `selectedValue` the component applied or emitted: a different one is a page write. */
113
+ private seenSelectedValue;
114
+ /** A page-written `selectedValue` no option matched yet: selected silently once `options` hold it. */
115
+ private pendingSelectedValue;
95
116
  /** Resolved visible options */
96
117
  protected visibleOptions: import("@angular/core").Signal<ComboboxOption[]>;
97
118
  /** Grouped options */
@@ -113,6 +134,10 @@ export declare class ComboboxComponent implements ControlValueAccessor {
113
134
  private onChange;
114
135
  private onTouched;
115
136
  constructor();
137
+ /** Shows the option with `value` (or clears for `null`) without emitting anything. */
138
+ private applySelectedValue;
139
+ /** Emits a user's single choice or clear through `selectedValueChange` (not in `multiple` mode). */
140
+ private emitSelectedValue;
116
141
  writeValue(value: ComboboxValue): void;
117
142
  registerOnChange(fn: (value: ComboboxValue) => void): void;
118
143
  registerOnTouched(fn: () => void): void;
@@ -123,6 +148,15 @@ export declare class ComboboxComponent implements ControlValueAccessor {
123
148
  */
124
149
  clear(): void;
125
150
  protected onClearClick(): void;
151
+ /** Whether a pointer event on the frame hit the input or a button in it (chip ×, clear). */
152
+ private hitsFrameControl;
153
+ /**
154
+ * 3.30 (request 047, regeln row 6): a press on the frame beside the input
155
+ * and its buttons keeps focus where it is (no blur, so the list stays open).
156
+ */
157
+ protected onFrameMousedown(event: MouseEvent): void;
158
+ /** A click on the frame beside the input and its buttons focuses the input. */
159
+ protected onFrameClick(event: MouseEvent): void;
126
160
  protected onInputChange(event: Event): void;
127
161
  protected onInputFocus(): void;
128
162
  protected onInputBlur(): void;
@@ -213,7 +247,13 @@ export declare class ComboboxComponent implements ControlValueAccessor {
213
247
  "required": false;
214
248
  "isSignal": true;
215
249
  };
250
+ "selectedValue": {
251
+ "alias": "selectedValue";
252
+ "required": false;
253
+ "isSignal": true;
254
+ };
216
255
  }, {
256
+ "selectedValue": "selectedValueChange";
217
257
  "valueChange": "valueChange";
218
258
  "queryChange": "queryChange";
219
259
  "optionSelected": "optionSelected";