ionbase-ui 0.48.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.
- package/LICENSE +21 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion.js +68 -0
- package/dist/components/Accordion.js.map +1 -0
- package/dist/components/Breadcrumb.d.ts +45 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb.js +31 -0
- package/dist/components/Breadcrumb.js.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -1
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox.js +4 -2
- package/dist/components/Checkbox.js.map +1 -1
- package/dist/components/Combobox.d.ts +86 -0
- package/dist/components/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox.js +261 -0
- package/dist/components/Combobox.js.map +1 -0
- package/dist/components/Drawer.d.ts +52 -0
- package/dist/components/Drawer.d.ts.map +1 -0
- package/dist/components/Drawer.js +66 -0
- package/dist/components/Drawer.js.map +1 -0
- package/dist/components/FileUpload.d.ts +79 -0
- package/dist/components/FileUpload.d.ts.map +1 -0
- package/dist/components/FileUpload.js +218 -0
- package/dist/components/FileUpload.js.map +1 -0
- package/dist/components/ProgressBar.d.ts +46 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +52 -0
- package/dist/components/ProgressBar.js.map +1 -0
- package/dist/components/Select.d.ts +9 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Select.js.map +1 -1
- package/dist/components/Skeleton.d.ts +39 -0
- package/dist/components/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton.js +50 -0
- package/dist/components/Skeleton.js.map +1 -0
- package/dist/components/Spinner.d.ts +45 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +44 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/Textarea.d.ts +50 -0
- package/dist/components/Textarea.d.ts.map +1 -0
- package/dist/components/Textarea.js +48 -0
- package/dist/components/Textarea.js.map +1 -0
- package/dist/components/Toggle.d.ts +2 -1
- package/dist/components/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle.js +4 -2
- package/dist/components/Toggle.js.map +1 -1
- package/dist/components/index.d.ts +18 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +9 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/resolve-selection.d.ts +36 -0
- package/dist/components/resolve-selection.d.ts.map +1 -0
- package/dist/components/resolve-selection.js +14 -0
- package/dist/components/resolve-selection.js.map +1 -0
- package/dist/figma-descriptions.json +204 -144
- package/dist/figma-map.json +2311 -1643
- package/dist/meta/Accordion.json +123 -0
- package/dist/meta/AccordionItem.json +90 -0
- package/dist/meta/Breadcrumb.json +87 -0
- package/dist/meta/BreadcrumbItem.json +78 -0
- package/dist/meta/Checkbox.json +18 -1
- package/dist/meta/Combobox.json +265 -0
- package/dist/meta/Drawer.json +234 -0
- package/dist/meta/EmptyState.json +1 -1
- package/dist/meta/FileUpload.json +246 -0
- package/dist/meta/Input.json +0 -1
- package/dist/meta/PhoneInput.json +39 -39
- package/dist/meta/Popover.json +3 -3
- package/dist/meta/ProgressBar.json +166 -0
- package/dist/meta/Select.json +4 -3
- package/dist/meta/Skeleton.json +120 -0
- package/dist/meta/Spinner.json +123 -0
- package/dist/meta/Textarea.json +520 -0
- package/dist/meta/Toggle.json +18 -1
- package/dist/meta/Tooltip.json +3 -3
- package/dist/meta/components.json +2447 -361
- package/dist/meta/contrast.json +3130 -227
- package/dist/meta/index.json +149 -7
- package/dist/meta/patterns/index.json +1 -1
- package/dist/styles/accordion.css +102 -0
- package/dist/styles/breadcrumb.css +72 -0
- package/dist/styles/combobox.css +154 -0
- package/dist/styles/drawer.css +181 -0
- package/dist/styles/empty-state.css +16 -4
- package/dist/styles/file-upload.css +236 -0
- package/dist/styles/index.css +9 -0
- package/dist/styles/input.css +34 -15
- package/dist/styles/progress-bar.css +107 -0
- package/dist/styles/skeleton.css +76 -0
- package/dist/styles/spinner.css +70 -0
- package/dist/styles/textarea.css +178 -0
- package/dist/styles/tokens/base.css +0 -1
- package/dist/styles/tokens/theme-dark.css +0 -1
- package/dist/tokens/index.d.ts +0 -6
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +0 -5
- package/dist/tokens/index.js.map +1 -1
- package/llms.txt +2 -2
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/styles/index.css
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
@import url('./alert.css');
|
|
33
33
|
@import url('./badge.css');
|
|
34
34
|
@import url('./input.css');
|
|
35
|
+
@import url('./textarea.css');
|
|
35
36
|
@import url('./phone-input.css');
|
|
36
37
|
@import url('./select.css');
|
|
37
38
|
@import url('./checkbox.css');
|
|
@@ -66,6 +67,14 @@
|
|
|
66
67
|
@import url('./citation.css');
|
|
67
68
|
@import url('./confidence-indicator.css');
|
|
68
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');
|
|
69
78
|
|
|
70
79
|
/*
|
|
71
80
|
* Visually hidden, but not hidden from assistive tech.
|
package/dist/styles/input.css
CHANGED
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
* padding box, so `padding-x: 12px` would put the text at 13px. The border
|
|
22
22
|
* width is subtracted back out — the same correction Badge and Tabs needed.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* stroke
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* The subtraction is against the LIVE border width, which used to matter:
|
|
25
|
+
* focus was a 2px inside stroke that ate a further 1px of padding, and
|
|
26
|
+
* subtracting it kept the text from shifting. Every state is 1px as of
|
|
27
|
+
* 5 Sep 2026, so the correction is now constant — but it stays written this
|
|
28
|
+
* way, because it is correct either way and the next state to need a
|
|
29
|
+
* different width gets it for free.
|
|
28
30
|
*/
|
|
29
31
|
.ion-input {
|
|
30
32
|
/*
|
|
@@ -172,9 +174,10 @@
|
|
|
172
174
|
* hover -> focus -> invalid -> read-only -> disabled.
|
|
173
175
|
*
|
|
174
176
|
* Invalid sits after focus and overrides the colour only, never the width. A
|
|
175
|
-
* focused invalid field therefore
|
|
176
|
-
* colour
|
|
177
|
-
*
|
|
177
|
+
* focused invalid field therefore shows the error colour rather than the focus
|
|
178
|
+
* colour — both states are true at once and the more urgent one is shown. That
|
|
179
|
+
* ordering mattered more when the two differed in width as well; it is still
|
|
180
|
+
* the behaviour, and still deliberate.
|
|
178
181
|
*/
|
|
179
182
|
|
|
180
183
|
/*
|
|
@@ -192,23 +195,39 @@
|
|
|
192
195
|
border-color: var(--border-strong);
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
/*
|
|
199
|
+
* 1px, not the 2px this used to carry. See the note on --invalid below: both
|
|
200
|
+
* states were thinned on the same design call, and the trade is the same one.
|
|
201
|
+
*/
|
|
195
202
|
.ion-input:focus-within,
|
|
196
203
|
.ion-input[data-focused='true'] {
|
|
197
|
-
--ion-input-border-width: var(--border-width-thick);
|
|
198
|
-
|
|
199
204
|
border-color: var(--border-focus);
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
/*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
208
|
+
* 1px. THIS COMMENT USED TO ARGUE FOR 2px AND THE ARGUMENT WAS SOUND, so read
|
|
209
|
+
* it before changing anything back:
|
|
210
|
+
*
|
|
211
|
+
* "2px, matching Focus and matching Select. At 1px the invalid state differed
|
|
212
|
+
* from default only in hue, which is the exact failure mode WCAG 1.4.1 is
|
|
213
|
+
* about — error has to be perceivable without relying on colour alone.
|
|
214
|
+
* Reconciled in Figma; Input was the one that was wrong."
|
|
215
|
+
*
|
|
216
|
+
* That defect was found once and fixed once, and thinning the border reopens
|
|
217
|
+
* it: an invalid field now differs from a default field by hue alone. It was
|
|
218
|
+
* changed on an explicit design call on 5 Sep 2026, applied to Input and
|
|
219
|
+
* Textarea and to their Figma sets, and it is recorded here rather than left to
|
|
220
|
+
* look like drift.
|
|
221
|
+
*
|
|
222
|
+
* Select, Checkbox, Radio, Tabs and Menu still carry border-width/thick, so
|
|
223
|
+
* these two are deliberately out of step with the rest of the system.
|
|
224
|
+
*
|
|
225
|
+
* IF THE ERROR STATE NEEDS A NON-COLOUR CUE AGAIN, this is the decision to
|
|
226
|
+
* revisit — and an icon or a message would satisfy 1.4.1 without the border,
|
|
227
|
+
* which is the better fix than putting the 2px back.
|
|
207
228
|
*/
|
|
208
229
|
.ion-input--invalid,
|
|
209
230
|
.ion-input[data-invalid='true'] {
|
|
210
|
-
--ion-input-border-width: var(--border-width-thick);
|
|
211
|
-
|
|
212
231
|
border-color: var(--border-error-strong);
|
|
213
232
|
}
|
|
214
233
|
|
|
@@ -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
|
+
}
|