ionbase-ui 0.32.0 → 0.38.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.32.0",
3
+ "version": "0.38.0",
4
4
  "usage": "Pick a component here, then read dist/meta/<Name>.json for its full contract.",
5
5
  "hooks": [
6
6
  "useToast"
@@ -170,6 +170,15 @@
170
170
  "warning",
171
171
  "error",
172
172
  "information"
173
+ ],
174
+ "size": [
175
+ "sm",
176
+ "md",
177
+ "lg"
178
+ ],
179
+ "shape": [
180
+ "pill",
181
+ "rounded"
173
182
  ]
174
183
  },
175
184
  "detail": "dist/meta/Badge.json",
@@ -262,6 +271,25 @@
262
271
  "detail": "dist/meta/Divider.json",
263
272
  "hasIntent": true
264
273
  },
274
+ "EmptyState": {
275
+ "summary": "The state a region shows when it has nothing to show, and why — first run, no results, no access, or a failed fetch.",
276
+ "status": "stable",
277
+ "variants": {
278
+ "reason": [
279
+ "error",
280
+ "first-run",
281
+ "no-results",
282
+ "no-access"
283
+ ],
284
+ "size": [
285
+ "inline",
286
+ "page",
287
+ "panel"
288
+ ]
289
+ },
290
+ "detail": "dist/meta/EmptyState.json",
291
+ "hasIntent": true
292
+ },
265
293
  "FullCard": {
266
294
  "summary": "A full-bleed row: a text column beside a framed media panel, mirrored by `alignment`.",
267
295
  "status": "stable",
@@ -0,0 +1,136 @@
1
+ {
2
+ "name": "AgentRun",
3
+ "summary": "A single agent task from start to finish: what it is doing, what it produced, and a way to stop it at any point.",
4
+ "useWhen": [
5
+ "an agent performs work that takes long enough for the user to wonder whether it is still running",
6
+ "the work happens in steps the user would want to see — a tool call, a search, a write",
7
+ "the output arrives incrementally rather than all at once"
8
+ ],
9
+ "composes": [
10
+ "AgentActivity",
11
+ "AgentActivityStep",
12
+ "AgentStop",
13
+ "StreamingText",
14
+ "Alert",
15
+ "Badge"
16
+ ],
17
+ "structure": [
18
+ "A header row carrying the task name and the AgentStop control. The stop is the first thing on screen and it stays there for the whole run.",
19
+ "An AgentActivity list of AgentActivityStep, one per step, in the order they happened. Exactly one step is `active` at a time.",
20
+ "The result below the log, in a StreamingText with `isStreaming` true until the run ends.",
21
+ "An Alert only when the run fails or is stopped — not for ordinary progress, which the step log already carries."
22
+ ],
23
+ "states": {
24
+ "loading": {
25
+ "must": "Show the step log from the first step, not after the first result. Set `isStreaming` on the StreamingText and give it `minLines` so the region does not grow under the reader. AgentStop stays enabled the entire time.",
26
+ "why": "a spinner with no steps and no stop is indistinguishable from a hang, and the user's only remaining move is to reload the page and lose the run"
27
+ },
28
+ "empty": {
29
+ "must": "Before the first step exists, render the header and the AgentStop and nothing else. Do not render an empty AgentActivity list.",
30
+ "why": "an empty `<ol>` announces as a list of zero items, which tells a screen-reader user the run produced nothing rather than that it has not started"
31
+ },
32
+ "error": {
33
+ "must": "Mark the step that failed `status=\"failed\"`, leave every earlier step visible, and put an Alert with `intent=\"error\"` below the log naming which step failed and what it was doing. Clear `isStreaming`.",
34
+ "why": "replacing the log with an error message destroys the only record of how far the run got, which is the thing a user needs to decide whether to retry or start over"
35
+ },
36
+ "partial": {
37
+ "must": "When the user stops a run mid-flight, keep every completed step, mark the interrupted one `status=\"skipped\"`, and say plainly what was and was not done. Leave whatever the run already wrote on screen.",
38
+ "why": "a stopped agent has usually already changed something, and a UI that clears itself on stop hides the side effects the user stopped it to prevent"
39
+ }
40
+ },
41
+ "propsUsed": {
42
+ "AgentActivity": [
43
+ "announceActive"
44
+ ],
45
+ "AgentActivityStep": [
46
+ "status",
47
+ "detail"
48
+ ],
49
+ "AgentStop": [
50
+ "onStop",
51
+ "isStopping",
52
+ "label",
53
+ "size",
54
+ "stopOnEscape"
55
+ ],
56
+ "StreamingText": [
57
+ "isStreaming",
58
+ "minLines",
59
+ "label",
60
+ "hideCursor"
61
+ ],
62
+ "Alert": [
63
+ "intent",
64
+ "title"
65
+ ],
66
+ "Badge": [
67
+ "intent",
68
+ "size"
69
+ ]
70
+ },
71
+ "variantsUsed": {
72
+ "AgentActivityStep": {
73
+ "status": [
74
+ "pending",
75
+ "active",
76
+ "done",
77
+ "failed",
78
+ "skipped"
79
+ ]
80
+ },
81
+ "AgentStop": {
82
+ "size": [
83
+ "sm",
84
+ "md"
85
+ ]
86
+ },
87
+ "Alert": {
88
+ "intent": [
89
+ "error",
90
+ "warning"
91
+ ]
92
+ },
93
+ "Badge": {
94
+ "intent": [
95
+ "neutral",
96
+ "success",
97
+ "error"
98
+ ]
99
+ }
100
+ },
101
+ "a11y": {
102
+ "requires": [
103
+ "`announceActive` on AgentActivity when the run is unattended — it announces only the step that becomes active, once",
104
+ "an AgentStop that is reachable by keyboard without passing through the step log",
105
+ "a `label` on StreamingText when the output is not preceded by a visible heading"
106
+ ],
107
+ "notes": [
108
+ "StreamingText is deliberately NOT a live region. `aria-live=\"polite\"` on streaming output queues an announcement per token, so the answer is re-read and stuttered dozens of times. The step log is where progress is announced; the output is ordinary readable text.",
109
+ "Do not also wrap the step log in your own live region. AgentActivity owns one, and two announcing regions produce interleaved speech.",
110
+ "`stopOnEscape` is off by default on purpose. Turn it on only when the run owns the whole screen — inside a dialog, Escape must close the dialog."
111
+ ]
112
+ },
113
+ "antiPatterns": [
114
+ {
115
+ "dont": "a stop control that appears only after the run has been going for a few seconds",
116
+ "why": "the moment a user most wants to stop is the moment they realise the agent misunderstood, which is usually within the first second"
117
+ },
118
+ {
119
+ "dont": "removing the AgentStop while the stop is in progress",
120
+ "do": "AgentStop",
121
+ "why": "the control vanishing is read as the click having failed; `isStopping` keeps it in place and says what is happening"
122
+ },
123
+ {
124
+ "dont": "collapsing finished steps to a single \"Done\" line",
125
+ "why": "the log is the audit trail — it is what the user reads when the result is wrong, and it is worthless after the fact"
126
+ },
127
+ {
128
+ "dont": "raw tool names or JSON in the step text",
129
+ "why": "\"Calling search_documents({q: ...})\" describes the implementation; \"Searching your documents\" describes the work, and only one of them is useful to the person deciding whether to stop"
130
+ },
131
+ {
132
+ "dont": "more than one step marked `active`",
133
+ "why": "the status is what tells a screen-reader user where the run is, and two active steps make that unanswerable"
134
+ }
135
+ ]
136
+ }
@@ -0,0 +1,130 @@
1
+ {
2
+ "name": "AssistantAnswer",
3
+ "summary": "A generated answer presented so the reader can check it: the text, what it was drawn from, and how far to trust it.",
4
+ "useWhen": [
5
+ "an answer is generated from sources the user could go and read",
6
+ "the answer will be acted on — a policy question, a number, a recommendation",
7
+ "the answer may be wrong in ways that are not visible from the text alone"
8
+ ],
9
+ "composes": [
10
+ "StreamingText",
11
+ "Citation",
12
+ "CitationList",
13
+ "CitationListItem",
14
+ "ConfidenceIndicator",
15
+ "Alert",
16
+ "Link"
17
+ ],
18
+ "structure": [
19
+ "The answer in a StreamingText, with inline Citation markers at the claims they support — not gathered at the end.",
20
+ "A CitationList below the answer, one CitationListItem per source, numbered to match the inline markers.",
21
+ "A ConfidenceIndicator beside the answer, with `basis` saying what the level is derived from.",
22
+ "An Alert only when the answer is degraded — sources unavailable, knowledge stale, question outside scope."
23
+ ],
24
+ "states": {
25
+ "loading": {
26
+ "must": "Stream the text with `isStreaming` and `minLines` set. Render no ConfidenceIndicator and no CitationList until the answer is complete.",
27
+ "why": "a confidence level attached to half an answer is a claim about text that does not exist yet, and citations that appear one by one move the reader's place on every token"
28
+ },
29
+ "empty": {
30
+ "must": "When there is no answer — no sources matched, the question was out of scope — say that in words and render neither an empty CitationList nor a ConfidenceIndicator.",
31
+ "why": "an answer with no sources and no confidence shown reads as a confident answer, which is the opposite of what an empty result means"
32
+ },
33
+ "error": {
34
+ "must": "Keep whatever text arrived, mark it plainly as incomplete with an Alert of `intent=\"warning\"`, and drop the ConfidenceIndicator entirely.",
35
+ "why": "a truncated answer that keeps its confidence badge is rated on evidence it never finished reading"
36
+ },
37
+ "partial": {
38
+ "must": "When some sources were unreachable, cite the ones that were, lower the `level`, and name the gap in `basis` — \"3 of 7 sources; 4 unavailable\".",
39
+ "why": "an answer built on half its evidence looks identical to one built on all of it, and `basis` is the only place that difference can be stated"
40
+ }
41
+ },
42
+ "propsUsed": {
43
+ "StreamingText": [
44
+ "isStreaming",
45
+ "minLines",
46
+ "label"
47
+ ],
48
+ "Citation": [
49
+ "index",
50
+ "source",
51
+ "href"
52
+ ],
53
+ "CitationList": [
54
+ "label"
55
+ ],
56
+ "CitationListItem": [
57
+ "index",
58
+ "source",
59
+ "href"
60
+ ],
61
+ "ConfidenceIndicator": [
62
+ "level",
63
+ "basis",
64
+ "label"
65
+ ],
66
+ "Alert": [
67
+ "intent",
68
+ "title"
69
+ ],
70
+ "Link": [
71
+ "variant",
72
+ "href"
73
+ ]
74
+ },
75
+ "variantsUsed": {
76
+ "ConfidenceIndicator": {
77
+ "level": [
78
+ "low",
79
+ "medium",
80
+ "high"
81
+ ]
82
+ },
83
+ "Alert": {
84
+ "intent": [
85
+ "warning",
86
+ "information"
87
+ ]
88
+ },
89
+ "Link": {
90
+ "variant": [
91
+ "standalone"
92
+ ]
93
+ }
94
+ },
95
+ "a11y": {
96
+ "requires": [
97
+ "an `index` on every Citation that matches its CitationListItem — the number is how a screen-reader user connects the two",
98
+ "a `basis` on ConfidenceIndicator, which the component requires and which must describe evidence, not feeling",
99
+ "a `label` on StreamingText when no visible heading precedes the answer"
100
+ ],
101
+ "notes": [
102
+ "A Citation with no `href` renders as a note, not a dead link. Pass `href` only when the source is genuinely reachable by the reader.",
103
+ "Citation's accessible name is `Source <index>: <source>`, so the source string should name the document, not say \"link\" or \"here\".",
104
+ "StreamingText is not a live region. Announcing that the answer finished is the caller's decision — a thread with ten answers on screen does not want ten announcements."
105
+ ]
106
+ },
107
+ "antiPatterns": [
108
+ {
109
+ "dont": "a percentage — \"87% confident\"",
110
+ "do": "ConfidenceIndicator",
111
+ "why": "the number is not calibrated against anything, and a false precision is trusted more than the vague statement it replaced"
112
+ },
113
+ {
114
+ "dont": "citations gathered only at the end of the answer",
115
+ "why": "the reader cannot tell which claim each source supports, so the list documents that sources exist rather than what they establish"
116
+ },
117
+ {
118
+ "dont": "citing a source the reader cannot open",
119
+ "why": "an unreachable citation is decoration that raises trust without supplying any way to check it"
120
+ },
121
+ {
122
+ "dont": "a confidence level derived from the model's own token probabilities and presented as certainty about the world",
123
+ "why": "fluency and correctness are different quantities, and only one of them is being measured"
124
+ },
125
+ {
126
+ "dont": "hiding the sources behind a disclosure that defaults closed",
127
+ "why": "evidence that takes a click to reach is evidence nobody checks"
128
+ }
129
+ ]
130
+ }
@@ -0,0 +1,139 @@
1
+ {
2
+ "name": "HumanApproval",
3
+ "summary": "An agent stops and asks before doing something consequential, and a person approves, rejects or edits it.",
4
+ "useWhen": [
5
+ "the action spends money, sends something to a third party, deletes data, or changes access",
6
+ "the action is irreversible by the person who would have to undo it",
7
+ "a regulation or an internal policy requires a human decision on the record — the EU AI Act's human-oversight duty is the common case"
8
+ ],
9
+ "composes": [
10
+ "ApprovalGate",
11
+ "AgentActivity",
12
+ "AgentActivityStep",
13
+ "AgentStop",
14
+ "Alert"
15
+ ],
16
+ "structure": [
17
+ "The gate appears in place, where the run reached it — inside the step log, not in a dialog that covers it.",
18
+ "The AgentActivityStep that is waiting stays `active`; the ApprovalGate is its `detail`.",
19
+ "An ApprovalGate whose `title` names the action and its object, with the specifics — amount, recipient, record count — as `children`.",
20
+ "`risk` set from the consequence, not from the agent's confidence.",
21
+ "After the decision, `status` becomes `approved`, `rejected` or `expired` and `resolution` records who decided and when."
22
+ ],
23
+ "states": {
24
+ "loading": {
25
+ "must": "While the decision is being submitted, set `isSubmitting` — the gate keeps its buttons in place and disables them. Do not remove the gate or advance the step log until the server confirms.",
26
+ "why": "an optimistically-advanced run tells the user the action was approved before anything approved it, which is the one thing this pattern exists to prevent"
27
+ },
28
+ "empty": {
29
+ "must": "Render no ApprovalGate when nothing is waiting. A queue view with no pending items says so in words and does not render an empty gate shell.",
30
+ "why": "a gate with no action in it still shows Approve and Reject buttons, and a user who presses one has approved nothing while believing they approved something"
31
+ },
32
+ "error": {
33
+ "must": "If the decision fails to submit, keep `status=\"pending\"`, clear `isSubmitting`, and show an Alert with `intent=\"error\"` inside the gate. The action must not proceed.",
34
+ "why": "a failed approval that looks like a successful one is an unapproved action running with a human's name on it"
35
+ },
36
+ "expired": {
37
+ "must": "Set `status=\"expired\"` when the window closes, state what happened by default in `resolution`, and remove the decision buttons.",
38
+ "why": "a gate that sits pending forever hides that the agent already gave up, and buttons that no longer do anything are worse than no buttons"
39
+ }
40
+ },
41
+ "propsUsed": {
42
+ "ApprovalGate": [
43
+ "title",
44
+ "risk",
45
+ "status",
46
+ "onApprove",
47
+ "onReject",
48
+ "onEdit",
49
+ "approveLabel",
50
+ "rejectLabel",
51
+ "isSubmitting",
52
+ "resolution"
53
+ ],
54
+ "AgentActivity": [
55
+ "announceActive"
56
+ ],
57
+ "AgentActivityStep": [
58
+ "status",
59
+ "detail"
60
+ ],
61
+ "AgentStop": [
62
+ "onStop",
63
+ "label",
64
+ "size"
65
+ ],
66
+ "Alert": [
67
+ "intent",
68
+ "title"
69
+ ]
70
+ },
71
+ "variantsUsed": {
72
+ "ApprovalGate": {
73
+ "risk": [
74
+ "low",
75
+ "medium",
76
+ "high"
77
+ ],
78
+ "status": [
79
+ "pending",
80
+ "approved",
81
+ "rejected",
82
+ "expired"
83
+ ]
84
+ },
85
+ "AgentActivityStep": {
86
+ "status": [
87
+ "active",
88
+ "done",
89
+ "skipped"
90
+ ]
91
+ },
92
+ "Alert": {
93
+ "intent": [
94
+ "error",
95
+ "warning"
96
+ ]
97
+ },
98
+ "AgentStop": {
99
+ "size": [
100
+ "sm"
101
+ ]
102
+ }
103
+ },
104
+ "a11y": {
105
+ "requires": [
106
+ "a `title` that names the action and the object — it is the gate's accessible name",
107
+ "`risk` set from consequence, so the level is the same for the same action every time",
108
+ "reject reachable before approve by keyboard, which the component's DOM order already guarantees"
109
+ ],
110
+ "notes": [
111
+ "ApprovalGate autofocuses nothing, deliberately. A dialog that focuses Approve turns Enter on an unread screen into consent.",
112
+ "Do not put the gate in a Modal that traps focus and hides the run behind it. The evidence for the decision is the step log, and a person cannot judge an action they can no longer see.",
113
+ "`risk` is a label on the consequence, not a colour scheme. Using `high` for emphasis on a routine action is how a team learns to ignore it."
114
+ ]
115
+ },
116
+ "antiPatterns": [
117
+ {
118
+ "dont": "\"The agent wants to continue. Approve?\"",
119
+ "why": "it names neither the action nor its object, so the only available answer is a reflex — and a reflex is not oversight"
120
+ },
121
+ {
122
+ "dont": "an approval that also acts as the progress indicator",
123
+ "do": "the AgentRun pattern",
124
+ "why": "conflating \"I am working\" with \"I need you\" teaches the user to dismiss both"
125
+ },
126
+ {
127
+ "dont": "approve-all across a queue of unrelated actions",
128
+ "why": "one click covering many consequences is indistinguishable from no gate at all, and it is the failure regulators look for"
129
+ },
130
+ {
131
+ "dont": "a countdown that approves on expiry",
132
+ "why": "silence is not consent; an expired gate must default to the safe outcome, which is not doing the thing"
133
+ },
134
+ {
135
+ "dont": "hiding the gate's earlier steps once a decision is made",
136
+ "why": "the record of what was approved, on what evidence, is the artefact an audit asks for"
137
+ }
138
+ ]
139
+ }
@@ -1,8 +1,45 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.32.0",
3
+ "version": "0.38.0",
4
4
  "usage": "A pattern is a documented composition of components, not a component. Read one here, then read the contracts of what it composes.",
5
5
  "patterns": {
6
+ "AgentRun": {
7
+ "summary": "A single agent task from start to finish: what it is doing, what it produced, and a way to stop it at any point.",
8
+ "composes": [
9
+ "AgentActivity",
10
+ "AgentActivityStep",
11
+ "AgentStop",
12
+ "StreamingText",
13
+ "Alert",
14
+ "Badge"
15
+ ],
16
+ "states": [
17
+ "loading",
18
+ "empty",
19
+ "error",
20
+ "partial"
21
+ ],
22
+ "detail": "dist/meta/patterns/AgentRun.json"
23
+ },
24
+ "AssistantAnswer": {
25
+ "summary": "A generated answer presented so the reader can check it: the text, what it was drawn from, and how far to trust it.",
26
+ "composes": [
27
+ "StreamingText",
28
+ "Citation",
29
+ "CitationList",
30
+ "CitationListItem",
31
+ "ConfidenceIndicator",
32
+ "Alert",
33
+ "Link"
34
+ ],
35
+ "states": [
36
+ "loading",
37
+ "empty",
38
+ "error",
39
+ "partial"
40
+ ],
41
+ "detail": "dist/meta/patterns/AssistantAnswer.json"
42
+ },
6
43
  "DataTable": {
7
44
  "summary": "A table of records with a toolbar, selection, and the three states everyone forgets.",
8
45
  "composes": [
@@ -64,6 +101,23 @@
64
101
  ],
65
102
  "detail": "dist/meta/patterns/Form.json"
66
103
  },
104
+ "HumanApproval": {
105
+ "summary": "An agent stops and asks before doing something consequential, and a person approves, rejects or edits it.",
106
+ "composes": [
107
+ "ApprovalGate",
108
+ "AgentActivity",
109
+ "AgentActivityStep",
110
+ "AgentStop",
111
+ "Alert"
112
+ ],
113
+ "states": [
114
+ "loading",
115
+ "empty",
116
+ "error",
117
+ "expired"
118
+ ],
119
+ "detail": "dist/meta/patterns/HumanApproval.json"
120
+ },
67
121
  "PageShell": {
68
122
  "summary": "The frame every screen sits in: header, navigation, a named content region.",
69
123
  "composes": [
@@ -1,25 +1,36 @@
1
1
  /*
2
2
  * Badge
3
3
  *
4
- * Geometry measured from Figma `Badge` (152:73). Single size, six intents.
4
+ * Geometry measured from Figma `Badge` (152:73). Six intents x three sizes x
5
+ * two shapes.
5
6
  *
6
7
  * v2: Badge owns no tokens. Every value here is an Interface token, a Semantics
7
8
  * token, or a spacing primitive — the same ones the Figma component binds, so
8
- * the two cannot drift. Badge is 20px tall, below control/sm's 32px, so it is
9
- * not a control and does not read the control scale.
9
+ * the two cannot drift. Even Large, at 32px, is not a control: it has no hit
10
+ * target and no state, so it does not read the control scale.
11
+ *
12
+ * HEIGHT IS PINNED, NOT DERIVED. Figma binds each size's height to
13
+ * spacing/20|24|32, and the 2px vertical padding it also carries is inert
14
+ * because the pinned height already exceeds it. Reproducing that padding
15
+ * instead of the height is exactly how Button's ramp once landed 2px off on
16
+ * every size, so this reads the height token and pads the inline axis only.
17
+ *
18
+ * SMALL IS THE DEFAULT, matching Figma's default variant and the single size
19
+ * this component shipped with before the ramp existed.
10
20
  */
11
21
  .ion-badge {
12
22
  display: inline-flex;
13
23
  align-items: center;
14
24
  justify-content: center;
25
+ min-height: var(--spacing-20);
15
26
 
16
27
  /*
17
- * Figma's 1px stroke is drawn INSIDE the box, so it does not add height. A CSS
18
- * border does, so the vertical padding absorbs it — otherwise the badge
19
- * renders 2px taller than the design. Same correction as the Tabs underline
20
- * indicator. Badge is 22px: padding 2 + caption line-height 18 + padding 2.
28
+ * Figma's 1px stroke is drawn INSIDE the box; a CSS border sits outside the
29
+ * padding. Subtracting one border width keeps the label the same distance
30
+ * from the outer edge in both. Same correction as Checkbox and Radio, and
31
+ * only needed on the inline axis now that height is pinned.
21
32
  */
22
- padding: calc(var(--spacing-2) - var(--border-width-default)) var(--spacing-8);
33
+ padding-inline: calc(var(--spacing-4) - var(--border-width-default));
23
34
  gap: var(--spacing-4);
24
35
  font-family: var(--font-family-sans);
25
36
 
@@ -55,12 +66,77 @@
55
66
  * error that is #dc2828 against the label's #ba1c1c. `@ionbase-ui/icons` renders
56
67
  * with `currentColor`, so each intent sets `color` on the svg directly.
57
68
  *
58
- * Icon size is deliberately not set here. Figma has it at 14px, which is not on
59
- * the spacing scale, so it is most likely a slip rather than a decision. Until
60
- * that is settled the icon inherits 1em, matching the 12px label.
69
+ * Icon size IS set now, and is on the ladder. The previous note here recorded
70
+ * Figma at a single off-scale 14px and declined to follow it, on the grounds
71
+ * that it looked like a slip. The retuned component reads icon-size/2xs, /sm
72
+ * and /md — 12, 16, 20 — one rung per size, so the slip is gone and there is
73
+ * something to follow.
61
74
  */
62
75
  .ion-badge svg {
63
76
  flex-shrink: 0;
77
+ width: var(--icon-size-2xs);
78
+ height: var(--icon-size-2xs);
79
+ }
80
+
81
+ /*
82
+ * The label carries its own inline padding, which is Figma's `Label Slot`
83
+ * frame. The dot and the icon are its siblings rather than its children, so
84
+ * they sit at the badge's own padding and only the text takes the extra inset.
85
+ * Collapsing the two into one padding would move the dot.
86
+ */
87
+ .ion-badge__label {
88
+ padding-inline: var(--spacing-2);
89
+ }
90
+
91
+ /* Size modifiers — Small is the base. */
92
+
93
+ .ion-badge--md {
94
+ min-height: var(--spacing-24);
95
+ font-size: var(--type-body-sm);
96
+ line-height: var(--type-body-sm-line-height);
97
+ }
98
+
99
+ .ion-badge--md .ion-badge__label {
100
+ padding-inline: var(--spacing-4);
101
+ }
102
+
103
+ .ion-badge--md svg {
104
+ width: var(--icon-size-sm);
105
+ height: var(--icon-size-sm);
106
+ }
107
+
108
+ .ion-badge--lg {
109
+ min-height: var(--spacing-32);
110
+ padding-inline: calc(var(--spacing-6) - var(--border-width-default));
111
+ gap: var(--spacing-6);
112
+ font-size: var(--type-body-sm);
113
+ line-height: var(--type-body-sm-line-height);
114
+ }
115
+
116
+ .ion-badge--lg .ion-badge__label {
117
+ padding-inline: var(--spacing-6);
118
+ }
119
+
120
+ .ion-badge--lg svg {
121
+ width: var(--icon-size-md);
122
+ height: var(--icon-size-md);
123
+ }
124
+
125
+ /*
126
+ * Shape. Pill is the base radius/full; Rounded steps with the size so the
127
+ * corner stays proportional — 4 / 6 / 8 against heights of 20 / 24 / 32.
128
+ */
129
+
130
+ .ion-badge--rounded {
131
+ border-radius: var(--radius-xs);
132
+ }
133
+
134
+ .ion-badge--md.ion-badge--rounded {
135
+ border-radius: var(--radius-sm);
136
+ }
137
+
138
+ .ion-badge--lg.ion-badge--rounded {
139
+ border-radius: var(--radius-md);
64
140
  }
65
141
 
66
142
  /* Intent modifiers */