ionbase-ui 0.18.1 → 0.19.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.
@@ -4,6 +4,55 @@
4
4
  "propsType": "LogoProps",
5
5
  "description": "The full lockup: mark + wordmark, matching Figma's `Logo-Ionbase`\n(52:21369).\n\nFigma also has a `Property=Name` axis — wordmark only, no mark — but both\nof its variants currently render the placeholder text \"raza\" rather than\n\"IonBase\", in an unbound raw colour. That is leftover debug content, not a\nreal asset, so it is not implemented here. Reproducing it would ship a\nstranger's name into a design system; extending `LogoMark` alone already\ncovers \"no wordmark\" for the cases that are real.",
6
6
  "import": "import { Logo } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "The full IonBase lockup — mark plus wordmark.",
9
+ "useWhen": [
10
+ "the product needs to identify itself: header, sign-in page, empty state, marketing surface"
11
+ ],
12
+ "useInstead": [
13
+ {
14
+ "when": "space is tight, or a wordmark already appears nearby",
15
+ "use": "LogoMark"
16
+ },
17
+ {
18
+ "when": "you need any graphic that is not the brand",
19
+ "use": "Icon"
20
+ }
21
+ ],
22
+ "variants": {
23
+ "size": {
24
+ "sm": {
25
+ "use": "the default — a 24px mark, which is what a header bar wants"
26
+ },
27
+ "lg": {
28
+ "use": "a 32px mark — sign-in, splash, or anywhere the brand is the subject"
29
+ }
30
+ },
31
+ "wordmark": {
32
+ "vector": {
33
+ "use": "the default and the safe choice — Figma's serif logotype as frozen artwork, immune to font substitution and visually distinct from the UI type"
34
+ },
35
+ "text": {
36
+ "use": "live, selectable Host Grotesk at `text/tertiary` — for surfaces where the name should behave like text (copyable, searchable) rather than like a mark"
37
+ }
38
+ }
39
+ },
40
+ "a11y": {
41
+ "guarantees": [
42
+ "the wordmark carries the accessible name, so the mark beside it is not announced twice",
43
+ "artwork uses `currentColor` rather than Figma's baked hex, so it themes with the rest of the icon layer"
44
+ ]
45
+ },
46
+ "antiPatterns": [
47
+ {
48
+ "dont": "recolouring the lockup with your own fill",
49
+ "why": "it inherits `icon/default`; overriding it is how a brand mark ends up failing contrast in one theme"
50
+ },
51
+ {
52
+ "dont": "putting the Logo inside a Link with its own text \"IonBase\"",
53
+ "why": "the name is announced twice; label the link, not both"
54
+ }
55
+ ],
7
56
  "stylesheet": "src/styles/logo.css",
8
57
  "tokens": [
9
58
  "--font-family-sans",
@@ -4,6 +4,44 @@
4
4
  "propsType": "LogoMarkProps",
5
5
  "description": "The bare icon glyph, no wordmark — what `Header` actually uses.\n\nColour comes from the `.ion-logo__mark` class (`icon/default`), not the SVG\nitself, so it themes the same way every other icon in the system does.",
6
6
  "import": "import { LogoMark } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "The bare mark, no wordmark — what Header uses.",
9
+ "useWhen": [
10
+ "a wordmark is already visible nearby, or the bar is too narrow for the lockup",
11
+ "a favicon-scale identity marker inside the product"
12
+ ],
13
+ "useInstead": [
14
+ {
15
+ "when": "this is the only place the product names itself",
16
+ "use": "Logo",
17
+ "why": "a mark alone identifies nothing to someone who has not learned it yet"
18
+ }
19
+ ],
20
+ "variants": {
21
+ "size": {
22
+ "sm": {
23
+ "use": "the default, 24px — header bars"
24
+ },
25
+ "lg": {
26
+ "use": "32px — sign-in and other brand-forward surfaces"
27
+ }
28
+ }
29
+ },
30
+ "a11y": {
31
+ "requires": [
32
+ "`label` when the mark stands alone — it is the only thing naming it"
33
+ ],
34
+ "notes": [
35
+ "Omit `label` when a visible wordmark sits beside it; that text already carries the name."
36
+ ]
37
+ },
38
+ "antiPatterns": [
39
+ {
40
+ "dont": "`label=\"logo\"`",
41
+ "do": "`label=\"IonBase\"`",
42
+ "why": "the name of the brand is the useful announcement; the word \"logo\" is not"
43
+ }
44
+ ],
7
45
  "stylesheet": "src/styles/logo.css",
8
46
  "tokens": [
9
47
  "--font-family-sans",
@@ -4,6 +4,60 @@
4
4
  "propsType": "MenuProps",
5
5
  "description": "Menu is the list surface, not a popover.\n\nFigma models the list alone — there is no trigger, anchor or open state in\nthe design — so this renders the surface and nothing else. Positioning it\nagainst a button is the caller's job until Figma has a component that says\nhow. Building a popover here would be inventing design, and it would be the\nhard half to unpick later.\n\n`role=\"menu\"` is deliberately NOT set. A real ARIA menu owes the user\nroving-tabindex arrow navigation, typeahead and focus containment; claiming\nthe role without them is worse for a screen-reader user than an honest list,\nbecause it promises interactions that are not there. When the popover exists,\nthe role comes with it.",
6
6
  "import": "import { Menu } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "The list surface only — no trigger, no anchor, no open state. Positioning it is the caller's job.",
9
+ "useWhen": [
10
+ "rendering a list of actions or options as a surface you have already positioned",
11
+ "the rows need icons, a selected check, or two lines — more than a native <select> can hold"
12
+ ],
13
+ "useInstead": [
14
+ {
15
+ "when": "you want a self-contained dropdown with a trigger and open state",
16
+ "use": "Popover",
17
+ "why": "Menu is the surface alone; Popover owns the trigger, positioning, focus containment and Escape"
18
+ },
19
+ {
20
+ "when": "one value is picked from a modest list of plain text options",
21
+ "use": "Select",
22
+ "why": "the native picker brings keyboard handling, type-ahead and the mobile platform picker for free"
23
+ },
24
+ {
25
+ "when": "it is primary navigation rather than a dropdown",
26
+ "use": "NavItem"
27
+ }
28
+ ],
29
+ "composition": {
30
+ "order": [
31
+ "Menu",
32
+ "MenuItem"
33
+ ],
34
+ "example": "<Menu><MenuItem icon={<Icon as={Copy} />}>Duplicate</MenuItem><MenuItem isSelected>Archive</MenuItem></Menu>"
35
+ },
36
+ "slots": {
37
+ "children": {
38
+ "accepts": "MenuItem"
39
+ }
40
+ },
41
+ "a11y": {
42
+ "role": "none — it is a plain <ul>",
43
+ "guarantees": [
44
+ "no `role=\"menu\"`, deliberately: a real ARIA menu owes the user roving tabindex, arrow-key navigation and typeahead, and claiming the role without them promises interactions that are not there"
45
+ ],
46
+ "notes": [
47
+ "Each MenuItem is a real <button>, so Tab reaches every row. That is honest, and it is not the same as arrow-key menu navigation.",
48
+ "The role arrives when the popover that owns it does."
49
+ ]
50
+ },
51
+ "antiPatterns": [
52
+ {
53
+ "dont": "adding `role=\"menu\"` yourself",
54
+ "why": "the keyboard behaviour that role promises is not implemented; an honest list is better for a screen-reader user than a lying menu"
55
+ },
56
+ {
57
+ "dont": "using Menu as a dropdown without positioning it",
58
+ "why": "it renders in flow — it has no anchor logic of its own"
59
+ }
60
+ ],
7
61
  "stylesheet": "src/styles/menu.css",
8
62
  "tokens": [
9
63
  "--border-focus",
@@ -3,6 +3,57 @@
3
3
  "source": "src/components/Menu.tsx",
4
4
  "propsType": "MenuItemProps",
5
5
  "import": "import { MenuItem } from 'ionbase-ui';",
6
+ "status": "stable",
7
+ "summary": "One row in a Menu. A real <button> inside an <li>, with an optional leading icon and a trailing selected check.",
8
+ "useWhen": [
9
+ "a row in a Menu surface"
10
+ ],
11
+ "useInstead": [
12
+ {
13
+ "when": "it is a top-level navigation destination",
14
+ "use": "NavItem"
15
+ },
16
+ {
17
+ "when": "it is a standalone action outside a list",
18
+ "use": "Button"
19
+ }
20
+ ],
21
+ "slots": {
22
+ "icon": {
23
+ "accepts": "Icon",
24
+ "note": "leading icon; omit its `label` — the row's text names it"
25
+ },
26
+ "children": {
27
+ "accepts": "text",
28
+ "note": "the row label"
29
+ }
30
+ },
31
+ "a11y": {
32
+ "role": "button",
33
+ "guarantees": [
34
+ "`isSelected` sets `aria-pressed`, so the selected state is announced and not only drawn",
35
+ "the check glyph always occupies its slot, so rows never reflow as the selection moves"
36
+ ],
37
+ "requires": [
38
+ "a text label in `children`"
39
+ ]
40
+ },
41
+ "antiPatterns": [
42
+ {
43
+ "dont": "using MenuItem outside a Menu",
44
+ "why": "it renders an <li>, which is only valid inside a list"
45
+ },
46
+ {
47
+ "dont": "an icon-only MenuItem with no children",
48
+ "why": "the row announces nothing"
49
+ }
50
+ ],
51
+ "deprecated": [
52
+ {
53
+ "prop": "disabled",
54
+ "replacement": "isDisabled"
55
+ }
56
+ ],
6
57
  "stylesheet": "src/styles/menu.css",
7
58
  "tokens": [
8
59
  "--border-focus",
@@ -4,6 +4,59 @@
4
4
  "propsType": "NavItemProps",
5
5
  "description": "Nav Item renders an `<a>` when given `href`, a `<button>` otherwise —\nchosen by what the caller is actually building, the same judgment call\nMenu and Select make. A primary nav bar is links; a nav item with\n`showChevron` that opens a menu (no `href` of its own) is a button.\n\nInteraction state comes from React Aria rather than CSS pseudo-classes,\nmatching Button: `useHover` is pointer-aware so a tap does not stay\n\"hovered\" until the next tap elsewhere, and `useFocusRing` shows the ring\nonly for keyboard navigation. The CSS keeps its own `:hover` /\n`:focus-visible` rules so the stylesheet still works without React.",
6
6
  "import": "import { NavItem } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "A primary navigation destination. Renders an <a> when given `href`, a <button> otherwise.",
9
+ "useWhen": [
10
+ "building a header nav bar, a sidebar or a section switcher",
11
+ "an item opens a menu rather than navigating — pass `showChevron` and no `href`"
12
+ ],
13
+ "useInstead": [
14
+ {
15
+ "when": "the link sits inside body copy or a content block",
16
+ "use": "Link"
17
+ },
18
+ {
19
+ "when": "it is a row in a dropdown list rather than a top-level destination",
20
+ "use": "MenuItem"
21
+ }
22
+ ],
23
+ "slots": {
24
+ "icon": {
25
+ "accepts": "Icon",
26
+ "note": "leading icon; omit its `label` — the item's text names it"
27
+ }
28
+ },
29
+ "a11y": {
30
+ "role": "link when `href` is set, button when it is not",
31
+ "guarantees": [
32
+ "hover state comes from React Aria's pointer-aware `useHover`, so a tap does not stay hovered until the next tap elsewhere",
33
+ "the focus ring shows for keyboard navigation only",
34
+ "the CSS keeps its own `:hover` / `:focus-visible` rules, so the styling survives without React"
35
+ ],
36
+ "requires": [
37
+ "an accessible name — `children`, or `aria-label` on an icon-only item"
38
+ ],
39
+ "notes": [
40
+ "NavItem is one item, not the landmark. Wrap the set in a <nav> with its own accessible name."
41
+ ]
42
+ },
43
+ "antiPatterns": [
44
+ {
45
+ "dont": "`showChevron` on a plain link that navigates",
46
+ "why": "the chevron promises a menu; nothing opens"
47
+ },
48
+ {
49
+ "dont": "marking the current page with colour only",
50
+ "do": "`aria-current=\"page\"`",
51
+ "why": "a screen-reader user is otherwise never told where they are"
52
+ }
53
+ ],
54
+ "deprecated": [
55
+ {
56
+ "prop": "disabled",
57
+ "replacement": "isDisabled"
58
+ }
59
+ ],
7
60
  "stylesheet": "src/styles/nav-item.css",
8
61
  "tokens": [
9
62
  "--border-focus",
@@ -4,6 +4,66 @@
4
4
  "propsType": "PhoneInputProps",
5
5
  "description": "PhoneInput — Figma `Input/Phone` (80:372).\n\nA dial-code block butted against an Input, sharing one outline: the block\ncarries the left radii and the control the right, so the seam is square on\nboth sides and reads as a single control. Both keep their full 1px border,\nwhich is what Figma draws — the 2px seam is the two strokes meeting, not an\naccident.\n\nThree sizes, no State axis. Every interaction state is the Input's, reached\nthrough the ordinary props, because Figma composes this from the same `Input`\ninstance rather than redrawing it.\n\nWHAT THIS DELIBERATELY DOES NOT DO\n\nIt does not pick countries. The chevron implies a menu and Figma specifies no\nopen state for it — no list, no flags, no search, no selected state. Country\ndata and the picker are application concerns with real editorial weight (which\nterritories, which names, which order), and inventing them here would be\ndesigning rather than implementing.\n\nSo the trigger is a real `<button>` with an accessible name, and\n`countryButtonProps` is how you wire it to a `Menu`, a popover or your own\nlistbox — including the `aria-haspopup` and `aria-expanded` that only the\nthing owning the popup can set honestly.",
6
6
  "import": "import { PhoneInput } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "A dial-code block butted against an Input, sharing one outline. It does not pick countries.",
9
+ "useWhen": [
10
+ "collecting a phone number where the country calling code matters"
11
+ ],
12
+ "useInstead": [
13
+ {
14
+ "when": "the number is free-form and the code does not matter",
15
+ "use": "Input"
16
+ }
17
+ ],
18
+ "variants": {
19
+ "size": {
20
+ "sm": {
21
+ "use": "dense forms"
22
+ },
23
+ "md": {
24
+ "use": "the default"
25
+ },
26
+ "lg": {
27
+ "use": "prominent single-field forms"
28
+ }
29
+ }
30
+ },
31
+ "slots": {
32
+ "trailingIcon": {
33
+ "accepts": "Icon",
34
+ "note": "passed through to the Input"
35
+ },
36
+ "countryButtonProps": {
37
+ "accepts": "button props",
38
+ "note": "how a country picker is attached — spread your trigger's props here, including the `aria-haspopup` and `aria-expanded` only the thing owning the popup can set honestly"
39
+ }
40
+ },
41
+ "a11y": {
42
+ "guarantees": [
43
+ "the dial-code trigger is a real <button> with an accessible name",
44
+ "every interaction state is the Input's, reached through the ordinary Input props"
45
+ ],
46
+ "requires": [
47
+ "`label`, or `aria-label` when no visible label is rendered",
48
+ "`countryLabel` if you change the default — the trigger shows only `+1`, which names a value rather than an action"
49
+ ]
50
+ },
51
+ "antiPatterns": [
52
+ {
53
+ "dont": "expecting the chevron to open a country list",
54
+ "why": "the picker is deliberately not implemented — country data and ordering are editorial application decisions, and Figma specifies no open state. Wire your own through `countryButtonProps`"
55
+ },
56
+ {
57
+ "dont": "wrapping it in your own bordered container",
58
+ "why": "the block and the control already share one outline; a second border reads as two controls"
59
+ }
60
+ ],
61
+ "deprecated": [
62
+ {
63
+ "prop": "disabled",
64
+ "replacement": "isDisabled"
65
+ }
66
+ ],
7
67
  "stylesheet": "src/styles/phone-input.css",
8
68
  "tokens": [
9
69
  "--border-default",
@@ -4,6 +4,98 @@
4
4
  "propsType": "PopoverProps",
5
5
  "description": "Popover — Figma `Popover` (825:1853).\n\nSITS BETWEEN TOOLTIP AND MODAL. A tooltip is a hint that cannot hold\nfocusable content; a modal is a task that takes over the page. A popover\nholds interactive content but stays attached to the control that opened it.\n\n`usePopover` contains focus, closes on Escape or an outside click, and hides\nthe rest of the page from assistive tech while open — the same guarantees\nModal gives. What differs is the framing, and it is deliberate: no visible\nscrim, anchored to its trigger, and `surface/raised` rather than Modal's\n`surface/overlay`. It reads as attached to the page rather than replacing it.",
6
6
  "import": "import { Popover } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "An anchored panel that holds interactive content. Sits between Tooltip and Modal.",
9
+ "useWhen": [
10
+ "the content is interactive — a form, a filter, a list of actions — but should stay attached to the control that opened it",
11
+ "the surrounding page should stay visible and in context"
12
+ ],
13
+ "useInstead": [
14
+ {
15
+ "when": "the content is a short hint with nothing focusable in it",
16
+ "use": "Tooltip",
17
+ "why": "a tooltip cannot be focused or scrolled and closes when the pointer leaves"
18
+ },
19
+ {
20
+ "when": "the task takes over the page or must be completed before anything else",
21
+ "use": "Modal"
22
+ },
23
+ {
24
+ "when": "you only need the list surface and will position it yourself",
25
+ "use": "Menu"
26
+ }
27
+ ],
28
+ "variants": {
29
+ "placement": {
30
+ "top": {
31
+ "use": "above the trigger"
32
+ },
33
+ "bottom": {
34
+ "use": "below the trigger — the usual choice for a control in a header"
35
+ },
36
+ "left": {
37
+ "use": "to the left of the trigger"
38
+ },
39
+ "right": {
40
+ "use": "to the right of the trigger"
41
+ }
42
+ },
43
+ "size": {
44
+ "sm": {
45
+ "use": "a couple of actions or one short field"
46
+ },
47
+ "md": {
48
+ "use": "the default"
49
+ },
50
+ "lg": {
51
+ "use": "a small form or a filter panel"
52
+ }
53
+ }
54
+ },
55
+ "slots": {
56
+ "children": {
57
+ "accepts": "Button",
58
+ "note": "the trigger. Must forward both a ref and DOM props — every component in this library does"
59
+ },
60
+ "content": {
61
+ "accepts": "any"
62
+ },
63
+ "title": {
64
+ "accepts": "text",
65
+ "note": "also gives the dialog its accessible name"
66
+ },
67
+ "footer": {
68
+ "accepts": "Button",
69
+ "note": "action row, right-aligned"
70
+ }
71
+ },
72
+ "a11y": {
73
+ "role": "dialog",
74
+ "guarantees": [
75
+ "focus is contained while open, Escape and outside clicks close it, and the rest of the page is hidden from assistive tech",
76
+ "`placement` is a preference, not a guarantee — it flips when there is no room, and the arrow follows"
77
+ ],
78
+ "requires": [
79
+ "`title`, or `aria-label` on the popover — a dialog with no name announces nothing about what opened"
80
+ ],
81
+ "notes": [
82
+ "`placement` names where the POPOVER sits, not where the arrow points."
83
+ ]
84
+ },
85
+ "antiPatterns": [
86
+ {
87
+ "dont": "a function-component trigger that drops its ref",
88
+ "why": "it renders, but the popover has nothing to position against"
89
+ },
90
+ {
91
+ "dont": "using a Popover where a Modal belongs",
92
+ "why": "a popover closes on an outside click; a task that must be finished should not be dismissible by a stray click"
93
+ },
94
+ {
95
+ "dont": "nesting a Popover inside another Popover",
96
+ "why": "two overlapping focus containments, and dismissing the outer one takes the inner one with it"
97
+ }
98
+ ],
7
99
  "stylesheet": "src/styles/popover.css",
8
100
  "tokens": [
9
101
  "--border-subtle",
@@ -3,6 +3,83 @@
3
3
  "source": "src/components/Radio.tsx",
4
4
  "propsType": "RadioProps",
5
5
  "import": "import { Radio } from 'ionbase-ui';",
6
+ "status": "stable",
7
+ "summary": "One option inside a RadioGroup. Meaningless on its own — the group owns the name and the selected value.",
8
+ "useWhen": [
9
+ "a child of RadioGroup"
10
+ ],
11
+ "useInstead": [
12
+ {
13
+ "when": "the user may pick more than one",
14
+ "use": "Checkbox"
15
+ },
16
+ {
17
+ "when": "there are more than about five options",
18
+ "use": "Select",
19
+ "why": "a long radio list costs vertical space no one reads"
20
+ }
21
+ ],
22
+ "variants": {
23
+ "size": {
24
+ "sm": {
25
+ "use": "dense forms"
26
+ },
27
+ "md": {
28
+ "use": "the default"
29
+ },
30
+ "lg": {
31
+ "use": "touch targets and prominent choices"
32
+ }
33
+ },
34
+ "intent": {
35
+ "brand": {
36
+ "use": "the default"
37
+ },
38
+ "neutral": {
39
+ "use": "a choice with no positive reading — sort order, display mode"
40
+ },
41
+ "danger": {
42
+ "use": "selecting a destructive option"
43
+ }
44
+ }
45
+ },
46
+ "slots": {
47
+ "children": {
48
+ "accepts": "text",
49
+ "note": "the option's visible label"
50
+ }
51
+ },
52
+ "a11y": {
53
+ "role": "radio",
54
+ "guarantees": [
55
+ "`name` and the selected value come from the group's context, so no Radio has to repeat them",
56
+ "`isDisabled` falls back to the group's"
57
+ ],
58
+ "requires": [
59
+ "a `value` unique within the group",
60
+ "a text label in `children`"
61
+ ]
62
+ },
63
+ "antiPatterns": [
64
+ {
65
+ "dont": "a Radio outside a RadioGroup",
66
+ "why": "it has no name and no siblings, so it can be selected but never unselected"
67
+ },
68
+ {
69
+ "dont": "wiring `checked`/`onChange` on each Radio",
70
+ "why": "the group owns selection; per-option wiring is what the context exists to remove"
71
+ },
72
+ {
73
+ "dont": "a group of one radio",
74
+ "why": "a single radio cannot be cleared once chosen — that is a Checkbox"
75
+ }
76
+ ],
77
+ "deprecated": [
78
+ {
79
+ "prop": "disabled",
80
+ "replacement": "isDisabled"
81
+ }
82
+ ],
6
83
  "stylesheet": "src/styles/radio.css",
7
84
  "tokens": [
8
85
  "--border-disabled",
@@ -4,6 +4,90 @@
4
4
  "propsType": "RadioGroupProps",
5
5
  "description": "Renders a `<fieldset>` with a `<legend>` rather than a div with\n`role=\"radiogroup\"`. Both are announced correctly, but a fieldset also groups\nthe inputs for form submission and native validation, which the ARIA version\ndoes not.",
6
6
  "import": "import { RadioGroup } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "A <fieldset>/<legend> group of mutually exclusive options. Owns the shared name and the selected value.",
9
+ "useWhen": [
10
+ "exactly one choice from a small set, all worth showing at once",
11
+ "the options need explaining — each one can carry its own label text"
12
+ ],
13
+ "useInstead": [
14
+ {
15
+ "when": "the list is long, or space is tight",
16
+ "use": "Select"
17
+ },
18
+ {
19
+ "when": "more than one may be picked",
20
+ "use": "Checkbox"
21
+ },
22
+ {
23
+ "when": "there are exactly two states and the change is immediate",
24
+ "use": "Toggle"
25
+ }
26
+ ],
27
+ "variants": {
28
+ "size": {
29
+ "sm": {
30
+ "use": "dense forms"
31
+ },
32
+ "md": {
33
+ "use": "the default"
34
+ },
35
+ "lg": {
36
+ "use": "touch targets and prominent choices"
37
+ }
38
+ },
39
+ "intent": {
40
+ "brand": {
41
+ "use": "the default"
42
+ },
43
+ "neutral": {
44
+ "use": "a choice with no positive reading — sort order, display mode"
45
+ },
46
+ "danger": {
47
+ "use": "a set where the options are destructive"
48
+ }
49
+ }
50
+ },
51
+ "slots": {
52
+ "label": {
53
+ "accepts": "text",
54
+ "note": "renders as the <legend> — the question the options answer"
55
+ },
56
+ "children": {
57
+ "accepts": "Radio"
58
+ }
59
+ },
60
+ "a11y": {
61
+ "role": "group, from <fieldset> and <legend>",
62
+ "guarantees": [
63
+ "a real fieldset rather than `role=\"radiogroup\"`: both announce correctly, but the fieldset also groups the inputs for form submission and native validation",
64
+ "`name` is generated when omitted, so two groups on one page never collide",
65
+ "`isDisabled` cascades to every Radio that has not set its own"
66
+ ],
67
+ "requires": [
68
+ "`label`, or `aria-label` when the question is already visible elsewhere"
69
+ ]
70
+ },
71
+ "antiPatterns": [
72
+ {
73
+ "dont": "an unlabelled group",
74
+ "why": "the options are announced with no question attached"
75
+ },
76
+ {
77
+ "dont": "passing both `value` and `defaultValue`",
78
+ "why": "controlled and uncontrolled at once; the controlled value wins and `defaultValue` silently does nothing"
79
+ },
80
+ {
81
+ "dont": "no option selected by default with no \"none\" option",
82
+ "why": "a radio group cannot be returned to empty once touched"
83
+ }
84
+ ],
85
+ "deprecated": [
86
+ {
87
+ "prop": "disabled",
88
+ "replacement": "isDisabled"
89
+ }
90
+ ],
7
91
  "stylesheet": "src/styles/radio.css",
8
92
  "tokens": [
9
93
  "--border-disabled",
@@ -4,6 +4,47 @@
4
4
  "propsType": "ScrollProgressProps",
5
5
  "description": "The compact rail is the disclosure trigger. It is a real `<button>`, so a\nkeyboard user reaches it by Tab and opens it the same way as any button —\nEnter or Space — no `:focus-within` trick required; a mouse user can also\njust hover it. Built as a WAI-ARIA Disclosure (`aria-expanded` +\n`aria-controls`) rather than a menu: see the CSS header for why\n`role=\"menu\"` is deliberately not used here.\n\nCloses on outside pointerdown and on Escape — the minimum a disclosure\nneeds to not trap the page once opened, not a full popover/focus-trap\nimplementation.",
6
6
  "import": "import { ScrollProgress } from 'ionbase-ui';",
7
+ "status": "stable",
8
+ "summary": "A compact progress rail that expands into a section list. A disclosure, not a menu — and it computes nothing itself.",
9
+ "useWhen": [
10
+ "a long document or page needs an in-page section index with reading progress",
11
+ "the caller already knows the scroll fraction and the active section"
12
+ ],
13
+ "useInstead": [
14
+ {
15
+ "when": "the sections are peer views rather than positions in one document",
16
+ "use": "Tabs"
17
+ },
18
+ {
19
+ "when": "it is site navigation rather than in-page position",
20
+ "use": "NavItem"
21
+ }
22
+ ],
23
+ "a11y": {
24
+ "role": "disclosure — a real <button> with aria-expanded and aria-controls",
25
+ "guarantees": [
26
+ "the rail is reachable by Tab and opens with Enter or Space, not just hover",
27
+ "closes on outside pointerdown and on Escape"
28
+ ],
29
+ "requires": [
30
+ "`progress` as 0–100 and `activeId` — the component has no opinion on where they come from",
31
+ "handling `onSelect` yourself: it reports the choice and does not scroll anywhere"
32
+ ],
33
+ "notes": [
34
+ "Deliberately not `role=\"menu\"` — see the stylesheet header. It is a disclosure with a list inside it, not a menu with roving focus.",
35
+ "Not a focus trap: it is the minimum a disclosure needs to not trap the page once opened."
36
+ ]
37
+ },
38
+ "antiPatterns": [
39
+ {
40
+ "dont": "expecting it to scroll the page when a section is chosen",
41
+ "why": "it reports the choice, matching Menu — what \"select this section\" means is the page's decision"
42
+ },
43
+ {
44
+ "dont": "deriving `progress` from a scroll listener without throttling",
45
+ "why": "this re-renders the rail on every scroll frame"
46
+ }
47
+ ],
7
48
  "stylesheet": "src/styles/scroll-progress.css",
8
49
  "tokens": [
9
50
  "--border-default",