ionbase-ui 0.59.0 → 0.70.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/dist/components/AgentActivity.d.ts +2 -1
- package/dist/components/AgentActivity.d.ts.map +1 -1
- package/dist/components/AgentActivity.js +4 -17
- package/dist/components/AgentActivity.js.map +1 -1
- package/dist/components/Calendar.d.ts +20 -0
- package/dist/components/Calendar.d.ts.map +1 -0
- package/dist/components/Calendar.js +151 -0
- package/dist/components/Calendar.js.map +1 -0
- package/dist/components/CalendarPopover.d.ts +26 -0
- package/dist/components/CalendarPopover.d.ts.map +1 -0
- package/dist/components/CalendarPopover.js +45 -0
- package/dist/components/CalendarPopover.js.map +1 -0
- package/dist/components/DateField.d.ts +15 -0
- package/dist/components/DateField.d.ts.map +1 -0
- package/dist/components/DateField.js +74 -0
- package/dist/components/DateField.js.map +1 -0
- package/dist/components/DatePicker.d.ts +81 -0
- package/dist/components/DatePicker.d.ts.map +1 -0
- package/dist/components/DatePicker.js +102 -0
- package/dist/components/DatePicker.js.map +1 -0
- package/dist/components/DateRangePicker.d.ts +117 -0
- package/dist/components/DateRangePicker.d.ts.map +1 -0
- package/dist/components/DateRangePicker.js +152 -0
- package/dist/components/DateRangePicker.js.map +1 -0
- package/dist/components/Header.d.ts +10 -0
- package/dist/components/Header.d.ts.map +1 -1
- package/dist/components/Header.js +18 -3
- package/dist/components/Header.js.map +1 -1
- package/dist/components/NumberInput.d.ts +63 -0
- package/dist/components/NumberInput.d.ts.map +1 -0
- package/dist/components/NumberInput.js +102 -0
- package/dist/components/NumberInput.js.map +1 -0
- package/dist/components/PromptInput.d.ts +89 -0
- package/dist/components/PromptInput.d.ts.map +1 -0
- package/dist/components/PromptInput.js +130 -0
- package/dist/components/PromptInput.js.map +1 -0
- package/dist/components/ScrollProgress.d.ts +6 -0
- package/dist/components/ScrollProgress.d.ts.map +1 -1
- package/dist/components/ScrollProgress.js +2 -2
- package/dist/components/ScrollProgress.js.map +1 -1
- package/dist/components/SegmentedControl.d.ts +63 -0
- package/dist/components/SegmentedControl.d.ts.map +1 -0
- package/dist/components/SegmentedControl.js +79 -0
- package/dist/components/SegmentedControl.js.map +1 -0
- package/dist/components/Sidebar.d.ts +109 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.js +106 -0
- package/dist/components/Sidebar.js.map +1 -0
- package/dist/components/Stepper.d.ts +73 -0
- package/dist/components/Stepper.d.ts.map +1 -0
- package/dist/components/Stepper.js +82 -0
- package/dist/components/Stepper.js.map +1 -0
- package/dist/components/Toast.d.ts +8 -1
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +2 -2
- package/dist/components/Toast.js.map +1 -1
- package/dist/components/ToolCall.d.ts +66 -0
- package/dist/components/ToolCall.d.ts.map +1 -0
- package/dist/components/ToolCall.js +78 -0
- package/dist/components/ToolCall.js.map +1 -0
- package/dist/components/agent-status.d.ts +14 -0
- package/dist/components/agent-status.d.ts.map +1 -0
- package/dist/components/agent-status.js +16 -0
- package/dist/components/agent-status.js.map +1 -0
- package/dist/components/index.d.ts +18 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +8 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/iso-date.d.ts +53 -0
- package/dist/components/iso-date.d.ts.map +1 -0
- package/dist/components/iso-date.js +102 -0
- package/dist/components/iso-date.js.map +1 -0
- package/dist/figma-descriptions.json +107 -52
- package/dist/figma-map.json +735 -2
- package/dist/meta/AgentActivityStep.json +2 -2
- package/dist/meta/DatePicker.json +270 -0
- package/dist/meta/DateRangePicker.json +284 -0
- package/dist/meta/Header.json +32 -2
- package/dist/meta/NumberInput.json +407 -0
- package/dist/meta/PromptInput.json +212 -0
- package/dist/meta/ScrollProgress.json +26 -1
- package/dist/meta/SegmentedControl.json +217 -0
- package/dist/meta/SegmentedControlItem.json +122 -0
- package/dist/meta/Sidebar.json +150 -0
- package/dist/meta/SidebarItem.json +184 -0
- package/dist/meta/SidebarSection.json +131 -0
- package/dist/meta/Stepper.json +135 -0
- package/dist/meta/StepperStep.json +148 -0
- package/dist/meta/Tabs.json +5 -0
- package/dist/meta/ToastProvider.json +9 -2
- package/dist/meta/ToolCall.json +192 -0
- package/dist/meta/components.json +5812 -3293
- package/dist/meta/contrast.json +7776 -2478
- package/dist/meta/index.json +145 -3
- package/dist/meta/patterns/AgentRun.json +27 -0
- package/dist/meta/patterns/Form.json +9 -0
- package/dist/meta/patterns/PageShell.json +43 -3
- package/dist/meta/patterns/Wizard.json +20 -1
- package/dist/meta/patterns/index.json +10 -1
- package/dist/styles/date-picker.css +420 -0
- package/dist/styles/date-range-picker.css +280 -0
- package/dist/styles/header.css +26 -0
- package/dist/styles/index.css +15 -0
- package/dist/styles/number-input.css +83 -0
- package/dist/styles/prompt-input.css +127 -0
- package/dist/styles/scroll-progress.css +42 -4
- package/dist/styles/segmented-control.css +182 -0
- package/dist/styles/sidebar.css +307 -0
- package/dist/styles/stepper.css +250 -0
- package/dist/styles/table.css +9 -0
- package/dist/styles/tool-call.css +202 -0
- package/llms.txt +2 -2
- package/package.json +20 -19
- package/LICENSE +0 -21
package/dist/styles/header.css
CHANGED
|
@@ -187,6 +187,32 @@
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/*
|
|
191
|
+
* menuType="dialog": the toggle opens the caller's Drawer, so there is no
|
|
192
|
+
* panel. The slots keep their bar layout below the breakpoint, and the end
|
|
193
|
+
* slot is pushed to the far edge because the toggle now leads.
|
|
194
|
+
*/
|
|
195
|
+
@media (width < 896px) {
|
|
196
|
+
.ion-header.ion-header--dialog-menu .ion-header__menu {
|
|
197
|
+
display: contents;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.ion-header--dialog-menu .ion-header__center {
|
|
201
|
+
flex-direction: row;
|
|
202
|
+
align-items: center;
|
|
203
|
+
width: auto;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.ion-header--dialog-menu .ion-header__end {
|
|
207
|
+
width: auto;
|
|
208
|
+
margin-inline-start: auto;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.ion-header--dialog-menu .ion-header__end > * {
|
|
212
|
+
flex: none;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
/*
|
|
191
217
|
* Forced-colors: box-shadow is dropped by the OS, so the toggle would lose its
|
|
192
218
|
* only edge. A border in ButtonText keeps it visible.
|
package/dist/styles/index.css
CHANGED
|
@@ -73,9 +73,24 @@
|
|
|
73
73
|
@import url('./breadcrumb.css');
|
|
74
74
|
@import url('./accordion.css');
|
|
75
75
|
@import url('./drawer.css');
|
|
76
|
+
@import url('./stepper.css');
|
|
77
|
+
@import url('./prompt-input.css');
|
|
78
|
+
@import url('./tool-call.css');
|
|
79
|
+
@import url('./number-input.css');
|
|
80
|
+
@import url('./segmented-control.css');
|
|
81
|
+
@import url('./sidebar.css');
|
|
76
82
|
@import url('./file-upload.css');
|
|
77
83
|
@import url('./combobox.css');
|
|
78
84
|
|
|
85
|
+
/*
|
|
86
|
+
* date-range-picker AFTER date-picker, and the order is load-bearing rather
|
|
87
|
+
* than alphabetical. The range band overrides `.ion-calendar__day`'s
|
|
88
|
+
* border-radius at equal specificity — square sides so consecutive days join
|
|
89
|
+
* into one bar — and at equal specificity the later rule is the one that wins.
|
|
90
|
+
*/
|
|
91
|
+
@import url('./date-picker.css');
|
|
92
|
+
@import url('./date-range-picker.css');
|
|
93
|
+
|
|
79
94
|
/*
|
|
80
95
|
* Visually hidden, but not hidden from assistive tech.
|
|
81
96
|
*
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* NumberInput
|
|
3
|
+
*
|
|
4
|
+
* Drawn in Figma as `Number Input` (1389:473), from this file — Input's 21
|
|
5
|
+
* variants cloned, so the box keeps Input's bindings. The box is `.ion-input` itself — the component renders
|
|
6
|
+
* Input's classes, so every size and state comes from input.css and cannot
|
|
7
|
+
* drift from it. This file owns only what Input does not have: the − and +
|
|
8
|
+
* buttons, and tabular figures.
|
|
9
|
+
*
|
|
10
|
+
* − and + sit side by side at (field height − 8px): 24 / 32 / 40. Stacked
|
|
11
|
+
* chevrons would halve that, and a Small field's 16px targets would fall under
|
|
12
|
+
* WCAG 2.5.8's 24px.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Restates Input's default surface, at zero specificity, for the contrast gate.
|
|
17
|
+
*
|
|
18
|
+
* The gate resolves a translucent `surface/hover` against the block's own
|
|
19
|
+
* background — looked up in THIS stylesheet, and the box's background is
|
|
20
|
+
* declared in input.css. Without this, the step buttons' hover and pressed
|
|
21
|
+
* pairings came back skipped in both modes: unmeasured, not passing.
|
|
22
|
+
*
|
|
23
|
+
* `:where()` is what makes it safe. It matches what the box already paints and
|
|
24
|
+
* loses to every Input state, so disabled and read-only still repaint the box.
|
|
25
|
+
*/
|
|
26
|
+
:where(.ion-number-input) {
|
|
27
|
+
background-color: var(--surface-default);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ion-number-input--stepper {
|
|
31
|
+
/* The buttons carry their own inset; full padding would double it. */
|
|
32
|
+
padding-inline-end: var(--spacing-4);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Figures line up down a column of fields, which is where numbers usually are. */
|
|
36
|
+
.ion-number-input__field {
|
|
37
|
+
font-variant-numeric: tabular-nums;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ion-number-input__steps {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
flex: none;
|
|
43
|
+
gap: var(--spacing-2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ion-number-input__step {
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
width: calc(var(--ion-input-height) - var(--spacing-8));
|
|
51
|
+
height: calc(var(--ion-input-height) - var(--spacing-8));
|
|
52
|
+
margin: 0;
|
|
53
|
+
padding: 0;
|
|
54
|
+
border: 0;
|
|
55
|
+
border-radius: var(--radius-sm);
|
|
56
|
+
background-color: transparent;
|
|
57
|
+
color: var(--icon-secondary);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ion-number-input__step svg {
|
|
62
|
+
width: var(--ion-input-icon-size);
|
|
63
|
+
height: var(--ion-input-icon-size);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (hover: hover) {
|
|
67
|
+
.ion-number-input__step:hover {
|
|
68
|
+
background-color: var(--surface-hover);
|
|
69
|
+
color: var(--icon-default);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ion-number-input__step:active {
|
|
74
|
+
background-color: var(--surface-pressed);
|
|
75
|
+
color: var(--icon-default);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* At the bound, or with the whole field disabled or read-only. */
|
|
79
|
+
.ion-number-input__step:disabled {
|
|
80
|
+
background-color: transparent;
|
|
81
|
+
color: var(--icon-disabled);
|
|
82
|
+
cursor: not-allowed;
|
|
83
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PromptInput
|
|
3
|
+
*
|
|
4
|
+
* Drawn in Figma as `Prompt Input` (1393:387), from this file, composing Icon
|
|
5
|
+
* Button and Agent Stop instances rather than redrawing them. The box is Textarea's — surface, border, hover and focus
|
|
6
|
+
* roles unchanged — one radius rung larger, because it holds a toolbar and
|
|
7
|
+
* reads as a surface rather than a field. The send control is Button `sm`
|
|
8
|
+
* made square; the stop control is AgentStop `sm`, unmodified.
|
|
9
|
+
*
|
|
10
|
+
* The border and focus live on the BOX, not the textarea. The box is what
|
|
11
|
+
* looks like the field, so it is what shows focus; a ring around only the text
|
|
12
|
+
* area inside it would draw a field within a field.
|
|
13
|
+
*
|
|
14
|
+
* States are BEM modifiers and `:focus-within`, never `[data-*]`, so the
|
|
15
|
+
* contrast gate measures them — see stepper.css for what happens otherwise.
|
|
16
|
+
*/
|
|
17
|
+
.ion-prompt-input {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: var(--spacing-8);
|
|
21
|
+
padding: var(--spacing-8);
|
|
22
|
+
border: var(--border-width-default) solid var(--border-default);
|
|
23
|
+
border-radius: var(--radius-lg);
|
|
24
|
+
background-color: var(--surface-default);
|
|
25
|
+
color: var(--text-default);
|
|
26
|
+
font-family: var(--font-family-sans);
|
|
27
|
+
cursor: text;
|
|
28
|
+
transition: border-color var(--ion-duration-base) var(--ion-ease-out);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@media (hover: hover) {
|
|
32
|
+
.ion-prompt-input:hover:not(.ion-prompt-input--disabled) {
|
|
33
|
+
border-color: var(--border-strong);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* Same specificity as the hover rule above and declared after it, so focus
|
|
39
|
+
* wins while the pointer is still over the box — which it always is right
|
|
40
|
+
* after a click into the field.
|
|
41
|
+
*/
|
|
42
|
+
.ion-prompt-input:focus-within:not(.ion-prompt-input--disabled) {
|
|
43
|
+
border-color: var(--border-focus);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ion-prompt-input__attachments {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
gap: var(--spacing-8);
|
|
50
|
+
padding-inline: var(--spacing-4);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ion-prompt-input__field {
|
|
54
|
+
display: block;
|
|
55
|
+
width: 100%;
|
|
56
|
+
margin: 0;
|
|
57
|
+
|
|
58
|
+
/* Lines the text up with the actions' left edge below it. */
|
|
59
|
+
padding: var(--spacing-4);
|
|
60
|
+
border: 0;
|
|
61
|
+
outline: none;
|
|
62
|
+
background-color: transparent;
|
|
63
|
+
color: var(--text-default);
|
|
64
|
+
font: inherit;
|
|
65
|
+
font-size: var(--type-body);
|
|
66
|
+
line-height: var(--type-body-line-height);
|
|
67
|
+
|
|
68
|
+
/* Height is set from the content, up to maxRows; a grip would fight it. */
|
|
69
|
+
resize: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* text/tertiary, as Textarea — text/placeholder was retired for 2.38:1. */
|
|
73
|
+
.ion-prompt-input__field::placeholder {
|
|
74
|
+
color: var(--text-tertiary);
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ion-prompt-input__toolbar {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: space-between;
|
|
82
|
+
gap: var(--spacing-8);
|
|
83
|
+
cursor: default;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ion-prompt-input__actions {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: var(--spacing-4);
|
|
90
|
+
min-width: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* Square, at Button's own `sm` height. Two classes, so it outranks
|
|
95
|
+
* `.ion-button--sm` without depending on which stylesheet loads first.
|
|
96
|
+
*/
|
|
97
|
+
.ion-prompt-input .ion-prompt-input__send {
|
|
98
|
+
--ion-button-padding-x: 0;
|
|
99
|
+
|
|
100
|
+
flex: none;
|
|
101
|
+
width: var(--spacing-32);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ion-prompt-input__stop {
|
|
105
|
+
flex: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.ion-prompt-input--disabled {
|
|
109
|
+
border-color: var(--border-disabled);
|
|
110
|
+
background-color: var(--surface-disabled);
|
|
111
|
+
cursor: not-allowed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.ion-prompt-input--disabled .ion-prompt-input__field {
|
|
115
|
+
color: var(--text-disabled);
|
|
116
|
+
cursor: not-allowed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.ion-prompt-input--disabled .ion-prompt-input__field::placeholder {
|
|
120
|
+
color: var(--text-disabled);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
.ion-prompt-input {
|
|
125
|
+
transition: none;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -160,13 +160,24 @@
|
|
|
160
160
|
* very percentage the reader opened the panel to act on. Keyboard users are
|
|
161
161
|
* unaffected either way: the rows are real buttons in tab order, so Tab
|
|
162
162
|
* scrolls each one into view without touching the wheel.
|
|
163
|
+
*
|
|
164
|
+
* CLOSED IS `display: none`, NOT ONLY INVISIBLE
|
|
165
|
+
*
|
|
166
|
+
* `visibility: hidden` removes the panel from sight and from tab order, but a
|
|
167
|
+
* hidden box still counts toward its scroll container's overflow. A rail near
|
|
168
|
+
* the right edge of a page therefore widened the page by however far the
|
|
169
|
+
* closed panel reached past it — 152px in the demo app — and a phone scrolled
|
|
170
|
+
* sideways into empty space. `display: none` takes it out of layout entirely.
|
|
171
|
+
* The reveal survives: `transition-behavior: allow-discrete` holds `display`
|
|
172
|
+
* until the fade-out finishes, and `@starting-style` gives the fade-in a
|
|
173
|
+
* starting point. A browser without either simply shows and hides it without
|
|
174
|
+
* the fade. `visibility` stays in the transition for the same browsers.
|
|
163
175
|
*/
|
|
164
176
|
.ion-scroll-progress__panel {
|
|
165
177
|
position: absolute;
|
|
166
|
-
left: 0;
|
|
167
178
|
top: 50%;
|
|
168
179
|
z-index: 10;
|
|
169
|
-
display:
|
|
180
|
+
display: none;
|
|
170
181
|
flex-direction: column;
|
|
171
182
|
gap: var(--spacing-4);
|
|
172
183
|
width: 220px;
|
|
@@ -181,20 +192,47 @@
|
|
|
181
192
|
opacity: 0;
|
|
182
193
|
visibility: hidden;
|
|
183
194
|
pointer-events: none;
|
|
184
|
-
transform: translateY(-50%) translateX(-
|
|
195
|
+
transform: translateY(-50%) translateX(var(--ion-scroll-progress-slide));
|
|
185
196
|
transition:
|
|
186
197
|
opacity var(--ion-duration-base) var(--ion-ease-out),
|
|
187
198
|
transform var(--ion-duration-base) var(--ion-ease-out),
|
|
188
|
-
visibility var(--ion-duration-base)
|
|
199
|
+
visibility var(--ion-duration-base),
|
|
200
|
+
display var(--ion-duration-base) allow-discrete;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/*
|
|
204
|
+
* Placement picks the edge the panel is anchored to and the direction it
|
|
205
|
+
* slides in from — always from the rail outward. `right` is the original
|
|
206
|
+
* behaviour; `left` is for a rail against the right edge of the page, where a
|
|
207
|
+
* rightward panel would leave the screen.
|
|
208
|
+
*/
|
|
209
|
+
.ion-scroll-progress__panel--right {
|
|
210
|
+
--ion-scroll-progress-slide: -4px;
|
|
211
|
+
|
|
212
|
+
left: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.ion-scroll-progress__panel--left {
|
|
216
|
+
--ion-scroll-progress-slide: 4px;
|
|
217
|
+
|
|
218
|
+
right: 0;
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
.ion-scroll-progress__panel[data-open='true'] {
|
|
222
|
+
display: flex;
|
|
192
223
|
opacity: 1;
|
|
193
224
|
visibility: visible;
|
|
194
225
|
pointer-events: auto;
|
|
195
226
|
transform: translateY(-50%) translateX(0);
|
|
196
227
|
}
|
|
197
228
|
|
|
229
|
+
@starting-style {
|
|
230
|
+
.ion-scroll-progress__panel[data-open='true'] {
|
|
231
|
+
opacity: 0;
|
|
232
|
+
transform: translateY(-50%) translateX(var(--ion-scroll-progress-slide));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
198
236
|
/*
|
|
199
237
|
* `display: block`, not `flex`: `text-overflow: ellipsis` does not apply to an
|
|
200
238
|
* anonymous flex item, so a flex row here would clip the label with no ellipsis
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SegmentedControl
|
|
3
|
+
*
|
|
4
|
+
* Drawn in Figma as `Segmented Control` (1390:435) and `Segmented Control
|
|
5
|
+
* Item` (1390:332), from this file. It is Tabs' pill track, token for token: the muted
|
|
6
|
+
* track, the 2px gap, the default-surface selected segment, the page-surface
|
|
7
|
+
* selected hover, and the overall height ladder (32/40/48, 12/16/20 padding). Two
|
|
8
|
+
* components that look identical must not drift into looking almost
|
|
9
|
+
* identical, so if tabs.css changes its pill, this changes with it.
|
|
10
|
+
*
|
|
11
|
+
* Two deliberate differences from the pill track. It is `inline-flex` in every
|
|
12
|
+
* orientation, because a radiogroup has none. And the track radius is `lg`
|
|
13
|
+
* rather than `full`, so `--full-width` does not stretch a stadium shape across
|
|
14
|
+
* a card.
|
|
15
|
+
*
|
|
16
|
+
* States are BEM modifiers (`--selected`, `--disabled`, `--focus-visible`),
|
|
17
|
+
* never `[data-*]`, and the track surface is on the block, so the contrast gate
|
|
18
|
+
* measures the translucent hover against it — see stepper.css and
|
|
19
|
+
* number-input.css for the two ways that goes wrong.
|
|
20
|
+
*/
|
|
21
|
+
.ion-segmented-control {
|
|
22
|
+
--ion-segmented-item-height: var(--spacing-32);
|
|
23
|
+
--ion-segmented-item-padding-x: var(--spacing-16);
|
|
24
|
+
--ion-segmented-item-gap: var(--spacing-8);
|
|
25
|
+
--ion-segmented-icon-size: var(--icon-size-md);
|
|
26
|
+
--ion-segmented-font-size: var(--type-body);
|
|
27
|
+
--ion-segmented-line-height: var(--type-body-line-height);
|
|
28
|
+
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
gap: var(--spacing-2);
|
|
31
|
+
padding: var(--spacing-4);
|
|
32
|
+
border-radius: var(--radius-lg);
|
|
33
|
+
background-color: var(--surface-muted);
|
|
34
|
+
color: var(--text-tertiary);
|
|
35
|
+
font-family: var(--font-family-sans);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Item heights are the pill's 40/32/48 less the track's 4px inset on each side. */
|
|
39
|
+
.ion-segmented-control--sm {
|
|
40
|
+
--ion-segmented-item-height: var(--spacing-24);
|
|
41
|
+
--ion-segmented-item-padding-x: var(--spacing-12);
|
|
42
|
+
--ion-segmented-item-gap: var(--spacing-6);
|
|
43
|
+
--ion-segmented-icon-size: var(--icon-size-sm);
|
|
44
|
+
--ion-segmented-font-size: var(--type-body-sm);
|
|
45
|
+
--ion-segmented-line-height: var(--type-body-sm-line-height);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ion-segmented-control--lg {
|
|
49
|
+
--ion-segmented-item-height: var(--spacing-40);
|
|
50
|
+
--ion-segmented-item-padding-x: var(--spacing-20);
|
|
51
|
+
--ion-segmented-font-size: var(--type-body-lg);
|
|
52
|
+
--ion-segmented-line-height: var(--type-body-lg-line-height);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ion-segmented-control--full-width {
|
|
56
|
+
display: flex;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ion-segmented-control__item {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
gap: var(--ion-segmented-item-gap);
|
|
64
|
+
min-height: var(--ion-segmented-item-height);
|
|
65
|
+
padding-inline: var(--ion-segmented-item-padding-x);
|
|
66
|
+
border-radius: var(--radius-md);
|
|
67
|
+
color: var(--text-tertiary);
|
|
68
|
+
font-size: var(--ion-segmented-font-size);
|
|
69
|
+
line-height: var(--ion-segmented-line-height);
|
|
70
|
+
font-weight: var(--font-weight-medium);
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
user-select: none;
|
|
74
|
+
transition:
|
|
75
|
+
color var(--ion-duration-base) var(--ion-ease-out),
|
|
76
|
+
background-color var(--ion-duration-base) var(--ion-ease-out);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ion-segmented-control--full-width .ion-segmented-control__item {
|
|
80
|
+
flex: 1 1 0;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.ion-segmented-control__icon {
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
flex: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ion-segmented-control__icon svg {
|
|
90
|
+
width: var(--ion-segmented-icon-size);
|
|
91
|
+
height: var(--ion-segmented-icon-size);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ion-segmented-control__label {
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
text-overflow: ellipsis;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* `.ion-field` is a column that stretches its children; a labelled control
|
|
100
|
+
* keeps its own width unless it was asked to fill. */
|
|
101
|
+
.ion-segmented-control-field {
|
|
102
|
+
align-items: flex-start;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ion-segmented-control-field--full-width {
|
|
106
|
+
align-items: stretch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* State order is specificity, not `:not()` lists. The contrast gate splits a
|
|
111
|
+
* selector at every comma and reads `--disabled` inside `:not(...)` as the
|
|
112
|
+
* disabled state, so a hover rule guarded with `:not(--selected, --disabled)`
|
|
113
|
+
* was measured as disabled text on a hover surface — two failures for a
|
|
114
|
+
* combination that never renders. Plain rules, ordered, say the same thing
|
|
115
|
+
* and are read correctly:
|
|
116
|
+
*
|
|
117
|
+
* hover (0,2,0) -> selected (0,1,0) -> selected hover (0,2,0)
|
|
118
|
+
* -> disabled (0,2,0) -> disabled selected (0,3,0)
|
|
119
|
+
*/
|
|
120
|
+
@media (hover: hover) {
|
|
121
|
+
.ion-segmented-control__item:hover {
|
|
122
|
+
background-color: var(--surface-hover);
|
|
123
|
+
color: var(--text-default);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ion-segmented-control__item--selected {
|
|
128
|
+
background-color: var(--surface-default);
|
|
129
|
+
color: var(--text-default);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (hover: hover) {
|
|
133
|
+
.ion-segmented-control__item--selected:hover {
|
|
134
|
+
background-color: var(--surface-page);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/*
|
|
139
|
+
* Focus lands on the hidden radio, so the ring is drawn on the segment around
|
|
140
|
+
* it. Tabs' ring: 2px `ring/focus`, offset by its own width.
|
|
141
|
+
*/
|
|
142
|
+
.ion-segmented-control__item--focus-visible {
|
|
143
|
+
outline: var(--border-width-thick) solid var(--ring-focus);
|
|
144
|
+
outline-offset: var(--border-width-thick);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
* Two classes, so it outranks hover: a disabled segment does not react.
|
|
149
|
+
*
|
|
150
|
+
* The track's own surface rather than `transparent` — identical on screen,
|
|
151
|
+
* because the segment sits on the track. It is explicit for the contrast gate:
|
|
152
|
+
* with `transparent` the surface is inherited from the block, and the gate
|
|
153
|
+
* treats a single modifier that takes its surface from the block, while a
|
|
154
|
+
* compound of it (`--selected` + `--disabled`) supplies its own, as a state
|
|
155
|
+
* that cannot render — and drops it without counting a skip.
|
|
156
|
+
*/
|
|
157
|
+
.ion-segmented-control .ion-segmented-control__item--disabled {
|
|
158
|
+
background-color: var(--surface-muted);
|
|
159
|
+
color: var(--text-disabled);
|
|
160
|
+
cursor: not-allowed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* A disabled SELECTED segment must still show which one is selected — a
|
|
165
|
+
* disabled control still has a value. Only the text dims.
|
|
166
|
+
*/
|
|
167
|
+
.ion-segmented-control
|
|
168
|
+
.ion-segmented-control__item--selected.ion-segmented-control__item--disabled {
|
|
169
|
+
background-color: var(--surface-default);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (forced-colors: active) {
|
|
173
|
+
.ion-segmented-control__item--selected {
|
|
174
|
+
outline: var(--border-width-thick) solid;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@media (prefers-reduced-motion: reduce) {
|
|
179
|
+
.ion-segmented-control__item {
|
|
180
|
+
transition: none;
|
|
181
|
+
}
|
|
182
|
+
}
|