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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/Icon.d.ts +72 -0
  4. package/dist/Icon.d.ts.map +1 -0
  5. package/dist/Icon.js +50 -0
  6. package/dist/Icon.js.map +1 -0
  7. package/dist/components/Avatar.d.ts +52 -0
  8. package/dist/components/Avatar.d.ts.map +1 -0
  9. package/dist/components/Avatar.js +59 -0
  10. package/dist/components/Avatar.js.map +1 -0
  11. package/dist/components/Badge.d.ts +26 -0
  12. package/dist/components/Badge.d.ts.map +1 -0
  13. package/dist/components/Badge.js +17 -0
  14. package/dist/components/Badge.js.map +1 -0
  15. package/dist/components/Button.d.ts +18 -0
  16. package/dist/components/Button.d.ts.map +1 -0
  17. package/dist/components/Button.js +43 -0
  18. package/dist/components/Button.js.map +1 -0
  19. package/dist/components/Checkbox.d.ts +30 -0
  20. package/dist/components/Checkbox.d.ts.map +1 -0
  21. package/dist/components/Checkbox.js +39 -0
  22. package/dist/components/Checkbox.js.map +1 -0
  23. package/dist/components/Divider.d.ts +17 -0
  24. package/dist/components/Divider.d.ts.map +1 -0
  25. package/dist/components/Divider.js +16 -0
  26. package/dist/components/Divider.js.map +1 -0
  27. package/dist/components/Header.d.ts +25 -0
  28. package/dist/components/Header.d.ts.map +1 -0
  29. package/dist/components/Header.js +18 -0
  30. package/dist/components/Header.js.map +1 -0
  31. package/dist/components/Input.d.ts +32 -0
  32. package/dist/components/Input.d.ts.map +1 -0
  33. package/dist/components/Input.js +66 -0
  34. package/dist/components/Input.js.map +1 -0
  35. package/dist/components/Logo.d.ts +41 -0
  36. package/dist/components/Logo.d.ts.map +1 -0
  37. package/dist/components/Logo.js +70 -0
  38. package/dist/components/Logo.js.map +1 -0
  39. package/dist/components/Menu.d.ts +29 -0
  40. package/dist/components/Menu.d.ts.map +1 -0
  41. package/dist/components/Menu.js +31 -0
  42. package/dist/components/Menu.js.map +1 -0
  43. package/dist/components/NavItem.d.ts +30 -0
  44. package/dist/components/NavItem.d.ts.map +1 -0
  45. package/dist/components/NavItem.js +45 -0
  46. package/dist/components/NavItem.js.map +1 -0
  47. package/dist/components/Radio.d.ts +30 -0
  48. package/dist/components/Radio.d.ts.map +1 -0
  49. package/dist/components/Radio.js +61 -0
  50. package/dist/components/Radio.js.map +1 -0
  51. package/dist/components/ScrollProgress.d.ts +33 -0
  52. package/dist/components/ScrollProgress.d.ts.map +1 -0
  53. package/dist/components/ScrollProgress.js +115 -0
  54. package/dist/components/ScrollProgress.js.map +1 -0
  55. package/dist/components/Select.d.ts +43 -0
  56. package/dist/components/Select.d.ts.map +1 -0
  57. package/dist/components/Select.js +80 -0
  58. package/dist/components/Select.js.map +1 -0
  59. package/dist/components/Table.d.ts +71 -0
  60. package/dist/components/Table.d.ts.map +1 -0
  61. package/dist/components/Table.js +60 -0
  62. package/dist/components/Table.js.map +1 -0
  63. package/dist/components/Tabs.d.ts +23 -0
  64. package/dist/components/Tabs.d.ts.map +1 -0
  65. package/dist/components/Tabs.js +47 -0
  66. package/dist/components/Tabs.js.map +1 -0
  67. package/dist/components/Toggle.d.ts +25 -0
  68. package/dist/components/Toggle.d.ts.map +1 -0
  69. package/dist/components/Toggle.js +32 -0
  70. package/dist/components/Toggle.js.map +1 -0
  71. package/dist/components/index.d.ts +33 -0
  72. package/dist/components/index.d.ts.map +1 -0
  73. package/dist/components/index.js +17 -0
  74. package/dist/components/index.js.map +1 -0
  75. package/dist/index.d.ts +25 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +24 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/styles/avatar.css +138 -0
  80. package/dist/styles/badge.css +125 -0
  81. package/dist/styles/button.css +331 -0
  82. package/dist/styles/checkbox.css +229 -0
  83. package/dist/styles/divider.css +31 -0
  84. package/dist/styles/header.css +95 -0
  85. package/dist/styles/index.css +44 -0
  86. package/dist/styles/input.css +264 -0
  87. package/dist/styles/logo.css +63 -0
  88. package/dist/styles/menu.css +150 -0
  89. package/dist/styles/nav-item.css +120 -0
  90. package/dist/styles/radio.css +200 -0
  91. package/dist/styles/scroll-progress.css +171 -0
  92. package/dist/styles/select.css +162 -0
  93. package/dist/styles/table.css +192 -0
  94. package/dist/styles/tabs.css +219 -0
  95. package/dist/styles/toggle.css +184 -0
  96. package/dist/styles/tokens/base.css +386 -0
  97. package/dist/styles/tokens/breakpoint-mobile.css +34 -0
  98. package/dist/styles/tokens/breakpoint-tablet.css +25 -0
  99. package/dist/styles/tokens/elevation.css +112 -0
  100. package/dist/styles/tokens/index.css +6 -0
  101. package/dist/styles/tokens/theme-dark.css +106 -0
  102. package/dist/styles/tokens/typography.css +124 -0
  103. package/dist/tokens/index.d.ts +2097 -0
  104. package/dist/tokens/index.d.ts.map +1 -0
  105. package/dist/tokens/index.js +1702 -0
  106. package/dist/tokens/index.js.map +1 -0
  107. package/package.json +70 -0
@@ -0,0 +1,162 @@
1
+ /*
2
+ * Select
3
+ *
4
+ * Measured from Figma `Select` (82:379). Three sizes x six states.
5
+ *
6
+ * Geometry is identical to Input — same heights, padding, radius, type ramp and
7
+ * the same inside-stroke correction (see input.css for why the border width is
8
+ * subtracted from the padding, and why these read the spacing scale directly
9
+ * rather than a shared control token). The two differ in two ways only:
10
+ *
11
+ * - a chevron replaces the trailing icon slot, and is always icon/tertiary
12
+ * rather than icon/interactive
13
+ * - there is no Read-only state, because a native <select> has no readonly
14
+ *
15
+ * Invalid used to be 2px here and 1px on Input. That was drift, not intent —
16
+ * neither component bound its stroke weight to a token, so nothing held the
17
+ * two together. Both are now 2px, bound to border-width/thick in Figma.
18
+ *
19
+ * The rules are not shared with Input despite the overlap. A shared base class
20
+ * would couple two components that Figma models separately, and the first time
21
+ * one size changes on its own the sharing has to be unpicked under pressure.
22
+ */
23
+ .ion-select {
24
+ --ion-select-height: var(--spacing-40);
25
+ --ion-select-padding-x: var(--spacing-12);
26
+ --ion-select-gap: var(--spacing-8);
27
+ --ion-select-icon-size: var(--icon-size-md);
28
+ --ion-select-radius: var(--radius-md);
29
+ --ion-select-font-size: var(--type-body);
30
+ --ion-select-line-height: var(--type-body-line-height);
31
+ --ion-select-border-width: var(--border-width-default);
32
+
33
+ position: relative;
34
+ display: flex;
35
+ align-items: center;
36
+ gap: var(--ion-select-gap);
37
+ min-height: var(--ion-select-height);
38
+ padding-inline: calc(
39
+ var(--ion-select-padding-x) - var(--ion-select-border-width)
40
+ );
41
+ background-color: var(--surface-default);
42
+ border-style: solid;
43
+ border-width: var(--ion-select-border-width);
44
+ border-color: var(--border-default);
45
+ border-radius: var(--ion-select-radius);
46
+ font-family: var(--font-family-sans);
47
+ font-size: var(--ion-select-font-size);
48
+ line-height: var(--ion-select-line-height);
49
+ transition:
50
+ border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
51
+ background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
52
+ }
53
+
54
+ /* Small shares Medium's padding — see input.css. */
55
+ .ion-select--sm {
56
+ --ion-select-height: var(--spacing-32);
57
+ --ion-select-padding-x: var(--spacing-12);
58
+ --ion-select-gap: var(--spacing-6);
59
+ --ion-select-icon-size: var(--icon-size-sm);
60
+ --ion-select-radius: var(--radius-sm);
61
+ --ion-select-font-size: var(--type-body-sm);
62
+ --ion-select-line-height: var(--type-body-sm-line-height);
63
+ }
64
+
65
+ .ion-select--lg {
66
+ --ion-select-height: var(--spacing-48);
67
+ --ion-select-padding-x: var(--spacing-16);
68
+ }
69
+
70
+ /*
71
+ * `appearance: none` removes the platform chevron so the Figma one can be drawn
72
+ * instead. The native control is kept underneath rather than rebuilt: it brings
73
+ * keyboard handling, type-ahead and the platform picker on mobile, none of
74
+ * which a div can be talked into. Figma's dropdown list is a separate component
75
+ * (the Menu page) and is not this file's concern.
76
+ */
77
+ .ion-select__field {
78
+ flex: 1 1 auto;
79
+ min-width: 0;
80
+ appearance: none;
81
+ padding: 0;
82
+ background-color: transparent;
83
+ border: none;
84
+ outline: none;
85
+ color: var(--text-secondary);
86
+ font-family: inherit;
87
+ font-size: inherit;
88
+ line-height: inherit;
89
+ font-weight: var(--font-weight-regular);
90
+ cursor: pointer;
91
+ }
92
+
93
+ /*
94
+ * A native <select> has no ::placeholder, so the empty state is a modifier the
95
+ * component sets from its value rather than something CSS can detect.
96
+ */
97
+ .ion-select--placeholder .ion-select__field {
98
+ color: var(--text-tertiary);
99
+ }
100
+
101
+ /*
102
+ * The chevron is decorative and sits behind the select in the stacking order,
103
+ * so a click anywhere in the box still opens the native picker.
104
+ */
105
+ .ion-select__chevron {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ flex-shrink: 0;
110
+ pointer-events: none;
111
+ color: var(--icon-tertiary);
112
+ }
113
+
114
+ .ion-select__chevron svg {
115
+ width: var(--ion-select-icon-size);
116
+ height: var(--ion-select-icon-size);
117
+ }
118
+
119
+ /* State order matches Input: hover -> focus -> invalid -> disabled. */
120
+
121
+ @media (hover: hover) {
122
+ .ion-select:hover {
123
+ border-color: var(--border-strong);
124
+ }
125
+ }
126
+
127
+ .ion-select[data-hovered='true'] {
128
+ border-color: var(--border-strong);
129
+ }
130
+
131
+ .ion-select:focus-within,
132
+ .ion-select[data-focused='true'] {
133
+ --ion-select-border-width: var(--border-width-thick);
134
+
135
+ border-color: var(--border-focus);
136
+ }
137
+
138
+ /* 2px, matching Focus and now matching Input. See input.css. */
139
+ .ion-select--invalid,
140
+ .ion-select[data-invalid='true'] {
141
+ --ion-select-border-width: var(--border-width-thick);
142
+
143
+ border-color: var(--border-error-strong);
144
+ }
145
+
146
+ .ion-select--disabled,
147
+ .ion-select[data-disabled='true'] {
148
+ background-color: var(--surface-disabled);
149
+ border-color: var(--border-disabled);
150
+ cursor: not-allowed;
151
+ }
152
+
153
+ .ion-select--disabled .ion-select__field,
154
+ .ion-select[data-disabled='true'] .ion-select__field {
155
+ color: var(--text-disabled);
156
+ cursor: not-allowed;
157
+ }
158
+
159
+ .ion-select--disabled .ion-select__chevron,
160
+ .ion-select[data-disabled='true'] .ion-select__chevron {
161
+ color: var(--icon-disabled);
162
+ }
@@ -0,0 +1,192 @@
1
+ /*
2
+ * Table
3
+ *
4
+ * Measured from Figma `Table Row` / `Table Cell` / `Cell Text` (173:42).
5
+ * Replaces a pre-v2 file that predated this measurement: its header used
6
+ * `font-weight: semibold` where Figma's header cell is Medium, and its three
7
+ * density paddings were wrong on both axes — see DENSITY below.
8
+ *
9
+ * DENSITY ONLY MOVES VERTICAL PADDING
10
+ *
11
+ * Compact/Default/Relaxed are 8/16/20 top-and-bottom, but all three share the
12
+ * SAME 16px horizontal padding. Columns do not get any narrower as density
13
+ * increases — only row height does. A density system that also squeezed the
14
+ * horizontal padding would have been a plausible-looking invention with
15
+ * nothing in Figma behind it.
16
+ *
17
+ * HEADER FILL IS PER-CELL, NOT PER-ROW
18
+ *
19
+ * `surface/page` is bound on the header CELL, not on `<thead>`. Visually
20
+ * identical when every header cell shares it, but it is why this reads
21
+ * `.ion-table th` rather than a `.ion-table__header` row class — matching
22
+ * where Figma actually put the token means a table that mixes header cells
23
+ * into a body row (a sticky first column, say) still gets the fill from the
24
+ * right place.
25
+ *
26
+ * ROW BORDER IS BOTTOM ONLY
27
+ *
28
+ * Figma's row stroke resolves to top 0 / right 0 / bottom 1 / left 0, not a
29
+ * mixed/mystery value — confirmed by reading all four edges rather than
30
+ * assuming "MIXED" meant "top and bottom". One `border-bottom`, not a
31
+ * 4-sided border that needs collapsing.
32
+ */
33
+ .ion-table-container {
34
+ width: 100%;
35
+ overflow-x: auto;
36
+ border: var(--border-width-default) solid var(--border-default);
37
+ border-radius: var(--radius-md);
38
+ }
39
+
40
+ .ion-table {
41
+ width: 100%;
42
+ border-collapse: collapse;
43
+ border-spacing: 0;
44
+ font-family: var(--font-family-sans);
45
+ font-size: var(--type-body-sm);
46
+ line-height: var(--type-body-sm-line-height);
47
+ text-align: left;
48
+ }
49
+
50
+ .ion-table th,
51
+ .ion-table td {
52
+ padding: var(--spacing-16) var(--spacing-16);
53
+ vertical-align: middle;
54
+ }
55
+
56
+ /*
57
+ * Density — vertical padding only. See the header.
58
+ *
59
+ * Sits here, next to the padding it overrides, rather than further down with
60
+ * the other modifiers: `.ion-table--compact td` is (0,1,1), lower than the
61
+ * `.ion-table thead td` below it, so trailing it would read as descending
62
+ * specificity. Grouping layout before appearance is the better order anyway.
63
+ */
64
+ .ion-table--compact th,
65
+ .ion-table--compact td {
66
+ padding-top: var(--spacing-8);
67
+ padding-bottom: var(--spacing-8);
68
+ }
69
+
70
+ .ion-table--relaxed th,
71
+ .ion-table--relaxed td {
72
+ padding-top: var(--spacing-20);
73
+ padding-bottom: var(--spacing-20);
74
+ }
75
+
76
+ .ion-table td {
77
+ color: var(--text-default);
78
+ font-weight: var(--font-weight-regular);
79
+ }
80
+
81
+ /*
82
+ * Header appearance follows EITHER condition: the cell is a `<th>`, or it sits
83
+ * inside `<thead>`.
84
+ *
85
+ * The `<th>` half is the Figma binding (see the header). The `thead` half
86
+ * exists because a header row can legitimately contain a cell that is not a
87
+ * column header — a selection-checkbox column has nothing to label, and an
88
+ * empty `<th>` is an accessibility failure rather than a neutral one: axe's
89
+ * `empty-table-header` flags it, and `aria-label` does not rescue it, since the
90
+ * rule is specifically about text a sighted screen-reader user can also see.
91
+ * Such a cell should be a `<td>`, which then still needs to look like a header.
92
+ *
93
+ * Declared after `.ion-table td` so specificity reads non-decreasing; the two
94
+ * `td` selectors would otherwise trip `no-descending-specificity`.
95
+ */
96
+ .ion-table th,
97
+ .ion-table thead td {
98
+ background-color: var(--surface-page);
99
+ color: var(--text-secondary);
100
+ font-weight: var(--font-weight-medium);
101
+ border-bottom: var(--border-width-default) solid var(--border-default);
102
+ }
103
+
104
+ .ion-table tr {
105
+ background-color: var(--surface-default);
106
+ transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
107
+ }
108
+
109
+ .ion-table tbody tr {
110
+ border-bottom: var(--border-width-default) solid var(--border-default);
111
+ }
112
+
113
+ /*
114
+ * Striped rows have to come BEFORE hover/selected in the file, not just
115
+ * "be a plain background" — `.ion-table--striped tbody tr:nth-of-type(even)`
116
+ * and `.ion-table tbody tr:hover` carry the same specificity (one class, one
117
+ * pseudo-class, two elements), so with equal specificity the later rule in
118
+ * the file wins. Checked, not assumed: swapping the order silently makes a
119
+ * striped, hovered row show the page stripe instead of the hover tint.
120
+ */
121
+ .ion-table--striped tbody tr:nth-of-type(even) {
122
+ background-color: var(--surface-page);
123
+ }
124
+
125
+ @media (hover: hover) {
126
+ .ion-table tbody tr:hover {
127
+ background-color: var(--surface-hover);
128
+ }
129
+ }
130
+
131
+ .ion-table tbody tr[data-hovered='true'] {
132
+ background-color: var(--surface-hover);
133
+ }
134
+
135
+ .ion-table tbody tr[data-selected='true'] {
136
+ background-color: var(--surface-primary-subtle);
137
+ }
138
+
139
+ .ion-table th[data-align='trailing'],
140
+ .ion-table td[data-align='trailing'] {
141
+ text-align: right;
142
+ }
143
+
144
+ .ion-table th[data-align='center'],
145
+ .ion-table td[data-align='center'] {
146
+ text-align: center;
147
+ }
148
+
149
+ /* Cell content — icon, label, icon, matching Figma's `Cell Text`. */
150
+ .ion-table__cell-content {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: var(--spacing-6);
154
+ }
155
+
156
+ .ion-table__cell-icon {
157
+ display: inline-flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ flex-shrink: 0;
161
+ color: var(--icon-interactive);
162
+ }
163
+
164
+ .ion-table__cell-icon--trailing {
165
+ color: var(--icon-tertiary);
166
+ }
167
+
168
+ .ion-table__cell-icon svg {
169
+ width: var(--icon-size-sm);
170
+ height: var(--icon-size-sm);
171
+ }
172
+
173
+ .ion-table th .ion-table__cell-icon--trailing {
174
+ color: var(--icon-secondary);
175
+ }
176
+
177
+ /* Link cells — Figma's `Type=Link`. */
178
+ .ion-table__cell-content--link {
179
+ color: var(--text-link);
180
+ }
181
+
182
+ /*
183
+ * Column divider — Figma's per-cell `Show Divider`. A border-right rather
184
+ * than the fixed-height rule Figma draws: Figma's rule is only ever as tall
185
+ * as that one demo cell's text, which is an artifact of the canvas, not a
186
+ * measurement to reproduce. A full-height border-right is the version that
187
+ * still looks right at any row height.
188
+ */
189
+ .ion-table th[data-divider='true'],
190
+ .ion-table td[data-divider='true'] {
191
+ border-right: var(--border-width-default) solid var(--border-default);
192
+ }
@@ -0,0 +1,219 @@
1
+ /*
2
+ * Tabs
3
+ *
4
+ * Geometry measured from Figma `Tabs Item` (156:259) and `Tabs` (157:126).
5
+ * Both types share padding, gap and height; only radius and the selected
6
+ * indicator differ, so size lives on the item and type lives on the track.
7
+ *
8
+ * Size is a set of slots defaulting to medium — a size modifier reassigns five
9
+ * variables rather than restating five declarations, and the icon rule needs no
10
+ * per-size override.
11
+ *
12
+ * Only the horizontal orientation is implemented. Figma also defines vertical;
13
+ * the track modifier is named `--horizontal` so vertical is additive rather
14
+ * than a rename.
15
+ */
16
+
17
+ /* Track */
18
+ .ion-tabs__track {
19
+ display: flex;
20
+ align-items: center;
21
+ list-style: none;
22
+ margin: 0;
23
+ padding: 0;
24
+ }
25
+
26
+ .ion-tabs__track--underline.ion-tabs__track--horizontal {
27
+ display: inline-flex;
28
+ gap: var(--spacing-4);
29
+ border-bottom: var(--border-width-default) solid var(--border-default);
30
+ }
31
+
32
+ .ion-tabs__track--pill.ion-tabs__track--horizontal {
33
+ display: inline-flex;
34
+ gap: var(--spacing-2);
35
+ padding: var(--spacing-4) var(--spacing-4);
36
+ background-color: var(--surface-muted);
37
+
38
+ /* `radius/full` — Figma's pill track is fully rounded, not an 8px rect. */
39
+ border-radius: var(--radius-full);
40
+ }
41
+
42
+ /* Item */
43
+ .ion-tabs__item {
44
+ --ion-tabs-item-size: var(--spacing-40);
45
+ --ion-tabs-item-padding-x: var(--spacing-16);
46
+ --ion-tabs-item-padding-y: var(--spacing-8);
47
+ --ion-tabs-item-gap: var(--spacing-8);
48
+ --ion-tabs-icon-size: var(--spacing-20);
49
+
50
+ /*
51
+ * Typography follows the Figma text styles: Body/Small Emphasis at Small,
52
+ * Body/Default Emphasis at Medium, Body/Large Emphasis at Large. Emphasis is
53
+ * the medium weight, which is why font-weight is size-invariant here.
54
+ */
55
+ --ion-tabs-item-font-size: var(--type-body);
56
+ --ion-tabs-item-line-height: var(--type-body-line-height);
57
+
58
+ display: inline-flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ min-height: var(--ion-tabs-item-size);
62
+ padding: var(--ion-tabs-item-padding-y) var(--ion-tabs-item-padding-x);
63
+ gap: var(--ion-tabs-item-gap);
64
+ font-family: var(--font-family-sans);
65
+ font-weight: var(--font-weight-medium);
66
+ font-size: var(--ion-tabs-item-font-size);
67
+ line-height: var(--ion-tabs-item-line-height);
68
+ background-color: transparent;
69
+ border: none;
70
+ text-decoration: none;
71
+ cursor: pointer;
72
+ user-select: none;
73
+ transition:
74
+ color 150ms cubic-bezier(0.4, 0, 0.2, 1),
75
+ background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
76
+ border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
77
+ }
78
+
79
+ .ion-tabs__item--sm {
80
+ --ion-tabs-item-size: var(--spacing-32);
81
+ --ion-tabs-item-padding-x: var(--spacing-12);
82
+ --ion-tabs-item-padding-y: var(--spacing-6);
83
+ --ion-tabs-item-gap: var(--spacing-6);
84
+ --ion-tabs-icon-size: var(--icon-size-sm);
85
+ --ion-tabs-item-font-size: var(--type-body-sm);
86
+ --ion-tabs-item-line-height: var(--type-body-sm-line-height);
87
+ }
88
+
89
+ .ion-tabs__item--lg {
90
+ --ion-tabs-item-size: var(--spacing-48);
91
+ --ion-tabs-item-padding-x: var(--spacing-20);
92
+ --ion-tabs-item-padding-y: var(--spacing-8);
93
+ --ion-tabs-item-gap: var(--spacing-8);
94
+ --ion-tabs-icon-size: var(--spacing-20);
95
+ --ion-tabs-item-font-size: var(--type-body-lg);
96
+ --ion-tabs-item-line-height: var(--type-body-lg-line-height);
97
+ }
98
+
99
+ /* Underline item */
100
+ .ion-tabs__item--underline {
101
+ color: var(--text-tertiary);
102
+ border-bottom: var(--border-width-thick) solid transparent;
103
+
104
+ /*
105
+ * Figma draws the indicator as an INSIDE stroke, so it does not add height.
106
+ * Absorbing it into the bottom padding keeps the item at exactly its token
107
+ * height instead of 2px taller than every other variant.
108
+ */
109
+ padding-bottom: calc(
110
+ var(--ion-tabs-item-padding-y) - var(--border-width-thick)
111
+ );
112
+
113
+ /* Overlap the track's own border so the indicator replaces it rather than
114
+ stacking below it. */
115
+ margin-bottom: calc(-1 * var(--border-width-default));
116
+ }
117
+
118
+ /*
119
+ * Hover is declared twice, as on Button: `:hover` is pointer-guarded because
120
+ * native hover latches on touch, and `[data-hovered]` comes from React Aria's
121
+ * useHover, which is already pointer-aware.
122
+ */
123
+ @media (hover: hover) {
124
+ .ion-tabs__item--underline:hover {
125
+ color: var(--text-default);
126
+ }
127
+ }
128
+
129
+ .ion-tabs__item--underline[data-hovered='true'] {
130
+ color: var(--text-default);
131
+ }
132
+
133
+ .ion-tabs__item--underline[data-selected='true'] {
134
+ color: var(--text-default);
135
+ border-bottom-color: var(--border-primary);
136
+ }
137
+
138
+ /* Pill item */
139
+ .ion-tabs__item--pill {
140
+ color: var(--text-tertiary);
141
+ border-radius: var(--radius-full);
142
+ }
143
+
144
+ @media (hover: hover) {
145
+ .ion-tabs__item--pill:hover {
146
+ color: var(--text-default);
147
+ background-color: var(--surface-hover);
148
+ }
149
+ }
150
+
151
+ .ion-tabs__item--pill[data-hovered='true'] {
152
+ color: var(--text-default);
153
+ background-color: var(--surface-hover);
154
+ }
155
+
156
+ .ion-tabs__item--pill[data-selected='true'] {
157
+ color: var(--text-default);
158
+ background-color: var(--surface-default);
159
+ }
160
+
161
+ /*
162
+ * Selected *and* hovered — a composed state, which is why the token is named
163
+ * `hover-selected` in vocabulary order rather than `selected-hover`.
164
+ */
165
+ @media (hover: hover) {
166
+ .ion-tabs__item--pill[data-selected='true']:hover {
167
+ background-color: var(--surface-page);
168
+ }
169
+ }
170
+
171
+ .ion-tabs__item--pill[data-selected='true'][data-hovered='true'] {
172
+ background-color: var(--surface-page);
173
+ }
174
+
175
+ /*
176
+ * Focus ring.
177
+ *
178
+ * Figma draws a 2px `border/brand/default/focus` stroke offset 2px outside the
179
+ * item, with a transparent gap. `outline` + `outline-offset` reproduces that
180
+ * exactly; a box-shadow cannot, because the gap would have to be painted in the
181
+ * backdrop colour and these items sit on a coloured pill track.
182
+ *
183
+ * Tabs previously borrowed Button's ring tokens. It has its own now.
184
+ */
185
+ .ion-tabs__item:focus-visible,
186
+ .ion-tabs__item[data-focused='true'] {
187
+ outline: var(--border-width-thick) solid var(--ring-focus);
188
+ outline-offset: var(--border-width-thick);
189
+ }
190
+
191
+ /* Underline items are square, but Figma rounds their focus ring to 8px. */
192
+ .ion-tabs__item--underline:focus-visible,
193
+ .ion-tabs__item--underline[data-focused='true'] {
194
+ border-radius: var(--radius-md);
195
+ }
196
+
197
+ /* Disabled */
198
+ .ion-tabs__item[data-disabled='true'] {
199
+ color: var(--text-disabled);
200
+ cursor: not-allowed;
201
+ background-color: transparent;
202
+ }
203
+
204
+ .ion-tabs__item--underline[data-disabled='true'] {
205
+ border-bottom-color: transparent;
206
+ }
207
+
208
+ /* Icons — one rule for every size; the slot is reassigned by the modifier. */
209
+ .ion-tabs__item svg {
210
+ width: var(--ion-tabs-icon-size);
211
+ height: var(--ion-tabs-icon-size);
212
+ flex-shrink: 0;
213
+ }
214
+
215
+ /* Panel */
216
+ .ion-tabs__panel:focus-visible {
217
+ outline: var(--border-width-thick) solid var(--ring-focus);
218
+ outline-offset: var(--border-width-thick);
219
+ }