ionbase-ui 0.5.0 → 0.6.1
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/README.md +2 -0
- package/dist/components/Header.d.ts +43 -11
- package/dist/components/Header.d.ts.map +1 -1
- package/dist/components/Header.js +61 -9
- package/dist/components/Header.js.map +1 -1
- package/dist/styles/header.css +138 -35
- package/dist/styles/icon.css +49 -0
- package/dist/styles/index.css +7 -0
- package/dist/styles/tokens/base.css +24 -23
- package/dist/styles/tokens/elevation.css +6 -6
- package/dist/styles/tokens/theme-dark.css +2 -1
- package/dist/styles/tokens/typography.css +3 -3
- package/dist/tokens/index.d.ts +200 -194
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +165 -160
- package/dist/tokens/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
export interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
|
|
2
|
+
export interface HeaderProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onToggle'> {
|
|
3
3
|
/** Figma's Logo slot. Rendered at the start, never shrinks. */
|
|
4
4
|
brand?: React.ReactNode;
|
|
5
|
-
/** Figma's `Center Slot
|
|
5
|
+
/** Figma's `Center Slot` — nav items. Inline on Desktop and Tablet; moves
|
|
6
|
+
* into the Mobile-Open menu below the mobile breakpoint. */
|
|
6
7
|
center?: React.ReactNode;
|
|
7
|
-
/** Figma's `End Slot`. */
|
|
8
|
+
/** Figma's `End Slot` — buttons and actions. Same placement rules as `center`. */
|
|
8
9
|
end?: React.ReactNode;
|
|
10
|
+
/** Controlled Mobile-Open state. Pair with `onOpenChange`. */
|
|
11
|
+
open?: boolean;
|
|
12
|
+
/** Initial Mobile-Open state when uncontrolled. */
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
/** Fires whenever the menu toggle changes the state, controlled or not. */
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
/** Accessible name for the mobile menu toggle. */
|
|
17
|
+
menuLabel?: string;
|
|
9
18
|
children?: React.ReactNode;
|
|
10
19
|
}
|
|
11
20
|
/**
|
|
12
|
-
* Header renders Figma's
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* Header renders Figma's four Device variants — Desktop, Tablet,
|
|
22
|
+
* Mobile-Closed, Mobile-Open — from one DOM tree.
|
|
23
|
+
*
|
|
24
|
+
* TWO AXES, AND THEY ARE NOT THE SAME KIND OF THING
|
|
25
|
+
*
|
|
26
|
+
* Figma spells Device as a single four-way variant, but it is really two: a
|
|
27
|
+
* *breakpoint* (Desktop / Tablet / Mobile) and a *state* (Closed / Open). They
|
|
28
|
+
* are modelled differently here because they are known by different people.
|
|
29
|
+
*
|
|
30
|
+
* Breakpoint is a media query. It is the one variant axis in the system the
|
|
31
|
+
* browser already knows the answer to — a header is Mobile because the
|
|
32
|
+
* viewport is narrow — and having React duplicate that judgement is how the
|
|
33
|
+
* two drift apart. Breakpoints match the Breakpoint collection's container
|
|
34
|
+
* widths: Tablet below 1216, Mobile below 896.
|
|
35
|
+
*
|
|
36
|
+
* Open/Closed is a prop, because only the caller knows. It follows the
|
|
37
|
+
* controlled/uncontrolled pair the rest of the system uses.
|
|
38
|
+
*
|
|
39
|
+
* ONE TREE, TWO LAYOUTS
|
|
40
|
+
*
|
|
41
|
+
* `center` and `end` sit inline in the bar on Desktop and Tablet, and inside
|
|
42
|
+
* the dropped Menu-Container on Mobile-Open. They are rendered once, in one
|
|
43
|
+
* wrapper, which is `display: contents` above the mobile breakpoint and an
|
|
44
|
+
* absolutely positioned panel below it. Rendering them twice — or moving them
|
|
45
|
+
* with JavaScript — would mean the same nav link exists twice in the
|
|
46
|
+
* accessibility tree, and would reset any state a caller put in a slot every
|
|
47
|
+
* time the viewport crossed 896px.
|
|
48
|
+
*
|
|
49
|
+
* The centre slot is no longer hidden on Tablet. Figma's Tablet variant ships
|
|
50
|
+
* it populated; the previous release collapsed it, which was correct for the
|
|
51
|
+
* previous design and is not for this one.
|
|
20
52
|
*
|
|
21
53
|
* A `<header>` element with no explicit role: it is a landmark already when it
|
|
22
54
|
* is a direct child of body, which is where a page header sits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.d.ts","sourceRoot":"","sources":["../../src/components/Header.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Header.d.ts","sourceRoot":"","sources":["../../src/components/Header.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AA2BxE,MAAM,WAAW,WAAY,SAAQ,IAAI,CACvC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EACjC,UAAU,CACX;IACC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB;iEAC6D;IAC7D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,kFAAkF;IAClF,GAAG,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mDAAmD;IACnD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,MAAM,iFAwElB,CAAC"}
|
|
@@ -1,18 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from 'react';
|
|
3
|
+
import { forwardRef, useCallback, useId, useState } from 'react';
|
|
4
|
+
/** Figma's `Icon` on the Mobile-Closed toggle. Inlined for the same reason
|
|
5
|
+
* NavItem's chevron is: it is part of the control, not a slot a caller fills. */
|
|
6
|
+
const MenuGlyph = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M3 6h18M3 12h18M3 18h18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
|
|
7
|
+
/** Figma's `Icon` on the Mobile-Open toggle. */
|
|
8
|
+
const CloseGlyph = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m5 5 14 14M19 5 5 19", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
|
|
3
9
|
/**
|
|
4
|
-
* Header renders Figma's
|
|
5
|
-
*
|
|
10
|
+
* Header renders Figma's four Device variants — Desktop, Tablet,
|
|
11
|
+
* Mobile-Closed, Mobile-Open — from one DOM tree.
|
|
6
12
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* TWO AXES, AND THEY ARE NOT THE SAME KIND OF THING
|
|
14
|
+
*
|
|
15
|
+
* Figma spells Device as a single four-way variant, but it is really two: a
|
|
16
|
+
* *breakpoint* (Desktop / Tablet / Mobile) and a *state* (Closed / Open). They
|
|
17
|
+
* are modelled differently here because they are known by different people.
|
|
18
|
+
*
|
|
19
|
+
* Breakpoint is a media query. It is the one variant axis in the system the
|
|
20
|
+
* browser already knows the answer to — a header is Mobile because the
|
|
21
|
+
* viewport is narrow — and having React duplicate that judgement is how the
|
|
22
|
+
* two drift apart. Breakpoints match the Breakpoint collection's container
|
|
23
|
+
* widths: Tablet below 1216, Mobile below 896.
|
|
24
|
+
*
|
|
25
|
+
* Open/Closed is a prop, because only the caller knows. It follows the
|
|
26
|
+
* controlled/uncontrolled pair the rest of the system uses.
|
|
27
|
+
*
|
|
28
|
+
* ONE TREE, TWO LAYOUTS
|
|
29
|
+
*
|
|
30
|
+
* `center` and `end` sit inline in the bar on Desktop and Tablet, and inside
|
|
31
|
+
* the dropped Menu-Container on Mobile-Open. They are rendered once, in one
|
|
32
|
+
* wrapper, which is `display: contents` above the mobile breakpoint and an
|
|
33
|
+
* absolutely positioned panel below it. Rendering them twice — or moving them
|
|
34
|
+
* with JavaScript — would mean the same nav link exists twice in the
|
|
35
|
+
* accessibility tree, and would reset any state a caller put in a slot every
|
|
36
|
+
* time the viewport crossed 896px.
|
|
37
|
+
*
|
|
38
|
+
* The centre slot is no longer hidden on Tablet. Figma's Tablet variant ships
|
|
39
|
+
* it populated; the previous release collapsed it, which was correct for the
|
|
40
|
+
* previous design and is not for this one.
|
|
12
41
|
*
|
|
13
42
|
* A `<header>` element with no explicit role: it is a landmark already when it
|
|
14
43
|
* is a direct child of body, which is where a page header sits.
|
|
15
44
|
*/
|
|
16
|
-
export const Header = forwardRef(({ brand, center, end,
|
|
45
|
+
export const Header = forwardRef(({ brand, center, end, open: openProp, defaultOpen = false, onOpenChange, menuLabel = 'Menu', className, children, ...rest }, ref) => {
|
|
46
|
+
const menuId = useId();
|
|
47
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
48
|
+
const isControlled = openProp !== undefined;
|
|
49
|
+
const isOpen = isControlled ? openProp : uncontrolledOpen;
|
|
50
|
+
const toggle = useCallback(() => {
|
|
51
|
+
const next = !isOpen;
|
|
52
|
+
if (!isControlled)
|
|
53
|
+
setUncontrolledOpen(next);
|
|
54
|
+
onOpenChange?.(next);
|
|
55
|
+
}, [isOpen, isControlled, onOpenChange]);
|
|
56
|
+
/*
|
|
57
|
+
* Escape closes the menu. It is a disclosure rather than a modal — no focus
|
|
58
|
+
* trap, no scroll lock — so Escape is the whole of the dismissal contract,
|
|
59
|
+
* and it is handled on the header rather than the document so a header that
|
|
60
|
+
* is never open never listens.
|
|
61
|
+
*/
|
|
62
|
+
const handleKeyDown = (event) => {
|
|
63
|
+
if (event.key === 'Escape' && isOpen)
|
|
64
|
+
toggle();
|
|
65
|
+
rest.onKeyDown?.(event);
|
|
66
|
+
};
|
|
67
|
+
return (_jsxs("header", { ...rest, onKeyDown: handleKeyDown, ref: ref, "data-open": isOpen || undefined, className: ['ion-header', className].filter(Boolean).join(' '), children: [brand && _jsx("div", { className: "ion-header__brand", children: brand }), _jsxs("div", { className: "ion-header__menu", id: menuId, children: [_jsx("div", { className: "ion-header__center", children: center }), _jsx("div", { className: "ion-header__end", children: end })] }), children, _jsx("button", { type: "button", className: "ion-header__toggle", "aria-expanded": isOpen, "aria-controls": menuId, "aria-label": menuLabel, onClick: toggle, children: isOpen ? _jsx(CloseGlyph, {}) : _jsx(MenuGlyph, {}) })] }));
|
|
68
|
+
});
|
|
17
69
|
Header.displayName = 'Header';
|
|
18
70
|
//# sourceMappingURL=Header.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.js","sourceRoot":"","sources":["../../src/components/Header.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Header.js","sourceRoot":"","sources":["../../src/components/Header.tsx"],"names":[],"mappings":"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"}
|
package/dist/styles/header.css
CHANGED
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Header
|
|
3
3
|
*
|
|
4
|
-
* Measured from Figma `Header` (
|
|
4
|
+
* Measured from Figma `Header` (69:335). Four Device variants, two axes:
|
|
5
5
|
*
|
|
6
|
-
* Desktop
|
|
7
|
-
* Tablet
|
|
8
|
-
* Mobile
|
|
6
|
+
* Desktop 56 tall, padding 8 / 64, end-slot gap 12
|
|
7
|
+
* Tablet 56 tall, padding 8 / 24, end-slot gap 8
|
|
8
|
+
* Mobile-Closed 64 tall, padding 12 / 16, toggle only
|
|
9
|
+
* Mobile-Open as Mobile-Closed, plus the dropped Menu-Container
|
|
9
10
|
*
|
|
10
|
-
* WHY
|
|
11
|
+
* WHY DEVICE IS A MEDIA QUERY AND OPEN IS A PROP
|
|
11
12
|
*
|
|
12
|
-
* Device is the one variant axis in the whole system
|
|
13
|
-
*
|
|
13
|
+
* Device is the one variant axis in the whole system the browser already knows
|
|
14
|
+
* the answer to. Every other component takes its variant from the caller
|
|
14
15
|
* because the caller is the only one who knows; a header is Mobile because the
|
|
15
16
|
* viewport is narrow, and asking React to duplicate that is how the two get out
|
|
16
|
-
* of step.
|
|
17
|
-
*
|
|
17
|
+
* of step. Breakpoints match the Breakpoint collection's own container widths —
|
|
18
|
+
* Tablet below 1216, Mobile below 896.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
+
* Open/Closed is the opposite: nothing in CSS knows it, so it is a prop, and it
|
|
21
|
+
* arrives here as `[data-open]`.
|
|
22
|
+
*
|
|
23
|
+
* Note the header is taller on Mobile (64) than on Desktop and Tablet (56).
|
|
20
24
|
* That looks inverted but is deliberate in the design: touch targets need the
|
|
21
25
|
* height more than the desktop pointer does.
|
|
26
|
+
*
|
|
27
|
+
* ONE TREE, TWO LAYOUTS
|
|
28
|
+
*
|
|
29
|
+
* `__menu` is `display: contents` above the mobile breakpoint, so `__center`
|
|
30
|
+
* and `__end` are flex items of the bar itself; below it, the same element
|
|
31
|
+
* becomes the absolutely positioned Menu-Container. The slots are therefore
|
|
32
|
+
* rendered once, never duplicated across breakpoints.
|
|
22
33
|
*/
|
|
23
34
|
.ion-header {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--ion-header-
|
|
27
|
-
--ion-header-
|
|
35
|
+
/* Desktop is the base; the two narrower breakpoints override downward, so a
|
|
36
|
+
* new rung is added in one place rather than three. */
|
|
37
|
+
--ion-header-height: calc(var(--spacing-48) + var(--spacing-8)); /* 56 */
|
|
38
|
+
--ion-header-padding-y: var(--spacing-8);
|
|
39
|
+
--ion-header-padding-x: var(--spacing-64);
|
|
40
|
+
--ion-header-center-gap: var(--spacing-8);
|
|
41
|
+
--ion-header-end-gap: var(--spacing-12);
|
|
28
42
|
|
|
43
|
+
position: relative;
|
|
29
44
|
display: flex;
|
|
30
45
|
align-items: center;
|
|
31
46
|
justify-content: space-between;
|
|
@@ -43,20 +58,18 @@
|
|
|
43
58
|
font-family: var(--font-family-sans);
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
@media (width
|
|
61
|
+
@media (width < 1216px) {
|
|
47
62
|
.ion-header {
|
|
48
|
-
--ion-header-padding-x: var(--spacing-
|
|
63
|
+
--ion-header-padding-x: var(--spacing-24);
|
|
64
|
+
--ion-header-end-gap: var(--spacing-8);
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
@media (width
|
|
68
|
+
@media (width < 896px) {
|
|
53
69
|
.ion-header {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
--ion-header-
|
|
57
|
-
--ion-header-padding-y: var(--spacing-8);
|
|
58
|
-
--ion-header-padding-x: var(--spacing-64);
|
|
59
|
-
--ion-header-end-gap: var(--spacing-12);
|
|
70
|
+
--ion-header-height: var(--spacing-64);
|
|
71
|
+
--ion-header-padding-y: var(--spacing-12);
|
|
72
|
+
--ion-header-padding-x: var(--spacing-16);
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
@@ -69,27 +82,117 @@
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
/*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
85
|
+
* Above the mobile breakpoint the wrapper contributes no box of its own — its
|
|
86
|
+
* two children are laid out by the bar. `display: contents` is what makes the
|
|
87
|
+
* single-tree approach possible; without it the slots would need to be either
|
|
88
|
+
* duplicated or moved by script.
|
|
75
89
|
*/
|
|
90
|
+
.ion-header__menu {
|
|
91
|
+
display: contents;
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
.ion-header__center {
|
|
77
|
-
display:
|
|
95
|
+
display: flex;
|
|
78
96
|
align-items: center;
|
|
79
|
-
gap: var(--
|
|
80
|
-
flex: 1 1 auto;
|
|
97
|
+
gap: var(--ion-header-center-gap);
|
|
81
98
|
min-width: 0;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
@media (width >= 1216px) {
|
|
85
|
-
.ion-header__center {
|
|
86
|
-
display: flex;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
101
|
.ion-header__end {
|
|
91
102
|
display: flex;
|
|
92
103
|
align-items: center;
|
|
93
104
|
gap: var(--ion-header-end-gap);
|
|
94
105
|
flex-shrink: 0;
|
|
95
106
|
}
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* The Mobile-Closed / Mobile-Open toggle. Figma builds it from the Icon Button
|
|
110
|
+
* component (Secondary, Small, Square) but the Header owns the instance, so the
|
|
111
|
+
* geometry is stated here rather than reached for across components: 32 square,
|
|
112
|
+
* radius/sm, 16px icon.
|
|
113
|
+
*/
|
|
114
|
+
.ion-header__toggle {
|
|
115
|
+
display: none;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
width: var(--spacing-32);
|
|
120
|
+
height: var(--spacing-32);
|
|
121
|
+
padding: 0;
|
|
122
|
+
color: var(--icon-default);
|
|
123
|
+
background-color: var(--surface-default);
|
|
124
|
+
border: var(--border-width-default) solid var(--border-strong);
|
|
125
|
+
border-radius: var(--radius-sm);
|
|
126
|
+
box-shadow: var(--ion-shadow-raised-lifted-xs);
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ion-header__toggle > svg {
|
|
131
|
+
width: var(--icon-size-sm);
|
|
132
|
+
height: var(--icon-size-sm);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.ion-header__toggle:focus-visible {
|
|
136
|
+
outline: var(--border-width-thick) solid var(--ring-focus);
|
|
137
|
+
outline-offset: var(--spacing-2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (width < 896px) {
|
|
141
|
+
.ion-header__toggle {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Menu-Container. Figma places it at y=67 against a 64-tall bar — a 3px nudge
|
|
147
|
+
* that is on no scale and matches no token, so it is anchored flush to the
|
|
148
|
+
* bar's bottom edge instead. Nothing else in the design depends on the gap.
|
|
149
|
+
*/
|
|
150
|
+
.ion-header__menu {
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: 100%;
|
|
153
|
+
left: 0;
|
|
154
|
+
z-index: 1;
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
gap: var(--spacing-12);
|
|
158
|
+
width: 100%;
|
|
159
|
+
padding: var(--spacing-12) var(--spacing-8);
|
|
160
|
+
background-color: var(--surface-default);
|
|
161
|
+
border: var(--border-width-default) solid var(--border-default);
|
|
162
|
+
border-radius: var(--radius-md);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ion-header[data-open] .ion-header__menu {
|
|
166
|
+
display: flex;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.ion-header:not([data-open]) .ion-header__menu {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Both slots stack full width inside the panel; the end slot keeps its row
|
|
174
|
+
* but shares the width evenly, as the two buttons do in Figma. */
|
|
175
|
+
.ion-header__center {
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
align-items: stretch;
|
|
178
|
+
width: 100%;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.ion-header__end {
|
|
182
|
+
width: 100%;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ion-header__end > * {
|
|
186
|
+
flex: 1 1 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Forced-colors: box-shadow is dropped by the OS, so the toggle would lose its
|
|
192
|
+
* only edge. A border in ButtonText keeps it visible.
|
|
193
|
+
*/
|
|
194
|
+
@media (forced-colors: active) {
|
|
195
|
+
.ion-header__toggle {
|
|
196
|
+
border-color: ButtonText;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Icon
|
|
3
|
+
*
|
|
4
|
+
* `Icon.tsx` has always emitted an `.ion-icon` class and nothing ever styled it.
|
|
5
|
+
* That was not benign. An `<svg>` is an inline-level replaced element, so when
|
|
6
|
+
* it shares a line box with anything else it sits on the *text baseline* and the
|
|
7
|
+
* font's descender space opens up underneath it — the icon then reads a couple
|
|
8
|
+
* of pixels high, and the box around it grows taller than the icon.
|
|
9
|
+
*
|
|
10
|
+
* Every slot in this system happens to hide that today: `.ion-button__icon-start`,
|
|
11
|
+
* `.ion-menu__icon`, `.ion-input__icon-start` and friends are all flex or grid
|
|
12
|
+
* containers, and a flex/grid item is blockified — `display: inline` becomes
|
|
13
|
+
* `block` — which removes the baseline from the picture entirely. Measured: a
|
|
14
|
+
* direct icon, an icon inside an `inline-grid` swap wrapper, and an icon inside
|
|
15
|
+
* a plain `display: block` wrapper all centre to within 0.00px of the label.
|
|
16
|
+
*
|
|
17
|
+
* The alignment was therefore correct by side effect of the container, not
|
|
18
|
+
* because the icon defended itself. It failed the moment the icon stopped being
|
|
19
|
+
* the only thing on its line: with one text node beside it inside a wrapper, the
|
|
20
|
+
* icon measured 3px above the label's centre and the button grew from 40px to
|
|
21
|
+
* 46px. Consumers nest icons inside their own wrappers all the time — swap and
|
|
22
|
+
* crossfade animations do it by construction — so "the parent will always be a
|
|
23
|
+
* flex container" is not an invariant this package can rely on.
|
|
24
|
+
*
|
|
25
|
+
* WHY inline-block AND NOT block
|
|
26
|
+
*
|
|
27
|
+
* `display: block` also kills the baseline, and it is what several resets reach
|
|
28
|
+
* for, but it is wrong here: it forces an icon used mid-sentence onto its own
|
|
29
|
+
* line. Measured on a 600px paragraph with one inline icon — 2 lines became 3.
|
|
30
|
+
* `inline-block` keeps the icon in the text flow, and `vertical-align: middle`
|
|
31
|
+
* centres it on the x-height axis, which is where an inline icon should sit.
|
|
32
|
+
*
|
|
33
|
+
* Both declarations are inert wherever it already worked: a flex or grid item is
|
|
34
|
+
* blockified regardless of the `display` it was given, and `vertical-align` does
|
|
35
|
+
* not apply to one. So this changes nothing in Button, Menu, Input, Tabs or
|
|
36
|
+
* anywhere else the icon is already a direct flex child — it only supplies the
|
|
37
|
+
* floor that was missing underneath them.
|
|
38
|
+
*
|
|
39
|
+
* NOT DONE HERE: an optical nudge. Icons centre on the line box while text is
|
|
40
|
+
* read on its cap-height band, so the two centres are not identical. Measured
|
|
41
|
+
* from rendered pixels on a descender-free label at Medium, the gap is 0.31px —
|
|
42
|
+
* below a device pixel at 1x, and smaller than the variation between individual
|
|
43
|
+
* Lucide glyphs. A `translateY` correction here would be a magic number papering
|
|
44
|
+
* over less than it introduces.
|
|
45
|
+
*/
|
|
46
|
+
.ion-icon {
|
|
47
|
+
display: inline-block;
|
|
48
|
+
vertical-align: middle;
|
|
49
|
+
}
|
package/dist/styles/index.css
CHANGED
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
* it bypasses next/font and friends, and it hands visitor IPs to Google.
|
|
21
21
|
*/
|
|
22
22
|
@import url('./tokens/index.css');
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Before the components, deliberately. `.ion-icon` is a primitive that every
|
|
26
|
+
* slot below sizes with its own `... svg` rule, and those rules have to be free
|
|
27
|
+
* to win on ordering as well as specificity.
|
|
28
|
+
*/
|
|
29
|
+
@import url('./icon.css');
|
|
23
30
|
@import url('./button.css');
|
|
24
31
|
@import url('./tabs.css');
|
|
25
32
|
@import url('./badge.css');
|
|
@@ -108,6 +108,28 @@
|
|
|
108
108
|
--spacing-80: 80px;
|
|
109
109
|
--spacing-96: 96px;
|
|
110
110
|
--spacing-128: 128px;
|
|
111
|
+
--scale-0: 0px;
|
|
112
|
+
--scale-1: 1px;
|
|
113
|
+
--scale-2: 2px;
|
|
114
|
+
--scale-4: 4px;
|
|
115
|
+
--scale-6: 6px;
|
|
116
|
+
--scale-8: 8px;
|
|
117
|
+
--scale-10: 10px;
|
|
118
|
+
--scale-12: 12px;
|
|
119
|
+
--scale-14: 14px;
|
|
120
|
+
--scale-16: 16px;
|
|
121
|
+
--scale-20: 20px;
|
|
122
|
+
--scale-24: 24px;
|
|
123
|
+
--scale-32: 32px;
|
|
124
|
+
--scale-full: 9999px;
|
|
125
|
+
--font-family-host-grotesk: "Host Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
126
|
+
--font-family-space-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
127
|
+
--font-family-merriweather: Merriweather, Georgia, "Times New Roman", serif;
|
|
128
|
+
--font-family-stix-two-text: "STIX Two Text", Georgia, "Times New Roman", serif;
|
|
129
|
+
--font-family-sans: "Host Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
130
|
+
--font-family-serif: Merriweather, Georgia, "Times New Roman", serif;
|
|
131
|
+
--font-family-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
132
|
+
--font-family-serif-display: "STIX Two Text", Georgia, "Times New Roman", serif;
|
|
111
133
|
--font-size-12: 12px;
|
|
112
134
|
--font-size-14: 14px;
|
|
113
135
|
--font-size-16: 16px;
|
|
@@ -122,14 +144,6 @@
|
|
|
122
144
|
--font-size-56: 56px;
|
|
123
145
|
--font-size-64: 64px;
|
|
124
146
|
--font-size-72: 72px;
|
|
125
|
-
--font-family-host-grotesk: "Host Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
126
|
-
--font-family-merriweather: Merriweather, Georgia, "Times New Roman", serif;
|
|
127
|
-
--font-family-space-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
128
|
-
--font-family-stix-two-text: "STIX Two Text", Georgia, "Times New Roman", serif;
|
|
129
|
-
--font-family-sans: "Host Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
130
|
-
--font-family-serif: Merriweather, Georgia, "Times New Roman", serif;
|
|
131
|
-
--font-family-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
132
|
-
--font-family-serif-display: "STIX Two Text", Georgia, "Times New Roman", serif;
|
|
133
147
|
--font-weight-400: 400;
|
|
134
148
|
--font-weight-500: 500;
|
|
135
149
|
--font-weight-600: 600;
|
|
@@ -138,20 +152,6 @@
|
|
|
138
152
|
--font-weight-medium: 500;
|
|
139
153
|
--font-weight-semibold: 600;
|
|
140
154
|
--font-weight-bold: 700;
|
|
141
|
-
--scale-0: 0px;
|
|
142
|
-
--scale-1: 1px;
|
|
143
|
-
--scale-2: 2px;
|
|
144
|
-
--scale-4: 4px;
|
|
145
|
-
--scale-6: 6px;
|
|
146
|
-
--scale-8: 8px;
|
|
147
|
-
--scale-10: 10px;
|
|
148
|
-
--scale-12: 12px;
|
|
149
|
-
--scale-14: 14px;
|
|
150
|
-
--scale-16: 16px;
|
|
151
|
-
--scale-20: 20px;
|
|
152
|
-
--scale-24: 24px;
|
|
153
|
-
--scale-32: 32px;
|
|
154
|
-
--scale-full: 9999px;
|
|
155
155
|
--primary-50: #ebf7ff;
|
|
156
156
|
--primary-100: #e2f1fd;
|
|
157
157
|
--primary-200: #c7e4fa;
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
--text-primary: #1c62e3;
|
|
261
261
|
--text-error: #ba1c1c;
|
|
262
262
|
--text-success: #0d7d38;
|
|
263
|
-
--text-warning: #
|
|
263
|
+
--text-warning: #af3e0e;
|
|
264
264
|
--text-information: #4715b2;
|
|
265
265
|
--text-link: #1541b2;
|
|
266
266
|
--text-link-hover: #0e2d8b;
|
|
@@ -271,6 +271,7 @@
|
|
|
271
271
|
--icon-secondary: #384252;
|
|
272
272
|
--icon-tertiary: #4b5563;
|
|
273
273
|
--icon-disabled: #6b7280;
|
|
274
|
+
--icon-placeholder: #9ca3b0;
|
|
274
275
|
--icon-on-color: #ffffff;
|
|
275
276
|
--icon-inverse: #f6f8f9;
|
|
276
277
|
--icon-primary: #1c62e3;
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
0px 8px 8px -4px rgb(0 0 0 / 4%);
|
|
35
35
|
|
|
36
36
|
--ion-shadow-raised-lifted-xs:
|
|
37
|
-
inset 0px -2px 1px 0px rgb(0 0 0 /
|
|
37
|
+
inset 0px -2px 1px 0px rgb(0 0 0 / 12%),
|
|
38
38
|
inset 0px 2px 1px 0px rgb(255 255 255 / 20%),
|
|
39
39
|
0px 1px 1px 0px rgb(7 10 13 / 6%),
|
|
40
40
|
0px 1px 2px 0px rgb(7 10 13 / 4%);
|
|
41
41
|
|
|
42
42
|
--ion-shadow-raised-lifted-sm:
|
|
43
|
-
inset 0px -3px 1px 0px rgb(0 0 0 /
|
|
43
|
+
inset 0px -3px 1px 0px rgb(0 0 0 / 15%),
|
|
44
44
|
inset 0px 3px 1px 0px rgb(255 255 255 / 20%),
|
|
45
45
|
0px 1px 2px 0px rgb(7 10 13 / 8%),
|
|
46
46
|
0px 2px 4px 0px rgb(7 10 13 / 5%);
|
|
47
47
|
|
|
48
48
|
--ion-shadow-raised-lifted-lg:
|
|
49
|
-
inset 0px -4px 2px 0px rgb(0 0 0 /
|
|
49
|
+
inset 0px -4px 2px 0px rgb(0 0 0 / 20%),
|
|
50
50
|
inset 0px 4px 2px 0px rgb(255 255 255 / 20%),
|
|
51
51
|
0px 1px 1px 0px rgb(7 10 13 / 32%),
|
|
52
52
|
0px 1px 2px 0px rgb(7 10 13 / 16%);
|
|
@@ -74,15 +74,15 @@
|
|
|
74
74
|
0px 2px 6px 0px rgb(7 10 13 / 20%);
|
|
75
75
|
|
|
76
76
|
--ion-shadow-raised-flush-xs:
|
|
77
|
-
inset 0px -2px 1px 0px rgb(0 0 0 /
|
|
77
|
+
inset 0px -2px 1px 0px rgb(0 0 0 / 12%),
|
|
78
78
|
inset 0px 2px 1px 0px rgb(255 255 255 / 20%);
|
|
79
79
|
|
|
80
80
|
--ion-shadow-raised-flush-sm:
|
|
81
|
-
inset 0px -3px 1px 0px rgb(0 0 0 /
|
|
81
|
+
inset 0px -3px 1px 0px rgb(0 0 0 / 15%),
|
|
82
82
|
inset 0px 3px 1px 0px rgb(255 255 255 / 20%);
|
|
83
83
|
|
|
84
84
|
--ion-shadow-raised-flush-lg:
|
|
85
|
-
inset 0px -4px 2px 0px rgb(0 0 0 /
|
|
85
|
+
inset 0px -4px 2px 0px rgb(0 0 0 / 20%),
|
|
86
86
|
inset 0px 4px 2px 0px rgb(255 255 255 / 20%);
|
|
87
87
|
|
|
88
88
|
--ion-shadow-raised-flush-xl:
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
--text-default: #f6f8f9;
|
|
7
7
|
--text-secondary: #dde0e4;
|
|
8
8
|
--text-tertiary: #9ca3b0;
|
|
9
|
-
--text-placeholder: #
|
|
9
|
+
--text-placeholder: #4b5563;
|
|
10
10
|
--text-on-color: #000000;
|
|
11
11
|
--text-inverse: #131923;
|
|
12
12
|
--text-primary: #609cf0;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
--icon-default: #f0f2f4;
|
|
23
23
|
--icon-secondary: #dde0e4;
|
|
24
24
|
--icon-tertiary: #9ca3b0;
|
|
25
|
+
--icon-placeholder: #4b5563;
|
|
25
26
|
--icon-inverse: #131923;
|
|
26
27
|
--icon-primary: #609cf0;
|
|
27
28
|
--icon-error: #f56b6b;
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.ion-text-h1 {
|
|
17
|
-
font-family: var(--font-family-
|
|
17
|
+
font-family: var(--font-family-sans);
|
|
18
18
|
font-weight: var(--font-weight-semibold);
|
|
19
19
|
font-size: var(--type-h1);
|
|
20
20
|
line-height: var(--type-h1-line-height);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.ion-text-h2 {
|
|
24
|
-
font-family: var(--font-family-
|
|
24
|
+
font-family: var(--font-family-sans);
|
|
25
25
|
font-weight: var(--font-weight-semibold);
|
|
26
26
|
font-size: var(--type-h2);
|
|
27
27
|
line-height: var(--type-h2-line-height);
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
|
|
86
86
|
.ion-text-caption {
|
|
87
87
|
font-family: var(--font-family-sans);
|
|
88
|
-
font-weight: var(--font-weight-
|
|
88
|
+
font-weight: var(--font-weight-regular);
|
|
89
89
|
font-size: var(--type-caption);
|
|
90
90
|
line-height: var(--type-caption-line-height);
|
|
91
91
|
}
|