ionbase-ui 0.51.0 → 0.59.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/Accordion.d.ts +54 -0
  3. package/dist/components/Accordion.d.ts.map +1 -0
  4. package/dist/components/Accordion.js +68 -0
  5. package/dist/components/Accordion.js.map +1 -0
  6. package/dist/components/Breadcrumb.d.ts +45 -0
  7. package/dist/components/Breadcrumb.d.ts.map +1 -0
  8. package/dist/components/Breadcrumb.js +31 -0
  9. package/dist/components/Breadcrumb.js.map +1 -0
  10. package/dist/components/Checkbox.d.ts +2 -1
  11. package/dist/components/Checkbox.d.ts.map +1 -1
  12. package/dist/components/Checkbox.js +4 -2
  13. package/dist/components/Checkbox.js.map +1 -1
  14. package/dist/components/Combobox.d.ts +86 -0
  15. package/dist/components/Combobox.d.ts.map +1 -0
  16. package/dist/components/Combobox.js +261 -0
  17. package/dist/components/Combobox.js.map +1 -0
  18. package/dist/components/Drawer.d.ts +52 -0
  19. package/dist/components/Drawer.d.ts.map +1 -0
  20. package/dist/components/Drawer.js +66 -0
  21. package/dist/components/Drawer.js.map +1 -0
  22. package/dist/components/FileUpload.d.ts +79 -0
  23. package/dist/components/FileUpload.d.ts.map +1 -0
  24. package/dist/components/FileUpload.js +218 -0
  25. package/dist/components/FileUpload.js.map +1 -0
  26. package/dist/components/ProgressBar.d.ts +46 -0
  27. package/dist/components/ProgressBar.d.ts.map +1 -0
  28. package/dist/components/ProgressBar.js +52 -0
  29. package/dist/components/ProgressBar.js.map +1 -0
  30. package/dist/components/Select.d.ts +9 -2
  31. package/dist/components/Select.d.ts.map +1 -1
  32. package/dist/components/Select.js.map +1 -1
  33. package/dist/components/Skeleton.d.ts +39 -0
  34. package/dist/components/Skeleton.d.ts.map +1 -0
  35. package/dist/components/Skeleton.js +50 -0
  36. package/dist/components/Skeleton.js.map +1 -0
  37. package/dist/components/Spinner.d.ts +45 -0
  38. package/dist/components/Spinner.d.ts.map +1 -0
  39. package/dist/components/Spinner.js +44 -0
  40. package/dist/components/Spinner.js.map +1 -0
  41. package/dist/components/Toggle.d.ts +2 -1
  42. package/dist/components/Toggle.d.ts.map +1 -1
  43. package/dist/components/Toggle.js +4 -2
  44. package/dist/components/Toggle.js.map +1 -1
  45. package/dist/components/index.d.ts +16 -0
  46. package/dist/components/index.d.ts.map +1 -1
  47. package/dist/components/index.js +8 -0
  48. package/dist/components/index.js.map +1 -1
  49. package/dist/components/resolve-selection.d.ts +36 -0
  50. package/dist/components/resolve-selection.d.ts.map +1 -0
  51. package/dist/components/resolve-selection.js +14 -0
  52. package/dist/components/resolve-selection.js.map +1 -0
  53. package/dist/figma-descriptions.json +201 -146
  54. package/dist/figma-map.json +2296 -1684
  55. package/dist/meta/Accordion.json +123 -0
  56. package/dist/meta/AccordionItem.json +90 -0
  57. package/dist/meta/Breadcrumb.json +87 -0
  58. package/dist/meta/BreadcrumbItem.json +78 -0
  59. package/dist/meta/Checkbox.json +18 -1
  60. package/dist/meta/Combobox.json +265 -0
  61. package/dist/meta/Drawer.json +234 -0
  62. package/dist/meta/EmptyState.json +1 -1
  63. package/dist/meta/FileUpload.json +246 -0
  64. package/dist/meta/PhoneInput.json +39 -39
  65. package/dist/meta/Popover.json +3 -3
  66. package/dist/meta/ProgressBar.json +166 -0
  67. package/dist/meta/Select.json +4 -3
  68. package/dist/meta/Skeleton.json +120 -0
  69. package/dist/meta/Spinner.json +123 -0
  70. package/dist/meta/Toggle.json +18 -1
  71. package/dist/meta/Tooltip.json +3 -3
  72. package/dist/meta/components.json +1837 -270
  73. package/dist/meta/contrast.json +2873 -194
  74. package/dist/meta/index.json +128 -7
  75. package/dist/meta/patterns/index.json +1 -1
  76. package/dist/styles/accordion.css +102 -0
  77. package/dist/styles/breadcrumb.css +72 -0
  78. package/dist/styles/combobox.css +154 -0
  79. package/dist/styles/drawer.css +181 -0
  80. package/dist/styles/empty-state.css +16 -4
  81. package/dist/styles/file-upload.css +236 -0
  82. package/dist/styles/index.css +8 -0
  83. package/dist/styles/progress-bar.css +107 -0
  84. package/dist/styles/skeleton.css +76 -0
  85. package/dist/styles/spinner.css +70 -0
  86. package/llms.txt +2 -2
  87. package/package.json +19 -19
@@ -0,0 +1,181 @@
1
+ /*
2
+ * Drawer
3
+ *
4
+ * Drawn in Figma as `Drawer` (1365:2404), from this file. Twelve variants:
5
+ * four placements by three sizes, the same cross product the props expose.
6
+ *
7
+ * The panel IS the block — `.ion-drawer`, not `.ion-drawer__panel` — which
8
+ * matches popover.css and is load-bearing rather than cosmetic. The contrast
9
+ * gate resolves a translucent hover colour's backdrop from the block's own
10
+ * background, so with the surface on an element class the close button's hover
11
+ * pairing came back "skipped": unmeasured, and silently so.
12
+ *
13
+ * The scrim lives here for the same reason it lives in modal.css: nothing can
14
+ * render an overlay without also rendering what sits behind it, so the split
15
+ * Figma makes between panel and overlay has no meaning in code.
16
+ *
17
+ * Placement uses logical properties throughout — `inset-inline-start` rather
18
+ * than `left` — so `start` and `end` follow writing direction and a right-hand
19
+ * drawer in English is a left-hand one in Arabic without a second variant.
20
+ */
21
+ .ion-drawer__scrim {
22
+ position: fixed;
23
+ inset: 0;
24
+ z-index: 1000;
25
+ display: flex;
26
+ background-color: var(--surface-scrim);
27
+ }
28
+
29
+ .ion-drawer {
30
+ --ion-drawer-size: 30rem;
31
+
32
+ position: fixed;
33
+ display: flex;
34
+ flex-direction: column;
35
+ max-width: 100%;
36
+ max-height: 100%;
37
+ background-color: var(--surface-raised);
38
+ color: var(--text-default);
39
+ font-family: var(--font-family-sans);
40
+ }
41
+
42
+ .ion-drawer--sm {
43
+ --ion-drawer-size: 22rem;
44
+ }
45
+
46
+ .ion-drawer--lg {
47
+ --ion-drawer-size: 40rem;
48
+ }
49
+
50
+ /* Edge panels are square against their own edge and rounded on the inner one:
51
+ * a drawer rounded on all four corners reads as a floating card that happens
52
+ * to touch the side. */
53
+ .ion-drawer--start,
54
+ .ion-drawer--end {
55
+ inset-block: 0;
56
+ width: var(--ion-drawer-size);
57
+ }
58
+
59
+ .ion-drawer--start {
60
+ inset-inline-start: 0;
61
+ border-start-end-radius: var(--radius-lg);
62
+ border-end-end-radius: var(--radius-lg);
63
+ }
64
+
65
+ .ion-drawer--end {
66
+ inset-inline-end: 0;
67
+ border-start-start-radius: var(--radius-lg);
68
+ border-end-start-radius: var(--radius-lg);
69
+ }
70
+
71
+ .ion-drawer--top,
72
+ .ion-drawer--bottom {
73
+ inset-inline: 0;
74
+ height: var(--ion-drawer-size);
75
+ width: 100%;
76
+ }
77
+
78
+ .ion-drawer--top {
79
+ inset-block-start: 0;
80
+ border-end-start-radius: var(--radius-lg);
81
+ border-end-end-radius: var(--radius-lg);
82
+ }
83
+
84
+ .ion-drawer--bottom {
85
+ inset-block-end: 0;
86
+ border-start-start-radius: var(--radius-lg);
87
+ border-start-end-radius: var(--radius-lg);
88
+ }
89
+
90
+ .ion-drawer__header {
91
+ display: flex;
92
+ align-items: flex-start;
93
+ justify-content: space-between;
94
+ gap: var(--spacing-12);
95
+ flex: none;
96
+ padding: var(--spacing-24);
97
+ border-bottom: var(--border-width-default) solid var(--border-subtle);
98
+ }
99
+
100
+ .ion-drawer__heading {
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: var(--spacing-4);
104
+ min-width: 0;
105
+ }
106
+
107
+ .ion-drawer__title {
108
+ margin: 0;
109
+ font-size: var(--type-h5);
110
+ line-height: var(--type-h5-line-height);
111
+ font-weight: var(--font-weight-semibold);
112
+ }
113
+
114
+ .ion-drawer__description {
115
+ margin: 0;
116
+ color: var(--text-secondary);
117
+ font-size: var(--type-body-sm);
118
+ line-height: var(--type-body-sm-line-height);
119
+ }
120
+
121
+ .ion-drawer__close {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ flex: none;
126
+ width: var(--spacing-32);
127
+ height: var(--spacing-32);
128
+ padding: var(--spacing-4);
129
+ border: 0;
130
+ border-radius: var(--radius-sm);
131
+ background: none;
132
+ color: var(--icon-secondary);
133
+ cursor: pointer;
134
+ }
135
+
136
+ /*
137
+ * Background only, no colour change — the same rule Modal's close button uses,
138
+ * and for a measurable reason as well as a consistency one. `surface/hover` is
139
+ * translucent, so a rule that changes BOTH creates a foreground/background pair
140
+ * whose backdrop the contrast gate cannot resolve, and it reports the pairing
141
+ * as skipped. A skipped pairing is an unmeasured one.
142
+ */
143
+ @media (hover: hover) {
144
+ .ion-drawer__close:hover {
145
+ background-color: var(--surface-hover);
146
+ }
147
+ }
148
+
149
+ .ion-drawer__close:focus-visible {
150
+ outline: var(--border-width-thick) solid var(--border-focus);
151
+ outline-offset: 2px;
152
+ }
153
+
154
+ .ion-drawer__close svg {
155
+ width: var(--icon-size-md);
156
+ height: var(--icon-size-md);
157
+ }
158
+
159
+ /*
160
+ * The body is the only scrolling region. A drawer whose whole panel scrolls
161
+ * takes its own title off screen, which is the one piece of context a user
162
+ * needs while working down a long list.
163
+ */
164
+ .ion-drawer__body {
165
+ flex: 1;
166
+ min-height: 0;
167
+ overflow-y: auto;
168
+ padding: var(--spacing-24);
169
+ font-size: var(--type-body);
170
+ line-height: var(--type-body-line-height);
171
+ }
172
+
173
+ .ion-drawer__footer {
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: flex-end;
177
+ gap: var(--spacing-8);
178
+ flex: none;
179
+ padding: var(--spacing-24);
180
+ border-top: var(--border-width-default) solid var(--border-subtle);
181
+ }
@@ -1,9 +1,11 @@
1
1
  /*
2
2
  * EmptyState
3
3
  *
4
- * Repo-owned, not measured from Figma — there is no Empty State component in
5
- * the file yet. Every value below is a token already on a ladder, so when one
6
- * is drawn the two can be reconciled without moving anything underneath.
4
+ * Drawn in Figma on 15 Sep 2026, as `Empty State` — 12 variants, Size x Reason.
5
+ * It was the last component in this package with no Figma counterpart, and it
6
+ * went the unusual direction: built here first from the pattern tier's
7
+ * requirement, then drawn to match. Every value was already a token on a
8
+ * ladder, which is what made the reconciliation cost one line.
7
9
  *
8
10
  * REASON CHANGES THE ICON COLOUR AND NOTHING ELSE
9
11
  *
@@ -117,7 +119,17 @@
117
119
  color: var(--text-default);
118
120
  font-size: var(--ion-empty-state-title);
119
121
  line-height: var(--ion-empty-state-title-line-height);
120
- font-weight: var(--font-weight-medium);
122
+
123
+ /*
124
+ * Semibold, not medium, settled when the component was drawn. Every heading
125
+ * style in the Figma file — `Heading/H3`, `Heading/H4` — is semibold, so a
126
+ * medium title here would have been the only heading-sized text in the system
127
+ * off the ramp, and would have needed a text style that exists nowhere else.
128
+ * The `inline` size sits at body-md rather than a heading rung, and its style
129
+ * (`Body/Medium Semibold`) was added to the file alongside the component, in
130
+ * the same shape as the `Body/Default Semibold` already there.
131
+ */
132
+ font-weight: var(--font-weight-semibold);
121
133
 
122
134
  /* A filter value or record id in the title must wrap, not widen the region. */
123
135
  overflow-wrap: anywhere;
@@ -0,0 +1,236 @@
1
+ /*
2
+ * FileUpload
3
+ *
4
+ * Drawn in Figma as `File Upload` (1367:2333), from this file. Nothing here was
5
+ * invented: the border, radius, background and disabled treatment are
6
+ * `Input`'s, and the centred icon-over-text stack with its padding ladder is
7
+ * `EmptyState`'s. The bet when this was written was that drawing it would move
8
+ * values and not structure; drawing it moved neither.
9
+ *
10
+ * Figma draws the DROP ZONE only, not the label and helper around it — the same
11
+ * split `Input` makes against `Form Field`. The file list has no Figma
12
+ * counterpart at all, because it is runtime content rather than a state.
13
+ *
14
+ * THE VISUALLY HIDDEN INPUT IS NOT `display: none`
15
+ *
16
+ * A `display: none` input leaves the tab order and the accessibility tree, and
17
+ * the component then has no control in it at all — which is how the common
18
+ * build of this component ends up keyboard-unreachable while looking identical.
19
+ * `.ion-visually-hidden` would work, but the input is also positioned to sit
20
+ * inside the zone so a browser can anchor its own validation bubble somewhere
21
+ * sensible rather than at the top-left of the page.
22
+ */
23
+ .ion-file-upload {
24
+ --ion-file-upload-padding: var(--spacing-32);
25
+ --ion-file-upload-gap: var(--spacing-8);
26
+ --ion-file-upload-icon-size: var(--icon-size-lg);
27
+
28
+ position: relative;
29
+ display: flex;
30
+ background-color: var(--surface-default);
31
+ border: var(--border-width-default) dashed var(--border-default);
32
+ border-radius: var(--radius-md);
33
+ transition:
34
+ border-color var(--ion-duration-base) var(--ion-ease-out),
35
+ background-color var(--ion-duration-base) var(--ion-ease-out);
36
+ }
37
+
38
+ .ion-file-upload--sm {
39
+ --ion-file-upload-padding: var(--spacing-20);
40
+ --ion-file-upload-gap: var(--spacing-6);
41
+ --ion-file-upload-icon-size: var(--icon-size-md);
42
+ }
43
+
44
+ /*
45
+ * 1px, not 0. The input has to have a box for the focus ring to be drawn
46
+ * against and for a validation bubble to point at; collapsing it to nothing
47
+ * puts both in the document corner. It is clipped, so the 1px is never seen.
48
+ */
49
+ .ion-file-upload__input {
50
+ position: absolute;
51
+ bottom: var(--spacing-8);
52
+ left: 50%;
53
+ width: 1px;
54
+ height: 1px;
55
+ padding: 0;
56
+ overflow: hidden;
57
+ clip-path: inset(50%);
58
+ border: 0;
59
+ }
60
+
61
+ .ion-file-upload__target {
62
+ display: flex;
63
+ flex: 1 1 auto;
64
+ flex-direction: column;
65
+ align-items: center;
66
+ justify-content: center;
67
+ gap: var(--ion-file-upload-gap);
68
+ padding: var(--ion-file-upload-padding) var(--spacing-24);
69
+ text-align: center;
70
+ cursor: pointer;
71
+ }
72
+
73
+ .ion-file-upload__icon {
74
+ display: inline-flex;
75
+ color: var(--icon-secondary);
76
+ }
77
+
78
+ .ion-file-upload__icon svg {
79
+ width: var(--ion-file-upload-icon-size);
80
+ height: var(--ion-file-upload-icon-size);
81
+ }
82
+
83
+ .ion-file-upload__prompt {
84
+ color: var(--text-default);
85
+ font-family: var(--font-family-sans);
86
+ font-size: var(--type-body);
87
+ line-height: var(--type-body-line-height);
88
+ font-weight: var(--font-weight-medium);
89
+ }
90
+
91
+ .ion-file-upload__hint {
92
+ color: var(--text-tertiary);
93
+ font-family: var(--font-family-sans);
94
+ font-size: var(--type-body-sm);
95
+ line-height: var(--type-body-sm-line-height);
96
+ }
97
+
98
+ @media (hover: hover) {
99
+ .ion-file-upload:hover {
100
+ border-color: var(--border-strong);
101
+ background-color: var(--surface-hover);
102
+ }
103
+ }
104
+
105
+ /*
106
+ * The focus ring is on the ZONE, driven by the hidden input's focus. The ring
107
+ * belongs where the user's eye is, and the input is one clipped pixel.
108
+ */
109
+ .ion-file-upload:focus-within {
110
+ outline: var(--border-width-thick) solid var(--ring-focus);
111
+ outline-offset: 2px;
112
+ }
113
+
114
+ /*
115
+ * Dragging reads as a solid border, not a colour change alone. A drop target
116
+ * that only shifts hue is invisible to anyone who cannot distinguish the two —
117
+ * WCAG 1.4.1, and it is also just easier to see.
118
+ */
119
+ .ion-file-upload--dragging,
120
+ .ion-file-upload[data-dragging='true'] {
121
+ border-style: solid;
122
+ border-color: var(--border-focus);
123
+ background-color: var(--surface-primary-subtle);
124
+ }
125
+
126
+ .ion-file-upload--invalid,
127
+ .ion-file-upload[data-invalid='true'] {
128
+ border-color: var(--border-error);
129
+ }
130
+
131
+ .ion-file-upload--disabled,
132
+ .ion-file-upload[data-disabled='true'] {
133
+ background-color: var(--surface-disabled);
134
+ border-color: var(--border-disabled);
135
+ }
136
+
137
+ .ion-file-upload--disabled .ion-file-upload__target,
138
+ .ion-file-upload[data-disabled='true'] .ion-file-upload__target {
139
+ cursor: not-allowed;
140
+ }
141
+
142
+ .ion-file-upload--disabled .ion-file-upload__prompt,
143
+ .ion-file-upload[data-disabled='true'] .ion-file-upload__prompt {
144
+ color: var(--text-disabled);
145
+ }
146
+
147
+ .ion-file-upload--disabled .ion-file-upload__icon,
148
+ .ion-file-upload[data-disabled='true'] .ion-file-upload__icon {
149
+ color: var(--icon-disabled);
150
+ }
151
+
152
+ /*
153
+ * The list of held files is a sibling of the zone, not a child of it.
154
+ *
155
+ * Inside the zone it would sit inside the `<label>`, which makes every row —
156
+ * including each remove button — a click target for the file picker. Removing
157
+ * a file would open the picker on the way out.
158
+ */
159
+ .ion-file-upload__list {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: var(--spacing-6);
163
+ margin: 0;
164
+ padding: 0;
165
+ list-style: none;
166
+ }
167
+
168
+ .ion-file-upload__file {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: var(--spacing-8);
172
+ min-height: var(--spacing-32);
173
+ padding-inline: var(--spacing-12);
174
+ background-color: var(--surface-subtle);
175
+ border-radius: var(--radius-sm);
176
+ font-family: var(--font-family-sans);
177
+ font-size: var(--type-body-sm);
178
+ line-height: var(--type-body-sm-line-height);
179
+ }
180
+
181
+ .ion-file-upload__name {
182
+ flex: 1 1 auto;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ color: var(--text-default);
186
+ text-overflow: ellipsis;
187
+ white-space: nowrap;
188
+ }
189
+
190
+ .ion-file-upload__size {
191
+ flex-shrink: 0;
192
+ color: var(--text-tertiary);
193
+ font-variant-numeric: tabular-nums;
194
+ }
195
+
196
+ /*
197
+ * 24px square — WCAG 2.2 SC 2.5.8's minimum pointer target, around a 16px
198
+ * glyph. Do not shrink it to the icon.
199
+ */
200
+ .ion-file-upload__remove {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ flex-shrink: 0;
205
+ width: var(--spacing-24);
206
+ height: var(--spacing-24);
207
+ padding: 0;
208
+ background: none;
209
+ border: none;
210
+ border-radius: var(--radius-sm);
211
+ color: var(--icon-secondary);
212
+ cursor: pointer;
213
+ transition: background-color var(--ion-duration-base) var(--ion-ease-out);
214
+ }
215
+
216
+ .ion-file-upload__remove svg {
217
+ width: var(--icon-size-sm);
218
+ height: var(--icon-size-sm);
219
+ }
220
+
221
+ @media (hover: hover) {
222
+ .ion-file-upload__remove:hover {
223
+ background-color: var(--surface-hover);
224
+ color: var(--icon-default);
225
+ }
226
+ }
227
+
228
+ .ion-file-upload__remove:focus-visible {
229
+ outline: var(--border-width-thick) solid var(--ring-focus);
230
+ outline-offset: 2px;
231
+ }
232
+
233
+ .ion-file-upload__remove:disabled {
234
+ color: var(--icon-disabled);
235
+ cursor: not-allowed;
236
+ }
@@ -67,6 +67,14 @@
67
67
  @import url('./citation.css');
68
68
  @import url('./confidence-indicator.css');
69
69
  @import url('./empty-state.css');
70
+ @import url('./spinner.css');
71
+ @import url('./progress-bar.css');
72
+ @import url('./skeleton.css');
73
+ @import url('./breadcrumb.css');
74
+ @import url('./accordion.css');
75
+ @import url('./drawer.css');
76
+ @import url('./file-upload.css');
77
+ @import url('./combobox.css');
70
78
 
71
79
  /*
72
80
  * Visually hidden, but not hidden from assistive tech.
@@ -0,0 +1,107 @@
1
+ /*
2
+ * ProgressBar
3
+ *
4
+ * The fill width arrives as a custom property rather than an inline `width`,
5
+ * so the only inline value a caller ever sees is a percentage on a token-named
6
+ * property — and `no-raw-style-values` stays true rather than exempted.
7
+ */
8
+ .ion-progress-bar {
9
+ --ion-progress-bar-height: var(--spacing-8);
10
+ --ion-progress-bar-fill: var(--surface-primary);
11
+ --ion-progress-bar-pct: 0%;
12
+
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--spacing-4);
16
+ font-family: var(--font-family-sans);
17
+ }
18
+
19
+ .ion-progress-bar--sm {
20
+ --ion-progress-bar-height: var(--spacing-4);
21
+ }
22
+
23
+ .ion-progress-bar--success {
24
+ --ion-progress-bar-fill: var(--surface-success);
25
+ }
26
+
27
+ .ion-progress-bar--warning {
28
+ --ion-progress-bar-fill: var(--surface-warning);
29
+ }
30
+
31
+ .ion-progress-bar--error {
32
+ --ion-progress-bar-fill: var(--surface-error);
33
+ }
34
+
35
+ .ion-progress-bar__header {
36
+ display: flex;
37
+ align-items: baseline;
38
+ justify-content: space-between;
39
+ gap: var(--spacing-8);
40
+ font-size: var(--type-body-sm);
41
+ line-height: var(--type-body-sm-line-height);
42
+ }
43
+
44
+ .ion-progress-bar__label {
45
+ color: var(--text-default);
46
+ }
47
+
48
+ .ion-progress-bar__value {
49
+ color: var(--text-secondary);
50
+ font-variant-numeric: tabular-nums;
51
+ }
52
+
53
+ .ion-progress-bar__track {
54
+ overflow: hidden;
55
+ height: var(--ion-progress-bar-height);
56
+ background-color: var(--surface-muted);
57
+ border-radius: var(--radius-full);
58
+ }
59
+
60
+ .ion-progress-bar__fill {
61
+ width: var(--ion-progress-bar-pct);
62
+ height: 100%;
63
+ background-color: var(--ion-progress-bar-fill);
64
+ border-radius: inherit;
65
+ transition: width var(--ion-duration-base) var(--ion-ease-out);
66
+ }
67
+
68
+ /*
69
+ * Indeterminate is a travelling segment rather than a filling bar: a bar that
70
+ * fills and resets reads as repeated failure and restart.
71
+ */
72
+ .ion-progress-bar--indeterminate .ion-progress-bar__fill {
73
+ width: 40%;
74
+ animation: ion-progress-bar-slide var(--ion-duration-slow)
75
+ var(--ion-ease-in-out) infinite;
76
+ }
77
+
78
+ @keyframes ion-progress-bar-slide {
79
+ from {
80
+ transform: translateX(-100%);
81
+ }
82
+
83
+ to {
84
+ transform: translateX(250%);
85
+ }
86
+ }
87
+
88
+ @media (prefers-reduced-motion: reduce) {
89
+ .ion-progress-bar__fill {
90
+ transition-duration: 1ms;
91
+ }
92
+
93
+ /* Still moving, still slow — see Spinner for the same reasoning. */
94
+ .ion-progress-bar--indeterminate .ion-progress-bar__fill {
95
+ animation-duration: 3000ms;
96
+ }
97
+ }
98
+
99
+ @media (forced-colors: active) {
100
+ .ion-progress-bar__track {
101
+ border: var(--border-width-default) solid CanvasText;
102
+ }
103
+
104
+ .ion-progress-bar__fill {
105
+ background-color: Highlight;
106
+ }
107
+ }
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Skeleton
3
+ *
4
+ * A pulse on one surface token rather than a sweeping gradient. The gradient
5
+ * version is prettier and costs more than it looks: `verify-contrast.mjs`
6
+ * resolves gradient stops as real pairings, so every skeleton would enter the
7
+ * contrast budget as text-shaped colour it is not — and a placeholder has no
8
+ * text to be legible.
9
+ */
10
+ .ion-skeleton {
11
+ --ion-skeleton-width: 100%;
12
+ --ion-skeleton-height: var(--spacing-16);
13
+ --ion-skeleton-radius: var(--radius-sm);
14
+
15
+ width: var(--ion-skeleton-width);
16
+ height: var(--ion-skeleton-height);
17
+ background-color: var(--surface-muted);
18
+ border-radius: var(--ion-skeleton-radius);
19
+ animation: ion-skeleton-pulse var(--ion-duration-slow) var(--ion-ease-in-out)
20
+ infinite;
21
+ }
22
+
23
+ .ion-skeleton-group {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--spacing-8);
27
+ width: var(--ion-skeleton-width, 100%);
28
+ }
29
+
30
+ /* Text sits on the line box so a skeleton paragraph occupies the height the
31
+ * real paragraph will, and the layout does not jump when content lands. */
32
+ .ion-skeleton--text {
33
+ --ion-skeleton-height: var(--type-body-line-height);
34
+ }
35
+
36
+ /* The last line short, because real paragraphs end mid-measure. */
37
+ .ion-skeleton--last {
38
+ --ion-skeleton-width: 60%;
39
+ }
40
+
41
+ .ion-skeleton--circle {
42
+ --ion-skeleton-height: var(--icon-size-lg);
43
+ --ion-skeleton-width: var(--icon-size-lg);
44
+ --ion-skeleton-radius: var(--radius-full);
45
+
46
+ flex: none;
47
+ }
48
+
49
+ .ion-skeleton--rect {
50
+ --ion-skeleton-height: var(--spacing-64);
51
+ --ion-skeleton-radius: var(--radius-md);
52
+ }
53
+
54
+ @keyframes ion-skeleton-pulse {
55
+ 50% {
56
+ opacity: 0.55;
57
+ }
58
+ }
59
+
60
+ /*
61
+ * Held still, unlike Spinner and ProgressBar — and the difference is the point.
62
+ * Those two are the only evidence that a wait is live, so they keep moving
63
+ * slowly. A skeleton says nothing a static block does not, so for a user who
64
+ * asked for less motion it simply stops.
65
+ */
66
+ @media (prefers-reduced-motion: reduce) {
67
+ .ion-skeleton {
68
+ animation: none;
69
+ }
70
+ }
71
+
72
+ @media (forced-colors: active) {
73
+ .ion-skeleton {
74
+ border: var(--border-width-default) solid GrayText;
75
+ }
76
+ }