@sebgroup/green-core 3.26.3 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { GdsElement } from '../../gds-element';
2
- import { GdsColorLevel } from '../../utils/helpers';
2
+ import type { GdsColorLevel } from '../../utils/helpers';
3
3
  declare const GdsAvatar_base: (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").SizeXProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").MarginProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").LayoutChildProps) & typeof GdsElement;
4
4
  /**
5
5
  * `gds-avatar` is a visual element used to represent a user or entity.
@@ -1,5 +1,5 @@
1
1
  import { GdsElement } from '../../gds-element';
2
- import { GdsColorLevel } from '../../utils/helpers';
2
+ import type { GdsColorLevel } from '../../utils/helpers';
3
3
  declare const GdsDiv_base: (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").SizeXProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").SizeYProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").MarginProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").PaddingProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").LayoutChildProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").PositioningProps) & typeof GdsElement;
4
4
  /**
5
5
  * @element gds-div
@@ -1,5 +1,5 @@
1
1
  import { GdsElement } from '../../gds-element';
2
- import { GdsColorLevel } from '../../utils/helpers';
2
+ import type { GdsColorLevel } from '../../utils/helpers';
3
3
  declare const GdsIcon_base: (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").MarginProps) & (new (...args: any[]) => import("../../utils/mixins/declarative-layout-mixins").LayoutChildProps) & typeof GdsElement;
4
4
  /**
5
5
  * @part icon - The icon SVG element.
@@ -1,6 +1,11 @@
1
1
  import "../../chunks/chunk.H2T4A5YY.js";
2
2
  import { css } from "lit";
3
3
  const style = css`
4
+ /* Specific enough to beat the shared .native-control default. */
5
+ input.native-control {
6
+ text-align: var(--_gds-input-text-align, left);
7
+ }
8
+
4
9
  input::-webkit-outer-spin-button,
5
10
  input::-webkit-inner-spin-button {
6
11
  -webkit-appearance: var(--_appearance-spin-button, auto);
@@ -1,3 +1,4 @@
1
+ import type { PropertyValues } from 'lit';
1
2
  import { GdsFormControlElement } from '../form/form-control';
2
3
  /** A plain number in single-value mode, or a `[start, end]` tuple when `range` is set. */
3
4
  export type GdsSliderValue = number | [number, number];
@@ -55,12 +56,23 @@ declare class Slider extends GdsFormControlElement<GdsSliderValue> {
55
56
  * Hides the interactive min/max value textboxes.
56
57
  */
57
58
  hideTextbox: boolean;
59
+ /**
60
+ * Label rendered underneath the control, pinned to its start edge. Works with or
61
+ * without the value textboxes.
62
+ */
63
+ minLabel: string;
64
+ /**
65
+ * Label rendered underneath the control, pinned to its end edge. Works with or
66
+ * without the value textboxes.
67
+ */
68
+ maxLabel: string;
58
69
  /**
59
70
  * Makes the entire control read-only — slider thumbs and textbox inputs are non-interactive.
60
71
  */
61
72
  readonly: boolean;
62
73
  private _startInput;
63
74
  private _endInput;
75
+ private _controlRow?;
64
76
  private _activeThumb;
65
77
  private _focusedThumb;
66
78
  private _focusVisibleThumb;
@@ -72,10 +84,14 @@ declare class Slider extends GdsFormControlElement<GdsSliderValue> {
72
84
  private _draftMinInvalid;
73
85
  private _draftMaxInvalid;
74
86
  private _quietUpdate;
87
+ private _stacked;
88
+ private _textboxesStacked;
75
89
  constructor();
76
90
  connectedCallback(): void;
77
91
  disconnectedCallback(): void;
78
92
  focus(options?: FocusOptions): void;
93
+ protected firstUpdated(changed: PropertyValues): void;
94
+ protected updated(changed: PropertyValues): void;
79
95
  render(): any;
80
96
  private _handleConstraintsUpdate;
81
97
  protected _getValidityAnchor(): HTMLInputElement;
@@ -5,7 +5,7 @@ import {
5
5
  __privateMethod,
6
6
  __privateSet
7
7
  } from "../../chunks/chunk.H2T4A5YY.js";
8
- var _internalStart, _internalEnd, _thumbRects, _lastPointerX, _Slider_instances, effectiveMin_fn, effectiveMax_fn, start_fn, end_fn, normalizeValues_fn, clamp_fn, percent_fn, ratio_fn, startPercent_fn, endPercent_fn, tickPosition_fn, renderTicks_fn, setQuietUpdate_fn, controlClasses_fn, thumbClasses_fn, bubbleClasses_fn, renderBubbles_fn, controlStyle_fn, syncFormValue_fn, labelledBy_fn, _handleStartInput, _handleStartChange, _handleEndInput, _handleEndChange, _handleStartPointerDown, _handleEndPointerDown, _clearActiveThumb, _handleThumbPointerMove, _updateHoveredThumb, _handleThumbPointerLeave, _handleStartFocus, _handleEndFocus, _handleStartBlur, _handleEndBlur, renderTextbox_fn, formatValue_fn, openTextbox_fn, handleTextboxDraftInput_fn, handleTextboxKeyDown_fn, commitTextbox_fn, closeTextbox_fn, isThumbVisible_fn;
8
+ var _internalStart, _internalEnd, _textboxesWidth, _textboxCount, _thumbRects, _lastPointerX, _stackObserver, _Slider_instances, updateStacked_fn, effectiveMin_fn, effectiveMax_fn, start_fn, end_fn, normalizeValues_fn, clamp_fn, percent_fn, ratio_fn, startPercent_fn, endPercent_fn, tickPosition_fn, renderTicks_fn, setQuietUpdate_fn, controlClasses_fn, thumbClasses_fn, bubbleClasses_fn, renderBubbles_fn, renderBoundLabels_fn, controlStyle_fn, syncFormValue_fn, labelledBy_fn, _handleStartInput, _handleStartChange, _handleEndInput, _handleEndChange, _handleStartPointerDown, _handleEndPointerDown, _clearActiveThumb, _handleThumbPointerMove, _updateHoveredThumb, _handleThumbPointerLeave, _handleStartFocus, _handleEndFocus, _handleStartBlur, _handleEndBlur, renderTextbox_fn, _stopTextboxChange, textboxLabel_fn, formatValue_fn, openTextbox_fn, handleTextboxDraftInput_fn, handleTextboxKeyDown_fn, commitTextbox_fn, closeTextbox_fn, isThumbVisible_fn;
9
9
  import { localized, msg } from "@lit/localize";
10
10
  import { property, query, state } from "lit/decorators.js";
11
11
  import { classMap } from "lit/directives/class-map.js";
@@ -22,7 +22,9 @@ import {
22
22
  withSizeXProps
23
23
  } from "../../utils/mixins/declarative-layout-mixins.js";
24
24
  import { GdsFormControlElement } from "../form/form-control.js";
25
+ import { GdsInput } from "../input/input.component.js";
25
26
  import SliderStyles from "./slider.styles.js";
27
+ const SLIDER_MIN_TRACK_WIDTH = 200;
26
28
  const sliderValueConverter = {
27
29
  fromAttribute(value) {
28
30
  const [start, end] = value.split(",").map((part) => {
@@ -52,6 +54,8 @@ let Slider = class extends GdsFormControlElement {
52
54
  __privateAdd(this, _internalStart);
53
55
  __privateAdd(this, _internalEnd);
54
56
  this.hideTextbox = false;
57
+ this.minLabel = "";
58
+ this.maxLabel = "";
55
59
  this.readonly = false;
56
60
  this._activeThumb = null;
57
61
  this._focusedThumb = null;
@@ -64,9 +68,16 @@ let Slider = class extends GdsFormControlElement {
64
68
  this._draftMinInvalid = false;
65
69
  this._draftMaxInvalid = false;
66
70
  this._quietUpdate = false;
71
+ this._stacked = false;
72
+ this._textboxesStacked = false;
73
+ __privateAdd(this, _textboxesWidth, 0);
74
+ __privateAdd(this, _textboxCount, 0);
67
75
  __privateAdd(this, _thumbRects, {});
68
76
  // Last known pointer X per thumb, used to re-check hover once a click-to-jump updates the thumb position.
69
77
  __privateAdd(this, _lastPointerX, {});
78
+ // Undebounced: ResizeObserver callbacks run after layout but before paint, and Lit's
79
+ // update is a microtask, so the class lands in the same frame and nothing shifts.
80
+ __privateAdd(this, _stackObserver, new ResizeObserver(() => __privateMethod(this, _Slider_instances, updateStacked_fn).call(this)));
70
81
  // The native "input" event is composed and already bubbles out of the shadow root, so it
71
82
  // doesn't need to be re-dispatched here (unlike "change", which is not composed).
72
83
  __privateAdd(this, _handleStartInput, (e) => {
@@ -177,6 +188,11 @@ let Slider = class extends GdsFormControlElement {
177
188
  if (this._focusedThumb === "end") this._focusedThumb = null;
178
189
  if (this._focusVisibleThumb === "end") this._focusVisibleThumb = null;
179
190
  });
191
+ // The inner gds-input re-dispatches a composed "change", which would otherwise escape the
192
+ // slider and be mistaken for the slider's own change event.
193
+ __privateAdd(this, _stopTextboxChange, (event) => {
194
+ event.stopPropagation();
195
+ });
180
196
  __privateSet(this, _internalStart, __privateMethod(this, _Slider_instances, effectiveMin_fn).call(this));
181
197
  __privateSet(this, _internalEnd, __privateMethod(this, _Slider_instances, effectiveMax_fn).call(this));
182
198
  }
@@ -204,14 +220,27 @@ let Slider = class extends GdsFormControlElement {
204
220
  super.connectedCallback();
205
221
  __privateMethod(this, _Slider_instances, normalizeValues_fn).call(this);
206
222
  window.addEventListener("pointerup", __privateGet(this, _clearActiveThumb));
223
+ __privateGet(this, _stackObserver).observe(this);
207
224
  }
208
225
  disconnectedCallback() {
209
226
  super.disconnectedCallback();
210
227
  window.removeEventListener("pointerup", __privateGet(this, _clearActiveThumb));
228
+ __privateGet(this, _stackObserver).disconnect();
211
229
  }
212
230
  focus(options) {
213
231
  this._getValidityAnchor()?.focus(options);
214
232
  }
233
+ firstUpdated(changed) {
234
+ super.firstUpdated(changed);
235
+ __privateMethod(this, _Slider_instances, updateStacked_fn).call(this);
236
+ }
237
+ // Toggling either of these changes the row's contents without necessarily resizing the
238
+ // host, so the resize observer alone can't be relied on to recalculate.
239
+ updated(changed) {
240
+ super.updated(changed);
241
+ if (changed.has("range") || changed.has("hideTextbox"))
242
+ __privateMethod(this, _Slider_instances, updateStacked_fn).call(this);
243
+ }
215
244
  render() {
216
245
  const style = __privateMethod(this, _Slider_instances, controlStyle_fn).call(this);
217
246
  const effectiveMin = __privateMethod(this, _Slider_instances, effectiveMin_fn).call(this);
@@ -246,7 +275,13 @@ let Slider = class extends GdsFormControlElement {
246
275
  >`
247
276
  )}
248
277
 
249
- <div id="control-row">
278
+ <div
279
+ id="control-row"
280
+ class=${classMap({
281
+ stacked: this._stacked,
282
+ "textboxes-stacked": this._textboxesStacked
283
+ })}
284
+ >
250
285
  ${when(
251
286
  this.range && !this.hideTextbox,
252
287
  () => __privateMethod(this, _Slider_instances, renderTextbox_fn).call(this, "min")
@@ -313,7 +348,7 @@ let Slider = class extends GdsFormControlElement {
313
348
  @change=${__privateGet(this, _handleEndChange)}
314
349
  />`
315
350
  )}
316
- ${__privateMethod(this, _Slider_instances, renderBubbles_fn).call(this)}
351
+ ${__privateMethod(this, _Slider_instances, renderBubbles_fn).call(this)} ${__privateMethod(this, _Slider_instances, renderBoundLabels_fn).call(this)}
317
352
  </div>
318
353
 
319
354
  ${when(!this.hideTextbox, () => __privateMethod(this, _Slider_instances, renderTextbox_fn).call(this, "max"))}
@@ -344,9 +379,38 @@ let Slider = class extends GdsFormControlElement {
344
379
  };
345
380
  _internalStart = new WeakMap();
346
381
  _internalEnd = new WeakMap();
382
+ _textboxesWidth = new WeakMap();
383
+ _textboxCount = new WeakMap();
347
384
  _thumbRects = new WeakMap();
348
385
  _lastPointerX = new WeakMap();
386
+ _stackObserver = new WeakMap();
349
387
  _Slider_instances = new WeakSet();
388
+ updateStacked_fn = function() {
389
+ const row = this._controlRow;
390
+ if (!row) return;
391
+ const textboxes = Array.from(row.querySelectorAll(".textbox"));
392
+ if (textboxes.length === 0) {
393
+ __privateSet(this, _textboxCount, 0);
394
+ this._stacked = false;
395
+ this._textboxesStacked = false;
396
+ return;
397
+ }
398
+ if (textboxes.length !== __privateGet(this, _textboxCount)) {
399
+ __privateSet(this, _textboxCount, textboxes.length);
400
+ if (this._textboxesStacked) {
401
+ this._textboxesStacked = false;
402
+ this.updateComplete.then(() => __privateMethod(this, _Slider_instances, updateStacked_fn).call(this));
403
+ return;
404
+ }
405
+ }
406
+ const gap = parseFloat(getComputedStyle(row).columnGap) || 0;
407
+ if (!this._textboxesStacked) {
408
+ __privateSet(this, _textboxesWidth, textboxes.reduce((total, textbox) => total + textbox.offsetWidth, 0) + gap * (textboxes.length - 1));
409
+ }
410
+ const available = row.clientWidth;
411
+ this._stacked = available - __privateGet(this, _textboxesWidth) - gap < SLIDER_MIN_TRACK_WIDTH;
412
+ this._textboxesStacked = __privateGet(this, _textboxesWidth) > available;
413
+ };
350
414
  effectiveMin_fn = function() {
351
415
  return Math.min(this.min, this.max);
352
416
  };
@@ -479,6 +543,19 @@ renderBubbles_fn = function() {
479
543
  )}
480
544
  `;
481
545
  };
546
+ // The labels sit inside the control and are taken out of flow, so they always span the track
547
+ // itself regardless of the textboxes, and never shift the control vertically.
548
+ renderBoundLabels_fn = function() {
549
+ if (!this.minLabel && !this.maxLabel) return html``;
550
+ return html`<div id="bound-labels">
551
+ <span class="bound-label min-label" aria-hidden="true"
552
+ >${this.minLabel}</span
553
+ >
554
+ <span class="bound-label max-label" aria-hidden="true"
555
+ >${this.maxLabel}</span
556
+ >
557
+ </div>`;
558
+ };
482
559
  controlStyle_fn = function() {
483
560
  const thumbAwarePos = (ratio) => `calc((100% - var(--_thumb-size)) * ${ratio} + (var(--_thumb-size) / 2))`;
484
561
  const clampedStart = __privateMethod(this, _Slider_instances, clamp_fn).call(this, __privateMethod(this, _Slider_instances, start_fn).call(this));
@@ -531,22 +608,36 @@ renderTextbox_fn = function(bound) {
531
608
  const draft = bound === "min" ? this._draftMin : this._draftMax;
532
609
  const isDraftInvalid = bound === "min" ? this._draftMinInvalid : this._draftMaxInvalid;
533
610
  const displayValue = isEditing ? draft : __privateMethod(this, _Slider_instances, formatValue_fn).call(this, currentValue);
534
- return html`<input
535
- class="textbox ${isDraftInvalid ? "invalid" : ""}"
611
+ return html`<gds-input
612
+ class="textbox"
536
613
  id="${bound}-textbox"
537
614
  type="number"
615
+ plain
616
+ .size=${this.size}
617
+ .label=${__privateMethod(this, _Slider_instances, textboxLabel_fn).call(this, bound)}
618
+ gds-aria-labelledby=${__privateMethod(this, _Slider_instances, labelledBy_fn).call(this, bound === "min" ? "start" : "end")}
538
619
  .value=${displayValue}
539
- .step=${String(this.step)}
540
- style="min-width: ${Math.min(Math.max(displayValue.length, 3), 12)}ch"
541
- aria-labelledby=${__privateMethod(this, _Slider_instances, labelledBy_fn).call(this, bound === "min" ? "start" : "end")}
542
- aria-invalid=${isDraftInvalid ? "true" : "false"}
543
- ?disabled=${this.disabled}
544
- ?readonly=${this.readonly}
620
+ .step=${this.step}
621
+ .disabled=${this.disabled}
622
+ .readonly=${this.readonly}
623
+ .invalid=${isDraftInvalid}
624
+ style="--_textbox-chars: ${Math.min(
625
+ Math.max(displayValue.length, 3),
626
+ 12
627
+ )}"
545
628
  @focus=${() => __privateMethod(this, _Slider_instances, openTextbox_fn).call(this, bound)}
546
629
  @input=${(event) => __privateMethod(this, _Slider_instances, handleTextboxDraftInput_fn).call(this, bound, event)}
630
+ @change=${__privateGet(this, _stopTextboxChange)}
547
631
  @blur=${() => __privateMethod(this, _Slider_instances, commitTextbox_fn).call(this, bound)}
548
632
  @keydown=${(event) => __privateMethod(this, _Slider_instances, handleTextboxKeyDown_fn).call(this, bound, event)}
549
- />`;
633
+ ></gds-input>`;
634
+ };
635
+ _stopTextboxChange = new WeakMap();
636
+ // Mirrors the gds-aria-labelledby wiring as a plain string, since cross-root element
637
+ // references reflect to an empty content attribute that name computation tools can't read.
638
+ textboxLabel_fn = function(bound) {
639
+ if (!this.range) return this.label;
640
+ return bound === "min" ? `${this.label} ${msg("Start value")}` : `${this.label} ${msg("End value")}`;
550
641
  };
551
642
  /** Displays values with the same number of decimals as `step`, so the readout doesn't change width. */
552
643
  formatValue_fn = function(value) {
@@ -686,6 +777,12 @@ __decorateClass([
686
777
  __decorateClass([
687
778
  property({ type: Boolean, attribute: "hide-textbox" })
688
779
  ], Slider.prototype, "hideTextbox", 2);
780
+ __decorateClass([
781
+ property({ attribute: "min-label" })
782
+ ], Slider.prototype, "minLabel", 2);
783
+ __decorateClass([
784
+ property({ attribute: "max-label" })
785
+ ], Slider.prototype, "maxLabel", 2);
689
786
  __decorateClass([
690
787
  property({ type: Boolean, reflect: true })
691
788
  ], Slider.prototype, "readonly", 2);
@@ -695,6 +792,9 @@ __decorateClass([
695
792
  __decorateClass([
696
793
  query("#slider-end")
697
794
  ], Slider.prototype, "_endInput", 2);
795
+ __decorateClass([
796
+ query("#control-row")
797
+ ], Slider.prototype, "_controlRow", 2);
698
798
  __decorateClass([
699
799
  state()
700
800
  ], Slider.prototype, "_activeThumb", 2);
@@ -728,6 +828,12 @@ __decorateClass([
728
828
  __decorateClass([
729
829
  state()
730
830
  ], Slider.prototype, "_quietUpdate", 2);
831
+ __decorateClass([
832
+ state()
833
+ ], Slider.prototype, "_stacked", 2);
834
+ __decorateClass([
835
+ state()
836
+ ], Slider.prototype, "_textboxesStacked", 2);
731
837
  __decorateClass([
732
838
  watch("min"),
733
839
  watch("max"),
@@ -743,7 +849,7 @@ let GdsSlider = class extends withSizeXProps(
743
849
  };
744
850
  GdsSlider = __decorateClass([
745
851
  gdsCustomElement("gds-slider", {
746
- dependsOn: [GdsFormControlHeader, GdsFormControlFooter]
852
+ dependsOn: [GdsFormControlHeader, GdsFormControlFooter, GdsInput]
747
853
  })
748
854
  ], GdsSlider);
749
855
  export {
@@ -8,9 +8,15 @@ const styles = css`
8
8
  --_track-height: var(--gds-sys-space-3xs);
9
9
  --_thumb-color: var(--gds-sys-color-l3-neutral-03);
10
10
  --_thumb-ring-color: var(--gds-sys-color-border-neutral-02);
11
- --_textbox-height: var(--gds-sys-space-2xl);
12
- --_textbox-padding: var(--gds-sys-space-s);
13
- --_textbox-font: var(--gds-sys-text-detail-s-regular);
11
+ --_textbox-chrome: var(--gds-sys-space-2xl);
12
+ --_textbox-lock-space: calc(
13
+ var(--gds-sys-space-l) + var(--gds-sys-space-xs)
14
+ );
15
+ --_textbox-padding-inline: var(--gds-sys-space-s);
16
+ --_textbox-font: var(--gds-sys-text-detail-m-regular);
17
+ --_bound-label-font: var(--gds-sys-text-detail-s-regular);
18
+ --_bound-label-line-height: var(--gds-sys-space-l);
19
+ --_bound-label-gap: var(--gds-sys-space-5xs);
14
20
  --_bubble-font: var(--gds-sys-text-detail-s-regular);
15
21
  --_bubble-padding-x: var(--gds-sys-space-xs);
16
22
  --_bubble-bg: var(--gds-sys-color-l3-neutral-03);
@@ -34,9 +40,14 @@ const styles = css`
34
40
  :host([size='small']) {
35
41
  --_thumb-size: var(--gds-sys-space-s);
36
42
  --_track-height: var(--gds-sys-space-3xs);
37
- --_textbox-height: var(--gds-sys-space-xl);
38
- --_textbox-padding: var(--gds-sys-space-xs);
39
- --_textbox-font: var(--gds-sys-text-detail-xs-regular);
43
+ --_textbox-chrome: var(--gds-sys-space-l);
44
+ --_textbox-lock-space: calc(
45
+ var(--gds-sys-space-m) + var(--gds-sys-space-3xs)
46
+ );
47
+ --_textbox-padding-inline: var(--gds-sys-space-xs);
48
+ --_textbox-font: var(--gds-sys-text-detail-s-regular);
49
+ --_bound-label-font: var(--gds-sys-text-detail-xs-regular);
50
+ --_bound-label-line-height: var(--gds-sys-space-m);
40
51
  --_bubble-font: var(--gds-sys-text-detail-xs-regular);
41
52
  --_bubble-gap-pressed: var(--gds-sys-space-4xs);
42
53
  --_bubble-padding-x: var(--gds-sys-space-2xs);
@@ -60,68 +71,119 @@ const styles = css`
60
71
  width: 1px;
61
72
  }
62
73
 
74
+ #control-row {
75
+ align-items: center;
76
+ display: flex;
77
+ flex-wrap: wrap;
78
+ gap: var(--_row-gap);
79
+ }
80
+
63
81
  #control {
64
82
  align-items: center;
65
83
  display: grid;
66
- flex: 1;
84
+ flex: 1 1 auto;
85
+ min-inline-size: 0;
86
+ order: 1;
67
87
  height: var(--_thumb-size);
68
88
  min-height: var(--_thumb-size);
69
89
  overflow: visible;
70
90
  position: relative;
71
91
  }
72
92
 
73
- #control-row {
74
- align-items: center;
75
- display: flex;
76
- gap: var(--_row-gap);
93
+ .textbox {
94
+ flex: 0 0 auto;
77
95
  }
78
96
 
79
- .textbox {
80
- -webkit-appearance: textfield;
81
- appearance: textfield;
82
- background: var(--gds-sys-color-l3-neutral-02);
83
- border-width: var(--gds-sys-space-5xs);
84
- border-style: solid;
85
- border-color: var(--gds-sys-color-border-neutral-01);
86
- border-radius: var(--gds-sys-radius-max);
87
- box-sizing: content-box;
88
- color: var(--gds-sys-color-content-neutral-01);
89
- font: var(--_textbox-font);
90
- min-height: calc(var(--_textbox-height) - 2 * var(--gds-sys-space-5xs));
91
- min-width: 3ch;
92
- max-width: 12ch;
93
- width: 3ch;
94
- outline: 2px solid transparent;
95
- outline-offset: 0px;
96
- padding: 0 var(--_textbox-padding);
97
- text-align: center;
98
- transition-property: outline-color, outline-offset;
99
- transition-duration: var(--_t-duration), var(--_t-duration);
100
- transition-timing-function: var(--_t-easing-out);
97
+ #min-textbox {
98
+ order: 0;
101
99
  }
102
100
 
103
- .textbox::-webkit-outer-spin-button,
104
- .textbox::-webkit-inner-spin-button {
105
- -webkit-appearance: none;
106
- margin: 0;
101
+ #max-textbox {
102
+ order: 2;
107
103
  }
108
104
 
109
- .textbox:focus-visible {
110
- outline-color: var(--gds-sys-color-content-neutral-01);
111
- outline-offset: 2px;
105
+ /* Set by the resize observer once too little is left for the track on the row. */
106
+ #control-row.stacked #control {
107
+ order: -1;
108
+ flex-basis: 100%;
112
109
  }
113
110
 
114
- .textbox:disabled {
115
- cursor: default;
111
+ #control-row.stacked #max-textbox {
112
+ margin-inline-start: auto;
116
113
  }
117
114
 
118
- :host([readonly]) .native-control {
119
- cursor: default;
120
- pointer-events: none;
115
+ /* Second stage: the two textboxes no longer fit on one row either. */
116
+ #control-row.textboxes-stacked .textbox {
117
+ flex-basis: 100%;
118
+ inline-size: 100%;
119
+ }
120
+
121
+ #control-row.textboxes-stacked #max-textbox {
122
+ margin-inline-start: 0;
123
+ }
124
+
125
+ /* Reserves room for the out-of-flow bound labels without moving the control itself,
126
+ since flex centering ignores the padding box. */
127
+ #control-row:has(#bound-labels) {
128
+ padding-block-end: calc(
129
+ var(--_bound-label-gap) + var(--_bound-label-line-height)
130
+ );
131
+ }
132
+
133
+ /* Stacked, the labels sit between the two rows, so the track carries the reservation. */
134
+ #control-row.stacked:has(#bound-labels) {
135
+ padding-block-end: 0;
136
+ }
137
+
138
+ #control-row.stacked #control:has(#bound-labels) {
139
+ margin-block-end: calc(
140
+ var(--_bound-label-gap) + var(--_bound-label-line-height)
141
+ );
142
+ }
143
+
144
+ #bound-labels {
145
+ display: flex;
146
+ gap: var(--gds-sys-space-xs);
147
+ inset-block-start: calc(100% + var(--_bound-label-gap));
148
+ inset-inline: 0;
149
+ justify-content: space-between;
150
+ position: absolute;
151
+ }
152
+
153
+ .bound-label {
154
+ color: var(--gds-sys-color-content-neutral-02);
155
+ font: var(--_bound-label-font);
156
+ line-height: var(--_bound-label-line-height);
157
+ white-space: nowrap;
158
+ }
159
+
160
+ .max-label {
161
+ margin-inline-start: auto;
162
+ text-align: end;
163
+ }
164
+
165
+ .textbox {
166
+ --_appearance-spin-button: none;
167
+ --_gds-input-radius: var(--gds-sys-radius-max);
168
+ --_gds-input-text-align: center;
169
+ --_gds-input-padding-inline: var(--_textbox-padding-inline);
170
+ --_gds-input-block-size: var(--gds-sys-space-2xl);
171
+
172
+ /* Matches the native input's font so the ch-based width measures the right glyphs. */
173
+ font: var(--_textbox-font);
174
+ inline-size: calc(
175
+ var(--_textbox-chars, 3) * 1ch + var(--_textbox-chrome) +
176
+ var(--_textbox-extras, 0px)
177
+ );
121
178
  }
122
179
 
123
180
  :host([readonly]) .textbox {
181
+ --_textbox-extras: var(--_textbox-lock-space);
182
+ }
183
+
184
+ :host([readonly]) .native-control {
124
185
  cursor: default;
186
+ pointer-events: none;
125
187
  }
126
188
 
127
189
  #track-before,