ionbase-ui 0.1.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/README.md +74 -0
- package/dist/Icon.d.ts +72 -0
- package/dist/Icon.d.ts.map +1 -0
- package/dist/Icon.js +50 -0
- package/dist/Icon.js.map +1 -0
- package/dist/components/Avatar.d.ts +52 -0
- package/dist/components/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar.js +59 -0
- package/dist/components/Avatar.js.map +1 -0
- package/dist/components/Badge.d.ts +26 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +17 -0
- package/dist/components/Badge.js.map +1 -0
- package/dist/components/Button.d.ts +18 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +43 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Checkbox.d.ts +30 -0
- package/dist/components/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox.js +39 -0
- package/dist/components/Checkbox.js.map +1 -0
- package/dist/components/Divider.d.ts +17 -0
- package/dist/components/Divider.d.ts.map +1 -0
- package/dist/components/Divider.js +16 -0
- package/dist/components/Divider.js.map +1 -0
- package/dist/components/Header.d.ts +25 -0
- package/dist/components/Header.d.ts.map +1 -0
- package/dist/components/Header.js +18 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/Input.d.ts +32 -0
- package/dist/components/Input.d.ts.map +1 -0
- package/dist/components/Input.js +66 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/Logo.d.ts +41 -0
- package/dist/components/Logo.d.ts.map +1 -0
- package/dist/components/Logo.js +70 -0
- package/dist/components/Logo.js.map +1 -0
- package/dist/components/Menu.d.ts +29 -0
- package/dist/components/Menu.d.ts.map +1 -0
- package/dist/components/Menu.js +31 -0
- package/dist/components/Menu.js.map +1 -0
- package/dist/components/NavItem.d.ts +30 -0
- package/dist/components/NavItem.d.ts.map +1 -0
- package/dist/components/NavItem.js +45 -0
- package/dist/components/NavItem.js.map +1 -0
- package/dist/components/Radio.d.ts +30 -0
- package/dist/components/Radio.d.ts.map +1 -0
- package/dist/components/Radio.js +61 -0
- package/dist/components/Radio.js.map +1 -0
- package/dist/components/ScrollProgress.d.ts +33 -0
- package/dist/components/ScrollProgress.d.ts.map +1 -0
- package/dist/components/ScrollProgress.js +115 -0
- package/dist/components/ScrollProgress.js.map +1 -0
- package/dist/components/Select.d.ts +43 -0
- package/dist/components/Select.d.ts.map +1 -0
- package/dist/components/Select.js +80 -0
- package/dist/components/Select.js.map +1 -0
- package/dist/components/Table.d.ts +71 -0
- package/dist/components/Table.d.ts.map +1 -0
- package/dist/components/Table.js +60 -0
- package/dist/components/Table.js.map +1 -0
- package/dist/components/Tabs.d.ts +23 -0
- package/dist/components/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs.js +47 -0
- package/dist/components/Tabs.js.map +1 -0
- package/dist/components/Toggle.d.ts +25 -0
- package/dist/components/Toggle.d.ts.map +1 -0
- package/dist/components/Toggle.js +32 -0
- package/dist/components/Toggle.js.map +1 -0
- package/dist/components/index.d.ts +33 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +17 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/styles/avatar.css +138 -0
- package/dist/styles/badge.css +125 -0
- package/dist/styles/button.css +331 -0
- package/dist/styles/checkbox.css +229 -0
- package/dist/styles/divider.css +31 -0
- package/dist/styles/header.css +95 -0
- package/dist/styles/index.css +44 -0
- package/dist/styles/input.css +264 -0
- package/dist/styles/logo.css +63 -0
- package/dist/styles/menu.css +150 -0
- package/dist/styles/nav-item.css +120 -0
- package/dist/styles/radio.css +200 -0
- package/dist/styles/scroll-progress.css +171 -0
- package/dist/styles/select.css +162 -0
- package/dist/styles/table.css +192 -0
- package/dist/styles/tabs.css +219 -0
- package/dist/styles/toggle.css +184 -0
- package/dist/styles/tokens/base.css +386 -0
- package/dist/styles/tokens/breakpoint-mobile.css +34 -0
- package/dist/styles/tokens/breakpoint-tablet.css +25 -0
- package/dist/styles/tokens/elevation.css +112 -0
- package/dist/styles/tokens/index.css +6 -0
- package/dist/styles/tokens/theme-dark.css +106 -0
- package/dist/styles/tokens/typography.css +124 -0
- package/dist/tokens/index.d.ts +2097 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +1702 -0
- package/dist/tokens/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Checkbox
|
|
3
|
+
*
|
|
4
|
+
* Measured from Figma `Checkbox` (54 variants: 3 colours x 3 sizes x 6 states).
|
|
5
|
+
*
|
|
6
|
+
* The indicator is a sibling of the label, not a wrapper, because Figma lays
|
|
7
|
+
* them out as a row with a gap. The native input is visually hidden rather than
|
|
8
|
+
* replaced: it keeps the control focusable, announceable and form-associated,
|
|
9
|
+
* and the styled square follows it via :checked / :focus-visible.
|
|
10
|
+
*
|
|
11
|
+
* THE MARK IS DERIVED
|
|
12
|
+
*
|
|
13
|
+
* The mark is 12 / 14 / 16 against boxes of 16 / 20 / 24. 14 is on no ladder —
|
|
14
|
+
* both spacing and icon-size skip it — but the mark is not arbitrary:
|
|
15
|
+
*
|
|
16
|
+
* mark = box / 2 + 4 16->12 20->14 24->16
|
|
17
|
+
*
|
|
18
|
+
* So the only number stated per size is the box, and nothing here reaches for
|
|
19
|
+
* the raw `scale/*` ramp. Same move as the toggle track and the avatar overlap:
|
|
20
|
+
* an off-ladder value is usually a question about what it is derived from.
|
|
21
|
+
*
|
|
22
|
+
* FOCUS RING
|
|
23
|
+
*
|
|
24
|
+
* Figma draws a separate `Focus Ring` frame 2px larger than the indicator on
|
|
25
|
+
* every side (16 -> 20, 20 -> 24, 24 -> 28). A CSS outline does exactly that at
|
|
26
|
+
* `outline-offset: 0`, so the ring needs no element of its own and cannot
|
|
27
|
+
* disturb the row's layout — which a real 24px sibling would.
|
|
28
|
+
*/
|
|
29
|
+
.ion-checkbox {
|
|
30
|
+
--ion-checkbox-size: var(--spacing-20);
|
|
31
|
+
--ion-checkbox-gap: var(--spacing-8);
|
|
32
|
+
--ion-checkbox-radius: var(--radius-xs);
|
|
33
|
+
--ion-checkbox-font-size: var(--type-body);
|
|
34
|
+
--ion-checkbox-line-height: var(--type-body-line-height);
|
|
35
|
+
|
|
36
|
+
/* Filled by the intent modifier; unchecked is intent-independent. */
|
|
37
|
+
--ion-checkbox-fill: var(--surface-primary);
|
|
38
|
+
--ion-checkbox-border: var(--border-primary-strong);
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* The checked box is bevelled, not flat. `Raised/Flush/*` — flush, with no
|
|
42
|
+
* cast shadow, because a 16-24px control sits ON the surface rather than
|
|
43
|
+
* above it. Indexed by size like Button's, and identical across all three
|
|
44
|
+
* intents: Figma varies only the fill.
|
|
45
|
+
*/
|
|
46
|
+
--ion-checkbox-raised: var(--ion-shadow-raised-flush-sm);
|
|
47
|
+
|
|
48
|
+
/* Derived — see the header. */
|
|
49
|
+
--ion-checkbox-mark: calc(var(--ion-checkbox-size) / 2 + var(--spacing-4));
|
|
50
|
+
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: flex-start;
|
|
53
|
+
gap: var(--ion-checkbox-gap);
|
|
54
|
+
font-family: var(--font-family-sans);
|
|
55
|
+
font-size: var(--ion-checkbox-font-size);
|
|
56
|
+
line-height: var(--ion-checkbox-line-height);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.ion-checkbox--sm {
|
|
61
|
+
--ion-checkbox-size: var(--spacing-16);
|
|
62
|
+
--ion-checkbox-font-size: var(--type-body-sm);
|
|
63
|
+
--ion-checkbox-line-height: var(--type-body-sm-line-height);
|
|
64
|
+
--ion-checkbox-raised: var(--ion-shadow-raised-flush-xs);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Large is the only size that widens the gap, and the only one with a larger
|
|
68
|
+
* corner — Figma moves it from radius/xs to radius/sm at 24px. */
|
|
69
|
+
.ion-checkbox--lg {
|
|
70
|
+
--ion-checkbox-size: var(--spacing-24);
|
|
71
|
+
--ion-checkbox-gap: var(--spacing-12);
|
|
72
|
+
--ion-checkbox-radius: var(--radius-sm);
|
|
73
|
+
--ion-checkbox-raised: var(--ion-shadow-raised-flush-lg);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* The native input carries every bit of behaviour and none of the appearance.
|
|
78
|
+
* Kept in the layout (not `display: none`) so it stays focusable and reachable
|
|
79
|
+
* by assistive tech; the indicator beside it renders the state.
|
|
80
|
+
*/
|
|
81
|
+
.ion-checkbox__input {
|
|
82
|
+
position: absolute;
|
|
83
|
+
width: 1px;
|
|
84
|
+
height: 1px;
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 0;
|
|
87
|
+
border: none;
|
|
88
|
+
opacity: 0;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ion-checkbox__indicator {
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
width: var(--ion-checkbox-size);
|
|
98
|
+
height: var(--ion-checkbox-size);
|
|
99
|
+
background-color: var(--surface-default);
|
|
100
|
+
border-style: solid;
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* The EMPTY box carries the heavier border: 2px `border/stronger`, where the
|
|
104
|
+
* filled box drops to 1px of its intent colour. An unchecked checkbox is
|
|
105
|
+
* nothing but its outline, so it has to hold the row on its own; once filled,
|
|
106
|
+
* the fill does that work and a thinner edge stops it reading as ringed.
|
|
107
|
+
*
|
|
108
|
+
* Figma strokes this INSIDE, and `box-sizing: border-box` (set globally in
|
|
109
|
+
* index.css) is what reproduces that — the box stays 16/20/24 and the border
|
|
110
|
+
* eats inward. Without it, switching to 2px would grow every checkbox by 2px.
|
|
111
|
+
*
|
|
112
|
+
* Radio matches this exactly; the two controls stay in step.
|
|
113
|
+
*/
|
|
114
|
+
border-width: var(--border-width-thick);
|
|
115
|
+
border-color: var(--border-stronger);
|
|
116
|
+
border-radius: var(--ion-checkbox-radius);
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
* Centred on the FIRST LINE of the label, not the whole block, so a wrapping
|
|
120
|
+
* label keeps its box beside line one. The label's line box is taller than
|
|
121
|
+
* the indicator at every size, so the offset is half the difference.
|
|
122
|
+
*/
|
|
123
|
+
margin-top: calc(
|
|
124
|
+
(var(--ion-checkbox-line-height) - var(--ion-checkbox-size)) / 2
|
|
125
|
+
);
|
|
126
|
+
transition:
|
|
127
|
+
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
128
|
+
border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.ion-checkbox__mark {
|
|
132
|
+
width: var(--ion-checkbox-mark);
|
|
133
|
+
height: var(--ion-checkbox-mark);
|
|
134
|
+
color: var(--icon-on-color);
|
|
135
|
+
|
|
136
|
+
/* Hidden rather than unmounted so the box never reflows when it appears. */
|
|
137
|
+
opacity: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.ion-checkbox__mark svg {
|
|
141
|
+
display: block;
|
|
142
|
+
width: 100%;
|
|
143
|
+
height: 100%;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ion-checkbox__label {
|
|
147
|
+
color: var(--text-secondary);
|
|
148
|
+
user-select: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Checked and indeterminate share one appearance — Figma differs only in the mark. */
|
|
152
|
+
.ion-checkbox__input:checked + .ion-checkbox__indicator,
|
|
153
|
+
.ion-checkbox__input:indeterminate + .ion-checkbox__indicator {
|
|
154
|
+
/* Back to 1px — see the indicator's border comment. */
|
|
155
|
+
border-width: var(--border-width-default);
|
|
156
|
+
background-color: var(--ion-checkbox-fill);
|
|
157
|
+
border-color: var(--ion-checkbox-border);
|
|
158
|
+
|
|
159
|
+
/* Only the filled box is bevelled; an empty box stays flat. */
|
|
160
|
+
box-shadow: var(--ion-checkbox-raised);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.ion-checkbox__input:checked + .ion-checkbox__indicator .ion-checkbox__mark,
|
|
164
|
+
.ion-checkbox__input:indeterminate
|
|
165
|
+
+ .ion-checkbox__indicator
|
|
166
|
+
.ion-checkbox__mark {
|
|
167
|
+
opacity: 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Intent modifiers. Only the checked fill and border differ. */
|
|
171
|
+
.ion-checkbox--neutral {
|
|
172
|
+
--ion-checkbox-fill: var(--surface-inverse);
|
|
173
|
+
--ion-checkbox-border: var(--border-inverse);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ion-checkbox--danger {
|
|
177
|
+
--ion-checkbox-fill: var(--surface-error);
|
|
178
|
+
--ion-checkbox-border: var(--border-error-strong);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* See the header: the ring is an outline, so it grows outside the box without
|
|
183
|
+
* taking part in layout. 2px at offset 0 reproduces Figma's +2px-per-side frame.
|
|
184
|
+
*/
|
|
185
|
+
.ion-checkbox__input:focus-visible + .ion-checkbox__indicator,
|
|
186
|
+
.ion-checkbox[data-focused='true'] .ion-checkbox__indicator {
|
|
187
|
+
outline: var(--border-width-thick) solid var(--border-focus);
|
|
188
|
+
outline-offset: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Disabled is driven off the input's own `:disabled`, not off a class on the
|
|
193
|
+
* wrapper, because it has to BEAT `:checked`.
|
|
194
|
+
*
|
|
195
|
+
* `.ion-checkbox--disabled .ion-checkbox__indicator` is (0,2,0) while
|
|
196
|
+
* `.ion-checkbox__input:checked + .ion-checkbox__indicator` is (0,3,0), so a
|
|
197
|
+
* disabled checked box would have kept its filled appearance no matter where
|
|
198
|
+
* the rule sat in the file. Matching `:checked:disabled` is (0,4,0) and wins.
|
|
199
|
+
*/
|
|
200
|
+
.ion-checkbox__input:disabled + .ion-checkbox__indicator,
|
|
201
|
+
.ion-checkbox__input:checked:disabled + .ion-checkbox__indicator,
|
|
202
|
+
.ion-checkbox__input:indeterminate:disabled + .ion-checkbox__indicator {
|
|
203
|
+
background-color: var(--surface-disabled);
|
|
204
|
+
border-color: var(--border-disabled);
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
* 2px in all three disabled states, checked included — measured. So this has
|
|
208
|
+
* to restate the width to beat `:checked`, which sets 1px; the compound
|
|
209
|
+
* `:checked:disabled` selector is what makes it win, not source order.
|
|
210
|
+
*/
|
|
211
|
+
border-width: var(--border-width-thick);
|
|
212
|
+
|
|
213
|
+
/* A disabled box is flat whether or not it is checked. */
|
|
214
|
+
box-shadow: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.ion-checkbox__input:disabled + .ion-checkbox__indicator .ion-checkbox__mark {
|
|
218
|
+
color: var(--icon-disabled);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.ion-checkbox--disabled,
|
|
222
|
+
.ion-checkbox[data-disabled='true'] {
|
|
223
|
+
cursor: not-allowed;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.ion-checkbox--disabled .ion-checkbox__label,
|
|
227
|
+
.ion-checkbox[data-disabled='true'] .ion-checkbox__label {
|
|
228
|
+
color: var(--text-disabled);
|
|
229
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Divider
|
|
3
|
+
*
|
|
4
|
+
* Measured from Figma `Border` (70:22153) — a single 1px line, `border/strong`
|
|
5
|
+
* rather than `border/default`. That is the one surprising fact here: every
|
|
6
|
+
* other hairline in the system (Input, Table row rules, Menu) reads
|
|
7
|
+
* `border/default`. A divider is drawn at a stronger weight deliberately —
|
|
8
|
+
* its only job is to separate, so it is allowed to be the most visible line
|
|
9
|
+
* in the system rather than competing with content-carrying borders.
|
|
10
|
+
*
|
|
11
|
+
* Horizontal and vertical are one component in Figma (`Style` variant), and
|
|
12
|
+
* one class here: a fixed 1px cross-axis with the main axis left to `100%`,
|
|
13
|
+
* since Figma's 80-unit length is canvas demo filler, not a real constraint.
|
|
14
|
+
*/
|
|
15
|
+
.ion-divider {
|
|
16
|
+
flex-shrink: 0;
|
|
17
|
+
background-color: var(--border-strong);
|
|
18
|
+
border: none;
|
|
19
|
+
margin: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ion-divider--horizontal {
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: var(--border-width-default);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ion-divider--vertical {
|
|
28
|
+
width: var(--border-width-default);
|
|
29
|
+
height: 100%;
|
|
30
|
+
align-self: stretch;
|
|
31
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Header
|
|
3
|
+
*
|
|
4
|
+
* Measured from Figma `Header` (70:22088). Three device variants:
|
|
5
|
+
*
|
|
6
|
+
* Desktop 56 tall, padding 8 / 64, end-slot gap 12
|
|
7
|
+
* Tablet 64 tall, padding 12 / 32, end-slot gap 8
|
|
8
|
+
* Mobile 64 tall, padding 12 / 16, end-slot gap 8
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS IS MEDIA QUERIES AND NOT A PROP
|
|
11
|
+
*
|
|
12
|
+
* Device is the one variant axis in the whole system that the browser already
|
|
13
|
+
* knows the answer to. Every other component takes its variant from the caller
|
|
14
|
+
* because the caller is the only one who knows; a header is Mobile because the
|
|
15
|
+
* viewport is narrow, and asking React to duplicate that is how the two get out
|
|
16
|
+
* of step. The breakpoints match the Breakpoint collection's own container
|
|
17
|
+
* widths — Tablet below 1216, Mobile below 896.
|
|
18
|
+
*
|
|
19
|
+
* Note the header is taller on Tablet and Mobile than on Desktop (64 vs 56).
|
|
20
|
+
* That looks inverted but is deliberate in the design: touch targets need the
|
|
21
|
+
* height more than the desktop pointer does.
|
|
22
|
+
*/
|
|
23
|
+
.ion-header {
|
|
24
|
+
--ion-header-height: var(--spacing-64);
|
|
25
|
+
--ion-header-padding-y: var(--spacing-12);
|
|
26
|
+
--ion-header-padding-x: var(--spacing-16);
|
|
27
|
+
--ion-header-end-gap: var(--spacing-8);
|
|
28
|
+
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
gap: var(--spacing-8);
|
|
33
|
+
width: 100%;
|
|
34
|
+
min-height: var(--ion-header-height);
|
|
35
|
+
padding: var(--ion-header-padding-y) var(--ion-header-padding-x);
|
|
36
|
+
background-color: var(--surface-default);
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* A bottom rule only. Figma reports the stroke weight as mixed because just
|
|
40
|
+
* the bottom edge carries one, which is exactly what a header divider is.
|
|
41
|
+
*/
|
|
42
|
+
border-bottom: var(--border-width-default) solid var(--border-default);
|
|
43
|
+
font-family: var(--font-family-sans);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (width >= 896px) {
|
|
47
|
+
.ion-header {
|
|
48
|
+
--ion-header-padding-x: var(--spacing-32);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (width >= 1216px) {
|
|
53
|
+
.ion-header {
|
|
54
|
+
/* 56 is off the spacing scale (it jumps 48 -> 64), so it is stated as
|
|
55
|
+
* arithmetic on two rungs rather than as a literal. */
|
|
56
|
+
--ion-header-height: calc(var(--spacing-64) - var(--spacing-8));
|
|
57
|
+
--ion-header-padding-y: var(--spacing-8);
|
|
58
|
+
--ion-header-padding-x: var(--spacing-64);
|
|
59
|
+
--ion-header-end-gap: var(--spacing-12);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ion-header__brand {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: var(--spacing-4);
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
color: var(--icon-default);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* The centre slot is hidden on Tablet and Mobile in Figma — both variants ship
|
|
73
|
+
* it collapsed to zero. It is display:none rather than visibility:hidden so it
|
|
74
|
+
* does not hold width the narrow layouts do not have.
|
|
75
|
+
*/
|
|
76
|
+
.ion-header__center {
|
|
77
|
+
display: none;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--spacing-8);
|
|
80
|
+
flex: 1 1 auto;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (width >= 1216px) {
|
|
85
|
+
.ion-header__center {
|
|
86
|
+
display: flex;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.ion-header__end {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: var(--ion-header-end-gap);
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Host+Grotesk:ital,wght@0,300..800;1,300..800&family=Merriweather:ital,wght@0,300..900;1,300..900&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&family=STIX+Two+Text:ital,wght@0,400..700;1,400..700&display=swap');
|
|
2
|
+
@import url('./tokens/index.css');
|
|
3
|
+
@import url('./button.css');
|
|
4
|
+
@import url('./tabs.css');
|
|
5
|
+
@import url('./badge.css');
|
|
6
|
+
@import url('./input.css');
|
|
7
|
+
@import url('./select.css');
|
|
8
|
+
@import url('./checkbox.css');
|
|
9
|
+
@import url('./radio.css');
|
|
10
|
+
@import url('./toggle.css');
|
|
11
|
+
@import url('./menu.css');
|
|
12
|
+
@import url('./avatar.css');
|
|
13
|
+
@import url('./header.css');
|
|
14
|
+
@import url('./logo.css');
|
|
15
|
+
@import url('./table.css');
|
|
16
|
+
@import url('./divider.css');
|
|
17
|
+
@import url('./nav-item.css');
|
|
18
|
+
@import url('./scroll-progress.css');
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Every IonBase element is border-box.
|
|
22
|
+
*
|
|
23
|
+
* Not cosmetic: `<button>` inherits border-box from the UA stylesheet but
|
|
24
|
+
* `<div>` does not, so a component built on a div sized its min-height against
|
|
25
|
+
* the CONTENT box and came out 16px taller than the design. Button looked
|
|
26
|
+
* correct only by accident of its tag name.
|
|
27
|
+
*
|
|
28
|
+
* Scoped to `ion-` classes so the design system never restyles a host app.
|
|
29
|
+
*/
|
|
30
|
+
[class^='ion-'],
|
|
31
|
+
[class*=' ion-'] {
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Global baseline resets using token variables only */
|
|
36
|
+
body {
|
|
37
|
+
font-family: var(--font-family-sans);
|
|
38
|
+
font-size: var(--type-body);
|
|
39
|
+
line-height: var(--type-body-line-height);
|
|
40
|
+
background-color: var(--surface-page);
|
|
41
|
+
color: var(--text-default);
|
|
42
|
+
-webkit-font-smoothing: antialiased;
|
|
43
|
+
-moz-osx-font-smoothing: grayscale;
|
|
44
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Input
|
|
3
|
+
*
|
|
4
|
+
* Measured from Figma `Input` (80:275) and `Form Field` (80:330).
|
|
5
|
+
* Three sizes x seven states, plus the label/helper wrapper.
|
|
6
|
+
*
|
|
7
|
+
* GEOMETRY COMES FROM THE SPACING SCALE, NOT FROM A CONTROL RECIPE
|
|
8
|
+
*
|
|
9
|
+
* 1. Every measurement below is a rung on `spacing/*` (or the `icon-size`
|
|
10
|
+
* ladder), picked by this component. There is no shared "control" token
|
|
11
|
+
* prescribing what a medium field's padding should be — that group existed,
|
|
12
|
+
* was reverse-engineered from Button's numbers, and was bound by 3 of 26
|
|
13
|
+
* components because nothing whose measurements differed could use it.
|
|
14
|
+
*
|
|
15
|
+
* So Input picking 12 where Button picks 16 is not a divergence to explain
|
|
16
|
+
* away. It is a component fact, recorded here, which is the only place that
|
|
17
|
+
* knows it. The scale is the shared contract; the rung is not.
|
|
18
|
+
*
|
|
19
|
+
* 2. Figma draws its 1px stroke INSIDE the frame, so padding is measured from
|
|
20
|
+
* the outer edge and the stroke overlaps it. A CSS border sits outside the
|
|
21
|
+
* padding box, so `padding-x: 12px` would put the text at 13px. The border
|
|
22
|
+
* width is subtracted back out — the same correction Badge and Tabs needed.
|
|
23
|
+
*
|
|
24
|
+
* This also makes the focus state free: Figma's focus ring is a 2px inside
|
|
25
|
+
* stroke, which eats 1px further into the padding and leaves the text
|
|
26
|
+
* exactly where it was. Subtracting the live border width reproduces that,
|
|
27
|
+
* so nothing shifts when the field takes focus.
|
|
28
|
+
*/
|
|
29
|
+
.ion-input {
|
|
30
|
+
/*
|
|
31
|
+
* Size is a set of slots defaulting to Medium, so a size modifier reassigns
|
|
32
|
+
* six variables rather than restating six declarations — same shape as
|
|
33
|
+
* Button, and it keeps the icon rule size-agnostic.
|
|
34
|
+
*/
|
|
35
|
+
--ion-input-height: var(--spacing-40);
|
|
36
|
+
--ion-input-padding-x: var(--spacing-12);
|
|
37
|
+
--ion-input-gap: var(--spacing-8);
|
|
38
|
+
--ion-input-icon-size: var(--icon-size-md);
|
|
39
|
+
--ion-input-radius: var(--radius-md);
|
|
40
|
+
--ion-input-font-size: var(--type-body);
|
|
41
|
+
--ion-input-line-height: var(--type-body-line-height);
|
|
42
|
+
--ion-input-border-width: var(--border-width-default);
|
|
43
|
+
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: var(--ion-input-gap);
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Figma pins each size to a fixed height (32/40/48) with no vertical padding.
|
|
50
|
+
* `min-height` reproduces that while still growing if the field is ever given
|
|
51
|
+
* a taller control inside it.
|
|
52
|
+
*/
|
|
53
|
+
min-height: var(--ion-input-height);
|
|
54
|
+
padding-inline: calc(
|
|
55
|
+
var(--ion-input-padding-x) - var(--ion-input-border-width)
|
|
56
|
+
);
|
|
57
|
+
background-color: var(--surface-default);
|
|
58
|
+
border-style: solid;
|
|
59
|
+
border-width: var(--ion-input-border-width);
|
|
60
|
+
border-color: var(--border-default);
|
|
61
|
+
border-radius: var(--ion-input-radius);
|
|
62
|
+
font-family: var(--font-family-sans);
|
|
63
|
+
font-size: var(--ion-input-font-size);
|
|
64
|
+
line-height: var(--ion-input-line-height);
|
|
65
|
+
transition:
|
|
66
|
+
border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
67
|
+
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* Small shares Medium's padding. It was 10px and unbound in Figma — the only
|
|
72
|
+
* untokenised measurement in either component, and off a scale that runs
|
|
73
|
+
* 8, 12, 16, 20. Resolved in Figma by moving to 12; height, gap, radius and the
|
|
74
|
+
* type ramp still separate the two sizes.
|
|
75
|
+
*/
|
|
76
|
+
.ion-input--sm {
|
|
77
|
+
--ion-input-height: var(--spacing-32);
|
|
78
|
+
--ion-input-padding-x: var(--spacing-12);
|
|
79
|
+
--ion-input-gap: var(--spacing-6);
|
|
80
|
+
--ion-input-icon-size: var(--icon-size-sm);
|
|
81
|
+
--ion-input-radius: var(--radius-sm);
|
|
82
|
+
--ion-input-font-size: var(--type-body-sm);
|
|
83
|
+
--ion-input-line-height: var(--type-body-sm-line-height);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Large keeps Medium's radius, icon size and type ramp — only height and padding grow. */
|
|
87
|
+
.ion-input--lg {
|
|
88
|
+
--ion-input-height: var(--spacing-48);
|
|
89
|
+
--ion-input-padding-x: var(--spacing-16);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* The field itself carries no box of its own — the wrapper owns the border,
|
|
94
|
+
* background and padding so the icons sit inside it. Everything here is either
|
|
95
|
+
* inherited or reset.
|
|
96
|
+
*/
|
|
97
|
+
.ion-input__field {
|
|
98
|
+
flex: 1 1 auto;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
background-color: transparent;
|
|
102
|
+
border: none;
|
|
103
|
+
outline: none;
|
|
104
|
+
color: var(--text-secondary);
|
|
105
|
+
font-family: inherit;
|
|
106
|
+
font-size: inherit;
|
|
107
|
+
line-height: inherit;
|
|
108
|
+
font-weight: var(--font-weight-regular);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* Figma's Filled state differs from Default only in the value colour
|
|
113
|
+
* (text/tertiary -> text/secondary). That is exactly the placeholder
|
|
114
|
+
* distinction, so it needs no variant: the field is text/secondary and the
|
|
115
|
+
* placeholder pseudo-element is text/tertiary.
|
|
116
|
+
*/
|
|
117
|
+
.ion-input__field::placeholder {
|
|
118
|
+
color: var(--text-tertiary);
|
|
119
|
+
opacity: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ion-input__icon-start,
|
|
123
|
+
.ion-input__icon-end {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* One rule for every size — the slot is reassigned by the size modifier. */
|
|
131
|
+
.ion-input__icon-start svg,
|
|
132
|
+
.ion-input__icon-end svg {
|
|
133
|
+
width: var(--ion-input-icon-size);
|
|
134
|
+
height: var(--ion-input-icon-size);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* The two icons carry different tokens. The leading icon is decorative
|
|
139
|
+
* (icon/tertiary); the trailing one is a control — clear, reveal, calendar —
|
|
140
|
+
* and sits at icon/interactive. v2 splits text and icon precisely so this can
|
|
141
|
+
* differ from the value colour beside it.
|
|
142
|
+
*/
|
|
143
|
+
.ion-input__icon-start {
|
|
144
|
+
color: var(--icon-tertiary);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ion-input__icon-end {
|
|
148
|
+
color: var(--icon-interactive);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* State order below is deliberate and equal-specificity, so later rules win:
|
|
153
|
+
* hover -> focus -> invalid -> read-only -> disabled.
|
|
154
|
+
*
|
|
155
|
+
* Invalid sits after focus and overrides the colour only, never the width. A
|
|
156
|
+
* focused invalid field therefore keeps the 2px ring but shows it in the error
|
|
157
|
+
* colour, which is both states being true at once rather than one masking the
|
|
158
|
+
* other.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* Hover is declared twice, as it is on Button. Native `:hover` latches on touch
|
|
163
|
+
* until you tap elsewhere; `[data-hovered]` comes from React Aria's useHover,
|
|
164
|
+
* which is pointer-aware. The media query keeps this working without React.
|
|
165
|
+
*/
|
|
166
|
+
@media (hover: hover) {
|
|
167
|
+
.ion-input:hover {
|
|
168
|
+
border-color: var(--border-strong);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.ion-input[data-hovered='true'] {
|
|
173
|
+
border-color: var(--border-strong);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ion-input:focus-within,
|
|
177
|
+
.ion-input[data-focused='true'] {
|
|
178
|
+
--ion-input-border-width: var(--border-width-thick);
|
|
179
|
+
|
|
180
|
+
border-color: var(--border-focus);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* 2px, matching Focus and matching Select. At 1px the invalid state differed
|
|
185
|
+
* from default only in hue, which is the exact failure mode WCAG 1.4.1 is
|
|
186
|
+
* about — error has to be perceivable without relying on colour alone.
|
|
187
|
+
* Reconciled in Figma; Input was the one that was wrong.
|
|
188
|
+
*/
|
|
189
|
+
.ion-input--invalid,
|
|
190
|
+
.ion-input[data-invalid='true'] {
|
|
191
|
+
--ion-input-border-width: var(--border-width-thick);
|
|
192
|
+
|
|
193
|
+
border-color: var(--border-error-strong);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Read-only keeps the value legible but drops the field out of the editable
|
|
197
|
+
* palette entirely — page surface, subtle border. */
|
|
198
|
+
.ion-input--readonly,
|
|
199
|
+
.ion-input[data-readonly='true'] {
|
|
200
|
+
background-color: var(--surface-page);
|
|
201
|
+
border-color: var(--border-subtle);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.ion-input--disabled,
|
|
205
|
+
.ion-input[data-disabled='true'] {
|
|
206
|
+
background-color: var(--surface-disabled);
|
|
207
|
+
border-color: var(--border-disabled);
|
|
208
|
+
cursor: not-allowed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.ion-input--disabled .ion-input__field,
|
|
212
|
+
.ion-input[data-disabled='true'] .ion-input__field {
|
|
213
|
+
color: var(--text-disabled);
|
|
214
|
+
cursor: not-allowed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.ion-input--disabled .ion-input__field::placeholder,
|
|
218
|
+
.ion-input[data-disabled='true'] .ion-input__field::placeholder {
|
|
219
|
+
color: var(--text-disabled);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ion-input--disabled .ion-input__icon-start,
|
|
223
|
+
.ion-input--disabled .ion-input__icon-end,
|
|
224
|
+
.ion-input[data-disabled='true'] .ion-input__icon-start,
|
|
225
|
+
.ion-input[data-disabled='true'] .ion-input__icon-end {
|
|
226
|
+
color: var(--icon-disabled);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* Form Field — Figma `Form Field` (80:330).
|
|
231
|
+
*
|
|
232
|
+
* Label + control + helper stacked at 6px. Named `ion-field` rather than
|
|
233
|
+
* `ion-input__field-wrapper` because Select uses it unchanged: the wrapper
|
|
234
|
+
* knows nothing about what control sits in the middle.
|
|
235
|
+
*
|
|
236
|
+
* Only the Error variant differs from Default. Figma's Disabled variant leaves
|
|
237
|
+
* both the label and the helper on their default tokens, so it needs no rule —
|
|
238
|
+
* the control inside carries the whole disabled treatment.
|
|
239
|
+
*/
|
|
240
|
+
.ion-field {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
gap: var(--spacing-6);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.ion-field__label {
|
|
247
|
+
color: var(--text-secondary);
|
|
248
|
+
font-family: var(--font-family-sans);
|
|
249
|
+
font-size: var(--type-body-sm);
|
|
250
|
+
line-height: var(--type-body-sm-line-height);
|
|
251
|
+
font-weight: var(--font-weight-medium);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.ion-field__helper {
|
|
255
|
+
color: var(--text-tertiary);
|
|
256
|
+
font-family: var(--font-family-sans);
|
|
257
|
+
font-size: var(--type-body-sm);
|
|
258
|
+
line-height: var(--type-body-sm-line-height);
|
|
259
|
+
font-weight: var(--font-weight-regular);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.ion-field--error .ion-field__helper {
|
|
263
|
+
color: var(--text-error);
|
|
264
|
+
}
|