ionbase-ui 0.20.0 → 0.21.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.20.0",
3
+ "version": "0.21.0",
4
4
  "generated": "by scripts/build-meta.mjs — do not edit; intent lives in meta/*.json",
5
5
  "hooks": [
6
6
  "useToast"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "usage": "Pick a component here, then read dist/meta/<Name>.json for its full contract.",
5
5
  "hooks": [
6
6
  "useToast"
@@ -0,0 +1,133 @@
1
+ {
2
+ "name": "DataTable",
3
+ "summary": "A table of records with a toolbar, selection, and the three states everyone forgets.",
4
+ "useWhen": [
5
+ "showing a list of records the user filters, sorts, selects or acts on in bulk",
6
+ "the row count is unknown at design time — which is what makes the empty and loading states mandatory rather than optional"
7
+ ],
8
+ "composes": [
9
+ "Table",
10
+ "TableHead",
11
+ "TableBody",
12
+ "TableRow",
13
+ "TableCell",
14
+ "Checkbox",
15
+ "Button",
16
+ "Badge",
17
+ "Alert",
18
+ "Input"
19
+ ],
20
+ "structure": [
21
+ "A toolbar above the table: a search Input, filter controls, and the bulk-action Buttons that appear once something is selected.",
22
+ "Table with a TableHead whose first cell carries the select-all Checkbox, and a TableBody whose rows each carry their own.",
23
+ "One region below the table for pagination or a load-more Button.",
24
+ "The table region is replaced — not overlaid — by the empty, loading and error states."
25
+ ],
26
+ "states": {
27
+ "loading": {
28
+ "must": "Render the table's header row and a fixed number of skeleton rows, keeping the column widths.",
29
+ "why": "swapping the whole region for a spinner collapses the layout and moves every control on the page when the data lands",
30
+ "a11y": "put `aria-busy=\"true\"` on the container and announce the result count in a live region once it resolves"
31
+ },
32
+ "empty": {
33
+ "must": "Say which of the two empties this is: no records exist yet, or the current filters match nothing. Offer the action that resolves it — create the first record, or clear the filters.",
34
+ "why": "\"No results\" with no way forward is the single most common dead end in enterprise SaaS, and the two cases need opposite actions",
35
+ "a11y": "the empty state carries the heading; do not leave the region unnamed"
36
+ },
37
+ "error": {
38
+ "must": "Keep the toolbar usable, show an `Alert` with `intent=\"error\"` naming what failed, and offer a retry.",
39
+ "why": "replacing the whole screen with an error throws away the user's filters and selection, so retrying means rebuilding the query by hand",
40
+ "a11y": "Alert picks `role=\"alert\"` from the intent — do not add your own"
41
+ },
42
+ "partial": {
43
+ "must": "When some rows load and others fail, show the rows you have and an Alert above them for the rest.",
44
+ "why": "hiding successful rows because a sibling request failed is data loss the user cannot see"
45
+ }
46
+ },
47
+ "propsUsed": {
48
+ "Table": [
49
+ "density",
50
+ "isStriped"
51
+ ],
52
+ "TableRow": [
53
+ "selection",
54
+ "isSelected"
55
+ ],
56
+ "TableCell": [
57
+ "scope",
58
+ "align",
59
+ "variant"
60
+ ],
61
+ "Checkbox": [
62
+ "isIndeterminate",
63
+ "isDisabled"
64
+ ],
65
+ "Button": [
66
+ "variant",
67
+ "size"
68
+ ],
69
+ "Alert": [
70
+ "intent",
71
+ "title",
72
+ "actions"
73
+ ],
74
+ "Input": [
75
+ "leadingIcon"
76
+ ]
77
+ },
78
+ "variantsUsed": {
79
+ "Alert": {
80
+ "intent": [
81
+ "error"
82
+ ]
83
+ },
84
+ "Button": {
85
+ "variant": [
86
+ "primary-brand",
87
+ "secondary",
88
+ "destructive"
89
+ ]
90
+ },
91
+ "TableCell": {
92
+ "align": [
93
+ "trailing"
94
+ ]
95
+ }
96
+ },
97
+ "a11y": {
98
+ "requires": [
99
+ "`aria-label` on Table — it names the scroll region, which is keyboard reachable",
100
+ "`scope=\"col\"` on every header cell",
101
+ "an `aria-label` on every row-selection Checkbox, and `isIndeterminate` on the select-all when the selection is partial",
102
+ "a live region announcing the row count after a filter or search changes it — otherwise a screen-reader user has no idea the table changed"
103
+ ],
104
+ "notes": [
105
+ "Bulk-action Buttons appearing on selection is a change of available actions, not just of styling. Announce it."
106
+ ]
107
+ },
108
+ "antiPatterns": [
109
+ {
110
+ "dont": "a spinner replacing the whole table region",
111
+ "do": "skeleton rows under the real header",
112
+ "why": "the layout collapses and every control below the table jumps when data arrives"
113
+ },
114
+ {
115
+ "dont": "one empty state for both \"nothing exists\" and \"nothing matches\"",
116
+ "why": "the resolving action is opposite in each case — create something, or clear the filter"
117
+ },
118
+ {
119
+ "dont": "a clickable <tr>",
120
+ "do": "a Link or Button inside a cell",
121
+ "why": "a row takes no focus, so a keyboard user can never fire it, and an interactive role on <tr> is not valid HTML"
122
+ },
123
+ {
124
+ "dont": "destructive bulk actions with no confirmation",
125
+ "do": "the DestructiveConfirm pattern",
126
+ "why": "select-all plus delete is one click away from removing everything the filter matched"
127
+ },
128
+ {
129
+ "dont": "wrapping Table in your own scroll container",
130
+ "why": "Table already owns one and makes it keyboard reachable; a second is unreachable"
131
+ }
132
+ ]
133
+ }
@@ -0,0 +1,112 @@
1
+ {
2
+ "name": "DestructiveConfirm",
3
+ "summary": "A confirmation the user cannot pass through by reflex, for actions that cannot be undone.",
4
+ "useWhen": [
5
+ "the action deletes data, revokes access, cancels a subscription, or otherwise cannot be reversed by the user",
6
+ "the action applies to more than one record at once — bulk destructive actions need this even when a single one would not"
7
+ ],
8
+ "composes": [
9
+ "Modal",
10
+ "Button",
11
+ "Input",
12
+ "Alert",
13
+ "Badge"
14
+ ],
15
+ "structure": [
16
+ "A Modal whose title names the action and the object — \"Delete 14 projects\", not \"Are you sure?\".",
17
+ "Body text stating what is lost and what is not, in that order.",
18
+ "For high-consequence actions, an Input requiring the object's name to be typed exactly before the confirm Button enables.",
19
+ "Footer: cancel first in the DOM, destructive second — so the safe option is reached first by keyboard."
20
+ ],
21
+ "states": {
22
+ "loading": {
23
+ "must": "Disable both footer Buttons and say what is happening on the destructive one. Keep the Modal open.",
24
+ "why": "closing the modal optimistically leaves the user unsure whether the deletion happened when it later fails"
25
+ },
26
+ "empty": {
27
+ "must": "Never open with nothing selected. If the selection emptied while the modal was opening, close it.",
28
+ "why": "\"Delete 0 items\" is a dialog with no purpose that still teaches the user to click through confirmations"
29
+ },
30
+ "error": {
31
+ "must": "Keep the Modal open, show an Alert with `intent=\"error\"` inside it, and leave the confirm Button usable for a retry.",
32
+ "why": "a failed deletion that closes the dialog reads as a successful one"
33
+ },
34
+ "partial": {
35
+ "must": "For bulk actions, report how many succeeded and name what did not, with a retry limited to the failures.",
36
+ "why": "\"some items could not be deleted\" with no list means the user must re-check every record by hand"
37
+ }
38
+ },
39
+ "propsUsed": {
40
+ "Modal": [
41
+ "isOpen",
42
+ "onOpenChange",
43
+ "size",
44
+ "title",
45
+ "description",
46
+ "footer",
47
+ "showClose"
48
+ ],
49
+ "Button": [
50
+ "variant",
51
+ "size",
52
+ "isDisabled"
53
+ ],
54
+ "Input": [
55
+ "label",
56
+ "isInvalid",
57
+ "errorMessage"
58
+ ],
59
+ "Alert": [
60
+ "intent",
61
+ "title"
62
+ ]
63
+ },
64
+ "variantsUsed": {
65
+ "Button": {
66
+ "variant": [
67
+ "destructive",
68
+ "tertiary"
69
+ ]
70
+ },
71
+ "Alert": {
72
+ "intent": [
73
+ "error"
74
+ ]
75
+ },
76
+ "Modal": {
77
+ "size": [
78
+ "sm"
79
+ ]
80
+ }
81
+ },
82
+ "a11y": {
83
+ "requires": [
84
+ "`title` on the Modal — it is the dialog's accessible name",
85
+ "the cancel Button before the destructive one in DOM order",
86
+ "a confirm Button that names the action, not \"OK\""
87
+ ],
88
+ "notes": [
89
+ "Modal already contains focus, closes on Escape and hides the rest of the page. Escape must cancel, never confirm.",
90
+ "Typed confirmation is friction on purpose. Use it where the consequence justifies it and nowhere else, or it becomes something to type without reading."
91
+ ]
92
+ },
93
+ "antiPatterns": [
94
+ {
95
+ "dont": "\"Are you sure?\" as the title",
96
+ "why": "it names neither the action nor the object, so the answer is a reflex"
97
+ },
98
+ {
99
+ "dont": "the destructive Button focused by default",
100
+ "why": "Enter on an unread dialog then performs the irreversible action"
101
+ },
102
+ {
103
+ "dont": "a Toast as the only confirmation of a destructive action",
104
+ "do": "Alert",
105
+ "why": "a toast disappears, and so does the undo it was carrying"
106
+ },
107
+ {
108
+ "dont": "a confirmation dialog on every action",
109
+ "why": "confirmations that appear for reversible actions train the user to dismiss the ones that matter"
110
+ }
111
+ ]
112
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "name": "Form",
3
+ "summary": "A form that validates, reports errors where the user can act on them, and survives a failed submit.",
4
+ "useWhen": [
5
+ "collecting more than one field from the user before an action takes effect",
6
+ "the submit can fail server-side — which is every real form"
7
+ ],
8
+ "composes": [
9
+ "Input",
10
+ "Select",
11
+ "Checkbox",
12
+ "Radio",
13
+ "RadioGroup",
14
+ "Toggle",
15
+ "PhoneInput",
16
+ "Button",
17
+ "Alert",
18
+ "Divider"
19
+ ],
20
+ "structure": [
21
+ "Fields in a single column. Two columns only for genuinely paired values, and they stack below the mobile breakpoint.",
22
+ "Each field carries its own label and, when invalid, its own errorMessage — the message sits with the control it is about.",
23
+ "An error summary Alert above the form after a failed submit, listing each invalid field as a link to it.",
24
+ "A submit Button and a cancel Link or Button, in that reading order, at the end."
25
+ ],
26
+ "states": {
27
+ "loading": {
28
+ "must": "Disable the submit Button and say what is happening on it — \"Saving…\" — rather than swapping the form for a spinner.",
29
+ "why": "the user needs to see what they typed while it is being saved, and a disabled control with unchanged text reads as broken",
30
+ "a11y": "announce the outcome in a live region; a disabled button cannot hold focus to be read"
31
+ },
32
+ "empty": {
33
+ "must": "A form's empty state is its initial state: no field shows an error before the user has touched it.",
34
+ "why": "validating on mount marks a form the user has not filled in as wrong, which teaches them to ignore the errors"
35
+ },
36
+ "error": {
37
+ "must": "Two levels, both required. Per-field `errorMessage` with `isInvalid`, plus an error summary Alert naming every failed field. Never clear what the user typed.",
38
+ "why": "the summary is how a screen-reader or zoomed user finds the failures without hunting the whole form; the per-field message is how they fix each one",
39
+ "a11y": "move focus to the summary after a failed submit — Alert's error intent already carries role=\"alert\""
40
+ },
41
+ "partial": {
42
+ "must": "When the server accepts some fields and rejects others, keep every value and mark only what failed.",
43
+ "why": "re-entering accepted data is the fastest way to lose a user mid-form"
44
+ }
45
+ },
46
+ "propsUsed": {
47
+ "Input": [
48
+ "label",
49
+ "description",
50
+ "errorMessage",
51
+ "isInvalid",
52
+ "isRequired"
53
+ ],
54
+ "Select": [
55
+ "label",
56
+ "description",
57
+ "errorMessage",
58
+ "isInvalid"
59
+ ],
60
+ "RadioGroup": [
61
+ "label",
62
+ "name",
63
+ "value",
64
+ "onChange"
65
+ ],
66
+ "Checkbox": [
67
+ "isDisabled"
68
+ ],
69
+ "Button": [
70
+ "variant",
71
+ "size",
72
+ "isDisabled"
73
+ ],
74
+ "Alert": [
75
+ "intent",
76
+ "title",
77
+ "actions"
78
+ ]
79
+ },
80
+ "variantsUsed": {
81
+ "Alert": {
82
+ "intent": [
83
+ "error"
84
+ ]
85
+ },
86
+ "Button": {
87
+ "variant": [
88
+ "primary-brand",
89
+ "tertiary"
90
+ ]
91
+ }
92
+ },
93
+ "a11y": {
94
+ "requires": [
95
+ "a visible `label` on every field, or `aria-label` where the label is genuinely elsewhere",
96
+ "`isInvalid` alongside `errorMessage` — the message alone styles the field without announcing it as invalid",
97
+ "focus moved to the error summary after a failed submit",
98
+ "`isRequired` on required fields rather than an asterisk in the label text"
99
+ ],
100
+ "notes": [
101
+ "Validate on blur and on submit, not on every keystroke: an error that appears while the user is still typing the value is wrong more often than right."
102
+ ]
103
+ },
104
+ "antiPatterns": [
105
+ {
106
+ "dont": "clearing fields after a failed submit",
107
+ "why": "it punishes the user for the server's answer"
108
+ },
109
+ {
110
+ "dont": "an error summary with no per-field messages, or per-field with no summary",
111
+ "why": "each solves a different problem — finding the failures, and fixing one"
112
+ },
113
+ {
114
+ "dont": "a disabled submit Button as the only validation feedback",
115
+ "why": "the user is told no and never told why; a disabled button also takes no focus, so it cannot explain itself"
116
+ },
117
+ {
118
+ "dont": "placeholder text in place of a label",
119
+ "why": "it disappears on focus, which is exactly when it is needed, and it is not reliably announced"
120
+ },
121
+ {
122
+ "dont": "a Toggle inside a form that is submitted",
123
+ "do": "Checkbox",
124
+ "why": "a switch says the change already took effect"
125
+ }
126
+ ]
127
+ }
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "PageShell",
3
+ "summary": "The frame every screen sits in: header, navigation, a named content region.",
4
+ "useWhen": [
5
+ "building any full application screen rather than an embedded widget"
6
+ ],
7
+ "composes": [
8
+ "Header",
9
+ "NavItem",
10
+ "Logo",
11
+ "LogoMark",
12
+ "Avatar",
13
+ "Button",
14
+ "Divider",
15
+ "ToastProvider"
16
+ ],
17
+ "structure": [
18
+ "Header at the top with the brand in `brand`, primary navigation in `center` and account actions in `end`.",
19
+ "One <main> content region, named, holding the page's own heading as its h1.",
20
+ "ToastProvider mounted once around the whole shell, not per route.",
21
+ "Breadcrumbs, when present, sit inside <main> above the h1 — they describe where the content is, not where the chrome is."
22
+ ],
23
+ "states": {
24
+ "loading": {
25
+ "must": "Render the shell immediately and load only the content region. Never block the header on the page's data.",
26
+ "why": "the navigation is what a user reaches for when a page is slow; hiding it makes a slow page feel like a broken one"
27
+ },
28
+ "empty": {
29
+ "must": "A route with nothing in it still renders the shell, the heading, and a statement of what would be here.",
30
+ "why": "a blank content region with working chrome reads as a failed load"
31
+ },
32
+ "error": {
33
+ "must": "Keep the shell. Put the error inside <main> with a retry and a way back to somewhere that works.",
34
+ "why": "a full-page error strands the user with no navigation, so their only option is the browser's back button"
35
+ }
36
+ },
37
+ "propsUsed": {
38
+ "Header": [
39
+ "brand",
40
+ "center",
41
+ "end",
42
+ "open",
43
+ "onOpenChange",
44
+ "menuLabel"
45
+ ],
46
+ "NavItem": [
47
+ "icon",
48
+ "showChevron",
49
+ "href"
50
+ ],
51
+ "ToastProvider": [
52
+ "placement",
53
+ "limit"
54
+ ],
55
+ "Logo": [
56
+ "size",
57
+ "wordmark"
58
+ ],
59
+ "LogoMark": [
60
+ "size",
61
+ "label"
62
+ ]
63
+ },
64
+ "variantsUsed": {
65
+ "ToastProvider": {
66
+ "placement": [
67
+ "top-right"
68
+ ]
69
+ }
70
+ },
71
+ "a11y": {
72
+ "requires": [
73
+ "`menuLabel` on Header — the mobile toggle is icon-only",
74
+ "a <nav> with its own accessible name wrapping the NavItems; Header supplies the banner landmark, not the navigation one",
75
+ "exactly one <main>, and exactly one h1 inside it",
76
+ "`aria-current=\"page\"` on the NavItem for the current route — colour alone does not tell a screen-reader user where they are"
77
+ ],
78
+ "notes": [
79
+ "The desktop/tablet/mobile switch is a media query inside Header. Do not duplicate that decision in React state."
80
+ ]
81
+ },
82
+ "antiPatterns": [
83
+ {
84
+ "dont": "a ToastProvider per route",
85
+ "why": "toasts queued before a navigation are unmounted mid-announcement, and two live regions announce over each other"
86
+ },
87
+ {
88
+ "dont": "the page title as an h2 because the logo is an h1",
89
+ "why": "the brand is not a heading; the page's subject is"
90
+ },
91
+ {
92
+ "dont": "rendering a second nav for mobile",
93
+ "why": "Header moves the one copy, and two copies duplicate every link in the accessibility tree"
94
+ }
95
+ ]
96
+ }
@@ -0,0 +1,136 @@
1
+ {
2
+ "name": "SettingsPanel",
3
+ "summary": "Grouped settings that either apply immediately or are saved together — never both in one panel.",
4
+ "useWhen": [
5
+ "the user configures preferences, feature flags or account options",
6
+ "the settings are grouped and the groups are worth naming"
7
+ ],
8
+ "composes": [
9
+ "Toggle",
10
+ "Checkbox",
11
+ "RadioGroup",
12
+ "Radio",
13
+ "Select",
14
+ "Input",
15
+ "Button",
16
+ "Alert",
17
+ "Divider",
18
+ "Badge",
19
+ "Tabs",
20
+ "TabItem"
21
+ ],
22
+ "structure": [
23
+ "Named groups, separated by Divider, each with a heading.",
24
+ "One row per setting: label and description on the left, control on the right.",
25
+ "For save-on-submit panels, a save bar that appears only when something is dirty, with save and discard.",
26
+ "Tabs for panels large enough that a single scroll is unreasonable — but never with unsaved changes hidden behind an inactive tab."
27
+ ],
28
+ "states": {
29
+ "loading": {
30
+ "must": "Show the groups and their labels with the controls disabled, rather than an empty panel.",
31
+ "why": "the shape of the settings is known before their values are; showing it makes the wait legible"
32
+ },
33
+ "empty": {
34
+ "must": "A group with nothing in it for this account states why — a plan limit, a permission — rather than being hidden.",
35
+ "why": "a silently absent setting reads as a missing feature, and generates support tickets"
36
+ },
37
+ "error": {
38
+ "must": "Revert the control to its previous value, show an Alert naming the setting that failed, and keep the rest of the panel usable.",
39
+ "why": "a Toggle left in the new position after a failed save is a lie about the server's state"
40
+ },
41
+ "partial": {
42
+ "must": "When a batched save partially fails, keep the successful values and mark only the failed rows dirty again.",
43
+ "why": "reverting everything discards changes the server accepted"
44
+ }
45
+ },
46
+ "propsUsed": {
47
+ "Toggle": [
48
+ "size",
49
+ "intent",
50
+ "isDisabled"
51
+ ],
52
+ "Checkbox": [
53
+ "isDisabled",
54
+ "isIndeterminate"
55
+ ],
56
+ "RadioGroup": [
57
+ "label",
58
+ "value",
59
+ "onChange",
60
+ "isDisabled"
61
+ ],
62
+ "Select": [
63
+ "label",
64
+ "description",
65
+ "isInvalid",
66
+ "errorMessage"
67
+ ],
68
+ "Button": [
69
+ "variant",
70
+ "isDisabled"
71
+ ],
72
+ "Alert": [
73
+ "intent",
74
+ "title",
75
+ "actions"
76
+ ],
77
+ "Tabs": [
78
+ "type",
79
+ "size"
80
+ ]
81
+ },
82
+ "variantsUsed": {
83
+ "Toggle": {
84
+ "intent": [
85
+ "brand",
86
+ "danger"
87
+ ]
88
+ },
89
+ "Alert": {
90
+ "intent": [
91
+ "error",
92
+ "warning"
93
+ ]
94
+ },
95
+ "Button": {
96
+ "variant": [
97
+ "primary-brand",
98
+ "tertiary"
99
+ ]
100
+ },
101
+ "Tabs": {
102
+ "type": [
103
+ "underline"
104
+ ]
105
+ }
106
+ },
107
+ "a11y": {
108
+ "requires": [
109
+ "a label on every control — the row's visible text, wired up, not just placed beside it",
110
+ "the description associated with the control rather than floating next to it, so it is announced with the setting",
111
+ "an announcement when an immediate-apply setting saves; the visual state change is not enough"
112
+ ],
113
+ "notes": [
114
+ "Toggle for immediate effect, Checkbox for values saved with a submit. Mixing both in one panel means the user cannot tell which is which."
115
+ ]
116
+ },
117
+ "antiPatterns": [
118
+ {
119
+ "dont": "Toggles that apply immediately alongside a Save button",
120
+ "why": "the switch says the change already happened; the button says it has not"
121
+ },
122
+ {
123
+ "dont": "a destructive setting with no confirmation",
124
+ "do": "the DestructiveConfirm pattern",
125
+ "why": "\"delete this workspace\" as a Toggle row is one mis-click"
126
+ },
127
+ {
128
+ "dont": "unsaved changes on an inactive tab",
129
+ "why": "the user is told the form is dirty with nothing on screen that is"
130
+ },
131
+ {
132
+ "dont": "hiding settings the account cannot use",
133
+ "why": "an absent control is indistinguishable from a missing feature"
134
+ }
135
+ ]
136
+ }
@@ -0,0 +1,130 @@
1
+ {
2
+ "name": "Wizard",
3
+ "summary": "A sequence of steps with per-step validation, where progress survives leaving.",
4
+ "useWhen": [
5
+ "a task genuinely has an order — later steps depend on earlier answers",
6
+ "the whole task is too long to present as one form without abandonment"
7
+ ],
8
+ "composes": [
9
+ "Button",
10
+ "Input",
11
+ "Select",
12
+ "Checkbox",
13
+ "RadioGroup",
14
+ "Radio",
15
+ "Alert",
16
+ "Badge",
17
+ "Divider",
18
+ "Modal"
19
+ ],
20
+ "structure": [
21
+ "A step indicator naming every step and marking the current one — not Tabs, because the steps are not peers and cannot be visited in any order.",
22
+ "One step's fields at a time, each step a Form in its own right.",
23
+ "Back and Next, plus save-and-exit for anything longer than three steps.",
24
+ "A final review step listing every answer with a link back to the step that set it."
25
+ ],
26
+ "states": {
27
+ "loading": {
28
+ "must": "Disable Next and say what is happening on it. Never advance optimistically.",
29
+ "why": "advancing before the server accepts step N means step N+1 may be built on rejected data"
30
+ },
31
+ "empty": {
32
+ "must": "A resumed wizard opens on the first incomplete step, not step one.",
33
+ "why": "restarting a half-finished task is why users abandon it the second time"
34
+ },
35
+ "error": {
36
+ "must": "Stay on the failing step, mark the fields, and keep every answer from every step.",
37
+ "why": "losing step 1's answers because step 3 failed is the worst version of this pattern"
38
+ },
39
+ "partial": {
40
+ "must": "Save each completed step as it is accepted, so leaving mid-wizard loses at most the current step.",
41
+ "why": "save-and-exit that only saves at the end is not save-and-exit"
42
+ }
43
+ },
44
+ "propsUsed": {
45
+ "Button": [
46
+ "variant",
47
+ "size",
48
+ "isDisabled"
49
+ ],
50
+ "Input": [
51
+ "label",
52
+ "isInvalid",
53
+ "errorMessage",
54
+ "isRequired"
55
+ ],
56
+ "Select": [
57
+ "label",
58
+ "isInvalid",
59
+ "errorMessage"
60
+ ],
61
+ "RadioGroup": [
62
+ "label",
63
+ "value",
64
+ "onChange"
65
+ ],
66
+ "Alert": [
67
+ "intent",
68
+ "title"
69
+ ],
70
+ "Badge": [
71
+ "intent"
72
+ ],
73
+ "Modal": [
74
+ "isOpen",
75
+ "onOpenChange",
76
+ "title",
77
+ "footer"
78
+ ]
79
+ },
80
+ "variantsUsed": {
81
+ "Button": {
82
+ "variant": [
83
+ "primary-brand",
84
+ "secondary",
85
+ "tertiary"
86
+ ]
87
+ },
88
+ "Alert": {
89
+ "intent": [
90
+ "error",
91
+ "information"
92
+ ]
93
+ },
94
+ "Badge": {
95
+ "intent": [
96
+ "success",
97
+ "neutral"
98
+ ]
99
+ }
100
+ },
101
+ "a11y": {
102
+ "requires": [
103
+ "the step change announced, and focus moved to the new step's heading — otherwise a screen-reader user does not know the page changed",
104
+ "the current step's position stated in text, not only drawn: \"Step 2 of 5\"",
105
+ "each step's fields validated on its own Next, not all at the end"
106
+ ],
107
+ "notes": [
108
+ "Do not build this from Tabs. The tablist role announces peers that may be visited in any order, which is the opposite of a wizard.",
109
+ "A Modal wraps the whole wizard only when abandoning it is safe. A long task in a dialog that closes on an outside click loses everything."
110
+ ]
111
+ },
112
+ "antiPatterns": [
113
+ {
114
+ "dont": "Tabs as the step indicator",
115
+ "why": "the role promises peers in any order, and nothing stops a user landing on the last step first"
116
+ },
117
+ {
118
+ "dont": "validating every step only at the end",
119
+ "why": "the user is sent back through steps they thought were finished"
120
+ },
121
+ {
122
+ "dont": "a wizard for a task that fits on one screen",
123
+ "why": "each step boundary is an opportunity to abandon"
124
+ },
125
+ {
126
+ "dont": "a progress bar with no step names",
127
+ "why": "a percentage does not tell the user what is left or whether they can finish now"
128
+ }
129
+ ]
130
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "package": "ionbase-ui",
3
+ "version": "0.21.0",
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
+ "patterns": {
6
+ "DataTable": {
7
+ "summary": "A table of records with a toolbar, selection, and the three states everyone forgets.",
8
+ "composes": [
9
+ "Table",
10
+ "TableHead",
11
+ "TableBody",
12
+ "TableRow",
13
+ "TableCell",
14
+ "Checkbox",
15
+ "Button",
16
+ "Badge",
17
+ "Alert",
18
+ "Input"
19
+ ],
20
+ "states": [
21
+ "loading",
22
+ "empty",
23
+ "error",
24
+ "partial"
25
+ ],
26
+ "detail": "dist/meta/patterns/DataTable.json"
27
+ },
28
+ "DestructiveConfirm": {
29
+ "summary": "A confirmation the user cannot pass through by reflex, for actions that cannot be undone.",
30
+ "composes": [
31
+ "Modal",
32
+ "Button",
33
+ "Input",
34
+ "Alert",
35
+ "Badge"
36
+ ],
37
+ "states": [
38
+ "loading",
39
+ "empty",
40
+ "error",
41
+ "partial"
42
+ ],
43
+ "detail": "dist/meta/patterns/DestructiveConfirm.json"
44
+ },
45
+ "Form": {
46
+ "summary": "A form that validates, reports errors where the user can act on them, and survives a failed submit.",
47
+ "composes": [
48
+ "Input",
49
+ "Select",
50
+ "Checkbox",
51
+ "Radio",
52
+ "RadioGroup",
53
+ "Toggle",
54
+ "PhoneInput",
55
+ "Button",
56
+ "Alert",
57
+ "Divider"
58
+ ],
59
+ "states": [
60
+ "loading",
61
+ "empty",
62
+ "error",
63
+ "partial"
64
+ ],
65
+ "detail": "dist/meta/patterns/Form.json"
66
+ },
67
+ "PageShell": {
68
+ "summary": "The frame every screen sits in: header, navigation, a named content region.",
69
+ "composes": [
70
+ "Header",
71
+ "NavItem",
72
+ "Logo",
73
+ "LogoMark",
74
+ "Avatar",
75
+ "Button",
76
+ "Divider",
77
+ "ToastProvider"
78
+ ],
79
+ "states": [
80
+ "loading",
81
+ "empty",
82
+ "error"
83
+ ],
84
+ "detail": "dist/meta/patterns/PageShell.json"
85
+ },
86
+ "SettingsPanel": {
87
+ "summary": "Grouped settings that either apply immediately or are saved together — never both in one panel.",
88
+ "composes": [
89
+ "Toggle",
90
+ "Checkbox",
91
+ "RadioGroup",
92
+ "Radio",
93
+ "Select",
94
+ "Input",
95
+ "Button",
96
+ "Alert",
97
+ "Divider",
98
+ "Badge",
99
+ "Tabs",
100
+ "TabItem"
101
+ ],
102
+ "states": [
103
+ "loading",
104
+ "empty",
105
+ "error",
106
+ "partial"
107
+ ],
108
+ "detail": "dist/meta/patterns/SettingsPanel.json"
109
+ },
110
+ "Wizard": {
111
+ "summary": "A sequence of steps with per-step validation, where progress survives leaving.",
112
+ "composes": [
113
+ "Button",
114
+ "Input",
115
+ "Select",
116
+ "Checkbox",
117
+ "RadioGroup",
118
+ "Radio",
119
+ "Alert",
120
+ "Badge",
121
+ "Divider",
122
+ "Modal"
123
+ ],
124
+ "states": [
125
+ "loading",
126
+ "empty",
127
+ "error",
128
+ "partial"
129
+ ],
130
+ "detail": "dist/meta/patterns/Wizard.json"
131
+ }
132
+ }
133
+ }
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > IonBase is a React design system for enterprise SaaS, built to be consumed by an agent rather than read by a developer. Every component ships a machine-readable contract carrying the judgement a type signature cannot: when to use it, what to use instead, the anti-patterns, and what it guarantees for accessibility versus what it requires of you.
4
4
 
5
- Version 0.20.0. 35 components.
5
+ Version 0.21.0. 35 components.
6
6
 
7
7
  ## Read these, in this order
8
8
 
@@ -15,6 +15,12 @@ Both are importable: `ionbase-ui/meta/index` and `ionbase-ui/meta/<Name>.json`.
15
15
 
16
16
  `Alert`, `Avatar`, `AvatarGroup`, `Badge`, `Button`, `Checkbox`, `Divider`, `FullCard`, `Header`, `Icon`, `Input`, `Link`, `Logo`, `LogoMark`, `Menu`, `MenuItem`, `Modal`, `NavItem`, `PhoneInput`, `Popover`, `Radio`, `RadioGroup`, `ScrollProgress`, `Select`, `TabItem`, `Table`, `TableBody`, `TableCell`, `TableHead`, `TableRow`, `Tabs`, `Toast`, `ToastProvider`, `Toggle`, `Tooltip`
17
17
 
18
+ ## Patterns
19
+
20
+ Compositions of the components above, in `dist/meta/patterns/`. Read `dist/meta/patterns/index.json`, then one recipe. They are the only place the empty, loading and error states are specified — no component owns those, which is why they are the part that gets left out.
21
+
22
+ `DataTable`, `DestructiveConfirm`, `Form`, `PageShell`, `SettingsPanel`, `Wizard`
23
+
18
24
  ## Rules that ship with this package
19
25
 
20
26
  - `ionbase-ui/eslint-plugin` — five rules driven from the contracts: deprecated props, missing accessible names, raw style values, measured contrast failures, competing primary actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionbase-ui",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "IonBase Design System — accessible React components, styles and design tokens in one package",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,8 @@
14
14
  "./meta": "./dist/meta/components.json",
15
15
  "./meta/index": "./dist/meta/index.json",
16
16
  "./meta/*": "./dist/meta/*",
17
+ "./patterns": "./dist/meta/patterns/index.json",
18
+ "./patterns/*": "./dist/meta/patterns/*",
17
19
  "./styles": "./dist/styles/index.css",
18
20
  "./styles/*": "./dist/styles/*",
19
21
  "./tokens": "./dist/styles/tokens/index.css",
@@ -59,7 +61,7 @@
59
61
  },
60
62
  "scripts": {
61
63
  "dev": "tsc --watch",
62
- "build": "node scripts/sync-tokens.mjs && tsc --build --force && node scripts/copy-css.mjs && node scripts/verify-client-boundaries.mjs && node scripts/verify-contrast.mjs && node scripts/build-meta.mjs && node scripts/verify-meta.mjs && node scripts/build-llms.mjs",
64
+ "build": "node scripts/sync-tokens.mjs && tsc --build --force && node scripts/copy-css.mjs && node scripts/verify-client-boundaries.mjs && node scripts/verify-contrast.mjs && node scripts/build-meta.mjs && node scripts/verify-meta.mjs && node scripts/build-patterns.mjs && node scripts/build-llms.mjs",
63
65
  "typecheck": "tsc --noEmit",
64
66
  "lint": "eslint . && stylelint \"src/styles/*.css\"",
65
67
  "format": "prettier --check .",
@@ -68,7 +70,8 @@
68
70
  "meta:verify": "node scripts/build-meta.mjs && node scripts/verify-meta.mjs",
69
71
  "contrast": "node scripts/verify-contrast.mjs",
70
72
  "contrast:list": "node scripts/verify-contrast.mjs --list",
71
- "llms": "node scripts/build-llms.mjs"
73
+ "llms": "node scripts/build-llms.mjs",
74
+ "patterns": "node scripts/build-patterns.mjs"
72
75
  },
73
76
  "peerDependencies": {
74
77
  "react": "^18.0.0 || ^19.0.0",