ionbase-ui 0.5.0 → 0.6.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/README.md CHANGED
@@ -9,6 +9,8 @@ npm i ionbase-ui
9
9
 
10
10
  React 18 or 19 is a peer dependency.
11
11
 
12
+ **[Browse every component in Storybook →](https://raza-ahmed.github.io/ionbase-design-system/)**
13
+
12
14
  ## Usage
13
15
 
14
16
  Import the stylesheet once, at your app entry:
@@ -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`. Hidden below 1216px, as in the Tablet and Mobile variants. */
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 three Device variants through media queries, not a
13
- * prop.
14
- *
15
- * Device is the one variant axis in the system the browser already knows the
16
- * answer to. Every other component takes its variant from the caller because
17
- * the caller is the only one who knows; a header is Mobile because the viewport
18
- * is narrow, and having React duplicate that judgement is how the two drift
19
- * apart. Breakpoints match the Breakpoint collection's container widths.
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":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAE1C,MAAM,WAAW,WAAY,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IACpE,+DAA+D;IAC/D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,wFAAwF;IACxF,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,0BAA0B;IAC1B,GAAG,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,iFAkBlB,CAAC"}
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 three Device variants through media queries, not a
5
- * prop.
10
+ * Header renders Figma's four Device variants — Desktop, Tablet,
11
+ * Mobile-Closed, Mobile-Open — from one DOM tree.
6
12
  *
7
- * Device is the one variant axis in the system the browser already knows the
8
- * answer to. Every other component takes its variant from the caller because
9
- * the caller is the only one who knows; a header is Mobile because the viewport
10
- * is narrow, and having React duplicate that judgement is how the two drift
11
- * apart. Breakpoints match the Breakpoint collection's container widths.
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, className, children, ...rest }, ref) => (_jsxs("header", { ...rest, ref: ref, className: ['ion-header', className].filter(Boolean).join(' '), children: [brand && _jsx("div", { className: "ion-header__brand", children: brand }), _jsx("div", { className: "ion-header__center", children: center }), children, end && _jsx("div", { className: "ion-header__end", children: 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":";AAAA,OAAc,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAY1C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAC9B,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAC7D,qBACM,IAAI,EACR,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAE7D,KAAK,IAAI,cAAK,SAAS,EAAC,mBAAmB,YAAE,KAAK,GAAO,EAM1D,cAAK,SAAS,EAAC,oBAAoB,YAAE,MAAM,GAAO,EACjD,QAAQ,EACR,GAAG,IAAI,cAAK,SAAS,EAAC,iBAAiB,YAAE,GAAG,GAAO,IAC7C,CACV,CACF,CAAC;AAEF,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC"}
1
+ {"version":3,"file":"Header.js","sourceRoot":"","sources":["../../src/components/Header.tsx"],"names":[],"mappings":"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"}
@@ -1,31 +1,46 @@
1
1
  /*
2
2
  * Header
3
3
  *
4
- * Measured from Figma `Header` (70:22088). Three device variants:
4
+ * Measured from Figma `Header` (69:335). Four Device variants, two axes:
5
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
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 THIS IS MEDIA QUERIES AND NOT A PROP
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 that the browser already
13
- * knows the answer to. Every other component takes its variant from the caller
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. The breakpoints match the Breakpoint collection's own container
17
- * widths — Tablet below 1216, Mobile below 896.
17
+ * of step. Breakpoints match the Breakpoint collection's own container widths —
18
+ * Tablet below 1216, Mobile below 896.
18
19
  *
19
- * Note the header is taller on Tablet and Mobile than on Desktop (64 vs 56).
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
- --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);
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 >= 896px) {
61
+ @media (width < 1216px) {
47
62
  .ion-header {
48
- --ion-header-padding-x: var(--spacing-32);
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 >= 1216px) {
68
+ @media (width < 896px) {
53
69
  .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);
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
- * 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.
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: none;
95
+ display: flex;
78
96
  align-items: center;
79
- gap: var(--spacing-8);
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
+ }
@@ -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: #8e2f07;
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 / 25%),
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 / 25%),
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 / 25%),
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 / 25%),
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 / 25%),
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 / 25%),
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: #6b7280;
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-serif-display);
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-serif-display);
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-medium);
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
  }