ngwr 13.0.1 → 14.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.
- package/README.md +13 -6
- package/color-picker/styles/_index.scss +43 -5
- package/fesm2022/ngwr-alert.mjs +5 -5
- package/fesm2022/ngwr-alert.mjs.map +1 -1
- package/fesm2022/ngwr-back-top.mjs +11 -1
- package/fesm2022/ngwr-back-top.mjs.map +1 -1
- package/fesm2022/ngwr-blur-text.mjs +13 -2
- package/fesm2022/ngwr-blur-text.mjs.map +1 -1
- package/fesm2022/ngwr-button.mjs +4 -4
- package/fesm2022/ngwr-button.mjs.map +1 -1
- package/fesm2022/ngwr-calendar-testing.mjs +15 -0
- package/fesm2022/ngwr-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-calendar.mjs +51 -5
- package/fesm2022/ngwr-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-circular-text.mjs +20 -1
- package/fesm2022/ngwr-circular-text.mjs.map +1 -1
- package/fesm2022/ngwr-collapse.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker-testing.mjs +23 -7
- package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker.mjs +89 -10
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-command-palette.mjs +27 -3
- package/fesm2022/ngwr-command-palette.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +7 -1
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-date-adapters-fns.mjs +101 -33
- package/fesm2022/ngwr-date-adapters-fns.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker-testing.mjs +10 -2
- package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker.mjs +103 -13
- package/fesm2022/ngwr-date-picker.mjs.map +1 -1
- package/fesm2022/ngwr-date.mjs +480 -79
- package/fesm2022/ngwr-date.mjs.map +1 -1
- package/fesm2022/ngwr-drag-drop.mjs +30 -2
- package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +2 -1
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar.mjs +1 -1
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-file-upload.mjs +52 -12
- package/fesm2022/ngwr-file-upload.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +63 -1
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +28 -1
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n.mjs +150 -22
- package/fesm2022/ngwr-i18n.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +3 -3
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-input-otp.mjs +21 -3
- package/fesm2022/ngwr-input-otp.mjs.map +1 -1
- package/fesm2022/ngwr-input.mjs +14 -7
- package/fesm2022/ngwr-input.mjs.map +1 -1
- package/fesm2022/ngwr-knob-testing.mjs.map +1 -1
- package/fesm2022/ngwr-knob.mjs +20 -2
- package/fesm2022/ngwr-knob.mjs.map +1 -1
- package/fesm2022/ngwr-loading-bar-router.mjs +73 -0
- package/fesm2022/ngwr-loading-bar-router.mjs.map +1 -0
- package/fesm2022/ngwr-loading-bar.mjs +21 -32
- package/fesm2022/ngwr-loading-bar.mjs.map +1 -1
- package/fesm2022/ngwr-mention.mjs +104 -14
- package/fesm2022/ngwr-mention.mjs.map +1 -1
- package/fesm2022/ngwr-pagination-testing.mjs +11 -10
- package/fesm2022/ngwr-pagination-testing.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +65 -30
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-popconfirm.mjs +3 -2
- package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +2 -1
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +93 -34
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-splitter-testing.mjs.map +1 -1
- package/fesm2022/ngwr-squircle.mjs +2 -1
- package/fesm2022/ngwr-squircle.mjs.map +1 -1
- package/fesm2022/ngwr-statistic.mjs +35 -4
- package/fesm2022/ngwr-statistic.mjs.map +1 -1
- package/fesm2022/ngwr-table.mjs +35 -12
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-tabs-router.mjs +158 -0
- package/fesm2022/ngwr-tabs-router.mjs.map +1 -0
- package/fesm2022/ngwr-tabs-testing.mjs +13 -11
- package/fesm2022/ngwr-tabs-testing.mjs.map +1 -1
- package/fesm2022/ngwr-tabs.mjs +98 -10
- package/fesm2022/ngwr-tabs.mjs.map +1 -1
- package/fesm2022/ngwr-textarea.mjs +20 -1
- package/fesm2022/ngwr-textarea.mjs.map +1 -1
- package/fesm2022/ngwr-tour.mjs +1 -1
- package/fesm2022/ngwr-tour.mjs.map +1 -1
- package/fesm2022/ngwr-transfer.mjs +2 -2
- package/fesm2022/ngwr-transfer.mjs.map +1 -1
- package/fesm2022/ngwr-tree.mjs +18 -2
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-utils.mjs +37 -1
- package/fesm2022/ngwr-utils.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-window-testing.mjs +10 -2
- package/fesm2022/ngwr-window-testing.mjs.map +1 -1
- package/fesm2022/ngwr-window.mjs +4 -4
- package/fesm2022/ngwr-window.mjs.map +1 -1
- package/i18n/en.json +33 -6
- package/i18n/ru.json +33 -6
- package/llms-full.txt +20 -11
- package/llms.txt +47 -16
- package/mcp/catalog.js +3 -3
- package/mcp/providers.js +11 -0
- package/package.json +9 -1
- package/schematics/migrations/v13/index.js +101 -0
- package/schematics/migrations/v14/index.js +422 -0
- package/schematics/migrations.json +10 -0
- package/schematics/page/templates.js +1 -1
- package/schematics/use/symbol-map.json +1 -0
- package/skills/ngwr/SKILL.md +4 -1
- package/skills/ngwr/references/catalog.md +2 -0
- package/skills/ngwr/references/setup.md +4 -0
- package/stepper/styles/_index.scss +19 -1
- package/textarea/styles/_index.scss +14 -6
- package/toast/styles/_index.scss +12 -4
- package/types/ngwr-alert.d.ts +3 -3
- package/types/ngwr-blur-text.d.ts +8 -0
- package/types/ngwr-button.d.ts +2 -2
- package/types/ngwr-calendar-testing.d.ts +21 -4
- package/types/ngwr-calendar.d.ts +37 -0
- package/types/ngwr-circular-text.d.ts +9 -0
- package/types/ngwr-collapse.d.ts +19 -15
- package/types/ngwr-color-picker-testing.d.ts +10 -4
- package/types/ngwr-color-picker.d.ts +70 -2
- package/types/ngwr-command-palette.d.ts +14 -0
- package/types/ngwr-context-menu.d.ts +1 -1
- package/types/ngwr-date-adapters-fns.d.ts +71 -29
- package/types/ngwr-date-picker-testing.d.ts +206 -185
- package/types/ngwr-date-picker.d.ts +7 -0
- package/types/ngwr-date.d.ts +143 -13
- package/types/ngwr-drag-drop.d.ts +19 -0
- package/types/ngwr-dropdown.d.ts +1 -1
- package/types/ngwr-event-calendar-testing.d.ts +12 -9
- package/types/ngwr-file-upload.d.ts +31 -3
- package/types/ngwr-i18n.d.ts +89 -7
- package/types/ngwr-input-number.d.ts +1 -1
- package/types/ngwr-input-otp.d.ts +3 -0
- package/types/ngwr-input.d.ts +11 -4
- package/types/ngwr-knob-testing.d.ts +8 -4
- package/types/ngwr-knob.d.ts +18 -0
- package/types/ngwr-loading-bar-router.d.ts +35 -0
- package/types/ngwr-loading-bar.d.ts +19 -9
- package/types/ngwr-mention.d.ts +13 -0
- package/types/ngwr-pagination-testing.d.ts +11 -10
- package/types/ngwr-pagination.d.ts +39 -10
- package/types/ngwr-popconfirm.d.ts +1 -1
- package/types/ngwr-popover.d.ts +1 -1
- package/types/ngwr-select.d.ts +65 -10
- package/types/ngwr-splitter-testing.d.ts +9 -4
- package/types/ngwr-squircle.d.ts +1 -1
- package/types/ngwr-statistic.d.ts +22 -2
- package/types/ngwr-table.d.ts +24 -5
- package/types/ngwr-tabs-router.d.ts +67 -0
- package/types/ngwr-tabs-testing.d.ts +12 -10
- package/types/ngwr-tabs.d.ts +103 -6
- package/types/ngwr-textarea.d.ts +9 -0
- package/types/ngwr-tree.d.ts +7 -0
- package/types/ngwr-utils.d.ts +33 -1
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-window-testing.d.ts +11 -3
- package/types/ngwr-window.d.ts +11 -4
- package/window/styles/_index.scss +1 -1
package/fesm2022/ngwr-knob.mjs
CHANGED
|
@@ -182,6 +182,24 @@ class WrKnob {
|
|
|
182
182
|
onBlur() {
|
|
183
183
|
this.touch.emit();
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The APG slider keys, and they do NOT mirror under `dir="rtl"` — which is a
|
|
187
|
+
* decision, not an oversight, and the one place it is written down.
|
|
188
|
+
*
|
|
189
|
+
* `wr-slider` swaps ArrowLeft and ArrowRight in RTL because its TRACK mirrors:
|
|
190
|
+
* the thumb is placed with `inset-inline-start`, so the visual maximum moves to
|
|
191
|
+
* the left and a key naming a side has to move with it. This dial does not
|
|
192
|
+
* mirror. It is an SVG gauge drawn in a fixed 100×100 viewBox, and SVG content
|
|
193
|
+
* is not reordered by the reading direction — the arc sweeps clockwise from 7
|
|
194
|
+
* o'clock to 5 o'clock in every locale, the same way a clock face or a car's
|
|
195
|
+
* rev counter does. So ArrowRight still moves the handle toward the visually
|
|
196
|
+
* right-hand side of the arc in RTL, and swapping the keys would be the thing
|
|
197
|
+
* that broke the mapping.
|
|
198
|
+
*
|
|
199
|
+
* That is why nothing here injects `Directionality`, and why the stylesheet
|
|
200
|
+
* carries no `rtl-ok:` marker: there is no physical CSS in the value path at
|
|
201
|
+
* all, only fixed SVG geometry.
|
|
202
|
+
*/
|
|
185
203
|
onKeydown(event) {
|
|
186
204
|
if (!this.interactive())
|
|
187
205
|
return;
|
|
@@ -257,11 +275,11 @@ class WrKnob {
|
|
|
257
275
|
return `M ${sx} ${sy} A ${r} ${r} 0 ${largeArc} 1 ${ex} ${ey}`;
|
|
258
276
|
}
|
|
259
277
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrKnob, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
260
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: WrKnob, isStandalone: true, selector: "wr-knob", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, trackColor: { classPropertyName: "trackColor", publicName: "trackColor", isSignal: true, isRequired: false, transformFunction: null }, valueColor: { classPropertyName: "valueColor", publicName: "valueColor", isSignal: true, isRequired: false, transformFunction: null }, showValue: { classPropertyName: "showValue", publicName: "showValue", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: "<div\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\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 <div class=\"wr-knob__text\">\n {{ value() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n", encapsulation: i0.ViewEncapsulation.None });
|
|
278
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: WrKnob, isStandalone: true, selector: "wr-knob", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, trackColor: { classPropertyName: "trackColor", publicName: "trackColor", isSignal: true, isRequired: false, transformFunction: null }, valueColor: { classPropertyName: "valueColor", publicName: "valueColor", isSignal: true, isRequired: false, transformFunction: null }, showValue: { classPropertyName: "showValue", publicName: "showValue", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: "<div\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\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` \u2014 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", encapsulation: i0.ViewEncapsulation.None });
|
|
261
279
|
}
|
|
262
280
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrKnob, decorators: [{
|
|
263
281
|
type: Component,
|
|
264
|
-
args: [{ selector: 'wr-knob', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, template: "<div\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\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 <div class=\"wr-knob__text\">\n {{ value() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n" }]
|
|
282
|
+
args: [{ selector: 'wr-knob', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, template: "<div\n class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\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` \u2014 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" }]
|
|
265
283
|
}], ctorParameters: () => [], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], trackColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackColor", required: false }] }], valueColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColor", required: false }] }], showValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValue", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
|
|
266
284
|
|
|
267
285
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-knob.mjs","sources":["../../../projects/lib/knob/knob.ts","../../../projects/lib/knob/knob.html","../../../projects/lib/knob/ngwr-knob.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\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} 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 rgba(--wr-color-light, 0.6) */\n readonly trackColor = input<string>('rgba(var(--wr-color-light-rgb), 0.6)');\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 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\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 // 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 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 class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\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 <div class=\"wr-knob__text\">\n {{ value() }}<span class=\"wr-knob__suffix\">{{ suffix() }}</span>\n </div>\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ngwr-knob.mjs","sources":["../../../projects/lib/knob/knob.ts","../../../projects/lib/knob/knob.html","../../../projects/lib/knob/ngwr-knob.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\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} 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 rgba(--wr-color-light, 0.6) */\n readonly trackColor = input<string>('rgba(var(--wr-color-light-rgb), 0.6)');\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 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\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 // 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`, and why the stylesheet\n * carries no `rtl-ok:` marker: there is no physical CSS in the value path at\n * 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 class=\"wr-knob__surface\"\n role=\"slider\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\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","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;;AAAA;;;;;AAKG;AAoBH,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC;AACvB,MAAM,OAAO,GAAG,GAAG,CAAC;AACpB,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS;AAErC;;;;;;;;;;;;;;;;;;AAkBG;MAOU,MAAM,CAAA;;IAER,SAAS,GAAG,KAAK,CAAgB,IAAI;kFAAC;IAE5B,iBAAiB,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC;;AAGhF,IAAA,GAAG,GAAG,KAAK,CAAqB,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;;AAGrG,IAAA,GAAG,GAAG,KAAK,CAAqB,GAAG,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,oBAAoB,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG;IAEjG,QAAQ,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC;iFAAC;IAC1C,QAAQ,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG;iFAAC;;IAGpD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,MAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAA,CAAG;;IAGpG,IAAI,GAAG,KAAK,CAAC,GAAG,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,MAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,oBAAoB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAA,CAAG;;IAGpG,WAAW,GAAG,KAAK,CAAC,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,aAAA,EAAA,8BAAA,EAAA,CAAA,EAC5B,SAAS,EAAE,CAAC,CAAU,KAAa,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAA,CAC1E;;IAGO,UAAU,GAAG,KAAK,CAAS,sCAAsC;mFAAC;;IAGlE,UAAU,GAAG,KAAK,CAAS,yBAAyB;mFAAC;;IAGrD,SAAS,GAAG,KAAK,CAAC,IAAI,iFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;;IAG7D,MAAM,GAAG,KAAK,CAAS,EAAE;+EAAC;;IAG1B,QAAQ,GAAG,KAAK,CAAC,KAAK,gFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;;IAG7D,QAAQ,GAAG,KAAK,CAAC,KAAK,gFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;;IAG7D,KAAK,GAAG,KAAK,CAAS,CAAC;8EAAC;;IAGxB,KAAK,GAAG,MAAM,EAAQ;IAEvB,QAAQ,GAAG,KAAK;AAEL,IAAA,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE;oFAAC;AAErF;;;;;;;;;AASG;IACgB,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE;kFAAC;;IAG5C,SAAS,GAAG,gBAAgB,EAAE;AAE9B,IAAA,OAAO,GAAG,QAAQ,CAAC,MAAK;AACzC,QAAA,MAAM,KAAK,GAAG,CAAC,SAAS,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,EAAE;AAAE,YAAA,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC;QACpD,IAAI,IAAI,CAAC,QAAQ,EAAE;AAAE,YAAA,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC;AACpD,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB,CAAC;gFAAC;;IAGiB,EAAE,GAAG,EAAE;IACP,EAAE,GAAG,EAAE;;;IAGP,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;+EAAC;;AAGvE,IAAA,KAAK,GAAG,QAAQ,CAAC,MAAK;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;QAC/C,IAAI,KAAK,IAAI,CAAC;AAAE,YAAA,OAAO,CAAC;QACxB,OAAO,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9D,CAAC;8EAAC;;AAGiB,IAAA,SAAS,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC;kFAAC;;AAG5D,IAAA,SAAS,GAAG,QAAQ,CAAC,MAAK;QAC3C,MAAM,GAAG,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE;QAChD,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;IACrC,CAAC;kFAAC;;AAGiB,IAAA,SAAS,GAAG,QAAQ,CAAC,MAAK;QAC3C,MAAM,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE;QAClD,MAAM,GAAG,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,IAAI,GAAG;QACnC,OAAO;AACL,YAAA,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAC1C,YAAA,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;SAC3C;IACH,CAAC;kFAAC;AAEe,IAAA,IAAI,GAAG,MAAM,CAA0B,UAAU,CAAC;AAEnE,IAAA,WAAA,GAAA;;;;QAIE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;AACrE,YAAA,IAAI,OAAO,KAAK,IAAI,CAAC,KAAK,EAAE;AAAE,gBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;AACvD,QAAA,CAAC,CAAC;IACJ;;AAIU,IAAA,aAAa,CAAC,KAAmB,EAAA;AACzC,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;YAAE;;;QAGzB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE;QAC5C,KAAK,CAAC,cAAc,EAAE;AACtB,QAAA,IAAI,CAAC,QAAQ,GAAG,IAAI;AACpB,QAAA,MAAM,OAAO,GAAG,KAAK,CAAC,aAA4B;AAClD,QAAA,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC;;;;QAI1C,OAAO,CAAC,KAAK,EAAE;AACf,QAAA,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;IAC/B;AAEU,IAAA,aAAa,CAAC,KAAmB,EAAA;QACzC,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE;AACpB,QAAA,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;IAC/B;AAEU,IAAA,WAAW,CAAC,KAAmB,EAAA;QACvC,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE;AACpB,QAAA,IAAI,CAAC,QAAQ,GAAG,KAAK;QACpB,KAAK,CAAC,aAA6B,CAAC,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC;AAC3E,QAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;IACnB;AAEA;;;;;AAKG;IACO,MAAM,GAAA;AACd,QAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;IACnB;AAEA;;;;;;;;;;;;;;;;;AAiBG;AACO,IAAA,SAAS,CAAC,KAAoB,EAAA;AACtC,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;YAAE;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE;AAC5D,QAAA,IAAI,IAAmB;AACvB,QAAA,QAAQ,KAAK,CAAC,GAAG;AACf,YAAA,KAAK,YAAY;AACjB,YAAA,KAAK,SAAS;AACZ,gBAAA,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI;gBAC1B;AACF,YAAA,KAAK,WAAW;AAChB,YAAA,KAAK,WAAW;AACd,gBAAA,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI;gBAC1B;AACF,YAAA,KAAK,MAAM;AACT,gBAAA,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACtB;AACF,YAAA,KAAK,KAAK;AACR,gBAAA,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACtB;AACF,YAAA;gBACE;;QAEJ,KAAK,CAAC,cAAc,EAAE;QACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9B;AAEA;;;;;;;;;AASG;AACK,IAAA,IAAI,CAAC,KAAa,EAAA;AACxB,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG;AAC3E,QAAA,OAAO,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IACvD;;AAIQ,IAAA,iBAAiB,CAAC,KAAmB,EAAA;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,qBAAqB,EAAE;AAC5D,QAAA,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;AACvD,QAAA,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;;AAEvD,QAAA,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,EAAE;;QAEjD,IAAI,KAAK,GAAG,SAAS;AAAE,YAAA,KAAK,GAAG,KAAK,GAAG,GAAG,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,GAAG,GAAG;QAC9E,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC;QACxC,MAAM,KAAK,GAAG,CAAC,KAAK,GAAG,SAAS,IAAI,SAAS;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;QAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,GAAG,KAAK;QAC3C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7B;AAEQ,IAAA,MAAM,CAAC,IAAY,EAAA;AACzB,QAAA,IAAI,IAAI,KAAK,IAAI,CAAC,KAAK,EAAE;YAAE;AAC3B,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;IACtB;;IAIQ,OAAO,CAAC,QAAgB,EAAE,MAAc,EAAA;AAC9C,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE;QACvB,MAAM,CAAC,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,EAAE,IAAI,GAAG;QACpC,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,IAAI,GAAG;AAClC,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpC,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpC,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpC,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpC,QAAA,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;AAChD,QAAA,OAAO,CAAA,EAAA,EAAK,EAAE,CAAA,CAAA,EAAI,EAAE,MAAM,CAAC,CAAA,CAAA,EAAI,CAAC,CAAA,GAAA,EAAM,QAAQ,CAAA,GAAA,EAAM,EAAE,CAAA,CAAA,EAAI,EAAE,EAAE;IAChE;uGA/PW,MAAM,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAN,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,MAAM,qxDCtDnB,23EAkEA,EAAA,aAAA,EAAA,EAAA,CAAA,iBAAA,CAAA,IAAA,EAAA,CAAA;;2FDZa,MAAM,EAAA,UAAA,EAAA,CAAA;kBANlB,SAAS;+BACE,SAAS,EAAA,aAAA,EAEJ,iBAAiB,CAAC,IAAI,QAC/B,EAAE,SAAS,EAAE,WAAW,EAAE,EAAA,QAAA,EAAA,23EAAA,EAAA;;;AEpDlC;;AAEG;;;;"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
2
|
+
import { makeEnvironmentProviders, provideEnvironmentInitializer, inject, PLATFORM_ID, DestroyRef } from '@angular/core';
|
|
3
|
+
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
|
|
4
|
+
import { WrLoadingBar } from 'ngwr/loading-bar';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @license
|
|
8
|
+
*
|
|
9
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
10
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Drive {@link WrLoadingBar} from router navigations: `NavigationStart` takes a
|
|
14
|
+
* slot, and each of `NavigationEnd` / `NavigationCancel` / `NavigationError`
|
|
15
|
+
* releases one.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { provideWrLoadingBarRouter } from 'ngwr/loading-bar/router';
|
|
20
|
+
*
|
|
21
|
+
* bootstrapApplication(App, {
|
|
22
|
+
* providers: [provideRouter(routes), provideWrLoadingBarRouter()],
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* **Why this is a call and not the default.** `WrLoadingBar` used to inject
|
|
27
|
+
* `Router` itself, so navigations drove the bar with no configuration — and
|
|
28
|
+
* every app that rendered `<wr-loading-bar>` paid 66.3 kB of `@angular/router`
|
|
29
|
+
* for it, two thirds of the entry point's total cost, whether or not it had
|
|
30
|
+
* routes. `{ optional: true }` does not help: an optional inject still names the
|
|
31
|
+
* class as the token, and it measured byte-for-byte identical to a required one.
|
|
32
|
+
* An automatic integration and a router-free bundle cannot both exist in a
|
|
33
|
+
* statically linked app, so the automatic half moved here.
|
|
34
|
+
*/
|
|
35
|
+
function provideWrLoadingBarRouter() {
|
|
36
|
+
return makeEnvironmentProviders([
|
|
37
|
+
provideEnvironmentInitializer(() => {
|
|
38
|
+
// Browser only. During prerender the initial navigation runs the whole
|
|
39
|
+
// cycle in the Node worker — including the bar's 150 ms interval — and
|
|
40
|
+
// leaves `progress` at 1, because the reset that clears it is deferred and
|
|
41
|
+
// the HTML is serialized first: every prerendered page then ships a
|
|
42
|
+
// full-width bar across the top until hydration.
|
|
43
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID)))
|
|
44
|
+
return;
|
|
45
|
+
const bar = inject(WrLoadingBar);
|
|
46
|
+
const destroyRef = inject(DestroyRef);
|
|
47
|
+
const sub = inject(Router).events.subscribe(event => {
|
|
48
|
+
if (event instanceof NavigationStart)
|
|
49
|
+
bar.start();
|
|
50
|
+
else if (event instanceof NavigationEnd ||
|
|
51
|
+
event instanceof NavigationCancel ||
|
|
52
|
+
event instanceof NavigationError) {
|
|
53
|
+
bar.complete();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
destroyRef.onDestroy(() => sub.unsubscribe());
|
|
57
|
+
}),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @license
|
|
63
|
+
*
|
|
64
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
65
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generated bundle index. Do not edit.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
export { provideWrLoadingBarRouter };
|
|
73
|
+
//# sourceMappingURL=ngwr-loading-bar-router.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-loading-bar-router.mjs","sources":["../../../projects/lib/loading-bar/router/provide-wr-loading-bar-router.ts","../../../projects/lib/loading-bar/router/public-api.ts","../../../projects/lib/loading-bar/router/ngwr-loading-bar-router.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\nimport { isPlatformBrowser } from '@angular/common';\nimport {\n DestroyRef,\n type EnvironmentProviders,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n provideEnvironmentInitializer,\n} from '@angular/core';\nimport { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';\n\nimport { WrLoadingBar } from 'ngwr/loading-bar';\n\n/**\n * Drive {@link WrLoadingBar} from router navigations: `NavigationStart` takes a\n * slot, and each of `NavigationEnd` / `NavigationCancel` / `NavigationError`\n * releases one.\n *\n * @example\n * ```ts\n * import { provideWrLoadingBarRouter } from 'ngwr/loading-bar/router';\n *\n * bootstrapApplication(App, {\n * providers: [provideRouter(routes), provideWrLoadingBarRouter()],\n * });\n * ```\n *\n * **Why this is a call and not the default.** `WrLoadingBar` used to inject\n * `Router` itself, so navigations drove the bar with no configuration — and\n * every app that rendered `<wr-loading-bar>` paid 66.3 kB of `@angular/router`\n * for it, two thirds of the entry point's total cost, whether or not it had\n * routes. `{ optional: true }` does not help: an optional inject still names the\n * class as the token, and it measured byte-for-byte identical to a required one.\n * An automatic integration and a router-free bundle cannot both exist in a\n * statically linked app, so the automatic half moved here.\n */\nexport function provideWrLoadingBarRouter(): EnvironmentProviders {\n return makeEnvironmentProviders([\n provideEnvironmentInitializer(() => {\n // Browser only. During prerender the initial navigation runs the whole\n // cycle in the Node worker — including the bar's 150 ms interval — and\n // leaves `progress` at 1, because the reset that clears it is deferred and\n // the HTML is serialized first: every prerendered page then ships a\n // full-width bar across the top until hydration.\n if (!isPlatformBrowser(inject(PLATFORM_ID))) return;\n\n const bar = inject(WrLoadingBar);\n const destroyRef = inject(DestroyRef);\n const sub = inject(Router).events.subscribe(event => {\n if (event instanceof NavigationStart) bar.start();\n else if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n bar.complete();\n }\n });\n destroyRef.onDestroy(() => sub.unsubscribe());\n }),\n ]);\n}\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\nexport { provideWrLoadingBarRouter } from './provide-wr-loading-bar-router';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;AAAA;;;;;AAKG;AAeH;;;;;;;;;;;;;;;;;;;;;;AAsBG;SACa,yBAAyB,GAAA;AACvC,IAAA,OAAO,wBAAwB,CAAC;QAC9B,6BAA6B,CAAC,MAAK;;;;;;AAMjC,YAAA,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;gBAAE;AAE7C,YAAA,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC;AAChC,YAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,YAAA,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,IAAG;gBAClD,IAAI,KAAK,YAAY,eAAe;oBAAE,GAAG,CAAC,KAAK,EAAE;qBAC5C,IACH,KAAK,YAAY,aAAa;AAC9B,oBAAA,KAAK,YAAY,gBAAgB;oBACjC,KAAK,YAAY,eAAe,EAChC;oBACA,GAAG,CAAC,QAAQ,EAAE;gBAChB;AACF,YAAA,CAAC,CAAC;YACF,UAAU,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;AAC/C,QAAA,CAAC,CAAC;AACH,KAAA,CAAC;AACJ;;ACpEA;;;;;AAKG;;ACLH;;AAEG;;;;"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isPlatformBrowser } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject,
|
|
4
|
-
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
|
|
3
|
+
import { inject, PLATFORM_ID, signal, computed, Service, input, ViewEncapsulation, Component } from '@angular/core';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* @license
|
|
@@ -10,12 +9,19 @@ import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationErr
|
|
|
10
9
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
11
10
|
*/
|
|
12
11
|
/**
|
|
13
|
-
* Singleton state machine for a top-of-page progress indicator
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Singleton state machine for a top-of-page progress indicator: a counter of
|
|
13
|
+
* in-flight tasks and a tween between 0 and 1, driven through `start()` /
|
|
14
|
+
* `complete()`.
|
|
16
15
|
*
|
|
17
16
|
* Pair with `<wr-loading-bar>` to render the bar at the top of your shell.
|
|
18
17
|
*
|
|
18
|
+
* It knows nothing about the router on purpose. Naming `Router` here — even
|
|
19
|
+
* `{ optional: true }`, which is byte-for-byte identical to a required inject —
|
|
20
|
+
* put 66.3 kB of `@angular/router` into every app that rendered the bar,
|
|
21
|
+
* two thirds of what `ngwr/loading-bar` cost and 27× the component's own code.
|
|
22
|
+
* Call `provideWrLoadingBarRouter()` from `ngwr/loading-bar/router` to have
|
|
23
|
+
* navigations drive it.
|
|
24
|
+
*
|
|
19
25
|
* @example
|
|
20
26
|
* ```ts
|
|
21
27
|
* // From an HttpInterceptor:
|
|
@@ -27,8 +33,6 @@ import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationErr
|
|
|
27
33
|
* @see https://ngwr.dev/reference/services/loading-bar
|
|
28
34
|
*/
|
|
29
35
|
class WrLoadingBar {
|
|
30
|
-
router = inject(Router, { optional: true });
|
|
31
|
-
destroyRef = inject(DestroyRef);
|
|
32
36
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
33
37
|
/** Live count of pending "tasks" (manual + router). */
|
|
34
38
|
count = signal(0, /* @ts-ignore */
|
|
@@ -45,27 +49,6 @@ class WrLoadingBar {
|
|
|
45
49
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
46
50
|
timer = null;
|
|
47
51
|
holdTimer = null;
|
|
48
|
-
constructor() {
|
|
49
|
-
// Router-driven only in the browser. During prerender the initial
|
|
50
|
-
// navigation ran the whole cycle in the Node worker — including a 150 ms
|
|
51
|
-
// `setInterval` — and left `progress` at 1, because the reset that clears
|
|
52
|
-
// it is deferred and the HTML is serialized first. Every prerendered page
|
|
53
|
-
// shipped a full-width bar across the top until hydration.
|
|
54
|
-
if (!this.router || !this.isBrowser)
|
|
55
|
-
return;
|
|
56
|
-
// Mirror router events: any in-flight navigation starts the bar,
|
|
57
|
-
// each terminal event releases one slot.
|
|
58
|
-
const sub = this.router.events.subscribe(event => {
|
|
59
|
-
if (event instanceof NavigationStart)
|
|
60
|
-
this.start();
|
|
61
|
-
else if (event instanceof NavigationEnd ||
|
|
62
|
-
event instanceof NavigationCancel ||
|
|
63
|
-
event instanceof NavigationError) {
|
|
64
|
-
this.complete();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
68
|
-
}
|
|
69
52
|
/** Reserve a slot. The bar starts trickling as long as `count > 0`. */
|
|
70
53
|
start() {
|
|
71
54
|
if (this.count() === 0)
|
|
@@ -153,7 +136,7 @@ class WrLoadingBar {
|
|
|
153
136
|
}
|
|
154
137
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrLoadingBar, decorators: [{
|
|
155
138
|
type: Service
|
|
156
|
-
}]
|
|
139
|
+
}] });
|
|
157
140
|
|
|
158
141
|
/**
|
|
159
142
|
* @license
|
|
@@ -164,9 +147,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
|
|
|
164
147
|
/**
|
|
165
148
|
* Thin progress bar fixed to the top of the viewport.
|
|
166
149
|
*
|
|
167
|
-
* Drop one at the root of your shell. Reads the singleton {@link WrLoadingBar}
|
|
168
|
-
*
|
|
169
|
-
*
|
|
150
|
+
* Drop one at the root of your shell. Reads the singleton {@link WrLoadingBar},
|
|
151
|
+
* which is driven manually by `start()` / `complete()` — an HTTP interceptor, a
|
|
152
|
+
* long save, anything with a beginning and an end.
|
|
153
|
+
*
|
|
154
|
+
* Router navigations drive it too, but only once you add
|
|
155
|
+
* `provideWrLoadingBarRouter()` from `ngwr/loading-bar/router`. That is an opt-in
|
|
156
|
+
* because the subscription is what pulls `@angular/router` into the bundle: this
|
|
157
|
+
* component is 2.4 kB of its own code and used to carry 66 kB of router into
|
|
158
|
+
* apps that never routed.
|
|
170
159
|
*
|
|
171
160
|
* @example
|
|
172
161
|
* ```html
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-loading-bar.mjs","sources":["../../../projects/lib/loading-bar/services/loading-bar.ts","../../../projects/lib/loading-bar/loading-bar.component.ts","../../../projects/lib/loading-bar/loading-bar.component.html","../../../projects/lib/loading-bar/ngwr-loading-bar.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\nimport { isPlatformBrowser } from '@angular/common';\nimport { DestroyRef, Service, PLATFORM_ID, type Signal, computed, inject, signal } from '@angular/core';\nimport { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';\n\nimport type { WrLoadingState } from '../interfaces';\n\n/**\n * Singleton state machine for a top-of-page progress indicator. Driven\n * by router events out of the box, plus a manual `start()` / `complete()`\n * API for HTTP-interceptor-style usage.\n *\n * Pair with `<wr-loading-bar>` to render the bar at the top of your shell.\n *\n * @example\n * ```ts\n * // From an HttpInterceptor:\n * const bar = inject(WrLoadingBar);\n * bar.start();\n * return next(req).pipe(finalize(() => bar.complete()));\n * ```\n *\n * @see https://ngwr.dev/reference/services/loading-bar\n */\n@Service()\nexport class WrLoadingBar {\n private readonly router = inject(Router, { optional: true });\n private readonly destroyRef = inject(DestroyRef);\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n /** Live count of pending \"tasks\" (manual + router). */\n private readonly count = signal(0);\n\n /** Tween value `[0, 1]` — the bar's actual width. */\n private readonly _progress = signal(0);\n readonly progress: Signal<number> = this._progress.asReadonly();\n\n readonly state: Signal<WrLoadingState> = computed(() => {\n if (this.count() > 0) return 'running';\n return this._progress() > 0 ? 'completing' : 'idle';\n });\n\n private timer: ReturnType<typeof setInterval> | null = null;\n private holdTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor() {\n // Router-driven only in the browser. During prerender the initial\n // navigation ran the whole cycle in the Node worker — including a 150 ms\n // `setInterval` — and left `progress` at 1, because the reset that clears\n // it is deferred and the HTML is serialized first. Every prerendered page\n // shipped a full-width bar across the top until hydration.\n if (!this.router || !this.isBrowser) return;\n\n // Mirror router events: any in-flight navigation starts the bar,\n // each terminal event releases one slot.\n const sub = this.router.events.subscribe(event => {\n if (event instanceof NavigationStart) this.start();\n else if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n this.complete();\n }\n });\n this.destroyRef.onDestroy(() => sub.unsubscribe());\n }\n\n /** Reserve a slot. The bar starts trickling as long as `count > 0`. */\n start(): void {\n if (this.count() === 0) this.beginTrickle();\n this.count.update(v => v + 1);\n }\n\n /** Release a slot. When the last one closes, the bar fast-forwards to 100% then resets. */\n complete(): void {\n // Only a call that actually RELEASED something may finish. `Math.max` turns\n // a stray `complete()` into a no-op on the count, but the old code still\n // read the resulting zero as \"the last task just ended\" and flashed the bar\n // to 100% — reachable from a `finalize` that runs twice, or any\n // `complete()` after a `reset()`.\n if (this.count() === 0) return;\n this.count.update(v => v - 1);\n if (this.count() === 0) this.finish();\n }\n\n /** Cancel without animating the bar to 100%. Resets immediately. */\n reset(): void {\n this.count.set(0);\n this.stopTrickle();\n this.clearHold();\n this._progress.set(0);\n }\n\n // Internals\n\n private beginTrickle(): void {\n this.stopTrickle();\n this.clearHold();\n\n // Nothing paints during prerender, so there is nothing to animate. The\n // count still moves — a task really is in flight — but no 150 ms interval\n // is left spinning inside the render worker, and the serialized bar stays\n // at zero width whether or not the task finishes before serialization.\n if (!this.isBrowser) return;\n\n // Unconditionally, not `if (progress === 0)`. A task that starts inside the\n // 220 ms hold found progress at 1, kept it, and then trickled the WRONG WAY:\n // `p + (0.9 - p) * 0.08` approaches 0.9 from above, so the bar sat at ~99%\n // and crept backwards for the whole task. A redirecting route guard hits\n // that window on every navigation.\n this._progress.set(0.08);\n this.timer = setInterval(() => {\n const p = this._progress();\n // Approach 90% asymptotically — we want the user-visible final jump\n // to clearly indicate completion.\n const next = p + (0.9 - p) * 0.08;\n this._progress.set(next);\n }, 150);\n }\n\n private stopTrickle(): void {\n if (this.timer === null) return;\n clearInterval(this.timer);\n this.timer = null;\n }\n\n private clearHold(): void {\n if (this.holdTimer === null) return;\n clearTimeout(this.holdTimer);\n this.holdTimer = null;\n }\n\n private finish(): void {\n this.stopTrickle();\n this.clearHold();\n\n // Nothing animates during prerender and nothing will run the reset before\n // the HTML is serialized, so there is no 100% worth holding — go straight\n // to rest and leave the markup clean.\n if (!this.isBrowser) {\n this._progress.set(0);\n return;\n }\n\n this._progress.set(1);\n // Give the bar a moment to render the final 100% width before resetting.\n // The handle is kept so a later cycle's hold is not cut short by this\n // one's leftover timer.\n this.holdTimer = setTimeout(() => {\n this.holdTimer = null;\n if (this.count() === 0) this._progress.set(0);\n }, 220);\n }\n}\n\nexport type { WrLoadingState } from '../interfaces';\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 { Component, ViewEncapsulation, computed, inject, input } from '@angular/core';\n\nimport { WrLoadingBar } from './services/loading-bar';\n\n/**\n * Thin progress bar fixed to the top of the viewport.\n *\n * Drop one at the root of your shell. Reads the singleton {@link WrLoadingBar}\n * — every router navigation drives it automatically; HTTP interceptors\n * can call `start()` / `complete()` to add their own slots.\n *\n * @example\n * ```html\n * <wr-loading-bar /> <!-- top, primary color -->\n * <wr-loading-bar color=\"#9b51e0\" height=\"3px\" />\n * ```\n */\n@Component({\n selector: 'wr-loading-bar',\n templateUrl: './loading-bar.component.html',\n styleUrl: './loading-bar.scss',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-loading-bar' },\n})\nexport class WrLoadingBarComponent {\n /** Bar colour. Defaults to the primary brand colour. */\n readonly color = input<string>('var(--wr-color-primary)');\n\n /** Bar height. */\n readonly height = input<string>('2px');\n\n protected readonly bar = inject(WrLoadingBar);\n\n protected readonly active = computed(() => this.bar.state() !== 'idle');\n}\n","<div class=\"wr-loading-bar__track\" [class.wr-loading-bar--active]=\"active()\">\n <div\n class=\"wr-loading-bar__fill\"\n [style.width.%]=\"bar.progress() * 100\"\n [style.background]=\"color()\"\n [style.height]=\"height()\"\n ></div>\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ngwr-loading-bar.mjs","sources":["../../../projects/lib/loading-bar/services/loading-bar.ts","../../../projects/lib/loading-bar/loading-bar.component.ts","../../../projects/lib/loading-bar/loading-bar.component.html","../../../projects/lib/loading-bar/ngwr-loading-bar.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\nimport { isPlatformBrowser } from '@angular/common';\nimport { Service, PLATFORM_ID, type Signal, computed, inject, signal } from '@angular/core';\n\nimport type { WrLoadingState } from '../interfaces';\n\n/**\n * Singleton state machine for a top-of-page progress indicator: a counter of\n * in-flight tasks and a tween between 0 and 1, driven through `start()` /\n * `complete()`.\n *\n * Pair with `<wr-loading-bar>` to render the bar at the top of your shell.\n *\n * It knows nothing about the router on purpose. Naming `Router` here — even\n * `{ optional: true }`, which is byte-for-byte identical to a required inject —\n * put 66.3 kB of `@angular/router` into every app that rendered the bar,\n * two thirds of what `ngwr/loading-bar` cost and 27× the component's own code.\n * Call `provideWrLoadingBarRouter()` from `ngwr/loading-bar/router` to have\n * navigations drive it.\n *\n * @example\n * ```ts\n * // From an HttpInterceptor:\n * const bar = inject(WrLoadingBar);\n * bar.start();\n * return next(req).pipe(finalize(() => bar.complete()));\n * ```\n *\n * @see https://ngwr.dev/reference/services/loading-bar\n */\n@Service()\nexport class WrLoadingBar {\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n /** Live count of pending \"tasks\" (manual + router). */\n private readonly count = signal(0);\n\n /** Tween value `[0, 1]` — the bar's actual width. */\n private readonly _progress = signal(0);\n readonly progress: Signal<number> = this._progress.asReadonly();\n\n readonly state: Signal<WrLoadingState> = computed(() => {\n if (this.count() > 0) return 'running';\n return this._progress() > 0 ? 'completing' : 'idle';\n });\n\n private timer: ReturnType<typeof setInterval> | null = null;\n private holdTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** Reserve a slot. The bar starts trickling as long as `count > 0`. */\n start(): void {\n if (this.count() === 0) this.beginTrickle();\n this.count.update(v => v + 1);\n }\n\n /** Release a slot. When the last one closes, the bar fast-forwards to 100% then resets. */\n complete(): void {\n // Only a call that actually RELEASED something may finish. `Math.max` turns\n // a stray `complete()` into a no-op on the count, but the old code still\n // read the resulting zero as \"the last task just ended\" and flashed the bar\n // to 100% — reachable from a `finalize` that runs twice, or any\n // `complete()` after a `reset()`.\n if (this.count() === 0) return;\n this.count.update(v => v - 1);\n if (this.count() === 0) this.finish();\n }\n\n /** Cancel without animating the bar to 100%. Resets immediately. */\n reset(): void {\n this.count.set(0);\n this.stopTrickle();\n this.clearHold();\n this._progress.set(0);\n }\n\n // Internals\n\n private beginTrickle(): void {\n this.stopTrickle();\n this.clearHold();\n\n // Nothing paints during prerender, so there is nothing to animate. The\n // count still moves — a task really is in flight — but no 150 ms interval\n // is left spinning inside the render worker, and the serialized bar stays\n // at zero width whether or not the task finishes before serialization.\n if (!this.isBrowser) return;\n\n // Unconditionally, not `if (progress === 0)`. A task that starts inside the\n // 220 ms hold found progress at 1, kept it, and then trickled the WRONG WAY:\n // `p + (0.9 - p) * 0.08` approaches 0.9 from above, so the bar sat at ~99%\n // and crept backwards for the whole task. A redirecting route guard hits\n // that window on every navigation.\n this._progress.set(0.08);\n this.timer = setInterval(() => {\n const p = this._progress();\n // Approach 90% asymptotically — we want the user-visible final jump\n // to clearly indicate completion.\n const next = p + (0.9 - p) * 0.08;\n this._progress.set(next);\n }, 150);\n }\n\n private stopTrickle(): void {\n if (this.timer === null) return;\n clearInterval(this.timer);\n this.timer = null;\n }\n\n private clearHold(): void {\n if (this.holdTimer === null) return;\n clearTimeout(this.holdTimer);\n this.holdTimer = null;\n }\n\n private finish(): void {\n this.stopTrickle();\n this.clearHold();\n\n // Nothing animates during prerender and nothing will run the reset before\n // the HTML is serialized, so there is no 100% worth holding — go straight\n // to rest and leave the markup clean.\n if (!this.isBrowser) {\n this._progress.set(0);\n return;\n }\n\n this._progress.set(1);\n // Give the bar a moment to render the final 100% width before resetting.\n // The handle is kept so a later cycle's hold is not cut short by this\n // one's leftover timer.\n this.holdTimer = setTimeout(() => {\n this.holdTimer = null;\n if (this.count() === 0) this._progress.set(0);\n }, 220);\n }\n}\n\nexport type { WrLoadingState } from '../interfaces';\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 { Component, ViewEncapsulation, computed, inject, input } from '@angular/core';\n\nimport { WrLoadingBar } from './services/loading-bar';\n\n/**\n * Thin progress bar fixed to the top of the viewport.\n *\n * Drop one at the root of your shell. Reads the singleton {@link WrLoadingBar},\n * which is driven manually by `start()` / `complete()` — an HTTP interceptor, a\n * long save, anything with a beginning and an end.\n *\n * Router navigations drive it too, but only once you add\n * `provideWrLoadingBarRouter()` from `ngwr/loading-bar/router`. That is an opt-in\n * because the subscription is what pulls `@angular/router` into the bundle: this\n * component is 2.4 kB of its own code and used to carry 66 kB of router into\n * apps that never routed.\n *\n * @example\n * ```html\n * <wr-loading-bar /> <!-- top, primary color -->\n * <wr-loading-bar color=\"#9b51e0\" height=\"3px\" />\n * ```\n */\n@Component({\n selector: 'wr-loading-bar',\n templateUrl: './loading-bar.component.html',\n styleUrl: './loading-bar.scss',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-loading-bar' },\n})\nexport class WrLoadingBarComponent {\n /** Bar colour. Defaults to the primary brand colour. */\n readonly color = input<string>('var(--wr-color-primary)');\n\n /** Bar height. */\n readonly height = input<string>('2px');\n\n protected readonly bar = inject(WrLoadingBar);\n\n protected readonly active = computed(() => this.bar.state() !== 'idle');\n}\n","<div class=\"wr-loading-bar__track\" [class.wr-loading-bar--active]=\"active()\">\n <div\n class=\"wr-loading-bar__fill\"\n [style.width.%]=\"bar.progress() * 100\"\n [style.background]=\"color()\"\n [style.height]=\"height()\"\n ></div>\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import * as i0 from '@angular/core';
|
|
|
6
6
|
import { input, output, ViewEncapsulation, Component, inject, ElementRef, DestroyRef, PLATFORM_ID, signal, computed, Directive } from '@angular/core';
|
|
7
7
|
import { readI18nText, useI18nFormatter } from 'ngwr/i18n';
|
|
8
8
|
import { WR_OVERLAY } from 'ngwr/overlay';
|
|
9
|
+
import { isComposing } from 'ngwr/utils';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @license
|
|
@@ -23,6 +24,13 @@ import { WR_OVERLAY } from 'ngwr/overlay';
|
|
|
23
24
|
*/
|
|
24
25
|
const MIRROR_PROPS = [
|
|
25
26
|
'boxSizing',
|
|
27
|
+
// Without this the mirror inherits `<body>`'s direction, and the two disagree
|
|
28
|
+
// whenever the field carries its own `dir` — measured at 230px of error for a
|
|
29
|
+
// 320px RTL field inside an LTR page, which put the panel outside the field
|
|
30
|
+
// entirely. `textAlign` below is the same story from the other side: an RTL
|
|
31
|
+
// field's computed value is the logical `start`, which only resolves to the
|
|
32
|
+
// right edge once the mirror knows the direction.
|
|
33
|
+
'direction',
|
|
26
34
|
'width',
|
|
27
35
|
'height',
|
|
28
36
|
'overflowX',
|
|
@@ -53,14 +61,55 @@ const MIRROR_PROPS = [
|
|
|
53
61
|
'whiteSpace',
|
|
54
62
|
'wordWrap',
|
|
55
63
|
];
|
|
64
|
+
/** A caret has no size, and an environment with no layout has no rects at all. */
|
|
65
|
+
const NO_RECT = { top: 0, left: 0 };
|
|
66
|
+
/**
|
|
67
|
+
* Where the browser would draw the caret this collapsed `range` marks.
|
|
68
|
+
*
|
|
69
|
+
* `getClientRects()` before `getBoundingClientRect()`: a collapsed range at a
|
|
70
|
+
* bidi boundary has TWO candidate positions and reports both, and the bounding
|
|
71
|
+
* box of the pair spans the whole run between them. The first rect is the one
|
|
72
|
+
* the caret is actually painted at.
|
|
73
|
+
*
|
|
74
|
+
* Both are optional-called because jsdom implements NEITHER on `Range` — it lays
|
|
75
|
+
* nothing out, so every element rect in it is already 0×0 and a zero here keeps
|
|
76
|
+
* the function answering what the rest of that environment answers rather than
|
|
77
|
+
* throwing halfway through an unrelated spec.
|
|
78
|
+
*/
|
|
79
|
+
function caretRectOf(range) {
|
|
80
|
+
const rects = range.getClientRects?.();
|
|
81
|
+
if (rects && rects.length > 0)
|
|
82
|
+
return rects[0];
|
|
83
|
+
return range.getBoundingClientRect?.() ?? NO_RECT;
|
|
84
|
+
}
|
|
56
85
|
/**
|
|
57
86
|
* Compute the viewport coordinates of a textarea / input caret at a given
|
|
58
|
-
* `position`. Uses the "mirror div" technique — a hidden div with the
|
|
59
|
-
*
|
|
60
|
-
*
|
|
87
|
+
* `position`. Uses the "mirror div" technique — a hidden div with the same
|
|
88
|
+
* styling holds the field's text, and a COLLAPSED RANGE at `position` reports
|
|
89
|
+
* where the caret would be.
|
|
61
90
|
*
|
|
62
91
|
* Returns viewport-relative `{ top, left }` — pass straight to a CDK
|
|
63
92
|
* Overlay anchored at that point.
|
|
93
|
+
*
|
|
94
|
+
* **A range, not a probe span, and the difference only shows in RTL.** The
|
|
95
|
+
* classic version of this technique puts the text before the caret in the div,
|
|
96
|
+
* the text after it in a `<span>`, and reads the span's LEFT edge. That reads
|
|
97
|
+
* the caret's side of the boundary only while the text runs left to right. Under
|
|
98
|
+
* the Unicode BiDi algorithm the span can be laid out to the LEFT of the run it
|
|
99
|
+
* follows, so the edge that faces the caret is its right one — and which edge
|
|
100
|
+
* that is depends on the bidi level of the text at the caret, not on the field's
|
|
101
|
+
* direction, so there is no side to pick. A collapsed range asks the browser
|
|
102
|
+
* where it would draw the caret and gets the reordering for free. Measured
|
|
103
|
+
* against a `contenteditable` twin of the field, in Chromium: the range agrees
|
|
104
|
+
* to within a pixel for Latin, Arabic and mixed text in both directions, where
|
|
105
|
+
* the span was out by up to 300px in RTL (see the spec next door).
|
|
106
|
+
*
|
|
107
|
+
* The `<input>` branch keeps the field's WIDTH for the same reason. It used to
|
|
108
|
+
* force `width: auto` so a long single line could not wrap; but an RTL field
|
|
109
|
+
* right-aligns its text inside its own box, so a shrink-to-fit mirror throws
|
|
110
|
+
* away the only measurement that decides where the text starts. `overflow:
|
|
111
|
+
* hidden` is what keeps the overflow from painting, and `input.scrollLeft`
|
|
112
|
+
* already accounts for a field scrolled past its box.
|
|
64
113
|
*/
|
|
65
114
|
function getCaretCoordinates(input, position) {
|
|
66
115
|
const doc = input.ownerDocument;
|
|
@@ -79,24 +128,24 @@ function getCaretCoordinates(input, position) {
|
|
|
79
128
|
if (input.tagName === 'INPUT') {
|
|
80
129
|
mirror.style.whiteSpace = 'pre';
|
|
81
130
|
mirror.style.height = 'auto';
|
|
82
|
-
mirror.style.width = 'auto';
|
|
83
131
|
mirror.style.overflow = 'hidden';
|
|
84
132
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
probe.textContent = input.value.substring(position) || '.';
|
|
90
|
-
mirror.appendChild(probe);
|
|
133
|
+
// One text node, so the range can address any offset in it. An empty field
|
|
134
|
+
// still needs a line box to measure, hence the fallback character.
|
|
135
|
+
const text = doc.createTextNode(input.value || '.');
|
|
136
|
+
mirror.appendChild(text);
|
|
91
137
|
doc.body.appendChild(mirror);
|
|
92
|
-
const
|
|
138
|
+
const range = doc.createRange();
|
|
139
|
+
range.setStart(text, Math.min(position, text.length));
|
|
140
|
+
range.collapse(true);
|
|
141
|
+
const caretRect = caretRectOf(range);
|
|
93
142
|
const mirrorRect = mirror.getBoundingClientRect();
|
|
94
143
|
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2;
|
|
95
144
|
doc.body.removeChild(mirror);
|
|
96
145
|
const inputRect = input.getBoundingClientRect();
|
|
97
146
|
return {
|
|
98
|
-
top: inputRect.top + (
|
|
99
|
-
left: inputRect.left + (
|
|
147
|
+
top: inputRect.top + (caretRect.top - mirrorRect.top) - input.scrollTop,
|
|
148
|
+
left: inputRect.left + (caretRect.left - mirrorRect.left) - input.scrollLeft,
|
|
100
149
|
lineHeight,
|
|
101
150
|
};
|
|
102
151
|
}
|
|
@@ -314,8 +363,33 @@ class WrMention {
|
|
|
314
363
|
this.liveRegion.textContent = message;
|
|
315
364
|
}
|
|
316
365
|
// Host listeners
|
|
366
|
+
/**
|
|
367
|
+
* True between `compositionstart` and `compositionend`.
|
|
368
|
+
*
|
|
369
|
+
* Detection must not run on the half-built text an IME leaves in the field:
|
|
370
|
+
* `@やま` is a reading on its way to `@山田`, and matching the reading opens a
|
|
371
|
+
* panel that then eats the Enter meant to accept the conversion. So the field
|
|
372
|
+
* is read once, on `compositionend`, when the text is real.
|
|
373
|
+
*/
|
|
374
|
+
composing = false;
|
|
375
|
+
/** @internal */
|
|
376
|
+
onCompositionStart() {
|
|
377
|
+
this.composing = true;
|
|
378
|
+
// A panel opened before the conversion started would take the arrows and the
|
|
379
|
+
// Enter that now belong to the candidate window.
|
|
380
|
+
this.close();
|
|
381
|
+
}
|
|
382
|
+
/** @internal */
|
|
383
|
+
onCompositionEnd() {
|
|
384
|
+
this.composing = false;
|
|
385
|
+
// The committed text is only in the field now, so this is the first honest
|
|
386
|
+
// read of it.
|
|
387
|
+
this.detect();
|
|
388
|
+
}
|
|
317
389
|
/** @internal */
|
|
318
390
|
onInput() {
|
|
391
|
+
if (this.composing)
|
|
392
|
+
return;
|
|
319
393
|
this.detect();
|
|
320
394
|
}
|
|
321
395
|
/** @internal */
|
|
@@ -324,6 +398,8 @@ class WrMention {
|
|
|
324
398
|
}
|
|
325
399
|
/** @internal */
|
|
326
400
|
onCaretMove(event) {
|
|
401
|
+
if (this.composing || isComposing(event))
|
|
402
|
+
return;
|
|
327
403
|
// Every key `onKeydown` already consumed, Escape included: its keyup would
|
|
328
404
|
// re-detect at a caret that is still sitting inside the mention and reopen the
|
|
329
405
|
// panel Escape had just dismissed. A synthetic keydown-only Escape hides that
|
|
@@ -339,6 +415,13 @@ class WrMention {
|
|
|
339
415
|
}
|
|
340
416
|
/** @internal */
|
|
341
417
|
onKeydown(event) {
|
|
418
|
+
// The key is the input method's while a conversion is open — Enter accepts a
|
|
419
|
+
// candidate, Escape cancels the reading, the arrows walk the candidate list.
|
|
420
|
+
// `this.composing` is deliberately not consulted: on Safari's committing
|
|
421
|
+
// keystroke it has already gone false, and `isComposing` covers that case
|
|
422
|
+
// through the 229 sentinel.
|
|
423
|
+
if (isComposing(event))
|
|
424
|
+
return;
|
|
342
425
|
if (!this.state)
|
|
343
426
|
return;
|
|
344
427
|
const list = this.filteredItems();
|
|
@@ -584,7 +667,7 @@ class WrMention {
|
|
|
584
667
|
this.announce(this.insertedText({ label }));
|
|
585
668
|
}
|
|
586
669
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrMention, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
587
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: WrMention, isStandalone: true, selector: "[wrMention]", inputs: { wrMentionItems: { classPropertyName: "wrMentionItems", publicName: "wrMentionItems", isSignal: true, isRequired: false, transformFunction: null }, triggers: { classPropertyName: "triggers", publicName: "triggers", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, valueWith: { classPropertyName: "valueWith", publicName: "valueWith", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { wrMentionSelected: "wrMentionSelected" }, host: { attributes: { "aria-autocomplete": "list", "aria-haspopup": "listbox" }, listeners: { "input": "onInput()", "click": "onClick()", "keyup": "onCaretMove($event)", "keydown": "onKeydown($event)", "blur": "onBlur()" }, properties: { "attr.aria-controls": "listboxId", "attr.aria-activedescendant": "activeOptionId()" } }, ngImport: i0 });
|
|
670
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: WrMention, isStandalone: true, selector: "[wrMention]", inputs: { wrMentionItems: { classPropertyName: "wrMentionItems", publicName: "wrMentionItems", isSignal: true, isRequired: false, transformFunction: null }, triggers: { classPropertyName: "triggers", publicName: "triggers", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, valueWith: { classPropertyName: "valueWith", publicName: "valueWith", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { wrMentionSelected: "wrMentionSelected" }, host: { attributes: { "aria-autocomplete": "list", "aria-haspopup": "listbox" }, listeners: { "input": "onInput()", "click": "onClick()", "keyup": "onCaretMove($event)", "keydown": "onKeydown($event)", "blur": "onBlur()", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd()" }, properties: { "attr.aria-controls": "listboxId", "attr.aria-activedescendant": "activeOptionId()" } }, ngImport: i0 });
|
|
588
671
|
}
|
|
589
672
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrMention, decorators: [{
|
|
590
673
|
type: Directive,
|
|
@@ -596,6 +679,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
|
|
|
596
679
|
'(keyup)': 'onCaretMove($event)',
|
|
597
680
|
'(keydown)': 'onKeydown($event)',
|
|
598
681
|
'(blur)': 'onBlur()',
|
|
682
|
+
// The IME pair. `isComposing` on a `keydown` answers "did the input method
|
|
683
|
+
// take this key", which is a different question from "is a conversion open
|
|
684
|
+
// right now" — on the committing keystroke the two disagree by design — and
|
|
685
|
+
// detection runs from `input` and `keyup`, where there is no such flag to
|
|
686
|
+
// read. So the state is tracked here and the detector consults it.
|
|
687
|
+
'(compositionstart)': 'onCompositionStart()',
|
|
688
|
+
'(compositionend)': 'onCompositionEnd()',
|
|
599
689
|
// The host keeps its implicit `role="textbox"`. It is NOT a combobox: this
|
|
600
690
|
// field holds prose and the mention is one fragment inside it, not the
|
|
601
691
|
// field's value. `role="combobox"` is also flatly disallowed on `<textarea>`
|