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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ionbase-ui — the whole design system in one package.
|
|
3
|
+
*
|
|
4
|
+
* Components, the icon wrapper, the stylesheets and the generated design
|
|
5
|
+
* tokens all ship from here. There is deliberately no second package: every
|
|
6
|
+
* release moved the four old ones in lockstep, so the split cost four
|
|
7
|
+
* manifests and a cross-package CSS import while buying no independent
|
|
8
|
+
* versioning.
|
|
9
|
+
*
|
|
10
|
+
* import 'ionbase-ui/styles'; // tokens + components, one import
|
|
11
|
+
* import { Button, Icon } from 'ionbase-ui';
|
|
12
|
+
*
|
|
13
|
+
* Icons are not bundled. `Icon` takes the icon as a prop, so any SVG component
|
|
14
|
+
* works — lucide-react, heroicons, react-icons, your own. See ./Icon.tsx.
|
|
15
|
+
*/
|
|
16
|
+
export * from './components/index.js';
|
|
17
|
+
export { Icon } from './Icon.js';
|
|
18
|
+
export type { IconProps, IconSize } from './Icon.js';
|
|
19
|
+
/**
|
|
20
|
+
* Generated token values, as `var(--…)` references. Copied in from the token
|
|
21
|
+
* pipeline at build time by scripts/sync-tokens.mjs — never hand-edited, and
|
|
22
|
+
* git-ignored, because Figma owns these names and values.
|
|
23
|
+
*/
|
|
24
|
+
export * as tokens from './tokens/index.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErD;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ionbase-ui — the whole design system in one package.
|
|
3
|
+
*
|
|
4
|
+
* Components, the icon wrapper, the stylesheets and the generated design
|
|
5
|
+
* tokens all ship from here. There is deliberately no second package: every
|
|
6
|
+
* release moved the four old ones in lockstep, so the split cost four
|
|
7
|
+
* manifests and a cross-package CSS import while buying no independent
|
|
8
|
+
* versioning.
|
|
9
|
+
*
|
|
10
|
+
* import 'ionbase-ui/styles'; // tokens + components, one import
|
|
11
|
+
* import { Button, Icon } from 'ionbase-ui';
|
|
12
|
+
*
|
|
13
|
+
* Icons are not bundled. `Icon` takes the icon as a prop, so any SVG component
|
|
14
|
+
* works — lucide-react, heroicons, react-icons, your own. See ./Icon.tsx.
|
|
15
|
+
*/
|
|
16
|
+
export * from './components/index.js';
|
|
17
|
+
export { Icon } from './Icon.js';
|
|
18
|
+
/**
|
|
19
|
+
* Generated token values, as `var(--…)` references. Copied in from the token
|
|
20
|
+
* pipeline at build time by scripts/sync-tokens.mjs — never hand-edited, and
|
|
21
|
+
* git-ignored, because Figma owns these names and values.
|
|
22
|
+
*/
|
|
23
|
+
export * as tokens from './tokens/index.js';
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Avatar
|
|
3
|
+
*
|
|
4
|
+
* Measured from Figma `Avatar` (77:22248) and `Avatar Group`.
|
|
5
|
+
*
|
|
6
|
+
* mini 24 type 12/18 icon 12 square radius/xs
|
|
7
|
+
* small 32 type 14/20 icon 16 square radius/sm
|
|
8
|
+
* medium 40 type 16/24 icon 20 square radius/md
|
|
9
|
+
* large 48 type 20/32 icon 24 square radius/md
|
|
10
|
+
*
|
|
11
|
+
* Circle is radius/full at every size; square steps up with the box, which is
|
|
12
|
+
* concentric-corner arithmetic rather than one shared corner.
|
|
13
|
+
*
|
|
14
|
+
* Surface is surface/muted with a 1px border/strong at every size and type —
|
|
15
|
+
* the border is what separates overlapping avatars in a group, so it is not
|
|
16
|
+
* decoration and must not be dropped.
|
|
17
|
+
*/
|
|
18
|
+
.ion-avatar {
|
|
19
|
+
--ion-avatar-size: var(--spacing-40);
|
|
20
|
+
--ion-avatar-radius: var(--radius-md);
|
|
21
|
+
--ion-avatar-font-size: var(--type-body);
|
|
22
|
+
--ion-avatar-line-height: var(--type-body-line-height);
|
|
23
|
+
--ion-avatar-icon: var(--icon-size-md);
|
|
24
|
+
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
flex-shrink: 0;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
width: var(--ion-avatar-size);
|
|
31
|
+
height: var(--ion-avatar-size);
|
|
32
|
+
background-color: var(--surface-muted);
|
|
33
|
+
border-style: solid;
|
|
34
|
+
border-width: var(--border-width-default);
|
|
35
|
+
border-color: var(--border-strong);
|
|
36
|
+
border-radius: var(--ion-avatar-radius);
|
|
37
|
+
color: var(--text-tertiary);
|
|
38
|
+
font-family: var(--font-family-sans);
|
|
39
|
+
font-size: var(--ion-avatar-font-size);
|
|
40
|
+
line-height: var(--ion-avatar-line-height);
|
|
41
|
+
font-weight: var(--font-weight-medium);
|
|
42
|
+
user-select: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ion-avatar--mini {
|
|
46
|
+
--ion-avatar-size: var(--spacing-24);
|
|
47
|
+
--ion-avatar-radius: var(--radius-xs);
|
|
48
|
+
--ion-avatar-font-size: var(--type-caption);
|
|
49
|
+
--ion-avatar-line-height: var(--type-caption-line-height);
|
|
50
|
+
--ion-avatar-icon: var(--icon-size-xs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ion-avatar--sm {
|
|
54
|
+
--ion-avatar-size: var(--spacing-32);
|
|
55
|
+
--ion-avatar-radius: var(--radius-sm);
|
|
56
|
+
--ion-avatar-font-size: var(--type-body-sm);
|
|
57
|
+
--ion-avatar-line-height: var(--type-body-sm-line-height);
|
|
58
|
+
--ion-avatar-icon: var(--icon-size-sm);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Large shares Medium's corner — the square ladder stops climbing at radius/md. */
|
|
62
|
+
.ion-avatar--lg {
|
|
63
|
+
--ion-avatar-size: var(--spacing-48);
|
|
64
|
+
--ion-avatar-font-size: var(--type-h5);
|
|
65
|
+
--ion-avatar-line-height: var(--type-h5-line-height);
|
|
66
|
+
--ion-avatar-icon: var(--icon-size-lg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ion-avatar--circle {
|
|
70
|
+
--ion-avatar-radius: var(--radius-full);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The image fills the box and is cropped by `overflow: hidden` on the parent,
|
|
74
|
+
* so it inherits the corner without repeating it. */
|
|
75
|
+
.ion-avatar__image {
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
object-fit: cover;
|
|
79
|
+
display: block;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ion-avatar__icon {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
color: var(--icon-tertiary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ion-avatar__icon svg {
|
|
90
|
+
width: var(--ion-avatar-icon);
|
|
91
|
+
height: var(--ion-avatar-icon);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* Avatar Group
|
|
96
|
+
*
|
|
97
|
+
* Figma overlaps the avatars with a NEGATIVE gap: -6 / -8 / -10 / -12, which is
|
|
98
|
+
* a quarter of each avatar's size. Negative space cannot be a spacing token —
|
|
99
|
+
* the scale has no negative rungs and inventing them to express overlap would
|
|
100
|
+
* be worse than the arithmetic. So the overlap is derived from the size, the
|
|
101
|
+
* same way the toggle's track is.
|
|
102
|
+
*
|
|
103
|
+
* STACKING DIRECTION IS FIGMA'S, AND IT IS THE LESS OBVIOUS ONE
|
|
104
|
+
*
|
|
105
|
+
* Each avatar paints OVER the one before it, so the last in the list — usually
|
|
106
|
+
* the `+N` overflow — sits on top. The first attempt here reversed the row to
|
|
107
|
+
* put the first avatar on top, which is the more common convention elsewhere
|
|
108
|
+
* and is not what this design does. Rendering in natural order gives Figma's
|
|
109
|
+
* order for free: later siblings paint later, so no z-index is needed at all.
|
|
110
|
+
*/
|
|
111
|
+
.ion-avatar-group {
|
|
112
|
+
--ion-avatar-group-overlap: calc(-1 * var(--ion-avatar-size) / 4);
|
|
113
|
+
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ion-avatar-group > * + * {
|
|
119
|
+
margin-left: var(--ion-avatar-group-overlap);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* The group publishes its size to the children so the overlap and the avatars
|
|
123
|
+
* agree without the caller setting both. */
|
|
124
|
+
.ion-avatar-group--mini {
|
|
125
|
+
--ion-avatar-size: var(--spacing-24);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ion-avatar-group--sm {
|
|
129
|
+
--ion-avatar-size: var(--spacing-32);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ion-avatar-group--md {
|
|
133
|
+
--ion-avatar-size: var(--spacing-40);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.ion-avatar-group--lg {
|
|
137
|
+
--ion-avatar-size: var(--spacing-48);
|
|
138
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Badge
|
|
3
|
+
*
|
|
4
|
+
* Geometry measured from Figma `Badge` (152:73). Single size, six intents.
|
|
5
|
+
*
|
|
6
|
+
* v2: Badge owns no tokens. Every value here is an Interface token, a Semantics
|
|
7
|
+
* token, or a spacing primitive — the same ones the Figma component binds, so
|
|
8
|
+
* the two cannot drift. Badge is 20px tall, below control/sm's 32px, so it is
|
|
9
|
+
* not a control and does not read the control scale.
|
|
10
|
+
*/
|
|
11
|
+
.ion-badge {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* Figma's 1px stroke is drawn INSIDE the box, so it does not add height. A CSS
|
|
18
|
+
* border does, so the vertical padding absorbs it — otherwise the badge
|
|
19
|
+
* renders 2px taller than the design. Same correction as the Tabs underline
|
|
20
|
+
* indicator. Badge is 22px: padding 2 + caption line-height 18 + padding 2.
|
|
21
|
+
*/
|
|
22
|
+
padding: calc(var(--spacing-2) - var(--border-width-default)) var(--spacing-8);
|
|
23
|
+
gap: var(--spacing-4);
|
|
24
|
+
font-family: var(--font-family-sans);
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Medium, not semibold. Figma's Badge label is Host Grotesk Medium; this was
|
|
28
|
+
* semibold in code and is the only typographic mismatch the measurement found.
|
|
29
|
+
*/
|
|
30
|
+
font-weight: var(--font-weight-medium);
|
|
31
|
+
font-size: var(--type-caption);
|
|
32
|
+
line-height: var(--type-caption-line-height);
|
|
33
|
+
border-radius: var(--radius-full);
|
|
34
|
+
border-width: var(--border-width-default);
|
|
35
|
+
border-style: solid;
|
|
36
|
+
user-select: none;
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Dot marker. It tracks the label exactly, so `currentcolor` is right — Figma
|
|
42
|
+
* binds it to the same token the label uses.
|
|
43
|
+
*/
|
|
44
|
+
.ion-badge__dot {
|
|
45
|
+
width: var(--spacing-6);
|
|
46
|
+
height: var(--spacing-6);
|
|
47
|
+
border-radius: var(--radius-full);
|
|
48
|
+
background-color: currentcolor;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* The icon does NOT track the label. v2 splits `fg` into `text` and `icon`
|
|
54
|
+
* precisely so an icon can sit a step brighter than the copy beside it — for
|
|
55
|
+
* error that is #dc2828 against the label's #ba1c1c. `@ionbase-ui/icons` renders
|
|
56
|
+
* with `currentColor`, so each intent sets `color` on the svg directly.
|
|
57
|
+
*
|
|
58
|
+
* Icon size is deliberately not set here. Figma has it at 14px, which is not on
|
|
59
|
+
* the spacing scale, so it is most likely a slip rather than a decision. Until
|
|
60
|
+
* that is settled the icon inherits 1em, matching the 12px label.
|
|
61
|
+
*/
|
|
62
|
+
.ion-badge svg {
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Intent modifiers */
|
|
67
|
+
.ion-badge--neutral {
|
|
68
|
+
background-color: var(--surface-muted);
|
|
69
|
+
color: var(--text-secondary);
|
|
70
|
+
border-color: var(--border-default);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ion-badge--neutral svg {
|
|
74
|
+
color: var(--icon-secondary);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ion-badge--primary {
|
|
78
|
+
background-color: var(--surface-primary-subtle);
|
|
79
|
+
color: var(--text-link);
|
|
80
|
+
border-color: var(--border-primary-subtle);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ion-badge--primary svg {
|
|
84
|
+
color: var(--icon-primary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ion-badge--success {
|
|
88
|
+
background-color: var(--surface-success-subtle);
|
|
89
|
+
color: var(--text-success);
|
|
90
|
+
border-color: var(--border-success-subtle);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ion-badge--success svg {
|
|
94
|
+
color: var(--icon-success);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ion-badge--warning {
|
|
98
|
+
background-color: var(--surface-warning-subtle);
|
|
99
|
+
color: var(--text-warning);
|
|
100
|
+
border-color: var(--border-warning-subtle);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ion-badge--warning svg {
|
|
104
|
+
color: var(--icon-warning);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ion-badge--error {
|
|
108
|
+
background-color: var(--surface-error-subtle);
|
|
109
|
+
color: var(--text-error);
|
|
110
|
+
border-color: var(--border-error-subtle);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.ion-badge--error svg {
|
|
114
|
+
color: var(--icon-error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.ion-badge--information {
|
|
118
|
+
background-color: var(--surface-information-subtle);
|
|
119
|
+
color: var(--text-information);
|
|
120
|
+
border-color: var(--border-information-subtle);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ion-badge--information svg {
|
|
124
|
+
color: var(--icon-information);
|
|
125
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Button
|
|
3
|
+
*
|
|
4
|
+
* The focus ring and the elevation are two independent slots composed into one
|
|
5
|
+
* box-shadow, rather than separate rules racing on specificity. A variant sets
|
|
6
|
+
* its elevation, `:focus-visible` sets its ring, and neither has to know about
|
|
7
|
+
* the other — otherwise `--primary-brand:active` (0,2,0) and `:focus-visible`
|
|
8
|
+
* (0,2,0) tie, and whichever is written last silently wins.
|
|
9
|
+
*
|
|
10
|
+
* Ring and elevation both come from elevation.css, generated from the Figma
|
|
11
|
+
* effect styles. Those are not variable-bound and so cannot be tokens — there
|
|
12
|
+
* is nothing for them to alias — which is why the values are exported and
|
|
13
|
+
* committed instead of aliased.
|
|
14
|
+
*/
|
|
15
|
+
.ion-button {
|
|
16
|
+
--ion-button-ring: var(--ion-shadow-none);
|
|
17
|
+
--ion-button-elevation: var(--ion-shadow-none);
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* ELEVATION IS INDEXED BY SIZE, NOT BY VARIANT
|
|
21
|
+
*
|
|
22
|
+
* Measured: Primary Brand, Primary Neutral, Secondary and Destructive all sit
|
|
23
|
+
* on the same rung at a given size — Small is `xs`, Medium `sm`, Large `lg`.
|
|
24
|
+
* The variant chooses only the *family* it presses into, so these are slots
|
|
25
|
+
* the size modifiers reassign and the variants select from. This replaces the
|
|
26
|
+
* old per-variant `button-raised` / `button-surface` pair, which encoded a
|
|
27
|
+
* distinction Figma no longer draws.
|
|
28
|
+
*/
|
|
29
|
+
--ion-button-raised: var(--ion-shadow-raised-lifted-sm);
|
|
30
|
+
--ion-button-inset-lifted: var(--ion-shadow-inset-lifted-sm);
|
|
31
|
+
--ion-button-inset-flush: var(--ion-shadow-inset-flush-sm);
|
|
32
|
+
|
|
33
|
+
/* Which inset family this variant presses into; overridden per variant. */
|
|
34
|
+
--ion-button-pressed: var(--ion-button-inset-lifted);
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Size is a set of slots defaulting to medium, so a size modifier reassigns
|
|
38
|
+
* four variables instead of restating four declarations. It also means the
|
|
39
|
+
* icon rule below needs no per-size overrides at all.
|
|
40
|
+
*
|
|
41
|
+
* These read the spacing scale directly. They used to read a `control/*`
|
|
42
|
+
* group in Semantics, which was deleted: its twelve tokens were all aliases
|
|
43
|
+
* of these same spacing steps, its icon sizes duplicated the `icon-size`
|
|
44
|
+
* ladder outright, and only Button and Tabs ever bound it — because it had
|
|
45
|
+
* been derived from Button's own measurements and fitted nothing else.
|
|
46
|
+
*
|
|
47
|
+
* 32/40/48 and 12/16/20 are Button's choices, not a system-wide rule. The
|
|
48
|
+
* spacing scale is what everything shares; which rung you pick is yours.
|
|
49
|
+
*/
|
|
50
|
+
--ion-button-size: var(--spacing-40);
|
|
51
|
+
--ion-button-padding-x: var(--spacing-16);
|
|
52
|
+
--ion-button-gap: var(--spacing-8);
|
|
53
|
+
--ion-button-icon-size: var(--icon-size-md);
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* Typography comes from the type ramp, not from button tokens. In Figma the
|
|
57
|
+
* Label carries a text style — Body/Default Emphasis at Medium, Body/Small
|
|
58
|
+
* Emphasis at Small, Body/Medium Emphasis at Large — and a text style owns
|
|
59
|
+
* fontSize/fontStyle/lineHeight outright. Button-level type tokens were
|
|
60
|
+
* tried and deleted: they are a second source of truth that the text style
|
|
61
|
+
* silently overrides, so they can never win.
|
|
62
|
+
*
|
|
63
|
+
* Weight is Medium (500) across all three sizes — the Emphasis body styles.
|
|
64
|
+
*/
|
|
65
|
+
--ion-button-font-size: var(--type-body);
|
|
66
|
+
--ion-button-line-height: var(--type-body-line-height);
|
|
67
|
+
|
|
68
|
+
box-shadow: var(--ion-button-ring), var(--ion-button-elevation);
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Figma pins each size to a fixed height (32/40/48) with no vertical padding.
|
|
75
|
+
* `min-height` reproduces that while still growing if a label ever wraps —
|
|
76
|
+
* deriving height from padding instead is what left all three sizes 2px off.
|
|
77
|
+
*/
|
|
78
|
+
min-height: var(--ion-button-size);
|
|
79
|
+
padding-left: var(--ion-button-padding-x);
|
|
80
|
+
padding-right: var(--ion-button-padding-x);
|
|
81
|
+
gap: var(--ion-button-gap);
|
|
82
|
+
font-family: var(--font-family-sans);
|
|
83
|
+
font-weight: var(--font-weight-medium);
|
|
84
|
+
font-size: var(--ion-button-font-size);
|
|
85
|
+
line-height: var(--ion-button-line-height);
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
user-select: none;
|
|
89
|
+
border-style: solid;
|
|
90
|
+
border-width: var(--border-width-default);
|
|
91
|
+
border-radius: var(--radius-md);
|
|
92
|
+
transition:
|
|
93
|
+
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
94
|
+
border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
95
|
+
box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Size modifiers — radius, border-width and font-weight are size-invariant. */
|
|
99
|
+
.ion-button--sm {
|
|
100
|
+
--ion-button-size: var(--spacing-32);
|
|
101
|
+
--ion-button-padding-x: var(--spacing-12);
|
|
102
|
+
--ion-button-gap: var(--spacing-6);
|
|
103
|
+
--ion-button-icon-size: var(--icon-size-sm);
|
|
104
|
+
--ion-button-font-size: var(--type-body-sm);
|
|
105
|
+
--ion-button-line-height: var(--type-body-sm-line-height);
|
|
106
|
+
--ion-button-raised: var(--ion-shadow-raised-lifted-xs);
|
|
107
|
+
--ion-button-inset-lifted: var(--ion-shadow-inset-lifted-xs);
|
|
108
|
+
--ion-button-inset-flush: var(--ion-shadow-inset-flush-xs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ion-button--lg {
|
|
112
|
+
--ion-button-size: var(--spacing-48);
|
|
113
|
+
--ion-button-padding-x: var(--spacing-20);
|
|
114
|
+
--ion-button-gap: var(--spacing-8);
|
|
115
|
+
--ion-button-icon-size: var(--icon-size-lg);
|
|
116
|
+
--ion-button-font-size: var(--type-body-md);
|
|
117
|
+
--ion-button-line-height: var(--type-body-md-line-height);
|
|
118
|
+
--ion-button-raised: var(--ion-shadow-raised-lifted-lg);
|
|
119
|
+
--ion-button-inset-lifted: var(--ion-shadow-inset-lifted-lg);
|
|
120
|
+
--ion-button-inset-flush: var(--ion-shadow-inset-flush-lg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* Focus ring — one ring for every variant and size, from the Figma `Focus/md`
|
|
125
|
+
* effect style.
|
|
126
|
+
*
|
|
127
|
+
* There used to be two, `Button/Raised Focus` and `Button/Surface Focus`, with
|
|
128
|
+
* secondary and tertiary opting into the second. They were byte-identical: two
|
|
129
|
+
* names over one value, which is why they collapsed into `Focus/md` and why
|
|
130
|
+
* there is no per-variant ring slot here any more.
|
|
131
|
+
*
|
|
132
|
+
* Before that they were assembled from `--button-offset-ring` / `--button-ring`,
|
|
133
|
+
* component tokens hand-mapped to the nearest solid semantic colour. That made
|
|
134
|
+
* the outer ring fully opaque where Figma renders it at 50% alpha, and nothing
|
|
135
|
+
* caught it: effect styles are not variable-bound, so the token pipeline cannot
|
|
136
|
+
* see them at all.
|
|
137
|
+
*/
|
|
138
|
+
.ion-button:focus-visible,
|
|
139
|
+
.ion-button[data-focused='true'] {
|
|
140
|
+
--ion-button-ring: var(--ion-shadow-focus-md);
|
|
141
|
+
|
|
142
|
+
outline: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Hover is declared twice per variant, on purpose.
|
|
147
|
+
*
|
|
148
|
+
* `:hover` is wrapped in `@media (hover: hover)` because native hover latches
|
|
149
|
+
* on touch — tap a button and it stays visually hovered until you tap
|
|
150
|
+
* elsewhere. `[data-hovered]` comes from React Aria's useHover, which is
|
|
151
|
+
* already pointer-aware. The media query keeps the stylesheet working without
|
|
152
|
+
* React; the attribute makes it correct with it.
|
|
153
|
+
*
|
|
154
|
+
* HOVER DOES NOT CHANGE ELEVATION — only the surface colour moves. Measured:
|
|
155
|
+
* every Hover variant in Figma carries the same effect style as its Default,
|
|
156
|
+
* across all four elevated types. Reproduced as measured rather than preserved
|
|
157
|
+
* from the old `button-raised-hover` step, which encodes a difference the
|
|
158
|
+
* current design no longer draws.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/* Primary Brand Variant */
|
|
162
|
+
.ion-button--primary-brand {
|
|
163
|
+
--ion-button-elevation: var(--ion-button-raised);
|
|
164
|
+
|
|
165
|
+
background-color: var(--surface-primary);
|
|
166
|
+
color: var(--text-on-color);
|
|
167
|
+
border-color: var(--border-primary-strong);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (hover: hover) {
|
|
171
|
+
.ion-button--primary-brand:hover {
|
|
172
|
+
background-color: var(--surface-primary-hover);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ion-button--primary-brand[data-hovered='true'] {
|
|
177
|
+
background-color: var(--surface-primary-hover);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.ion-button--primary-brand:active,
|
|
181
|
+
.ion-button--primary-brand[data-pressed='true'] {
|
|
182
|
+
--ion-button-elevation: var(--ion-button-pressed);
|
|
183
|
+
|
|
184
|
+
background-color: var(--surface-primary-pressed);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* Primary Neutral Variant
|
|
189
|
+
*
|
|
190
|
+
* The one variant whose press does NOT track its own rung: Figma pins all three
|
|
191
|
+
* sizes to `Inset/Flush/lg` while their rest states scale xs/sm/lg like every
|
|
192
|
+
* other type. Reproduced as measured, not normalised — but flagged, because it
|
|
193
|
+
* is the only place in the component where size stops mattering, and it reads
|
|
194
|
+
* more like an unfinished edit than a decision.
|
|
195
|
+
*/
|
|
196
|
+
.ion-button--primary-neutral {
|
|
197
|
+
--ion-button-elevation: var(--ion-button-raised);
|
|
198
|
+
--ion-button-pressed: var(--ion-shadow-inset-flush-lg);
|
|
199
|
+
|
|
200
|
+
background-color: var(--surface-inverse);
|
|
201
|
+
color: var(--text-on-color);
|
|
202
|
+
border-color: var(--border-inverse);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@media (hover: hover) {
|
|
206
|
+
.ion-button--primary-neutral:hover {
|
|
207
|
+
background-color: var(--surface-inverse);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.ion-button--primary-neutral[data-hovered='true'] {
|
|
212
|
+
background-color: var(--surface-inverse);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.ion-button--primary-neutral:active,
|
|
216
|
+
.ion-button--primary-neutral[data-pressed='true'] {
|
|
217
|
+
--ion-button-elevation: var(--ion-button-pressed);
|
|
218
|
+
|
|
219
|
+
background-color: var(--surface-inverse);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Secondary Variant — presses flush, losing its lift; brand and destructive keep theirs. */
|
|
223
|
+
.ion-button--secondary {
|
|
224
|
+
--ion-button-elevation: var(--ion-button-raised);
|
|
225
|
+
--ion-button-pressed: var(--ion-button-inset-flush);
|
|
226
|
+
|
|
227
|
+
background-color: var(--surface-default);
|
|
228
|
+
color: var(--text-secondary);
|
|
229
|
+
border-color: var(--border-strong);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@media (hover: hover) {
|
|
233
|
+
.ion-button--secondary:hover {
|
|
234
|
+
background-color: var(--surface-page);
|
|
235
|
+
border-color: var(--border-stronger);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.ion-button--secondary[data-hovered='true'] {
|
|
240
|
+
background-color: var(--surface-page);
|
|
241
|
+
border-color: var(--border-stronger);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.ion-button--secondary:active,
|
|
245
|
+
.ion-button--secondary[data-pressed='true'] {
|
|
246
|
+
--ion-button-elevation: var(--ion-button-pressed);
|
|
247
|
+
|
|
248
|
+
background-color: var(--surface-muted);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* Tertiary Variant — intentionally flat; a ghost button has no surface to raise. */
|
|
252
|
+
.ion-button--tertiary {
|
|
253
|
+
background-color: transparent;
|
|
254
|
+
color: var(--text-secondary);
|
|
255
|
+
border-color: transparent;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@media (hover: hover) {
|
|
259
|
+
.ion-button--tertiary:hover {
|
|
260
|
+
background-color: var(--surface-page);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.ion-button--tertiary[data-hovered='true'] {
|
|
265
|
+
background-color: var(--surface-page);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.ion-button--tertiary:active,
|
|
269
|
+
.ion-button--tertiary[data-pressed='true'] {
|
|
270
|
+
background-color: var(--surface-muted);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Destructive Variant */
|
|
274
|
+
.ion-button--destructive {
|
|
275
|
+
--ion-button-elevation: var(--ion-button-raised);
|
|
276
|
+
|
|
277
|
+
background-color: var(--surface-error);
|
|
278
|
+
color: var(--text-on-color);
|
|
279
|
+
border-color: var(--border-error-strong);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (hover: hover) {
|
|
283
|
+
.ion-button--destructive:hover {
|
|
284
|
+
background-color: var(--surface-error-hover);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.ion-button--destructive[data-hovered='true'] {
|
|
289
|
+
background-color: var(--surface-error-hover);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.ion-button--destructive:active,
|
|
293
|
+
.ion-button--destructive[data-pressed='true'] {
|
|
294
|
+
--ion-button-elevation: var(--ion-button-pressed);
|
|
295
|
+
|
|
296
|
+
background-color: var(--surface-error-pressed);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Disabled State */
|
|
300
|
+
.ion-button:disabled,
|
|
301
|
+
.ion-button[data-disabled='true'] {
|
|
302
|
+
--ion-button-ring: var(--ion-shadow-none);
|
|
303
|
+
--ion-button-elevation: var(--ion-shadow-none);
|
|
304
|
+
|
|
305
|
+
background-color: var(--surface-disabled);
|
|
306
|
+
color: var(--text-disabled);
|
|
307
|
+
border-color: var(--border-disabled);
|
|
308
|
+
cursor: not-allowed;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.ion-button--tertiary:disabled,
|
|
312
|
+
.ion-button--tertiary[data-disabled='true'] {
|
|
313
|
+
background-color: transparent;
|
|
314
|
+
border-color: transparent;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Icon Containers & Alignment */
|
|
318
|
+
.ion-button__icon-start,
|
|
319
|
+
.ion-button__icon-end {
|
|
320
|
+
display: inline-flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
flex-shrink: 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* One rule for every size — the slot is reassigned by the size modifier. */
|
|
327
|
+
.ion-button__icon-start svg,
|
|
328
|
+
.ion-button__icon-end svg {
|
|
329
|
+
width: var(--ion-button-icon-size);
|
|
330
|
+
height: var(--ion-button-icon-size);
|
|
331
|
+
}
|