ngwr 15.0.0-rc.3 → 15.0.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Component, ElementRef, ViewEncapsulation, computed, effect, inject, input, model, output, viewChild } from "@angular/core";
2
+ import { Component, ElementRef, ViewEncapsulation, computed, inject, input, model, output, viewChild } from "@angular/core";
3
3
  import { coerceBooleanProperty, coerceNumberProperty } from "@angular/cdk/coercion";
4
4
  import { useI18nText } from "ngwr/i18n";
5
5
  import { clamp, round } from "ngwr/utils";
@@ -70,10 +70,11 @@ var WrKnob = class WrKnob {
70
70
  cx = 50;
71
71
  cy = 50;
72
72
  radius = computed(() => Math.max(1, 50 - this.strokeWidth() / 2 - .5), ...ngDevMode ? [{ debugName: "radius" }] : /* istanbul ignore next */ []);
73
+ shown = computed(() => clamp(this.value(), this.minValue(), this.maxValue()), ...ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []);
73
74
  ratio = computed(() => {
74
75
  const range = this.maxValue() - this.minValue();
75
76
  if (range <= 0) return 0;
76
- return clamp((this.value() - this.minValue()) / range, 0, 1);
77
+ return (this.shown() - this.minValue()) / range;
77
78
  }, ...ngDevMode ? [{ debugName: "ratio" }] : /* istanbul ignore next */ []);
78
79
  trackPath = computed(() => this.arcPath(ARC_START, ARC_END), ...ngDevMode ? [{ debugName: "trackPath" }] : /* istanbul ignore next */ []);
79
80
  valuePath = computed(() => {
@@ -89,12 +90,6 @@ var WrKnob = class WrKnob {
89
90
  }, ...ngDevMode ? [{ debugName: "handlePos" }] : /* istanbul ignore next */ []);
90
91
  host = inject(ElementRef);
91
92
  surface = viewChild.required("surface", ...ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []);
92
- constructor() {
93
- effect(() => {
94
- const clamped = clamp(this.value(), this.minValue(), this.maxValue());
95
- if (clamped !== this.value()) this.value.set(clamped);
96
- });
97
- }
98
93
  focus(options) {
99
94
  if (this.focusable()) this.surface().nativeElement.focus(options);
100
95
  }
@@ -128,11 +123,11 @@ var WrKnob = class WrKnob {
128
123
  switch (event.key) {
129
124
  case "ArrowRight":
130
125
  case "ArrowUp":
131
- next = this.value() + step;
126
+ next = this.shown() + step;
132
127
  break;
133
128
  case "ArrowLeft":
134
129
  case "ArrowDown":
135
- next = this.value() - step;
130
+ next = this.shown() - step;
136
131
  break;
137
132
  case "Home":
138
133
  next = this.minValue();
@@ -296,7 +291,7 @@ var WrKnob = class WrKnob {
296
291
  isSignal: true
297
292
  }],
298
293
  ngImport: i0,
299
- template: "<div\n #surface\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value()\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.tabindex]=\"focusable() ? 0 : -1\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n>\n <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n <path\n class=\"wr-knob__track\"\n [attr.d]=\"trackPath()\"\n [attr.stroke]=\"trackColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <path\n class=\"wr-knob__value\"\n [attr.d]=\"valuePath()\"\n [attr.stroke]=\"valueColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <circle\n class=\"wr-knob__handle\"\n [attr.cx]=\"handlePos().x\"\n [attr.cy]=\"handlePos().y\"\n [attr.r]=\"strokeWidth() / 2\"\n [attr.fill]=\"valueColor()\"\n />\n </svg>\n @if (showValue()) {\n <!--\n `dir=\"auto\"` because this is a number and a unit read as one phrase, and\n the browser is the only thing that knows which way that phrase runs. In an\n RTL paragraph the Unicode BiDi algorithm treats a digit run as neutral\n against the letters beside it, so `40 dB` was drawn `dB 40` — and `-5 dB`\n as `dB 5-`, with the sign on the wrong end of the number. `dir=\"auto\"`\n picks the direction from the first strong character in the composed text,\n so a Latin `suffix` keeps the number first and an Arabic one reads\n right-to-left, which is correct for both.\n\n `unicode-bidi: isolate` does NOT fix this and was measured doing nothing:\n isolation seals the element off from its SURROUNDINGS, and the flip\n happens inside it.\n -->\n <div class=\"wr-knob__text\" dir=\"auto\">\n {{ value() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n",
294
+ template: "<div\n #surface\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"shown()\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.tabindex]=\"focusable() ? 0 : -1\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n>\n <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n <path\n class=\"wr-knob__track\"\n [attr.d]=\"trackPath()\"\n [attr.stroke]=\"trackColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <path\n class=\"wr-knob__value\"\n [attr.d]=\"valuePath()\"\n [attr.stroke]=\"valueColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <circle\n class=\"wr-knob__handle\"\n [attr.cx]=\"handlePos().x\"\n [attr.cy]=\"handlePos().y\"\n [attr.r]=\"strokeWidth() / 2\"\n [attr.fill]=\"valueColor()\"\n />\n </svg>\n @if (showValue()) {\n <!--\n `dir=\"auto\"` because this is a number and a unit read as one phrase, and\n the browser is the only thing that knows which way that phrase runs. In an\n RTL paragraph the Unicode BiDi algorithm treats a digit run as neutral\n against the letters beside it, so `40 dB` was drawn `dB 40` — and `-5 dB`\n as `dB 5-`, with the sign on the wrong end of the number. `dir=\"auto\"`\n picks the direction from the first strong character in the composed text,\n so a Latin `suffix` keeps the number first and an Arabic one reads\n right-to-left, which is correct for both.\n\n `unicode-bidi: isolate` does NOT fix this and was measured doing nothing:\n isolation seals the element off from its SURROUNDINGS, and the flip\n happens inside it.\n -->\n <div class=\"wr-knob__text\" dir=\"auto\">\n {{ shown() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n",
300
295
  encapsulation: i0.ViewEncapsulation.None
301
296
  });
302
297
  };
@@ -311,10 +306,9 @@ i0.ɵɵngDeclareClassMetadata({
311
306
  selector: "wr-knob",
312
307
  encapsulation: ViewEncapsulation.None,
313
308
  host: { "[class]": "classes()" },
314
- template: "<div\n #surface\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value()\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.tabindex]=\"focusable() ? 0 : -1\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n>\n <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n <path\n class=\"wr-knob__track\"\n [attr.d]=\"trackPath()\"\n [attr.stroke]=\"trackColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <path\n class=\"wr-knob__value\"\n [attr.d]=\"valuePath()\"\n [attr.stroke]=\"valueColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <circle\n class=\"wr-knob__handle\"\n [attr.cx]=\"handlePos().x\"\n [attr.cy]=\"handlePos().y\"\n [attr.r]=\"strokeWidth() / 2\"\n [attr.fill]=\"valueColor()\"\n />\n </svg>\n @if (showValue()) {\n <!--\n `dir=\"auto\"` because this is a number and a unit read as one phrase, and\n the browser is the only thing that knows which way that phrase runs. In an\n RTL paragraph the Unicode BiDi algorithm treats a digit run as neutral\n against the letters beside it, so `40 dB` was drawn `dB 40` — and `-5 dB`\n as `dB 5-`, with the sign on the wrong end of the number. `dir=\"auto\"`\n picks the direction from the first strong character in the composed text,\n so a Latin `suffix` keeps the number first and an Arabic one reads\n right-to-left, which is correct for both.\n\n `unicode-bidi: isolate` does NOT fix this and was measured doing nothing:\n isolation seals the element off from its SURROUNDINGS, and the flip\n happens inside it.\n -->\n <div class=\"wr-knob__text\" dir=\"auto\">\n {{ value() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n"
309
+ template: "<div\n #surface\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"shown()\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.tabindex]=\"focusable() ? 0 : -1\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n>\n <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n <path\n class=\"wr-knob__track\"\n [attr.d]=\"trackPath()\"\n [attr.stroke]=\"trackColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <path\n class=\"wr-knob__value\"\n [attr.d]=\"valuePath()\"\n [attr.stroke]=\"valueColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <circle\n class=\"wr-knob__handle\"\n [attr.cx]=\"handlePos().x\"\n [attr.cy]=\"handlePos().y\"\n [attr.r]=\"strokeWidth() / 2\"\n [attr.fill]=\"valueColor()\"\n />\n </svg>\n @if (showValue()) {\n <!--\n `dir=\"auto\"` because this is a number and a unit read as one phrase, and\n the browser is the only thing that knows which way that phrase runs. In an\n RTL paragraph the Unicode BiDi algorithm treats a digit run as neutral\n against the letters beside it, so `40 dB` was drawn `dB 40` — and `-5 dB`\n as `dB 5-`, with the sign on the wrong end of the number. `dir=\"auto\"`\n picks the direction from the first strong character in the composed text,\n so a Latin `suffix` keeps the number first and an Arabic one reads\n right-to-left, which is correct for both.\n\n `unicode-bidi: isolate` does NOT fix this and was measured doing nothing:\n isolation seals the element off from its SURROUNDINGS, and the flip\n happens inside it.\n -->\n <div class=\"wr-knob__text\" dir=\"auto\">\n {{ shown() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n"
315
310
  }]
316
311
  }],
317
- ctorParameters: () => [],
318
312
  propDecorators: {
319
313
  ariaLabel: [{
320
314
  type: i0.Input,
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-knob.mjs","names":[],"sources":["../../../projects/lib/knob/knob.ts","../../../projects/lib/knob/knob.html"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport {\n Component,\n ElementRef,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n model,\n output,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport { useFormFieldAria } from 'ngwr/form';\nimport { useI18nText } from 'ngwr/i18n';\nimport { clamp, round } from 'ngwr/utils';\n\nconst ARC_START = -135; // 7 o'clock\nconst ARC_END = 135; // 5 o'clock — 270° sweep total\nconst ARC_SWEEP = ARC_END - ARC_START;\n\n/**\n * Radial dial input. A signal-forms native control: it implements\n * `FormValueControl<number>`, so `[formField]` binds straight to its `value`\n * model — no `ControlValueAccessor` in between. `[(value)]` works standalone.\n *\n * The value is clamped to `[min, max]`. Drag the indicator around the dial or\n * use ←→ / ↑↓ to step.\n *\n * @example\n * ```html\n * <!-- signal forms -->\n * <wr-knob [formField]=\"form.volume\" [min]=\"0\" [max]=\"100\" />\n *\n * <!-- standalone two-way binding -->\n * <wr-knob [(value)]=\"vol\" suffix=\"%\" valueColor=\"var(--wr-color-success)\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/knob\n */\n@Component({\n selector: 'wr-knob',\n templateUrl: './knob.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()' },\n})\nexport class WrKnob implements FormValueControl<number> {\n /** Accessible name — the role needs one. Falls back to `knob.label`. */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'knob.label', 'Value');\n\n /** Minimum allowed value. @default 0 */\n readonly min = input<number | undefined>(0, { transform: (v: unknown): number => coerceNumberProperty(v, 0) });\n\n /** Maximum allowed value. @default 100 */\n readonly max = input<number | undefined>(100, { transform: (v: unknown): number => coerceNumberProperty(v, 100) });\n\n private readonly minValue = computed(() => this.min() ?? 0);\n private readonly maxValue = computed(() => this.max() ?? 100);\n\n /** Step granularity. @default 1 */\n readonly step = input(1, { transform: (v: unknown): number => Math.max(0.0001, coerceNumberProperty(v, 1)) });\n\n /** Dial diameter in CSS pixels. @default 120 */\n readonly size = input(120, { transform: (v: unknown): number => Math.max(32, coerceNumberProperty(v, 120)) });\n\n /** Stroke width of the dial arc, in CSS pixels. @default 8 */\n readonly strokeWidth = input(8, {\n transform: (v: unknown): number => Math.max(1, coerceNumberProperty(v, 8)),\n });\n\n /** Track (unfilled) color. @default color-mix(in srgb, var(--wr-border-base) 60%, transparent) */\n readonly trackColor = input<string>('color-mix(in srgb, var(--wr-border-base) 60%, transparent)');\n\n /** Filled-arc color. @default var(--wr-color-primary) */\n readonly valueColor = input<string>('var(--wr-color-primary)');\n\n /** Show the value text in the center. @default true */\n readonly showValue = input(true, { transform: coerceBooleanProperty });\n\n /** Optional suffix appended to the center text (e.g. `'%'`). */\n readonly suffix = input<string>('');\n\n /** Read-only — disables interaction but keeps the visual. @default false */\n readonly readonly = input(false, { transform: coerceBooleanProperty });\n\n /** Disable interaction. @default false */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n\n /** Bound by `[formField]`, or two-way via `[(value)]`. */\n readonly value = model<number>(0);\n\n /** Emitted on blur so a bound field can mark itself touched. */\n readonly touch = output<void>();\n\n private dragging = false;\n\n protected readonly interactive = computed(() => !this.disabled() && !this.readonly());\n\n /**\n * Whether the control keeps its tab stop — `disabled` only, never `readonly`.\n *\n * The two states differ precisely here: a disabled control leaves the tab\n * order, a read-only one stays in it and keeps announcing `aria-valuenow`,\n * because a keyboard user still has to be able to READ a value they cannot\n * change. Driving `tabindex` off `interactive()` collapsed that distinction\n * and made `readonly()` unreachable — with an `aria-describedby` pointing at\n * an error message on an element nobody could focus.\n */\n protected readonly focusable = computed(() => !this.disabled());\n\n /** The surrounding `<wr-form-field>`'s error state. @internal */\n protected readonly fieldAria = useFormFieldAria();\n\n /**\n * The field's label wins over the built-in default, and an explicit\n * `ariaLabel` wins over both.\n *\n * `<wr-form-field label=\"…\">` renders a `<label for>` that cannot reach this\n * control — `for` binds only to a LABELABLE element and the focusable part\n * here carries a role instead — so the field's label named nothing and this\n * announced its own generic default. A consumer who wrote\n * `<wr-form-field label=\"Overall satisfaction\">` heard \"Value\".\n */\n protected readonly labelledBy = computed(() => (this.ariaLabel() ? null : this.fieldAria.labelledBy()));\n\n /** `null` once `aria-labelledby` is carrying the name, or the two are announced together. */\n protected readonly nameLabel = computed(() => (this.labelledBy() ? null : this.resolvedAriaLabel()));\n\n protected readonly classes = computed(() => {\n const parts = ['wr-knob'];\n if (this.disabled()) parts.push('wr-knob--disabled');\n if (this.readonly()) parts.push('wr-knob--readonly');\n return parts.join(' ');\n });\n\n // Geometry — drawn into a 100×100 viewBox, scaled by `size`.\n protected readonly cx = 50;\n protected readonly cy = 50;\n // A negative radius is invalid in the SVG path grammar, so the browser drops\n // the arc and the dial vanishes — which a fat stroke reaches at 99.\n protected readonly radius = computed(() => Math.max(1, 50 - this.strokeWidth() / 2 - 0.5));\n\n /** Ratio of the filled arc — `0` at min, `1` at max. */\n protected readonly ratio = computed(() => {\n const range = this.maxValue() - this.minValue();\n if (range <= 0) return 0;\n return clamp((this.value() - this.minValue()) / range, 0, 1);\n });\n\n /** SVG path `d` for the background arc. */\n protected readonly trackPath = computed(() => this.arcPath(ARC_START, ARC_END));\n\n /** SVG path `d` for the filled arc. */\n protected readonly valuePath = computed(() => {\n const end = ARC_START + ARC_SWEEP * this.ratio();\n return this.arcPath(ARC_START, end);\n });\n\n /** Position of the handle dot — `{ x, y }` in viewBox units. */\n protected readonly handlePos = computed(() => {\n const angle = ARC_START + ARC_SWEEP * this.ratio();\n const rad = (angle * Math.PI) / 180;\n return {\n x: this.cx + this.radius() * Math.sin(rad),\n y: this.cy - this.radius() * Math.cos(rad),\n };\n });\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly surface = viewChild.required<ElementRef<HTMLElement>>('surface');\n\n constructor() {\n // Keep the value within `[min, max]` even when written externally (via\n // `[formField]` / `[(value)]`). Interactive changes are already clamped in\n // `commit`, so this only normalizes out-of-range external writes.\n effect(() => {\n const clamped = clamp(this.value(), this.minValue(), this.maxValue());\n if (clamped !== this.value()) this.value.set(clamped);\n });\n }\n\n /**\n * Focus the dial — what Signal Forms calls to focus this control, from\n * `focusBoundControl()`. The host carries no tab stop, so the call goes to\n * the `role=\"slider\"` surface. A disabled dial is left alone: its surface\n * keeps `tabindex=\"-1\"`, which a script could still focus, and a disabled\n * native control refuses the same call.\n */\n focus(options?: FocusOptions): void {\n if (this.focusable()) this.surface().nativeElement.focus(options);\n }\n\n // Pointer / keyboard\n\n protected onPointerDown(event: PointerEvent): void {\n if (!this.interactive()) return;\n // Any pointerdown used to start a drag, so the right button turned the dial;\n // the same guard keeps a second finger out of a drag already in progress.\n if (event.button !== 0 || !event.isPrimary) return;\n event.preventDefault();\n this.dragging = true;\n const surface = event.currentTarget as HTMLElement;\n surface.setPointerCapture(event.pointerId);\n // `preventDefault` above also suppresses the click's default focus, so the\n // arrows did nothing after a mouse drag until the dial was found again with\n // Tab. `wr-slider` focuses its thumb for the same reason.\n surface.focus();\n this.updateFromPointer(event);\n }\n\n protected onPointerMove(event: PointerEvent): void {\n if (!this.dragging) return;\n this.updateFromPointer(event);\n }\n\n protected onPointerUp(event: PointerEvent): void {\n if (!this.dragging) return;\n this.dragging = false;\n (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);\n this.touch.emit();\n }\n\n /**\n * A bound field learns it may show its validation copy from `touch`, and\n * pointer-up cannot be the only source: a knob reached by Tab and moved with\n * the arrows would leave the field untouched forever. `wr-rating`, whose\n * template this one otherwise mirrors, emits on blur for exactly this reason.\n */\n protected onBlur(): void {\n this.touch.emit();\n }\n\n /**\n * The APG slider keys, and they do NOT mirror under `dir=\"rtl\"` — which is a\n * decision, not an oversight, and the one place it is written down.\n *\n * `wr-slider` swaps ArrowLeft and ArrowRight in RTL because its TRACK mirrors:\n * the thumb is placed with `inset-inline-start`, so the visual maximum moves to\n * the left and a key naming a side has to move with it. This dial does not\n * mirror. It is an SVG gauge drawn in a fixed 100×100 viewBox, and SVG content\n * is not reordered by the reading direction — the arc sweeps clockwise from 7\n * o'clock to 5 o'clock in every locale, the same way a clock face or a car's\n * rev counter does. So ArrowRight still moves the handle toward the visually\n * right-hand side of the arc in RTL, and swapping the keys would be the thing\n * that broke the mapping.\n *\n * That is why nothing here injects `Directionality`: there is no physical CSS\n * in the value path at all, only fixed SVG geometry.\n */\n protected onKeydown(event: KeyboardEvent): void {\n if (!this.interactive()) return;\n const step = event.shiftKey ? this.step() * 10 : this.step();\n let next: number | null;\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n next = this.value() + step;\n break;\n case 'ArrowLeft':\n case 'ArrowDown':\n next = this.value() - step;\n break;\n case 'Home':\n next = this.minValue();\n break;\n case 'End':\n next = this.maxValue();\n break;\n default:\n return;\n }\n event.preventDefault();\n this.commit(this.snap(next));\n }\n\n /**\n * Nearest step, measured FROM `min`. A grid anchored at zero offers values the\n * arrows can never reach whenever `min` is not a multiple of `step` — with\n * `min: 5, step: 10` the middle of the arc used to commit 60, off a grid whose\n * points are 5, 15, … 55, 65. Rounded at six decimals because a tenth is not a\n * binary fraction: three arrow presses at `step: 0.1` otherwise announce\n * `aria-valuenow=\"0.30000000000000004\"` and print it in the dial. Same shape as\n * `wr-slider`'s `snap`, deliberately — including Home / End, so a range that is\n * not a whole number of steps stops at the last reachable value.\n */\n private snap(value: number): number {\n const min = this.minValue();\n const stepped = Math.round((value - min) / this.step()) * this.step() + min;\n return clamp(round(stepped, 6), min, this.maxValue());\n }\n\n // Internals\n\n private updateFromPointer(event: PointerEvent): void {\n const rect = this.host.nativeElement.getBoundingClientRect();\n const dx = event.clientX - (rect.left + rect.width / 2);\n const dy = event.clientY - (rect.top + rect.height / 2);\n // Angle measured from 12 o'clock, clockwise.\n let angle = (Math.atan2(dx, -dy) * 180) / Math.PI;\n // Map [-180, 180] into the dial's arc range, clamping outside it.\n if (angle < ARC_START) angle = angle + 360 > ARC_END ? ARC_START : angle + 360;\n angle = clamp(angle, ARC_START, ARC_END);\n const ratio = (angle - ARC_START) / ARC_SWEEP;\n const range = this.maxValue() - this.minValue();\n const raw = this.minValue() + ratio * range;\n this.commit(this.snap(raw));\n }\n\n private commit(next: number): void {\n if (next === this.value()) return;\n this.value.set(next);\n }\n\n // Arc helper\n\n private arcPath(startDeg: number, endDeg: number): string {\n const r = this.radius();\n const s = (startDeg * Math.PI) / 180;\n const e = (endDeg * Math.PI) / 180;\n const sx = this.cx + r * Math.sin(s);\n const sy = this.cy - r * Math.cos(s);\n const ex = this.cx + r * Math.sin(e);\n const ey = this.cy - r * Math.cos(e);\n const largeArc = endDeg - startDeg > 180 ? 1 : 0;\n return `M ${sx} ${sy} A ${r} ${r} 0 ${largeArc} 1 ${ex} ${ey}`;\n }\n}\n","<div\n #surface\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value()\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.tabindex]=\"focusable() ? 0 : -1\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n>\n <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n <path\n class=\"wr-knob__track\"\n [attr.d]=\"trackPath()\"\n [attr.stroke]=\"trackColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <path\n class=\"wr-knob__value\"\n [attr.d]=\"valuePath()\"\n [attr.stroke]=\"valueColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <circle\n class=\"wr-knob__handle\"\n [attr.cx]=\"handlePos().x\"\n [attr.cy]=\"handlePos().y\"\n [attr.r]=\"strokeWidth() / 2\"\n [attr.fill]=\"valueColor()\"\n />\n </svg>\n @if (showValue()) {\n <!--\n `dir=\"auto\"` because this is a number and a unit read as one phrase, and\n the browser is the only thing that knows which way that phrase runs. In an\n RTL paragraph the Unicode BiDi algorithm treats a digit run as neutral\n against the letters beside it, so `40 dB` was drawn `dB 40` — and `-5 dB`\n as `dB 5-`, with the sign on the wrong end of the number. `dir=\"auto\"`\n picks the direction from the first strong character in the composed text,\n so a Latin `suffix` keeps the number first and an Arabic one reads\n right-to-left, which is correct for both.\n\n `unicode-bidi: isolate` does NOT fix this and was measured doing nothing:\n isolation seals the element off from its SURROUNDINGS, and the flip\n happens inside it.\n -->\n <div class=\"wr-knob__text\" dir=\"auto\">\n {{ value() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-knob.mjs","names":[],"sources":["../../../projects/lib/knob/knob.ts","../../../projects/lib/knob/knob.html"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport {\n Component,\n ElementRef,\n ViewEncapsulation,\n computed,\n inject,\n input,\n model,\n output,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport { useFormFieldAria } from 'ngwr/form';\nimport { useI18nText } from 'ngwr/i18n';\nimport { clamp, round } from 'ngwr/utils';\n\nconst ARC_START = -135; // 7 o'clock\nconst ARC_END = 135; // 5 o'clock — 270° sweep total\nconst ARC_SWEEP = ARC_END - ARC_START;\n\n/**\n * Radial dial input. A signal-forms native control: it implements\n * `FormValueControl<number>`, so `[formField]` binds straight to its `value`\n * model — no `ControlValueAccessor` in between. `[(value)]` works standalone.\n *\n * The dial shows the value clamped to `[min, max]`, and a drag or a key press\n * commits one in range; a value written from outside stays as it was. Drag the\n * indicator around the dial or use ←→ / ↑↓ to step.\n *\n * @example\n * ```html\n * <!-- signal forms -->\n * <wr-knob [formField]=\"form.volume\" [min]=\"0\" [max]=\"100\" />\n *\n * <!-- standalone two-way binding -->\n * <wr-knob [(value)]=\"vol\" suffix=\"%\" valueColor=\"var(--wr-color-success)\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/knob\n */\n@Component({\n selector: 'wr-knob',\n templateUrl: './knob.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()' },\n})\nexport class WrKnob implements FormValueControl<number> {\n /** Accessible name — the role needs one. Falls back to `knob.label`. */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'knob.label', 'Value');\n\n /** Minimum allowed value. @default 0 */\n readonly min = input<number | undefined>(0, { transform: (v: unknown): number => coerceNumberProperty(v, 0) });\n\n /** Maximum allowed value. @default 100 */\n readonly max = input<number | undefined>(100, { transform: (v: unknown): number => coerceNumberProperty(v, 100) });\n\n private readonly minValue = computed(() => this.min() ?? 0);\n private readonly maxValue = computed(() => this.max() ?? 100);\n\n /** Step granularity. @default 1 */\n readonly step = input(1, { transform: (v: unknown): number => Math.max(0.0001, coerceNumberProperty(v, 1)) });\n\n /** Dial diameter in CSS pixels. @default 120 */\n readonly size = input(120, { transform: (v: unknown): number => Math.max(32, coerceNumberProperty(v, 120)) });\n\n /** Stroke width of the dial arc, in CSS pixels. @default 8 */\n readonly strokeWidth = input(8, {\n transform: (v: unknown): number => Math.max(1, coerceNumberProperty(v, 8)),\n });\n\n /** Track (unfilled) color. @default color-mix(in srgb, var(--wr-border-base) 60%, transparent) */\n readonly trackColor = input<string>('color-mix(in srgb, var(--wr-border-base) 60%, transparent)');\n\n /** Filled-arc color. @default var(--wr-color-primary) */\n readonly valueColor = input<string>('var(--wr-color-primary)');\n\n /** Show the value text in the center. @default true */\n readonly showValue = input(true, { transform: coerceBooleanProperty });\n\n /** Optional suffix appended to the center text (e.g. `'%'`). */\n readonly suffix = input<string>('');\n\n /** Read-only — disables interaction but keeps the visual. @default false */\n readonly readonly = input(false, { transform: coerceBooleanProperty });\n\n /** Disable interaction. @default false */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n\n /** Bound by `[formField]`, or two-way via `[(value)]`. */\n readonly value = model<number>(0);\n\n /** Emitted on blur so a bound field can mark itself touched. */\n readonly touch = output<void>();\n\n private dragging = false;\n\n protected readonly interactive = computed(() => !this.disabled() && !this.readonly());\n\n /**\n * Whether the control keeps its tab stop — `disabled` only, never `readonly`.\n *\n * The two states differ precisely here: a disabled control leaves the tab\n * order, a read-only one stays in it and keeps announcing `aria-valuenow`,\n * because a keyboard user still has to be able to READ a value they cannot\n * change. Driving `tabindex` off `interactive()` collapsed that distinction\n * and made `readonly()` unreachable — with an `aria-describedby` pointing at\n * an error message on an element nobody could focus.\n */\n protected readonly focusable = computed(() => !this.disabled());\n\n /** The surrounding `<wr-form-field>`'s error state. @internal */\n protected readonly fieldAria = useFormFieldAria();\n\n /**\n * The field's label wins over the built-in default, and an explicit\n * `ariaLabel` wins over both.\n *\n * `<wr-form-field label=\"…\">` renders a `<label for>` that cannot reach this\n * control — `for` binds only to a LABELABLE element and the focusable part\n * here carries a role instead — so the field's label named nothing and this\n * announced its own generic default. A consumer who wrote\n * `<wr-form-field label=\"Overall satisfaction\">` heard \"Value\".\n */\n protected readonly labelledBy = computed(() => (this.ariaLabel() ? null : this.fieldAria.labelledBy()));\n\n /** `null` once `aria-labelledby` is carrying the name, or the two are announced together. */\n protected readonly nameLabel = computed(() => (this.labelledBy() ? null : this.resolvedAriaLabel()));\n\n protected readonly classes = computed(() => {\n const parts = ['wr-knob'];\n if (this.disabled()) parts.push('wr-knob--disabled');\n if (this.readonly()) parts.push('wr-knob--readonly');\n return parts.join(' ');\n });\n\n // Geometry — drawn into a 100×100 viewBox, scaled by `size`.\n protected readonly cx = 50;\n protected readonly cy = 50;\n // A negative radius is invalid in the SVG path grammar, so the browser drops\n // the arc and the dial vanishes — which a fat stroke reaches at 99.\n protected readonly radius = computed(() => Math.max(1, 50 - this.strokeWidth() / 2 - 0.5));\n\n /**\n * The value the dial shows and announces: the model, held to `[min, max]`.\n * Only the display is clamped. Writing the clamped number back into the model\n * turned a form's out-of-range value into a different one and marked the\n * field dirty before anyone touched it — the rule `wr-input-number` and\n * `wr-slider` already follow. A key press or a drag commits a value in range.\n */\n protected readonly shown = computed(() => clamp(this.value(), this.minValue(), this.maxValue()));\n\n /** Ratio of the filled arc — `0` at min, `1` at max. */\n protected readonly ratio = computed(() => {\n const range = this.maxValue() - this.minValue();\n if (range <= 0) return 0;\n return (this.shown() - this.minValue()) / range;\n });\n\n /** SVG path `d` for the background arc. */\n protected readonly trackPath = computed(() => this.arcPath(ARC_START, ARC_END));\n\n /** SVG path `d` for the filled arc. */\n protected readonly valuePath = computed(() => {\n const end = ARC_START + ARC_SWEEP * this.ratio();\n return this.arcPath(ARC_START, end);\n });\n\n /** Position of the handle dot — `{ x, y }` in viewBox units. */\n protected readonly handlePos = computed(() => {\n const angle = ARC_START + ARC_SWEEP * this.ratio();\n const rad = (angle * Math.PI) / 180;\n return {\n x: this.cx + this.radius() * Math.sin(rad),\n y: this.cy - this.radius() * Math.cos(rad),\n };\n });\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly surface = viewChild.required<ElementRef<HTMLElement>>('surface');\n\n /**\n * Focus the dial — what Signal Forms calls to focus this control, from\n * `focusBoundControl()`. The host carries no tab stop, so the call goes to\n * the `role=\"slider\"` surface. A disabled dial is left alone: its surface\n * keeps `tabindex=\"-1\"`, which a script could still focus, and a disabled\n * native control refuses the same call.\n */\n focus(options?: FocusOptions): void {\n if (this.focusable()) this.surface().nativeElement.focus(options);\n }\n\n // Pointer / keyboard\n\n protected onPointerDown(event: PointerEvent): void {\n if (!this.interactive()) return;\n // Any pointerdown used to start a drag, so the right button turned the dial;\n // the same guard keeps a second finger out of a drag already in progress.\n if (event.button !== 0 || !event.isPrimary) return;\n event.preventDefault();\n this.dragging = true;\n const surface = event.currentTarget as HTMLElement;\n surface.setPointerCapture(event.pointerId);\n // `preventDefault` above also suppresses the click's default focus, so the\n // arrows did nothing after a mouse drag until the dial was found again with\n // Tab. `wr-slider` focuses its thumb for the same reason.\n surface.focus();\n this.updateFromPointer(event);\n }\n\n protected onPointerMove(event: PointerEvent): void {\n if (!this.dragging) return;\n this.updateFromPointer(event);\n }\n\n protected onPointerUp(event: PointerEvent): void {\n if (!this.dragging) return;\n this.dragging = false;\n (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);\n this.touch.emit();\n }\n\n /**\n * A bound field learns it may show its validation copy from `touch`, and\n * pointer-up cannot be the only source: a knob reached by Tab and moved with\n * the arrows would leave the field untouched forever. `wr-rating`, whose\n * template this one otherwise mirrors, emits on blur for exactly this reason.\n */\n protected onBlur(): void {\n this.touch.emit();\n }\n\n /**\n * The APG slider keys, and they do NOT mirror under `dir=\"rtl\"` — which is a\n * decision, not an oversight, and the one place it is written down.\n *\n * `wr-slider` swaps ArrowLeft and ArrowRight in RTL because its TRACK mirrors:\n * the thumb is placed with `inset-inline-start`, so the visual maximum moves to\n * the left and a key naming a side has to move with it. This dial does not\n * mirror. It is an SVG gauge drawn in a fixed 100×100 viewBox, and SVG content\n * is not reordered by the reading direction — the arc sweeps clockwise from 7\n * o'clock to 5 o'clock in every locale, the same way a clock face or a car's\n * rev counter does. So ArrowRight still moves the handle toward the visually\n * right-hand side of the arc in RTL, and swapping the keys would be the thing\n * that broke the mapping.\n *\n * That is why nothing here injects `Directionality`: there is no physical CSS\n * in the value path at all, only fixed SVG geometry.\n */\n protected onKeydown(event: KeyboardEvent): void {\n if (!this.interactive()) return;\n const step = event.shiftKey ? this.step() * 10 : this.step();\n let next: number | null;\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n next = this.shown() + step;\n break;\n case 'ArrowLeft':\n case 'ArrowDown':\n next = this.shown() - step;\n break;\n case 'Home':\n next = this.minValue();\n break;\n case 'End':\n next = this.maxValue();\n break;\n default:\n return;\n }\n event.preventDefault();\n this.commit(this.snap(next));\n }\n\n /**\n * Nearest step, measured FROM `min`. A grid anchored at zero offers values the\n * arrows can never reach whenever `min` is not a multiple of `step` — with\n * `min: 5, step: 10` the middle of the arc used to commit 60, off a grid whose\n * points are 5, 15, … 55, 65. Rounded at six decimals because a tenth is not a\n * binary fraction: three arrow presses at `step: 0.1` otherwise announce\n * `aria-valuenow=\"0.30000000000000004\"` and print it in the dial. Same shape as\n * `wr-slider`'s `snap`, deliberately — including Home / End, so a range that is\n * not a whole number of steps stops at the last reachable value.\n */\n private snap(value: number): number {\n const min = this.minValue();\n const stepped = Math.round((value - min) / this.step()) * this.step() + min;\n return clamp(round(stepped, 6), min, this.maxValue());\n }\n\n // Internals\n\n private updateFromPointer(event: PointerEvent): void {\n const rect = this.host.nativeElement.getBoundingClientRect();\n const dx = event.clientX - (rect.left + rect.width / 2);\n const dy = event.clientY - (rect.top + rect.height / 2);\n // Angle measured from 12 o'clock, clockwise.\n let angle = (Math.atan2(dx, -dy) * 180) / Math.PI;\n // Map [-180, 180] into the dial's arc range, clamping outside it.\n if (angle < ARC_START) angle = angle + 360 > ARC_END ? ARC_START : angle + 360;\n angle = clamp(angle, ARC_START, ARC_END);\n const ratio = (angle - ARC_START) / ARC_SWEEP;\n const range = this.maxValue() - this.minValue();\n const raw = this.minValue() + ratio * range;\n this.commit(this.snap(raw));\n }\n\n private commit(next: number): void {\n if (next === this.value()) return;\n this.value.set(next);\n }\n\n // Arc helper\n\n private arcPath(startDeg: number, endDeg: number): string {\n const r = this.radius();\n const s = (startDeg * Math.PI) / 180;\n const e = (endDeg * Math.PI) / 180;\n const sx = this.cx + r * Math.sin(s);\n const sy = this.cy - r * Math.cos(s);\n const ex = this.cx + r * Math.sin(e);\n const ey = this.cy - r * Math.cos(e);\n const largeArc = endDeg - startDeg > 180 ? 1 : 0;\n return `M ${sx} ${sy} A ${r} ${r} 0 ${largeArc} 1 ${ex} ${ey}`;\n }\n}\n","<div\n #surface\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"shown()\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.tabindex]=\"focusable() ? 0 : -1\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n>\n <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n <path\n class=\"wr-knob__track\"\n [attr.d]=\"trackPath()\"\n [attr.stroke]=\"trackColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <path\n class=\"wr-knob__value\"\n [attr.d]=\"valuePath()\"\n [attr.stroke]=\"valueColor()\"\n [attr.stroke-width]=\"strokeWidth()\"\n fill=\"none\"\n stroke-linecap=\"round\"\n />\n <circle\n class=\"wr-knob__handle\"\n [attr.cx]=\"handlePos().x\"\n [attr.cy]=\"handlePos().y\"\n [attr.r]=\"strokeWidth() / 2\"\n [attr.fill]=\"valueColor()\"\n />\n </svg>\n @if (showValue()) {\n <!--\n `dir=\"auto\"` because this is a number and a unit read as one phrase, and\n the browser is the only thing that knows which way that phrase runs. In an\n RTL paragraph the Unicode BiDi algorithm treats a digit run as neutral\n against the letters beside it, so `40 dB` was drawn `dB 40` — and `-5 dB`\n as `dB 5-`, with the sign on the wrong end of the number. `dir=\"auto\"`\n picks the direction from the first strong character in the composed text,\n so a Latin `suffix` keeps the number first and an Arabic one reads\n right-to-left, which is correct for both.\n\n `unicode-bidi: isolate` does NOT fix this and was measured doing nothing:\n isolation seals the element off from its SURROUNDINGS, and the flip\n happens inside it.\n -->\n <div class=\"wr-knob__text\" dir=\"auto\">\n {{ shown() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n"],"mappings":";;;;;;;;;;;;AAyBA,MAAM,YAAY;AAClB,MAAM,UAAU;AAChB,MAAM,YAAY;AA4BlB,IAAa,SAAb,MAAa,OAAM;CAER,YAAY,MAAqB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAE5B,oBAAoB,YAAY,KAAK,WAAW,cAAc,OAAO;CAG/E,MAAM,MAA0B,GAAC;EAAA,GAAA,YAAA,EAAA,WAAA,MAAA,+BAAA,CAAA;EAAI,YAAY,MAAuB,qBAAqB,GAAG,CAAC;CAAC,CAAA;CAGlG,MAAM,MAA0B,KAAG;EAAA,GAAA,YAAA,EAAA,WAAA,MAAA,+BAAA,CAAA;EAAI,YAAY,MAAuB,qBAAqB,GAAG,GAAG;CAAC,CAAA;CAE9F,WAAW,eAAe,KAAK,IAAG,KAAM,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAC1C,WAAW,eAAe,KAAK,IAAG,KAAM,KAAI,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGpD,OAAO,MAAM,GAAC;EAAA,GAAA,YAAA,EAAA,WAAA,OAAA,+BAAA,CAAA;EAAI,YAAY,MAAuB,KAAK,IAAI,MAAQ,qBAAqB,GAAG,CAAC,CAAC;CAAC,CAAA;CAGjG,OAAO,MAAM,KAAG;EAAA,GAAA,YAAA,EAAA,WAAA,OAAA,+BAAA,CAAA;EAAI,YAAY,MAAuB,KAAK,IAAI,IAAI,qBAAqB,GAAG,GAAG,CAAC;CAAC,CAAA;CAGjG,cAAc,MAAM,GAAC;EAAA,GAAA,YAAA,EAAA,WAAA,cAAA,+BAAA,CAAA;EAC5B,YAAY,MAAuB,KAAK,IAAI,GAAG,qBAAqB,GAAG,CAAC,CAAC;CAAC,CAAA;CAInE,aAAa,MAAc,8DAA6D,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGxF,aAAa,MAAc,2BAA0B,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGrD,YAAY,MAAM,MAAI;EAAA,GAAA,YAAA,EAAA,WAAA,YAAA,+BAAA,CAAA;EAAI,WAAW;CAAqB,CAAA;CAG1D,SAAS,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAG1B,WAAW,MAAM,OAAK;EAAA,GAAA,YAAA,EAAA,WAAA,WAAA,+BAAA,CAAA;EAAI,WAAW;CAAqB,CAAA;CAG1D,WAAW,MAAM,OAAK;EAAA,GAAA,YAAA,EAAA,WAAA,WAAA,+BAAA,CAAA;EAAI,WAAW;CAAqB,CAAA;CAG1D,QAAQ,MAAc,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAGxB,QAAQ,OAAM;CAEf,WAAW;CAEA,cAAc,eAAe,CAAC,KAAK,SAAQ,KAAM,CAAC,KAAK,SAAQ,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAYlE,YAAY,eAAe,CAAC,KAAK,SAAQ,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG5C,YAAY,iBAAgB;CAY5B,aAAa,eAAgB,KAAK,UAAS,IAAK,OAAO,KAAK,UAAU,WAAU,GAAI,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGpF,YAAY,eAAgB,KAAK,WAAU,IAAK,OAAO,KAAK,kBAAiB,GAAI,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEjF,UAAU,eAAc;EACzC,MAAM,QAAQ,CAAC,SAAS;EACxB,IAAI,KAAK,SAAQ,GAAI,MAAM,KAAK,mBAAmB;EACnD,IAAI,KAAK,SAAQ,GAAI,MAAM,KAAK,mBAAmB;EACnD,OAAO,MAAM,KAAK,GAAG;CACvB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAGiB,KAAK;CACL,KAAK;CAGL,SAAS,eAAe,KAAK,IAAI,GAAG,KAAK,KAAK,YAAW,IAAK,IAAI,EAAG,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CASvE,QAAQ,eAAe,MAAM,KAAK,MAAK,GAAI,KAAK,SAAQ,GAAI,KAAK,SAAQ,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAG7E,QAAQ,eAAc;EACvC,MAAM,QAAQ,KAAK,SAAQ,IAAK,KAAK,SAAQ;EAC7C,IAAI,SAAS,GAAG,OAAO;EACvB,QAAQ,KAAK,MAAK,IAAK,KAAK,SAAQ,KAAM;CAC5C,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAGiB,YAAY,eAAe,KAAK,QAAQ,WAAW,OAAO,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG5D,YAAY,eAAc;EAC3C,MAAM,MAAM,YAAY,YAAY,KAAK,MAAK;EAC9C,OAAO,KAAK,QAAQ,WAAW,GAAG;CACpC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAGiB,YAAY,eAAc;EAE3C,MAAM,OADQ,YAAY,YAAY,KAAK,MAAK,KAC3B,KAAK,KAAM;EAChC,OAAO;GACL,GAAG,KAAK,KAAK,KAAK,OAAM,IAAK,KAAK,IAAI,GAAG;GACzC,GAAG,KAAK,KAAK,KAAK,OAAM,IAAK,KAAK,IAAI,GAAG;EAC1C;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEe,OAAO,OAAgC,UAAU;CACjD,UAAU,UAAU,SAAkC,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CASjF,MAAM,SAAsB;EAC1B,IAAI,KAAK,UAAS,GAAI,KAAK,QAAO,CAAE,CAAC,cAAc,MAAM,OAAO;CAClE;CAIU,cAAc,OAAmB;EACzC,IAAI,CAAC,KAAK,YAAW,GAAI;EAGzB,IAAI,MAAM,WAAW,KAAK,CAAC,MAAM,WAAW;EAC5C,MAAM,eAAc;EACpB,KAAK,WAAW;EAChB,MAAM,UAAU,MAAM;EACtB,QAAQ,kBAAkB,MAAM,SAAS;EAIzC,QAAQ,MAAK;EACb,KAAK,kBAAkB,KAAK;CAC9B;CAEU,cAAc,OAAmB;EACzC,IAAI,CAAC,KAAK,UAAU;EACpB,KAAK,kBAAkB,KAAK;CAC9B;CAEU,YAAY,OAAmB;EACvC,IAAI,CAAC,KAAK,UAAU;EACpB,KAAK,WAAW;EACf,MAAM,cAA8B,sBAAsB,MAAM,SAAS;EAC1E,KAAK,MAAM,KAAI;CACjB;CAQU,SAAM;EACd,KAAK,MAAM,KAAI;CACjB;CAmBU,UAAU,OAAoB;EACtC,IAAI,CAAC,KAAK,YAAW,GAAI;EACzB,MAAM,OAAO,MAAM,WAAW,KAAK,KAAI,IAAK,KAAK,KAAK,KAAI;EAC1D,IAAI;EACJ,QAAQ,MAAM,KAAd;GACE,KAAK;GACL,KAAK;IACH,OAAO,KAAK,MAAK,IAAK;IACtB;GACF,KAAK;GACL,KAAK;IACH,OAAO,KAAK,MAAK,IAAK;IACtB;GACF,KAAK;IACH,OAAO,KAAK,SAAQ;IACpB;GACF,KAAK;IACH,OAAO,KAAK,SAAQ;IACpB;GACF,SACE;EACJ;EACA,MAAM,eAAc;EACpB,KAAK,OAAO,KAAK,KAAK,IAAI,CAAC;CAC7B;CAYQ,KAAK,OAAa;EACxB,MAAM,MAAM,KAAK,SAAQ;EACzB,MAAM,UAAU,KAAK,OAAO,QAAQ,OAAO,KAAK,KAAI,CAAE,IAAI,KAAK,KAAI,IAAK;EACxE,OAAO,MAAM,MAAM,SAAS,CAAC,GAAG,KAAK,KAAK,SAAQ,CAAE;CACtD;CAIQ,kBAAkB,OAAmB;EAC3C,MAAM,OAAO,KAAK,KAAK,cAAc,sBAAqB;EAC1D,MAAM,KAAK,MAAM,WAAW,KAAK,OAAO,KAAK,QAAQ;EACrD,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM,KAAK,SAAS;EAErD,IAAI,QAAS,KAAK,MAAM,IAAI,CAAC,EAAE,IAAI,MAAO,KAAK;EAE/C,IAAI,QAAQ,WAAW,QAAQ,QAAQ,MAAM,UAAU,YAAY,QAAQ;EAC3E,QAAQ,MAAM,OAAO,WAAW,OAAO;EACvC,MAAM,SAAS,QAAQ,aAAa;EACpC,MAAM,QAAQ,KAAK,SAAQ,IAAK,KAAK,SAAQ;EAC7C,MAAM,MAAM,KAAK,SAAQ,IAAK,QAAQ;EACtC,KAAK,OAAO,KAAK,KAAK,GAAG,CAAC;CAC5B;CAEQ,OAAO,MAAY;EACzB,IAAI,SAAS,KAAK,MAAK,GAAI;EAC3B,KAAK,MAAM,IAAI,IAAI;CACrB;CAIQ,QAAQ,UAAkB,QAAc;EAC9C,MAAM,IAAI,KAAK,OAAM;EACrB,MAAM,IAAK,WAAW,KAAK,KAAM;EACjC,MAAM,IAAK,SAAS,KAAK,KAAM;EAC/B,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;EACnC,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;EACnC,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;EACnC,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;EAEnC,OAAO,KAAK,GAAE,GAAI,GAAE,KAAM,EAAC,GAAI,EAAC,KADf,SAAS,WAAW,MAAM,IAAI,EACD,KAAM,GAAE,GAAI;CAC5D;CAxRW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAM,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAN,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAM,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,KAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,KAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,aAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,OAAA;GAAA,OAAA;EAAA;EAAA,MAAA,EAAA,YAAA,EAAA,SAAA,YAAA,EAAA;EAAA,aAAA,CAAA;GAAA,cAAA;GAAA,OAAA;GAAA,WAAA,CAAA,SAAA;GAAA,aAAA;GAAA,UAAA;EAAA,CAAA;EAAA,UAAA;EAAA,UCvDnB;EAoEA,eAAA,GAAA,kBAAA;CAAA,CAAA;;ADba,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAM,YAAA,CAAA;EANlB,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAS,eAEJ,kBAAkB;GAAI,MAC/B,EAAE,WAAW,YAAW;GAAE,UAAA;EAAA,CAAA;CAwIuC,CAAA;CAAA,gBAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,KAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,KAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,MAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,MAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,aAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,QAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,UAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,UAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,OAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,GAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,aAAA;EAAA,CAAA;EAAA,OAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,OAAA;EAAA,CAAA;EAAA,SAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,WAAS,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;CAAA;AAAA,CAAA"}
@@ -5,7 +5,7 @@ import { InjectionToken } from "@angular/core";
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
7
7
  */
8
- const NGWR_VERSION = "15.0.0-rc.3";
8
+ const NGWR_VERSION = "15.0.0";
9
9
  /**
10
10
  * @license
11
11
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-version.mjs","names":[],"sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release/prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '15.0.0-rc.3';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n"],"mappings":";;;;;;;AAiBA,MAAa,eAAe;;;;;;;ACQ5B,MAAa,qBAAqB,IAAI,eAAuB,sBAAsB;CACjF,YAAY;CACZ,eAAe;AAChB,CAAA"}
1
+ {"version":3,"file":"ngwr-version.mjs","names":[],"sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release/prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '15.0.0';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n"],"mappings":";;;;;;;AAiBA,MAAa,eAAe;;;;;;;ACQ5B,MAAa,qBAAqB,IAAI,eAAuB,sBAAsB;CACjF,YAAY;CACZ,eAAe;AAChB,CAAA"}
package/package.json CHANGED
@@ -1103,5 +1103,5 @@
1103
1103
  "sass": "./styles/_typography.scss"
1104
1104
  }
1105
1105
  },
1106
- "version": "15.0.0-rc.3"
1106
+ "version": "15.0.0"
1107
1107
  }
@@ -5,8 +5,9 @@ import { FormValueControl } from "@angular/forms/signals";
5
5
  * `FormValueControl<number>`, so `[formField]` binds straight to its `value`
6
6
  * model — no `ControlValueAccessor` in between. `[(value)]` works standalone.
7
7
  *
8
- * The value is clamped to `[min, max]`. Drag the indicator around the dial or
9
- * use ←→ / ↑↓ to step.
8
+ * The dial shows the value clamped to `[min, max]`, and a drag or a key press
9
+ * commits one in range; a value written from outside stays as it was. Drag the
10
+ * indicator around the dial or use ←→ / ↑↓ to step.
10
11
  *
11
12
  * @example
12
13
  * ```html
@@ -83,6 +84,14 @@ export declare class WrKnob implements FormValueControl<number> {
83
84
  protected readonly cx = 50;
84
85
  protected readonly cy = 50;
85
86
  protected readonly radius: import("@angular/core").Signal<number>;
87
+ /**
88
+ * The value the dial shows and announces: the model, held to `[min, max]`.
89
+ * Only the display is clamped. Writing the clamped number back into the model
90
+ * turned a form's out-of-range value into a different one and marked the
91
+ * field dirty before anyone touched it — the rule `wr-input-number` and
92
+ * `wr-slider` already follow. A key press or a drag commits a value in range.
93
+ */
94
+ protected readonly shown: import("@angular/core").Signal<number>;
86
95
  /** Ratio of the filled arc — `0` at min, `1` at max. */
87
96
  protected readonly ratio: import("@angular/core").Signal<number>;
88
97
  /** SVG path `d` for the background arc. */
@@ -96,7 +105,6 @@ export declare class WrKnob implements FormValueControl<number> {
96
105
  }>;
97
106
  private readonly host;
98
107
  private readonly surface;
99
- constructor();
100
108
  /**
101
109
  * Focus the dial — what Signal Forms calls to focus this control, from
102
110
  * `focusBoundControl()`. The host carries no tab stop, so the call goes to
@@ -15,7 +15,7 @@ import { InjectionToken } from "@angular/core";
15
15
  * Distinct from `Version` exported by `@angular/core`, which reports the
16
16
  * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.
17
17
  */
18
- export declare const NGWR_VERSION = "15.0.0-rc.3";
18
+ export declare const NGWR_VERSION = "15.0.0";
19
19
  /**
20
20
  * Injection token holding the ngwr library version string.
21
21
  *