polymorph-ui-components 0.7.0 → 0.8.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.
@@ -11,6 +11,7 @@
11
11
  import chevronRightSvg from '../assets/chevron-right-lg.svg?raw';
12
12
  import editSvg from '../assets/edit.svg?raw';
13
13
  import deleteSvg from '../assets/delete.svg?raw';
14
+ import { activeElementOf, deepActiveElement, lockDocumentScroll } from '../utils';
14
15
 
15
16
  let {
16
17
  images,
@@ -54,16 +55,17 @@
54
55
  let showItemActions = $derived(showEditButton || showDeleteButton);
55
56
 
56
57
  function lightboxAction(node: HTMLElement) {
57
- if (openerElement === null && document.activeElement instanceof HTMLElement) {
58
- openerElement = document.activeElement;
58
+ const active = deepActiveElement();
59
+ if (openerElement === null && active instanceof HTMLElement) {
60
+ openerElement = active;
59
61
  }
60
- document.body.style.overflow = 'hidden';
62
+ const unlockScroll = lockDocumentScroll();
61
63
  tick().then(() => {
62
64
  node.focus();
63
65
  });
64
66
  return {
65
67
  destroy() {
66
- document.body.style.overflow = '';
68
+ unlockScroll();
67
69
  if (openerElement !== null) {
68
70
  openerElement.focus();
69
71
  openerElement = null;
@@ -93,7 +95,7 @@
93
95
  activeIndex = index;
94
96
  onchange?.(activeIndex);
95
97
  await tick();
96
- if (lightboxDiv !== null && !lightboxDiv.contains(document.activeElement)) {
98
+ if (lightboxDiv !== null && !lightboxDiv.contains(activeElementOf(lightboxDiv))) {
97
99
  lightboxDiv.focus();
98
100
  }
99
101
  }
@@ -135,13 +137,11 @@
135
137
  if (first === null || last === null) {
136
138
  return;
137
139
  }
138
- if (
139
- event.shiftKey &&
140
- (document.activeElement === first || document.activeElement === lightboxDiv)
141
- ) {
140
+ const active = activeElementOf(first);
141
+ if (event.shiftKey && (active === first || active === lightboxDiv)) {
142
142
  event.preventDefault();
143
143
  last.focus();
144
- } else if (!event.shiftKey && document.activeElement === last) {
144
+ } else if (!event.shiftKey && active === last) {
145
145
  event.preventDefault();
146
146
  first.focus();
147
147
  }
@@ -1,4 +1,4 @@
1
1
  import type { GalleryProperties } from './properties';
2
- declare const Gallery: import("svelte").Component<GalleryProperties, {}, "activeIndex" | "open">;
2
+ declare const Gallery: import("svelte").Component<GalleryProperties, {}, "open" | "activeIndex">;
3
3
  type Gallery = ReturnType<typeof Gallery>;
4
4
  export default Gallery;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { validateInput } from '../utils';
2
+ import { activeElementOf, validateInput } from '../utils';
3
3
  import type { InputProperties } from './properties';
4
4
  import type { ValidationState } from '../types';
5
5
 
@@ -23,6 +23,8 @@
23
23
  useTextArea = false,
24
24
  autoComplete = 'on',
25
25
  name = '',
26
+ id,
27
+ ariaLabel,
26
28
  testId = '',
27
29
  textTransformers = [],
28
30
  textViewPresentation = [],
@@ -42,6 +44,9 @@
42
44
  ariaActivedescendant
43
45
  }: InputProperties = $props();
44
46
 
47
+ const generatedId = $props.id();
48
+ const inputId = $derived(id ?? generatedId);
49
+
45
50
  export function focus() {
46
51
  try {
47
52
  inputElement?.focus();
@@ -77,7 +82,7 @@
77
82
  valueValidation === 'InProgress' &&
78
83
  value.length > 0 &&
79
84
  inputElement !== null &&
80
- inputElement !== document.activeElement
85
+ inputElement !== activeElementOf(inputElement)
81
86
  ) {
82
87
  return 'Invalid';
83
88
  }
@@ -194,7 +199,7 @@
194
199
 
195
200
  <div class="input-container {classes ?? ''}" class:input-error={showErrorMessage && !actionInput}>
196
201
  {#if typeof label === 'string' && label !== '' && !actionInput}
197
- <label class="label" for={name}>
202
+ <label class="label" for={inputId}>
198
203
  {label}
199
204
  </label>
200
205
  {/if}
@@ -206,7 +211,9 @@
206
211
  {placeholder}
207
212
  autocomplete={autoComplete}
208
213
  {name}
214
+ id={inputId}
209
215
  {role}
216
+ aria-label={ariaLabel}
210
217
  aria-expanded={ariaExpanded}
211
218
  aria-autocomplete={ariaAutocomplete}
212
219
  aria-controls={ariaControls}
@@ -231,7 +238,9 @@
231
238
  {placeholder}
232
239
  autocomplete={autoComplete}
233
240
  {name}
241
+ id={inputId}
234
242
  {role}
243
+ aria-label={ariaLabel}
235
244
  aria-expanded={ariaExpanded}
236
245
  aria-autocomplete={ariaAutocomplete}
237
246
  aria-controls={ariaControls}
@@ -23,6 +23,8 @@ export type OptionalInputProperties = {
23
23
  useTextArea?: boolean;
24
24
  autoComplete?: HTMLInputAttributes['autocomplete'];
25
25
  name?: string;
26
+ id?: string;
27
+ ariaLabel?: string;
26
28
  textTransformers?: TextTransformer[];
27
29
  textViewPresentation?: TextTransformer[];
28
30
  testId?: string;
@@ -23,6 +23,9 @@
23
23
 
24
24
  let validationState = $state<ValidationState>('InProgress');
25
25
 
26
+ const generatedId = $props.id();
27
+ const inputId = $derived(inputProperties.id ?? generatedId);
28
+
26
29
  let inputRef: SvelteComponent | null = $state(null);
27
30
 
28
31
  // Derive enable state for right button
@@ -61,8 +64,8 @@
61
64
  </script>
62
65
 
63
66
  <div class="container {classes ?? ''}" data-pw={testId}>
64
- {#if inputProperties.label && inputProperties.label !== ''}
65
- <label class="label" for={inputProperties.name}>
67
+ {#if typeof inputProperties.label === 'string' && inputProperties.label.length > 0}
68
+ <label class="label" for={inputId}>
66
69
  {inputProperties.label}
67
70
  </label>
68
71
  {/if}
@@ -79,6 +82,7 @@
79
82
  <Input
80
83
  {...inputProperties}
81
84
  {...inputEventProperties}
85
+ id={inputId}
82
86
  bind:value
83
87
  bind:this={inputRef}
84
88
  onstatechange={handleStateChange}
@@ -102,12 +106,12 @@
102
106
  </div>
103
107
  {/if}
104
108
  </div>
105
- {#if inputProperties.onErrorMessage !== '' && validationState === 'Invalid'}
109
+ {#if typeof inputProperties.onErrorMessage === 'string' && inputProperties.onErrorMessage.length > 0 && validationState === 'Invalid'}
106
110
  <div class="error-message">
107
111
  {inputProperties.onErrorMessage}
108
112
  </div>
109
113
  {/if}
110
- {#if inputProperties.infoMessage !== ''}
114
+ {#if typeof inputProperties.infoMessage === 'string' && inputProperties.infoMessage.length > 0}
111
115
  <div class="info-message">
112
116
  {inputProperties.infoMessage}
113
117
  </div>
@@ -127,7 +131,6 @@
127
131
  --input-focus-border: none;
128
132
  --input-box-shadow: none;
129
133
  --input-margin: none;
130
- --input-width: fit-content;
131
134
  height: var(--input-height, fit-content);
132
135
  font-size: var(--input-font-size, 16px) !important;
133
136
  font-weight: 500;
@@ -141,6 +144,7 @@
141
144
  .input-button {
142
145
  display: flex;
143
146
  align-items: stretch;
147
+ height: 100%;
144
148
  border-radius: var(--input-button-radius, 6px);
145
149
  border: var(--input-button-border, 1px solid currentColor);
146
150
  box-shadow: var(--input-button-box-shadow, none);
@@ -151,7 +155,11 @@
151
155
  border: var(--input-button-focus-border);
152
156
  }
153
157
  .input {
158
+ --input-container-width: 100%;
159
+ --input-width: 100%;
160
+ --input-height: 100%;
154
161
  flex: 2;
162
+ display: flex;
155
163
  min-width: 0px;
156
164
  }
157
165
 
@@ -1,10 +1,16 @@
1
1
  <script lang="ts">
2
2
  import type { LoaderProperties } from './properties';
3
3
 
4
- let { testId, classes }: LoaderProperties = $props();
4
+ let { label, testId, classes }: LoaderProperties = $props();
5
+
6
+ let announced = $derived(typeof label === 'string' && label.length > 0);
5
7
  </script>
6
8
 
7
- <div class="loader {classes ?? ''}" data-pw={testId}></div>
9
+ <div class="loader {classes ?? ''}" role={announced ? 'status' : null} data-pw={testId}>
10
+ {#if announced}
11
+ <span class="loader-label">{label}</span>
12
+ {/if}
13
+ </div>
8
14
 
9
15
  <style>
10
16
  .loader {
@@ -40,8 +46,8 @@
40
46
  var(--loader-foreground-end, transparent) 42%
41
47
  );
42
48
  position: relative;
43
- -webkit-animation: load3 1.4s infinite linear;
44
- animation: load3 1.4s infinite linear;
49
+ -webkit-animation: load3 var(--loader-duration, 1.4s) infinite linear;
50
+ animation: load3 var(--loader-duration, 1.4s) infinite linear;
45
51
  -webkit-transform: translateZ(0);
46
52
  -ms-transform: translateZ(0);
47
53
  transform: translateZ(0);
@@ -67,6 +73,16 @@
67
73
  left: var(--loader-after-left, 50%);
68
74
  transform: translate(-50%, -50%);
69
75
  }
76
+
77
+ .loader-label {
78
+ position: absolute;
79
+ width: 1px;
80
+ height: 1px;
81
+ overflow: hidden;
82
+ clip-path: inset(50%);
83
+ white-space: nowrap;
84
+ }
85
+
70
86
  @-webkit-keyframes load3 {
71
87
  0% {
72
88
  -webkit-transform: rotate(0deg);
@@ -1,4 +1,5 @@
1
1
  export type LoaderProperties = {
2
+ label?: string;
2
3
  testId?: string;
3
4
  classes?: string;
4
5
  };
@@ -3,12 +3,13 @@
3
3
  import { onMount, onDestroy, untrack } from 'svelte';
4
4
  import ModalAnimation from '../Animations/ModalAnimation.svelte';
5
5
  import OverlayAnimation from '../Animations/OverlayAnimation.svelte';
6
- import { createDebouncer } from '../utils';
6
+ import { createDebouncer, lockDocumentScroll } from '../utils';
7
7
  import Button from '../Button/Button.svelte';
8
8
 
9
9
  let overlayDiv: HTMLDivElement | null = $state(null);
10
10
  let backPressed = false;
11
11
  let dismissTimer: ReturnType<typeof setTimeout> | null = null;
12
+ let unlockScroll: (() => void) | null = null;
12
13
 
13
14
  let {
14
15
  size = 'fit-content',
@@ -79,7 +80,7 @@
79
80
 
80
81
  onMount(() => {
81
82
  if (lockScroll) {
82
- document.body.style.overflow = 'hidden';
83
+ unlockScroll = lockDocumentScroll();
83
84
  }
84
85
  if (typeof autoDismissAfter === 'number') {
85
86
  dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
@@ -95,9 +96,7 @@
95
96
  clearTimeout(dismissTimer);
96
97
  }
97
98
  if (typeof window !== 'undefined') {
98
- if (lockScroll) {
99
- document.body.style.overflow = '';
100
- }
99
+ unlockScroll?.();
101
100
  if (supportHardwareBackPress) {
102
101
  if (!backPressed) {
103
102
  history.back();
@@ -0,0 +1,154 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import type { NumberStepperProperties } from './properties';
4
+ import Button from '../Button/Button.svelte';
5
+ import Loader from '../Loader/Loader.svelte';
6
+ import minusSvg from '../assets/minus.svg?raw';
7
+ import addSvg from '../assets/add.svg?raw';
8
+ import { deepActiveElement } from '../utils';
9
+
10
+ let {
11
+ value,
12
+ min = 0,
13
+ max = Number.POSITIVE_INFINITY,
14
+ step = 1,
15
+ disabled = false,
16
+ ariaLabel = 'Quantity',
17
+ decrementLabel = 'Decrease',
18
+ incrementLabel = 'Increase',
19
+ loadingLabel = 'Updating',
20
+ decrementIcon,
21
+ incrementIcon,
22
+ testId,
23
+ onchange,
24
+ classes
25
+ }: NumberStepperProperties = $props();
26
+
27
+ let pending = $state(false);
28
+ let decrementButton: ReturnType<typeof Button> | null = $state(null);
29
+ let incrementButton: ReturnType<typeof Button> | null = $state(null);
30
+ let canDecrement = $derived(!disabled && value - step >= min);
31
+ let canIncrement = $derived(!disabled && value + step <= max);
32
+
33
+ async function change(next: number, pressed: ReturnType<typeof Button> | null): Promise<void> {
34
+ if (pending) {
35
+ return;
36
+ }
37
+ const pressedElement = pressed?.getButtonRef() ?? null;
38
+ const hadFocus = pressedElement !== null && deepActiveElement() === pressedElement;
39
+
40
+ const result = onchange?.(next);
41
+ if (result instanceof Promise) {
42
+ pending = true;
43
+ try {
44
+ await result;
45
+ } finally {
46
+ pending = false;
47
+ }
48
+ }
49
+
50
+ await tick();
51
+ const other = (pressed === incrementButton ? decrementButton : incrementButton)?.getButtonRef();
52
+ if (hadFocus && pressedElement.disabled && other instanceof HTMLElement && !other.disabled) {
53
+ other.focus();
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <div
59
+ class="number-stepper {classes ?? ''}"
60
+ class:pending
61
+ role="group"
62
+ aria-label={ariaLabel}
63
+ aria-busy={pending}
64
+ data-pw={testId}
65
+ >
66
+ <div class="number-stepper-button">
67
+ <Button
68
+ bind:this={decrementButton}
69
+ ariaLabel={decrementLabel}
70
+ disabled={!canDecrement}
71
+ onclick={() => change(value - step, decrementButton)}
72
+ {...typeof testId === 'string' ? { testId: `${testId}-decrement` } : {}}
73
+ >
74
+ {#if typeof decrementIcon === 'function'}
75
+ {@render decrementIcon(value)}
76
+ {:else}
77
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
78
+ {@html minusSvg}
79
+ {/if}
80
+ </Button>
81
+ </div>
82
+ <span class="number-stepper-value" aria-live="polite">
83
+ {#if pending}
84
+ <Loader label={loadingLabel} />
85
+ {:else}
86
+ {value}
87
+ {/if}
88
+ </span>
89
+ <div class="number-stepper-button">
90
+ <Button
91
+ bind:this={incrementButton}
92
+ ariaLabel={incrementLabel}
93
+ disabled={!canIncrement}
94
+ onclick={() => change(value + step, incrementButton)}
95
+ {...typeof testId === 'string' ? { testId: `${testId}-increment` } : {}}
96
+ >
97
+ {#if typeof incrementIcon === 'function'}
98
+ {@render incrementIcon(value)}
99
+ {:else}
100
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
101
+ {@html addSvg}
102
+ {/if}
103
+ </Button>
104
+ </div>
105
+ </div>
106
+
107
+ <style>
108
+ .number-stepper {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ gap: var(--number-stepper-gap, 0);
112
+ padding: var(--number-stepper-padding, 0);
113
+ border: var(--number-stepper-border, 1px solid currentColor);
114
+ border-radius: var(--number-stepper-border-radius, 6px);
115
+ background: var(--number-stepper-background, transparent);
116
+ color: var(--number-stepper-color, currentColor);
117
+ }
118
+
119
+ .number-stepper-button {
120
+ --button-color: var(--number-stepper-button-background, transparent);
121
+ --button-text-color: var(--number-stepper-button-color, currentColor);
122
+ --button-hover-color: var(--number-stepper-button-hover-background, transparent);
123
+ --button-padding: var(--number-stepper-button-padding, 6px);
124
+ --button-border: none;
125
+ --button-border-radius: var(--number-stepper-button-border-radius, 6px);
126
+ --button-disabled-opacity: var(--number-stepper-button-disabled-opacity, 0.4);
127
+ display: flex;
128
+ }
129
+
130
+ .pending .number-stepper-button {
131
+ --button-cursor: var(--number-stepper-pending-cursor, progress);
132
+ opacity: var(--number-stepper-button-disabled-opacity, 0.4);
133
+ }
134
+
135
+ .number-stepper-button :global(svg) {
136
+ width: var(--number-stepper-icon-size, 14px);
137
+ height: var(--number-stepper-icon-size, 14px);
138
+ }
139
+
140
+ .number-stepper-value {
141
+ --loader-width: var(--number-stepper-loader-size, 14px);
142
+ --loader-height: var(--number-stepper-loader-size, 14px);
143
+ --loader-before-width: calc(var(--number-stepper-loader-size, 14px) / 2);
144
+ --loader-before-height: calc(var(--number-stepper-loader-size, 14px) / 2);
145
+ --loader-after-width: calc(var(--number-stepper-loader-size, 14px) * 0.75);
146
+ --loader-after-height: calc(var(--number-stepper-loader-size, 14px) * 0.75);
147
+ display: flex;
148
+ justify-content: center;
149
+ min-width: var(--number-stepper-value-min-width, 24px);
150
+ font-size: var(--number-stepper-value-font-size, 14px);
151
+ font-weight: var(--number-stepper-value-font-weight, 500);
152
+ font-variant-numeric: var(--number-stepper-value-font-variant-numeric, tabular-nums);
153
+ }
154
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { NumberStepperProperties } from './properties';
2
+ declare const NumberStepper: import("svelte").Component<NumberStepperProperties, {}, "">;
3
+ type NumberStepper = ReturnType<typeof NumberStepper>;
4
+ export default NumberStepper;
@@ -0,0 +1,22 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type NumberStepperProperties = MandatoryNumberStepperProperties & OptionalNumberStepperProperties & NumberStepperEventProperties;
3
+ export type MandatoryNumberStepperProperties = {
4
+ value: number;
5
+ };
6
+ export type OptionalNumberStepperProperties = {
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ disabled?: boolean;
11
+ ariaLabel?: string;
12
+ decrementLabel?: string;
13
+ incrementLabel?: string;
14
+ loadingLabel?: string;
15
+ decrementIcon?: Snippet<[number]>;
16
+ incrementIcon?: Snippet<[number]>;
17
+ testId?: string;
18
+ classes?: string;
19
+ };
20
+ export type NumberStepperEventProperties = {
21
+ onchange?: ((value: number) => void) | ((value: number) => Promise<void>);
22
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -6,6 +6,7 @@
6
6
  let {
7
7
  text,
8
8
  dismissible = false,
9
+ dismissLabel = 'Dismiss',
9
10
  disabled = false,
10
11
  testId,
11
12
  dismissIcon,
@@ -58,7 +59,7 @@
58
59
  <Button
59
60
  {disabled}
60
61
  onclick={handleDismiss}
61
- ariaLabel="Dismiss"
62
+ ariaLabel={dismissLabel}
62
63
  {...typeof testId === 'string' ? { testId: `${testId}-dismiss` } : {}}
63
64
  >
64
65
  {#if typeof dismissIcon === 'function'}
@@ -5,6 +5,7 @@ export type MandatoryPillProperties = {
5
5
  };
6
6
  export type OptionalPillProperties = {
7
7
  dismissible?: boolean;
8
+ dismissLabel?: string;
8
9
  disabled?: boolean;
9
10
  testId?: string;
10
11
  dismissIcon?: Snippet;
@@ -1,19 +1,88 @@
1
1
  <script lang="ts">
2
2
  import type { ProgressProperties } from './properties';
3
3
 
4
- let { value, max = 100, showLabel = false, testId, classes }: ProgressProperties = $props();
4
+ let {
5
+ value,
6
+ max = 100,
7
+ showLabel = false,
8
+ ariaLabel,
9
+ valueText,
10
+ milestones = [],
11
+ milestone,
12
+ testId,
13
+ classes
14
+ }: ProgressProperties = $props();
5
15
 
6
- let percentage = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
16
+ let clampedValue = $derived(Math.min(max, Math.max(0, value)));
17
+ let percentage = $derived(max > 0 ? (clampedValue / max) * 100 : 0);
7
18
  let isIndeterminate = $derived(value < 0);
19
+
20
+ let stops = $derived.by(() => {
21
+ let previous = 0;
22
+ return [...milestones]
23
+ .sort((a, b) => a.value - b.value)
24
+ .map((stop) => {
25
+ const position = Math.min(max, Math.max(0, stop.value));
26
+ const span = position - previous;
27
+ previous = position;
28
+ return { stop, span, reached: !isIndeterminate && value >= stop.value };
29
+ });
30
+ });
31
+ let trailingSpan = $derived(max - Math.min(max, Math.max(0, stops.at(-1)?.stop.value ?? max)));
32
+ let hasMilestoneLabels = $derived(
33
+ stops.some(({ stop }) => typeof stop.label === 'string' && stop.label.length > 0)
34
+ );
8
35
  </script>
9
36
 
10
37
  <div class="container {classes ?? ''}" data-pw={testId}>
11
- <div class="track">
12
- <div
13
- class="bar"
14
- class:indeterminate={isIndeterminate}
15
- style:width={isIndeterminate ? null : `${percentage}%`}
16
- ></div>
38
+ <div class="meter">
39
+ <div class="rail" class:with-milestones={stops.length > 0}>
40
+ <div class="track-layer">
41
+ <div
42
+ class="track"
43
+ role="progressbar"
44
+ aria-label={ariaLabel ?? 'Progress'}
45
+ aria-valuemin={0}
46
+ aria-valuemax={max}
47
+ aria-valuenow={isIndeterminate ? null : clampedValue}
48
+ aria-valuetext={valueText}
49
+ >
50
+ <div
51
+ class="bar"
52
+ class:indeterminate={isIndeterminate}
53
+ style:width={isIndeterminate ? null : `${percentage}%`}
54
+ ></div>
55
+ </div>
56
+ </div>
57
+ {#if stops.length > 0}
58
+ <div class="milestones">
59
+ {#each stops as { stop, span, reached }, index (index)}
60
+ <div class="segment" style:flex-grow={span}>
61
+ <div class="milestone" class:reached>
62
+ {#if typeof milestone === 'function'}
63
+ {@render milestone(stop, reached)}
64
+ {:else}
65
+ <span class="milestone-marker"></span>
66
+ {/if}
67
+ </div>
68
+ </div>
69
+ {/each}
70
+ {#if trailingSpan > 0}
71
+ <div class="segment" style:flex-grow={trailingSpan}></div>
72
+ {/if}
73
+ </div>
74
+ {/if}
75
+ </div>
76
+ {#if hasMilestoneLabels}
77
+ <div class="milestone-labels">
78
+ {#each stops as { stop, span }, index (index)}
79
+ <span class="segment milestone-label" style:flex-grow={span}>{stop.label ?? ''}</span>
80
+ {/each}
81
+ {#if trailingSpan > 0}
82
+ <span class="segment" style:flex-grow={trailingSpan}></span>
83
+ {/if}
84
+ </div>
85
+ {/if}
17
86
  </div>
18
87
  {#if showLabel && !isIndeterminate}
19
88
  <div class="label">{Math.round(percentage)}%</div>
@@ -29,6 +98,32 @@
29
98
  gap: var(--progress-container-gap, 8px);
30
99
  }
31
100
 
101
+ .meter {
102
+ flex: 1;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: var(--progress-milestone-label-gap, 8px);
106
+ }
107
+
108
+ .rail {
109
+ position: relative;
110
+ display: flex;
111
+ }
112
+
113
+ .track-layer {
114
+ flex: 1;
115
+ display: flex;
116
+ align-items: center;
117
+ }
118
+
119
+ .with-milestones .track-layer {
120
+ position: absolute;
121
+ top: 0;
122
+ right: 0;
123
+ bottom: 0;
124
+ left: 0;
125
+ }
126
+
32
127
  .track {
33
128
  flex: 1;
34
129
  height: var(--progress-track-height, 8px);
@@ -49,6 +144,49 @@
49
144
  animation: indeterminate var(--progress-indeterminate-duration, 1.5s) ease-in-out infinite;
50
145
  }
51
146
 
147
+ .milestones {
148
+ flex: 1;
149
+ display: flex;
150
+ position: relative;
151
+ }
152
+
153
+ .segment {
154
+ flex-basis: 0;
155
+ flex-shrink: 1;
156
+ min-width: 0;
157
+ display: flex;
158
+ justify-content: flex-end;
159
+ }
160
+
161
+ .milestone {
162
+ display: flex;
163
+ }
164
+
165
+ .milestone-marker {
166
+ width: var(--progress-milestone-size, 12px);
167
+ height: var(--progress-milestone-size, 12px);
168
+ border: var(--progress-milestone-border, 2px solid #ffffff);
169
+ border-radius: var(--progress-milestone-border-radius, 50%);
170
+ background: var(--progress-milestone-background, #e4e4e7);
171
+ box-sizing: border-box;
172
+ }
173
+
174
+ .reached .milestone-marker {
175
+ background: var(--progress-milestone-reached-background, currentColor);
176
+ }
177
+
178
+ .milestone-labels {
179
+ display: flex;
180
+ font-size: var(--progress-milestone-label-font-size, 12px);
181
+ font-weight: var(--progress-milestone-label-font-weight, 400);
182
+ color: var(--progress-milestone-label-color, currentColor);
183
+ font-family: var(--progress-milestone-label-font-family, inherit);
184
+ }
185
+
186
+ .milestone-label {
187
+ text-align: var(--progress-milestone-label-text-align, end);
188
+ }
189
+
52
190
  .label {
53
191
  font-size: var(--progress-label-font-size, 14px);
54
192
  font-weight: var(--progress-label-font-weight, 500);