@neumorphism-ui/elements 1.0.0-next.0 → 1.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +12 -1
  2. package/cdn/neu.min.js +184 -51
  3. package/custom-elements.json +2232 -282
  4. package/dist/all.d.ts +8 -0
  5. package/dist/all.js +8 -0
  6. package/dist/checkbox.d.ts +3 -0
  7. package/dist/checkbox.js +4 -0
  8. package/dist/components/button/button.d.ts +0 -2
  9. package/dist/components/button/button.js +3 -36
  10. package/dist/components/checkbox/checkbox.d.ts +44 -0
  11. package/dist/components/checkbox/checkbox.def.d.ts +31 -0
  12. package/dist/components/checkbox/checkbox.def.js +37 -0
  13. package/dist/components/checkbox/checkbox.js +40 -0
  14. package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
  15. package/dist/components/checkbox/checkbox.styles.js +28 -0
  16. package/dist/components/icon/system-icons.d.ts +10 -0
  17. package/dist/components/icon/system-icons.js +22 -11
  18. package/dist/components/number-field/number-field.d.ts +84 -0
  19. package/dist/components/number-field/number-field.def.d.ts +125 -0
  20. package/dist/components/number-field/number-field.def.js +62 -0
  21. package/dist/components/number-field/number-field.js +121 -0
  22. package/dist/components/number-field/number-field.styles.d.ts +1 -0
  23. package/dist/components/number-field/number-field.styles.js +15 -0
  24. package/dist/components/option/option-data.d.ts +8 -0
  25. package/dist/components/option/option-data.js +12 -0
  26. package/dist/components/option/option.d.ts +19 -0
  27. package/dist/components/option/option.def.d.ts +10 -0
  28. package/dist/components/option/option.def.js +19 -0
  29. package/dist/components/option/option.js +17 -0
  30. package/dist/components/option/option.styles.d.ts +2 -0
  31. package/dist/components/option/option.styles.js +5 -0
  32. package/dist/components/radio/radio-link.d.ts +11 -0
  33. package/dist/components/radio/radio-link.js +5 -0
  34. package/dist/components/radio/radio.d.ts +29 -0
  35. package/dist/components/radio/radio.def.d.ts +11 -0
  36. package/dist/components/radio/radio.def.js +25 -0
  37. package/dist/components/radio/radio.js +43 -0
  38. package/dist/components/radio/radio.styles.d.ts +1 -0
  39. package/dist/components/radio/radio.styles.js +21 -0
  40. package/dist/components/radio-group/radio-group.d.ts +77 -0
  41. package/dist/components/radio-group/radio-group.def.d.ts +40 -0
  42. package/dist/components/radio-group/radio-group.def.js +55 -0
  43. package/dist/components/radio-group/radio-group.js +184 -0
  44. package/dist/components/radio-group/radio-group.styles.d.ts +1 -0
  45. package/dist/components/radio-group/radio-group.styles.js +15 -0
  46. package/dist/components/select/select.d.ts +119 -0
  47. package/dist/components/select/select.def.d.ts +38 -0
  48. package/dist/components/select/select.def.js +59 -0
  49. package/dist/components/select/select.js +385 -0
  50. package/dist/components/select/select.styles.d.ts +1 -0
  51. package/dist/components/select/select.styles.js +39 -0
  52. package/dist/components/shared/checkable.d.ts +45 -0
  53. package/dist/components/shared/checkable.js +108 -0
  54. package/dist/components/shared/field.styles.d.ts +1 -0
  55. package/dist/components/shared/field.styles.js +36 -0
  56. package/dist/components/shared/press.d.ts +7 -0
  57. package/dist/components/shared/press.js +42 -0
  58. package/dist/components/shared/step.d.ts +2 -0
  59. package/dist/components/shared/step.js +2 -0
  60. package/dist/components/shared/text-control.d.ts +72 -0
  61. package/dist/components/shared/text-control.js +191 -0
  62. package/dist/components/slider/slider.d.ts +69 -0
  63. package/dist/components/slider/slider.def.d.ts +39 -0
  64. package/dist/components/slider/slider.def.js +76 -0
  65. package/dist/components/slider/slider.js +170 -0
  66. package/dist/components/slider/slider.styles.d.ts +1 -0
  67. package/dist/components/slider/slider.styles.js +16 -0
  68. package/dist/components/switch/switch.d.ts +4 -30
  69. package/dist/components/switch/switch.js +5 -117
  70. package/dist/components/text-field/implicit-submit.d.ts +5 -0
  71. package/dist/components/text-field/implicit-submit.js +8 -3
  72. package/dist/components/text-field/text-field.d.ts +9 -36
  73. package/dist/components/text-field/text-field.js +21 -178
  74. package/dist/components/text-field/text-field.styles.d.ts +1 -1
  75. package/dist/components/text-field/text-field.styles.js +2 -35
  76. package/dist/components/textarea/textarea.d.ts +98 -0
  77. package/dist/components/textarea/textarea.def.d.ts +139 -0
  78. package/dist/components/textarea/textarea.def.js +65 -0
  79. package/dist/components/textarea/textarea.js +101 -0
  80. package/dist/components/textarea/textarea.styles.d.ts +1 -0
  81. package/dist/components/textarea/textarea.styles.js +9 -0
  82. package/dist/core/define.d.ts +16 -1
  83. package/dist/core/form-control.js +3 -1
  84. package/dist/core/validation.d.ts +6 -2
  85. package/dist/core/validation.js +15 -8
  86. package/dist/number-field.d.ts +3 -0
  87. package/dist/number-field.js +4 -0
  88. package/dist/option.d.ts +3 -0
  89. package/dist/option.js +4 -0
  90. package/dist/radio-group.d.ts +4 -0
  91. package/dist/radio-group.js +6 -0
  92. package/dist/radio.d.ts +3 -0
  93. package/dist/radio.js +4 -0
  94. package/dist/select.d.ts +3 -0
  95. package/dist/select.js +4 -0
  96. package/dist/slider.d.ts +3 -0
  97. package/dist/slider.js +4 -0
  98. package/dist/ssr.d.ts +9 -5
  99. package/dist/ssr.js +25 -5
  100. package/dist/textarea.d.ts +3 -0
  101. package/dist/textarea.js +4 -0
  102. package/package.json +41 -1
@@ -0,0 +1,42 @@
1
+ import { afterDispatch, isActiveInRoot } from '../../core/dom.js';
2
+ /**
3
+ * Native-like keyboard activation shared by button, switch and checkbox: Space arms on keydown and
4
+ * clicks on keyup, Enter clicks on keydown. `inactive` blocks both arming and clicking.
5
+ */
6
+ export function pressable(host, states, enter, inactive) {
7
+ const press = (on) => (on ? states.add('pressed') : states.delete('pressed'));
8
+ // Not cancelled at the keydown/keyup itself, so a handler further up can still cancel the click.
9
+ const clickUnlessCancelled = (event) => afterDispatch(event, () => {
10
+ if (!event.defaultPrevented && !inactive())
11
+ host.click();
12
+ });
13
+ host.addEventListener('keydown', (event) => {
14
+ if (inactive())
15
+ return;
16
+ if (event.key === ' ') {
17
+ if (event.defaultPrevented)
18
+ return;
19
+ // Cancelled at once: deferred, an ancestor that stops propagation would let the page scroll.
20
+ // An ancestor claims Space in the capture phase, or by cancelling the keyup.
21
+ event.preventDefault();
22
+ press(true);
23
+ }
24
+ else if (event.key === 'Enter' &&
25
+ (enter === 'repeat' || (enter === 'once' && !event.repeat))) {
26
+ clickUnlessCancelled(event);
27
+ }
28
+ });
29
+ host.addEventListener('keyup', (event) => {
30
+ if (event.key !== ' ' || !states.has('pressed'))
31
+ return;
32
+ press(false);
33
+ clickUnlessCancelled(event);
34
+ });
35
+ // Disarm when focus moves away, not when only the window loses focus.
36
+ host.addEventListener('blur', () => {
37
+ if (!isActiveInRoot(host))
38
+ press(false);
39
+ });
40
+ // Focus coming back means Space may have been released while it was away.
41
+ host.addEventListener('focus', () => press(false));
42
+ }
@@ -0,0 +1,2 @@
1
+ /** A step a native input would accept; anything at or below zero means the default of 1. */
2
+ export declare const stepUnit: (step: number) => number;
@@ -0,0 +1,2 @@
1
+ /** A step a native input would accept; anything at or below zero means the default of 1. */
2
+ export const stepUnit = (step) => (step > 0 ? step : 1);
@@ -0,0 +1,72 @@
1
+ import { FormControlElement } from '../../core/form-control.js';
2
+ type TextControl = HTMLInputElement | HTMLTextAreaElement;
3
+ /** The props every text control shares; a subclass's def may leave some out. */
4
+ interface TextControlProps {
5
+ value: string;
6
+ placeholder: string;
7
+ required: boolean;
8
+ readOnly: boolean;
9
+ minLength: number;
10
+ maxLength: number;
11
+ autocomplete: string;
12
+ errorText: string;
13
+ label: string;
14
+ helpText: string;
15
+ enterKeyHint: string;
16
+ }
17
+ /** The props that can change the form value, its restore state or its validity in every text control. */
18
+ export declare const baseCommitProps: readonly ["value", "required", "disabled", "readOnly", "minLength", "maxLength", "errorText", "autocomplete"];
19
+ /**
20
+ * The behaviour text-field, textarea and number-field share: a native control in the shadow root
21
+ * (`data-ref="input"`) holds the value, and the host mirrors its form value, restore state and
22
+ * validity. update() touches a prop only when it is in `changed`, so props a def does not declare
23
+ * never reach it.
24
+ */
25
+ export declare abstract class TextControlElement extends FormControlElement {
26
+ name: string;
27
+ label: string;
28
+ placeholder: string;
29
+ helpText: string;
30
+ errorText: string;
31
+ required: boolean;
32
+ disabled: boolean;
33
+ readOnly: boolean;
34
+ autocomplete: string;
35
+ /** True once the value was edited or set as a property; the default then stops applying. */
36
+ protected dirty: boolean;
37
+ /** A value set before the shadow root exists. */
38
+ protected pendingValue: string | null;
39
+ /** Props that can change the form value, its restore state or its validity. */
40
+ protected abstract readonly commitProps: readonly string[];
41
+ /** A detached control holding `value`, sanitized and constrained the way the real one is. */
42
+ protected abstract probe(value: string): TextControl;
43
+ get value(): string;
44
+ set value(next: string);
45
+ get defaultValue(): string;
46
+ set defaultValue(next: string);
47
+ /** Focuses the inner control directly, so the caret stays where it was (no select-all). */
48
+ focus(options?: FocusOptions): void;
49
+ select(): void;
50
+ setSelectionRange(start: number, end: number, direction?: 'forward' | 'backward' | 'none'): void;
51
+ protected get p(): TextControlProps;
52
+ protected get input(): TextControl | null;
53
+ /** Hook for keys pressed in the inner control. */
54
+ protected controlKeyDown(_event: KeyboardEvent): void;
55
+ private needsCommit;
56
+ /** Stands in for the inner control until it exists, so validity is right from the start. */
57
+ private validityProbe;
58
+ protected rendered(): void;
59
+ protected update(changed: ReadonlySet<string>): void;
60
+ /** update() only runs once connected; before that, keep the value and validity current here. */
61
+ protected willUpdate(changed: ReadonlySet<string>): void;
62
+ protected formValue(): string;
63
+ /**
64
+ * Like a native control, one with autocomplete=off is never saved for restore, nor is one
65
+ * that sets none of its own inside a form with autocomplete=off.
66
+ */
67
+ protected formState(): string | null;
68
+ protected validate(): void;
69
+ protected resetToDefault(): void;
70
+ protected restoreState(state: string): void;
71
+ }
72
+ export {};
@@ -0,0 +1,191 @@
1
+ import { dispatch, setOptionalAttribute, setText } from '../../core/dom.js';
2
+ import { FormControlElement } from '../../core/form-control.js';
3
+ import { copyValidity } from '../../core/validation.js';
4
+ /** The props that can change the form value, its restore state or its validity in every text control. */
5
+ export const baseCommitProps = [
6
+ 'value',
7
+ 'required',
8
+ 'disabled',
9
+ 'readOnly',
10
+ 'minLength',
11
+ 'maxLength',
12
+ 'errorText',
13
+ 'autocomplete',
14
+ ];
15
+ function autocompleteOff(value) {
16
+ return (value ?? '').trim().toLowerCase() === 'off';
17
+ }
18
+ /**
19
+ * The behaviour text-field, textarea and number-field share: a native control in the shadow root
20
+ * (`data-ref="input"`) holds the value, and the host mirrors its form value, restore state and
21
+ * validity. update() touches a prop only when it is in `changed`, so props a def does not declare
22
+ * never reach it.
23
+ */
24
+ export class TextControlElement extends FormControlElement {
25
+ /** True once the value was edited or set as a property; the default then stops applying. */
26
+ dirty = false;
27
+ /** A value set before the shadow root exists. */
28
+ pendingValue = null;
29
+ get value() {
30
+ return this.input?.value ?? this.pendingValue ?? this.p.value;
31
+ }
32
+ set value(next) {
33
+ const text = next === null || next === undefined ? '' : String(next);
34
+ this.dirty = true;
35
+ const input = this.input;
36
+ if (input)
37
+ input.value = text;
38
+ else
39
+ this.pendingValue = text;
40
+ this.commit();
41
+ }
42
+ get defaultValue() {
43
+ return this.p.value;
44
+ }
45
+ set defaultValue(next) {
46
+ this.setAttribute('value', String(next));
47
+ }
48
+ /** Focuses the inner control directly, so the caret stays where it was (no select-all). */
49
+ focus(options) {
50
+ const input = this.input;
51
+ if (input)
52
+ input.focus(options);
53
+ else
54
+ super.focus(options);
55
+ }
56
+ select() {
57
+ this.input?.select();
58
+ }
59
+ setSelectionRange(start, end, direction) {
60
+ this.input?.setSelectionRange(start, end, direction);
61
+ }
62
+ get p() {
63
+ return this.props;
64
+ }
65
+ get input() {
66
+ return this.ref('input');
67
+ }
68
+ /** Hook for keys pressed in the inner control. */
69
+ controlKeyDown(_event) { }
70
+ needsCommit(changed) {
71
+ return this.commitProps.some((key) => changed.has(key));
72
+ }
73
+ /** Stands in for the inner control until it exists, so validity is right from the start. */
74
+ validityProbe() {
75
+ const p = this.p;
76
+ const probe = this.probe(this.value);
77
+ probe.required = p.required;
78
+ probe.readOnly = p.readOnly;
79
+ probe.disabled = this.isDisabled;
80
+ if (p.minLength >= 0)
81
+ probe.setAttribute('minlength', String(p.minLength));
82
+ if (p.maxLength >= 0)
83
+ probe.setAttribute('maxlength', String(p.maxLength));
84
+ return probe;
85
+ }
86
+ rendered() {
87
+ const input = this.input;
88
+ // A declarative root adopted without its control (cut short by the parser): nothing to wire up.
89
+ if (!input)
90
+ return;
91
+ // An SSR'd control works before JS loads; keep whatever was typed or autofilled meanwhile.
92
+ // Compare with the sanitized default: the browser trims or strips some values by itself.
93
+ if (input.value !== this.probe(this.p.value).value)
94
+ this.dirty = true;
95
+ if (this.pendingValue !== null) {
96
+ input.value = this.pendingValue;
97
+ this.pendingValue = null;
98
+ }
99
+ input.addEventListener('input', () => {
100
+ this.dirty = true;
101
+ this.commit();
102
+ });
103
+ input.addEventListener('change', () => {
104
+ this.markInteracted();
105
+ dispatch(this, 'change');
106
+ });
107
+ input.addEventListener('keydown', (event) => this.controlKeyDown(event));
108
+ }
109
+ update(changed) {
110
+ const p = this.p;
111
+ const input = this.input;
112
+ if (input) {
113
+ if (changed.has('value')) {
114
+ if (input.defaultValue !== p.value)
115
+ input.defaultValue = p.value;
116
+ if (!this.dirty)
117
+ input.value = p.value;
118
+ }
119
+ if (changed.has('placeholder'))
120
+ setOptionalAttribute(input, 'placeholder', p.placeholder);
121
+ if (changed.has('required'))
122
+ input.required = p.required;
123
+ if (changed.has('disabled'))
124
+ input.disabled = this.isDisabled;
125
+ if (changed.has('readOnly'))
126
+ input.readOnly = p.readOnly;
127
+ if (changed.has('minLength')) {
128
+ setOptionalAttribute(input, 'minlength', p.minLength >= 0 ? String(p.minLength) : '');
129
+ }
130
+ if (changed.has('maxLength')) {
131
+ setOptionalAttribute(input, 'maxlength', p.maxLength >= 0 ? String(p.maxLength) : '');
132
+ }
133
+ if (changed.has('autocomplete'))
134
+ setOptionalAttribute(input, 'autocomplete', p.autocomplete);
135
+ if (changed.has('enterKeyHint'))
136
+ setOptionalAttribute(input, 'enterkeyhint', p.enterKeyHint);
137
+ }
138
+ if (changed.has('label'))
139
+ setText(this.ref('label-text'), p.label);
140
+ if (changed.has('helpText'))
141
+ setText(this.ref('help-text'), p.helpText);
142
+ if (this.needsCommit(changed))
143
+ this.commit();
144
+ }
145
+ /** update() only runs once connected; before that, keep the value and validity current here. */
146
+ willUpdate(changed) {
147
+ if (!this.isConnected && !this.input && this.needsCommit(changed))
148
+ this.commit();
149
+ }
150
+ formValue() {
151
+ return this.value;
152
+ }
153
+ /**
154
+ * Like a native control, one with autocomplete=off is never saved for restore, nor is one
155
+ * that sets none of its own inside a form with autocomplete=off.
156
+ */
157
+ formState() {
158
+ const own = this.p.autocomplete?.trim();
159
+ const off = own
160
+ ? autocompleteOff(own)
161
+ : autocompleteOff(this.internals.form?.getAttribute('autocomplete'));
162
+ return off ? null : this.value;
163
+ }
164
+ validate() {
165
+ const input = this.input;
166
+ const source = input ?? this.validityProbe();
167
+ // A disabled or readonly control still sets flags such as typeMismatch, but is barred from
168
+ // validation and has no message; setValidity() would throw on flags without a message.
169
+ if (!source.willValidate || source.validity.valid)
170
+ this.internals.setValidity({});
171
+ else {
172
+ this.internals.setValidity(copyValidity(source.validity), source.validationMessage, input ?? undefined);
173
+ }
174
+ setText(this.ref('error-text'), this.p.errorText || input?.validationMessage || '');
175
+ }
176
+ resetToDefault() {
177
+ this.dirty = false;
178
+ this.pendingValue = null;
179
+ const input = this.input;
180
+ if (input)
181
+ input.value = this.p.value;
182
+ }
183
+ restoreState(state) {
184
+ this.dirty = true;
185
+ const input = this.input;
186
+ if (input)
187
+ input.value = state;
188
+ else
189
+ this.pendingValue = state;
190
+ }
191
+ }
@@ -0,0 +1,69 @@
1
+ import { FormControlElement } from '../../core/form-control.js';
2
+ export declare class NeuSlider extends FormControlElement {
3
+ static def: import("../../ssr.js").ComponentDef<{
4
+ readonly value: {
5
+ readonly type: "number";
6
+ readonly default: number;
7
+ };
8
+ readonly min: {
9
+ readonly type: "number";
10
+ readonly default: 0;
11
+ };
12
+ readonly max: {
13
+ readonly type: "number";
14
+ readonly default: 100;
15
+ };
16
+ readonly step: {
17
+ readonly type: "number";
18
+ readonly default: 1;
19
+ };
20
+ readonly name: {
21
+ readonly type: "string";
22
+ readonly reflect: true;
23
+ };
24
+ readonly label: {
25
+ readonly type: "string";
26
+ };
27
+ readonly disabled: {
28
+ readonly type: "boolean";
29
+ readonly reflect: true;
30
+ };
31
+ }>;
32
+ static extraProperties: string[];
33
+ name: string;
34
+ label: string;
35
+ min: number;
36
+ max: number;
37
+ step: number;
38
+ disabled: boolean;
39
+ private current;
40
+ private dirty;
41
+ private dragging;
42
+ private startValue;
43
+ private formatter;
44
+ constructor();
45
+ protected disconnected(): void;
46
+ get value(): number;
47
+ set value(next: number);
48
+ get defaultValue(): number;
49
+ set defaultValue(next: number);
50
+ get valueFormatter(): ((value: number) => string) | undefined;
51
+ set valueFormatter(next: ((value: number) => string) | undefined);
52
+ private snap;
53
+ /** Pushes the value to ARIA and the thumb, without touching the form. */
54
+ private sync;
55
+ private setCurrent;
56
+ /** The value as a user change: marks the form interaction and fires input. Returns false if nothing changed. */
57
+ private userSet;
58
+ private onKey;
59
+ private onDown;
60
+ private onMove;
61
+ private moveTo;
62
+ private endDrag;
63
+ protected willUpdate(changed: ReadonlySet<string>): void;
64
+ protected update(changed: ReadonlySet<string>): void;
65
+ protected formValue(): string;
66
+ protected validate(): void;
67
+ protected resetToDefault(): void;
68
+ protected restoreState(state: string): void;
69
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The value `raw` becomes on a slider: the nearest step counted from min, inside [min, max]
3
+ * (plan decision 9). max < min counts as max = min, a step of 0 or less as 1, NaN as the midpoint.
4
+ * Rounded to the decimals of step and min, so 0.1 + 0.2 on a 0.1 step is 0.3.
5
+ */
6
+ export declare function sliderValue(raw: number, min: number, max: number, step: number): number;
7
+ /** What PageUp and PageDown move by: a tenth of the range in whole steps, at least one step. */
8
+ export declare function sliderBigStep(min: number, max: number, step: number): number;
9
+ /** The `style` of `.base`: where the thumb sits, 0 to 1. Template and update() write the same bytes. */
10
+ export declare function sliderStyle(value: number, min: number, max: number): string;
11
+ export declare const sliderDef: import("../../core/define.js").ComponentDef<{
12
+ readonly value: {
13
+ readonly type: "number";
14
+ readonly default: number;
15
+ };
16
+ readonly min: {
17
+ readonly type: "number";
18
+ readonly default: 0;
19
+ };
20
+ readonly max: {
21
+ readonly type: "number";
22
+ readonly default: 100;
23
+ };
24
+ readonly step: {
25
+ readonly type: "number";
26
+ readonly default: 1;
27
+ };
28
+ readonly name: {
29
+ readonly type: "string";
30
+ readonly reflect: true;
31
+ };
32
+ readonly label: {
33
+ readonly type: "string";
34
+ };
35
+ readonly disabled: {
36
+ readonly type: "boolean";
37
+ readonly reflect: true;
38
+ };
39
+ }>;
@@ -0,0 +1,76 @@
1
+ import { defineComponent } from '../../core/define.js';
2
+ import { html } from '../../core/html.js';
3
+ import { themeCssProperties } from '../../core/styles.js';
4
+ import { stepUnit } from '../shared/step.js';
5
+ import { sliderStyles } from './slider.styles.js';
6
+ /** Decimal places of a number as written, exponent notation included. */
7
+ function decimals(n) {
8
+ const [, fraction = '', exponent = '0'] = /(?:\.(\d+))?(?:e([+-]\d+))?$/.exec(String(n)) ?? [];
9
+ return Math.min(100, Math.max(0, fraction.length - Number(exponent)));
10
+ }
11
+ /**
12
+ * The value `raw` becomes on a slider: the nearest step counted from min, inside [min, max]
13
+ * (plan decision 9). max < min counts as max = min, a step of 0 or less as 1, NaN as the midpoint.
14
+ * Rounded to the decimals of step and min, so 0.1 + 0.2 on a 0.1 step is 0.3.
15
+ */
16
+ export function sliderValue(raw, min, max, step) {
17
+ const hi = Math.max(min, max);
18
+ const unit = stepUnit(step);
19
+ const wanted = Number.isNaN(raw) ? min + (hi - min) / 2 : raw;
20
+ let steps = Math.round((wanted - min) / unit);
21
+ // Rounding up may pass max: the largest step that fits is the answer. The epsilon absorbs
22
+ // float noise such as (0.3 - 0) / 0.1 = 2.9999999999999996.
23
+ if (min + steps * unit > hi)
24
+ steps = Math.floor((hi - min) / unit + 1e-9);
25
+ const value = min + Math.max(0, steps) * unit;
26
+ return Number(Math.min(value, hi).toFixed(Math.max(decimals(unit), decimals(min))));
27
+ }
28
+ /** What PageUp and PageDown move by: a tenth of the range in whole steps, at least one step. */
29
+ export function sliderBigStep(min, max, step) {
30
+ const unit = stepUnit(step);
31
+ const tenth = Math.round((Math.max(min, max) - min) / 10 / unit) * unit;
32
+ return Number(Math.max(unit, tenth).toFixed(decimals(unit)));
33
+ }
34
+ /** The `style` of `.base`: where the thumb sits, 0 to 1. Template and update() write the same bytes. */
35
+ export function sliderStyle(value, min, max) {
36
+ const range = Math.max(min, max) - min;
37
+ const pos = range > 0 ? Math.round(((value - min) / range) * 1e4) / 1e4 : 0;
38
+ return `--_neu-pos:${pos}`;
39
+ }
40
+ export const sliderDef = defineComponent({
41
+ tag: 'neu-slider',
42
+ props: {
43
+ value: { type: 'number', default: Number.NaN },
44
+ min: { type: 'number', default: 0 },
45
+ max: { type: 'number', default: 100 },
46
+ step: { type: 'number', default: 1 },
47
+ name: { type: 'string', reflect: true },
48
+ label: { type: 'string' },
49
+ disabled: { type: 'boolean', reflect: true },
50
+ },
51
+ form: { valueProp: 'value' },
52
+ properties: { valueFormatter: '(value: number) => string' },
53
+ styles: sliderStyles,
54
+ template: (p) => html `<span part="base" class="base" data-ref="base" style="${sliderStyle(sliderValue(p.value, p.min, p.max, p.step), p.min, p.max)}"><span part="track" class="track" data-ref="track"><span part="fill" class="fill"></span><span part="thumb" class="thumb"></span></span></span>`,
55
+ docs: {
56
+ summary: 'A horizontal slider for a number. The value attribute is the default; the value property is the current value. The valueFormatter property turns the value into the text a screen reader announces.',
57
+ events: [
58
+ {
59
+ name: 'input',
60
+ description: 'Fired on every change while the user drags or presses a key.',
61
+ },
62
+ {
63
+ name: 'change',
64
+ description: 'Fired after the user commits a change: on release, or per key.',
65
+ },
66
+ ],
67
+ slots: [],
68
+ parts: [
69
+ { name: 'base', description: 'Wrapper around the track.' },
70
+ { name: 'track', description: 'The inset groove.' },
71
+ { name: 'fill', description: 'The part of the track up to the thumb.' },
72
+ { name: 'thumb', description: 'The raised handle.' },
73
+ ],
74
+ cssProperties: themeCssProperties,
75
+ },
76
+ });
@@ -0,0 +1,170 @@
1
+ import { dispatch } from '../../core/dom.js';
2
+ import { FormControlElement } from '../../core/form-control.js';
3
+ import { stepUnit } from '../shared/step.js';
4
+ import { sliderBigStep, sliderDef, sliderStyle, sliderValue } from './slider.def.js';
5
+ export class NeuSlider extends FormControlElement {
6
+ static def = sliderDef;
7
+ static extraProperties = ['defaultValue', 'valueFormatter'];
8
+ current;
9
+ dirty = false;
10
+ dragging = false;
11
+ startValue = 0;
12
+ formatter;
13
+ constructor() {
14
+ super();
15
+ this.internals.role = 'slider';
16
+ this.internals.ariaOrientation = 'horizontal';
17
+ this.current = this.snap(this.props.value);
18
+ this.addEventListener('keydown', (event) => this.onKey(event));
19
+ this.addEventListener('pointerdown', (event) => this.onDown(event));
20
+ this.addEventListener('pointermove', (event) => this.onMove(event));
21
+ this.addEventListener('pointerup', () => this.endDrag());
22
+ this.addEventListener('pointercancel', () => this.endDrag());
23
+ // Capture lost without a pointerup reaching us (the browser took it back, say): the drag is over.
24
+ this.addEventListener('lostpointercapture', () => this.endDrag());
25
+ }
26
+ disconnected() {
27
+ super.disconnected();
28
+ // A drag cut short by removal does not resume when the slider is put back.
29
+ this.dragging = false;
30
+ }
31
+ get value() {
32
+ return this.current;
33
+ }
34
+ set value(next) {
35
+ this.dirty = true;
36
+ this.setCurrent(this.snap(Number(next)));
37
+ }
38
+ get defaultValue() {
39
+ return this.snap(this.props.value);
40
+ }
41
+ set defaultValue(next) {
42
+ this.setAttribute('value', String(next));
43
+ }
44
+ get valueFormatter() {
45
+ return this.formatter;
46
+ }
47
+ set valueFormatter(next) {
48
+ this.formatter = typeof next === 'function' ? next : undefined;
49
+ this.sync();
50
+ }
51
+ snap(raw) {
52
+ return sliderValue(raw, this.props.min, this.props.max, this.props.step);
53
+ }
54
+ /** Pushes the value to ARIA and the thumb, without touching the form. */
55
+ sync() {
56
+ const { min, max } = this.props;
57
+ const internals = this.internals;
58
+ internals.ariaLabel = this.props.label === '' ? null : String(this.props.label);
59
+ internals.ariaValueMin = String(min);
60
+ internals.ariaValueMax = String(Math.max(min, max));
61
+ internals.ariaValueNow = String(this.current);
62
+ internals.ariaValueText = this.formatter ? this.formatter(this.current) : null;
63
+ // The template writes the same string, so a server-rendered root is left alone.
64
+ const base = this.ref('base');
65
+ const style = sliderStyle(this.current, min, max);
66
+ if (base && base.getAttribute('style') !== style)
67
+ base.setAttribute('style', style);
68
+ }
69
+ setCurrent(next) {
70
+ this.current = next;
71
+ this.sync();
72
+ this.commit();
73
+ }
74
+ /** The value as a user change: marks the form interaction and fires input. Returns false if nothing changed. */
75
+ userSet(next) {
76
+ if (next === this.current)
77
+ return false;
78
+ this.markInteracted();
79
+ this.setCurrent(next);
80
+ dispatch(this, 'input');
81
+ return true;
82
+ }
83
+ onKey(event) {
84
+ if (this.isDisabled || event.altKey || event.ctrlKey || event.metaKey)
85
+ return;
86
+ const { min, max } = this.props;
87
+ const step = stepUnit(this.props.step);
88
+ const direction = this.matches(':dir(rtl)') ? -1 : 1;
89
+ const big = sliderBigStep(min, max, step);
90
+ const wanted = {
91
+ ArrowRight: this.current + step * direction,
92
+ ArrowLeft: this.current - step * direction,
93
+ ArrowUp: this.current + step,
94
+ ArrowDown: this.current - step,
95
+ PageUp: this.current + big,
96
+ PageDown: this.current - big,
97
+ Home: Number.NEGATIVE_INFINITY,
98
+ End: Number.POSITIVE_INFINITY,
99
+ };
100
+ const raw = wanted[event.key];
101
+ if (raw === undefined)
102
+ return;
103
+ event.preventDefault();
104
+ if (this.userSet(this.snap(raw)))
105
+ dispatch(this, 'change');
106
+ }
107
+ onDown(event) {
108
+ if (this.isDisabled || event.button !== 0)
109
+ return;
110
+ this.dragging = true;
111
+ this.startValue = this.current;
112
+ this.focus();
113
+ try {
114
+ this.setPointerCapture(event.pointerId);
115
+ }
116
+ catch {
117
+ // A pointer the browser does not know (synthetic events): the drag is tracked by the flag.
118
+ }
119
+ this.moveTo(event);
120
+ }
121
+ onMove(event) {
122
+ if (this.dragging)
123
+ this.moveTo(event);
124
+ }
125
+ moveTo(event) {
126
+ const track = this.ref('track');
127
+ if (!track)
128
+ return;
129
+ const rect = track.getBoundingClientRect();
130
+ let fraction = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0;
131
+ if (this.matches(':dir(rtl)'))
132
+ fraction = 1 - fraction;
133
+ const { min, max } = this.props;
134
+ const clamped = Math.min(1, Math.max(0, fraction));
135
+ this.userSet(this.snap(min + clamped * (Math.max(min, max) - min)));
136
+ }
137
+ endDrag() {
138
+ if (!this.dragging)
139
+ return;
140
+ this.dragging = false;
141
+ if (this.current !== this.startValue)
142
+ dispatch(this, 'change');
143
+ }
144
+ willUpdate(changed) {
145
+ if (changed.has('value') || changed.has('min') || changed.has('max') || changed.has('step')) {
146
+ // Until the user or a script sets one, the value is the attribute's; a new range re-snaps it.
147
+ this.current = this.snap(this.dirty ? this.current : this.props.value);
148
+ }
149
+ }
150
+ update(changed) {
151
+ if (changed.has('disabled'))
152
+ this.syncDisabledState();
153
+ this.sync();
154
+ this.commit();
155
+ }
156
+ formValue() {
157
+ return String(this.current);
158
+ }
159
+ validate() {
160
+ this.internals.setValidity({});
161
+ }
162
+ resetToDefault() {
163
+ this.dirty = false;
164
+ this.setCurrent(this.snap(this.props.value));
165
+ }
166
+ restoreState(state) {
167
+ this.dirty = true;
168
+ this.setCurrent(this.snap(Number(state)));
169
+ }
170
+ }
@@ -0,0 +1 @@
1
+ export declare const sliderStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; min-block-size: 1.25rem; touch-action: pan-y; cursor: pointer; user-select: none; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n.base { position: relative; display: flex; align-items: center; block-size: 1.25rem; padding-inline: 0.625rem; }\n.track { position: relative; flex: 1; block-size: 0.5rem; border-radius: 999px; background: var(--_neu-bg); box-shadow: var(--_neu-inset-sm); }\n.fill { position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: calc(var(--_neu-pos) * 100%); border-radius: inherit; background: var(--_neu-accent-soft); }\n.thumb {\n position: absolute; inset-block-start: 50%; inset-inline-start: calc(var(--_neu-pos) * 100%);\n inline-size: 1.25rem; block-size: 1.25rem; margin-block-start: -0.625rem; margin-inline-start: -0.625rem;\n border-radius: 50%; background: var(--_neu-bg); box-shadow: var(--_neu-raised-sm);\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host(:focus-visible) .thumb { box-shadow: var(--_neu-raised-sm), var(--_neu-focus); }\n:host(:disabled) .base, :host([disabled]) .base { opacity: 0.55; }\n";