@pastweb/components-core 2.0.0 → 2.2.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.
@@ -0,0 +1,251 @@
1
+ # Accordion
2
+
3
+ Accordion presents related sections whose content can be expanded and collapsed.
4
+ Its rendering contract follows the WAI-ARIA Accordion pattern and targets WCAG
5
+ 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use an accordion when research shows that users benefit from seeing an overview
10
+ of several related sections and selectively revealing their content. Do not use
11
+ it for content every user must read, for a sequence of form questions, or inside
12
+ another accordion. Prefer ordinary headings, links to page sections, separate
13
+ pages, or native `details` elements when those are simpler.
14
+
15
+ ## Accordion props
16
+
17
+ | PropName | Type | Required | Default | Description |
18
+ |---|---|---:|---:|---|
19
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
20
+ | `ariaLabel` | `string` | no | `"Accordion"` | Labels the accordion group. Provide a task-specific label when possible; pass an empty string to omit the group semantics. |
21
+ | `className` | `string` | no | `""` | Adds classes to the accordion wrapper. |
22
+ | `disabled` | `boolean` | no | - | Disables every panel trigger. Prefer removing unavailable sections or explaining their unavailable content. |
23
+ | `id` | `string` | no | - | Supplies the root ID. |
24
+ | `rtl` | `boolean` | no | - | Enables right-to-left layout. |
25
+ | `transitionDuration` | `number` | no | `500` | Default panel transition duration in milliseconds. |
26
+
27
+ ## Accordion mediator state
28
+
29
+ | Property | Type | Description |
30
+ |---|---|---|
31
+ | `ariaLabel` | `string \| undefined` | Normalized accessible name. An empty string is stored as `undefined` so the group role and label are omitted. |
32
+ | `isDisabled` | `true \| undefined` | Normalized disabled state. Renderers must use this value for the root `is-disabled` class, `aria-disabled`, and must never convert `false`. |
33
+ | `role` | `'group' \| undefined` | Group role present only when `ariaLabel` is a non-empty string. |
34
+
35
+ ## Accordion mediator context
36
+
37
+ `accordionMediator.getContext()` exposes live getters consumed by each
38
+ `AccordionPanel`. Values that also exist on mediator state are read from that
39
+ normalized state rather than recomputed from raw props.
40
+
41
+ | Property | Type | Description |
42
+ |---|---|---|
43
+ | `className` | `string` | Root classes inherited by every panel for coordinated styling. |
44
+ | `disabled` | `true \| undefined` | Normalized accordion disabled state. |
45
+ | `rtl` | `boolean` | Root text-direction preference combined with each panel's local `rtl` prop. |
46
+ | `transitionDuration` | `number` | Default panel transition duration in milliseconds. |
47
+
48
+ ## Accordion slots
49
+
50
+ | SlotName | Description | Default content |
51
+ |---|---|---|
52
+ | default | Supplies the ordered `AccordionPanel` children rendered inside the accordion wrapper. | Empty. |
53
+
54
+ ## CSS Variables
55
+
56
+ Accordion variables follow the
57
+ `--pw_<ComponentName>_<part>_<state>_<css-property>` token grammar. The
58
+ component name is PascalCase; part and state names are lowercase kebab case.
59
+
60
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
61
+ |---|---|---|---|---|---|---|
62
+ | `--pw_Accordion_root_default_background-color` | `root` | `default` | `background-color` | `var(--pw_background-color)` | Panel background colour. | Web, Capacitor, NativeScript |
63
+ | `--pw_Accordion_root_default_color` | `root` | `default` | `color` | `var(--pw_text_color)` | Default accordion text colour. | Web, Capacitor, NativeScript |
64
+ | `--pw_Accordion_root_default_font-size` | `root` | `default` | `font-size` | `var(--pw_font-size)` | Base accordion font size. | Web, Capacitor, NativeScript |
65
+ | `--pw_Accordion_root_default_line-height` | `root` | `default` | `line-height` | `var(--pw_line-height)` | Base accordion line height. | Web, Capacitor, NativeScript |
66
+ | `--pw_Accordion_root_default_border-color` | `root` | `default` | `border-color` | `var(--pw_border-color)` | Border colour used by the wrapper and panel separators. | Web, Capacitor, NativeScript |
67
+ | `--pw_Accordion_root_default_border-width` | `root` | `default` | `border-width` | `var(--pw_border-width)` | Border width used by the wrapper and panel separators. | Web, Capacitor, NativeScript |
68
+ | `--pw_Accordion_root_default_border-style` | `root` | `default` | `border-style` | `var(--pw_border-style)` | Border style used by the wrapper and panel separators. | Web, Capacitor, NativeScript |
69
+ | `--pw_Accordion_root_default_border-radius` | `root` | `default` | `border-radius` | `var(--pw_border-radius)` | Outer accordion corner radius. | Web, Capacitor, NativeScript |
70
+ | `--pw_Accordion_root_default_border` | `root` | `default` | `border` | width, style and colour tokens | Composed border shorthand. | Web, Capacitor, NativeScript |
71
+ | `--pw_Accordion_root_default_transition-duration` | `root` | `default` | `transition-duration` | `0.5s` | Duration of panel open and close transitions. | Web, Capacitor; NativeScript adapter |
72
+ | `--pw_Accordion_root_disabled_opacity` | `root` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Disabled accordion opacity. Descendant panels reset their own opacity to avoid compounding it. | Web, Capacitor, NativeScript |
73
+ | `--pw_Accordion_header_default_padding` | `header` | `default` | `padding` | `var(--pw_padding)` | Space inside the panel trigger. | Web, Capacitor, NativeScript |
74
+ | `--pw_Accordion_header_default_background-color` | `header` | `default` | `background-color` | `var(--pw_control_background-color)` | Default trigger background colour. | Web, Capacitor, NativeScript |
75
+ | `--pw_Accordion_header_default_color` | `header` | `default` | `color` | `var(--pw_text_strong_color)` | Trigger text and icon colour. | Web, Capacitor, NativeScript |
76
+ | `--pw_Accordion_header_default_font-size` | `header` | `default` | `font-size` | `1em` | Trigger font size. | Web, Capacitor, NativeScript |
77
+ | `--pw_Accordion_header_default_font-weight` | `header` | `default` | `font-weight` | `var(--bold_font-weight)` | Trigger font weight. | Web, Capacitor, NativeScript |
78
+ | `--pw_Accordion_header_default_line-height` | `header` | `default` | `line-height` | `var(--pw_line-height)` | Trigger line height. | Web, Capacitor, NativeScript |
79
+ | `--pw_Accordion_header_disabled_opacity` | `header` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Opacity of an individually disabled panel. | Web, Capacitor, NativeScript |
80
+ | `--pw_Accordion_close-button_default_background-color` | `close-button` | `default` | `background-color` | header background token | Default expand/collapse icon background. | Web, Capacitor, NativeScript |
81
+ | `--pw_Accordion_close-button_hover_background-color` | `close-button` | `hover` | `background-color` | header colour token | Expand/collapse icon background while the web trigger is hovered. | Web, Capacitor |
82
+ | `--pw_Accordion_close-bar_default_background-color` | `close-bar` | `default` | `background-color` | header colour token | Default expand/collapse icon bar colour. | Web, Capacitor, NativeScript |
83
+ | `--pw_Accordion_close-bar_hover_background-color` | `close-bar` | `hover` | `background-color` | header background token | Expand/collapse icon bar colour while the web trigger is hovered. | Web, Capacitor |
84
+ | `--pw_Accordion_content_default_padding` | `content` | `default` | `padding` | header padding token | Space inside expanded panel content. | Web, Capacitor, NativeScript |
85
+
86
+ ```xts
87
+ // Accordion
88
+ import { setContext, useBeforeUnmount, useMediator, withDefaultProps } from '@pastweb/x';
89
+ import { accordionMediator } from '@pastweb/components-core/Accordion';
90
+ import { DEFAULT_PROPS, ACCORDION_CONTEXT_KEY } from './constants';
91
+
92
+ export function Accordion(props: AccordionProps) {
93
+ const p = withDefaultProps<AccordionDefaultProps>(props, DEFAULT_PROPS);
94
+ const m = useMediator<AccordionMediator>(accordionMediator, p);
95
+ const s = m.state;
96
+ const c = m.getContext();
97
+
98
+ setContext(ACCORDION_CONTEXT_KEY, c);
99
+ useBeforeUnmount(m.useBeforeUnmount);
100
+
101
+ return (
102
+ <div
103
+ id={m.id}
104
+ class={p.cls('Accordion', p.className, { 'is-disabled': s.isDisabled })}
105
+ role={s.role}
106
+ aria-label={s.ariaLabel}
107
+ aria-disabled={s.isDisabled}
108
+ >
109
+ <slot />
110
+ </div>
111
+ );
112
+ }
113
+ ```
114
+
115
+ ## AccordionPanel props
116
+
117
+ | PropName | Type | Required | Default | Description |
118
+ |---|---|---:|---:|---|
119
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
120
+ | `className` | `string` | no | `""` | Adds classes to the panel. |
121
+ | `disabled` | `boolean` | no | - | Disables the native trigger button. |
122
+ | `headingLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | no | `2` | Web renderer property. Selects the heading level required by the surrounding document. |
123
+ | `id` | `string` | no | generated | Supplies the stable base ID used for trigger and panel relationships. |
124
+ | `isOpen` | `boolean` | no | - | Sets the panel's initial open state. |
125
+ | `landmark` | `boolean` | no | - | Gives the content `role="region"`. Enable selectively to avoid landmark proliferation. |
126
+ | `onOpenChange` | `(isOpen: boolean) => void` | no | - | Reports a user-requested state change. |
127
+ | `title` | `string \| number` | no | `""` | Provides the default accessible trigger content. |
128
+ | `rtl` | `boolean` | no | - | Enables right-to-left layout for this panel. |
129
+ | `transitionDuration` | `number` | no | `500` | Sets this panel's transition duration in milliseconds. |
130
+
131
+ ## AccordionPanel mediator state
132
+
133
+ | Property | Type | Description |
134
+ |---|---|---|
135
+ | `className` | `string` | Complete panel class name, including inherited Accordion classes and `is-disabled` / `is-open`. |
136
+ | `hideBorderBottom` | `boolean` | Indicates that the closed panel trigger should hide its bottom border after the close transition. |
137
+ | `isDisabled` | `true \| undefined` | Normalized disabled state from the panel or parent Accordion. Renderers must use this value for `disabled`, `aria-disabled`, and must never convert `false`. |
138
+ | `isOpen` | `boolean` | Current expanded state. |
139
+ | `isRtl` | `true \| undefined` | Normalized right-to-left layout from the panel or parent Accordion. |
140
+ | `role` | `'region' \| undefined` | Optional landmark role when `landmark` is enabled. |
141
+ | `transitionDuration` | `string` | CSS duration used by the open and close transition. |
142
+
143
+ ## AccordionPanel slots
144
+
145
+ | SlotName | Description | Default content |
146
+ |---|---|---|
147
+ | `header` | Replaces content inside the native trigger button. It does not replace the heading or button. | The title and decorative expand/collapse icon. |
148
+ | default | Supplies panel content. | Empty. |
149
+
150
+ Custom header content must provide a meaningful accessible name. Decorative
151
+ icons must use `aria-hidden="true"`.
152
+
153
+ ```xts
154
+ // AccordionPanel
155
+ import {
156
+ useBeforeUnmount,
157
+ useMediator,
158
+ useMounted,
159
+ withDefaultProps,
160
+ } from '@pastweb/x';
161
+ import { accordionPanelMediator } from '@pastweb/components-core/Accordion';
162
+ import { DEFAULT_ACCORDION_PANEL_PROPS } from './constants';
163
+
164
+ export function AccordionPanel(props: AccordionPanelProps) {
165
+ const p = withDefaultProps<AccordionPanelDefaultProps>(props, DEFAULT_ACCORDION_PANEL_PROPS);
166
+ const m = useMediator<AccordionPanelMediator>(accordionPanelMediator, p);
167
+ const s = m.state;
168
+ const Heading = `h${p.headingLevel}`;
169
+
170
+ useMounted(trigger => {
171
+ m.useMounted({ focus: () => trigger.focus() });
172
+ });
173
+ useBeforeUnmount(m.useBeforeUnmount);
174
+
175
+ return (
176
+ <section
177
+ id={m.id}
178
+ class={s.className}
179
+ dir={s.isRtl ? 'rtl' : undefined}
180
+ aria-disabled={s.isDisabled}
181
+ style={{
182
+ transitionDuration: s.transitionDuration,
183
+ '--pw_Accordion_root_default_transition-duration': s.transitionDuration,
184
+ }}
185
+ >
186
+ <Heading class={p.cls('heading')}>
187
+ <button
188
+ id={m.triggerId}
189
+ type="button"
190
+ class={p.cls('header', { rtl: s.isRtl, 'hide-border': s.hideBorderBottom })}
191
+ aria-expanded={s.isOpen}
192
+ aria-controls={m.panelId}
193
+ disabled={s.isDisabled}
194
+ @click={m.onToggle}
195
+ >
196
+ <slot name="header">
197
+ <span class={p.cls('title')}>
198
+ <render content={p.title} />
199
+ </span>
200
+ <span class={p.cls('closeButton', s.isOpen ? 'is-open' : '')} aria-hidden="true">
201
+ <span class={p.cls('closeBar')} />
202
+ <span class={p.cls('closeBar')} />
203
+ </span>
204
+ </slot>
205
+ </button>
206
+ </Heading>
207
+
208
+ <div
209
+ id={m.panelId}
210
+ class={p.cls('container', s.isOpen ? 'is-open' : '')}
211
+ role={s.role}
212
+ aria-labelledby={m.triggerId}
213
+ aria-hidden={!s.isOpen}
214
+ inert={!s.isOpen}
215
+ @transitionend={m.onTransitionEnd}
216
+ >
217
+ <div class={p.cls('contentWrapper')}>
218
+ <div class={p.cls('content')}>
219
+ <slot />
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </section>
224
+ );
225
+ }
226
+ ```
227
+
228
+ ## Keyboard interaction
229
+
230
+ - `Tab` and `Shift+Tab` move through panel triggers and focusable open content in normal document order.
231
+ - `Enter` or `Space` on a trigger toggles its panel through native button behavior.
232
+ - Collapsed panel content is removed from sequential focus navigation.
233
+
234
+ ## Accessibility notes
235
+
236
+ - The trigger is a native button inside a native heading.
237
+ - `aria-expanded` communicates state, while `aria-controls` and
238
+ `aria-labelledby` establish the trigger/content relationship.
239
+ - Closing content becomes inert and hidden from the accessibility tree
240
+ immediately. Web renderers keep it mounted so the grid-row transition can
241
+ animate the real content height in both directions.
242
+ - `role="region"` is opt-in because repeated regions can create excessive
243
+ landmarks.
244
+ - Shared styles retain visible focus, support `forced-colors`, and disable
245
+ non-essential transitions under `prefers-reduced-motion`.
246
+
247
+ ## Standards
248
+
249
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
250
+ - WAI-ARIA Accordion pattern: https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
251
+ - GOV.UK Accordion guidance: https://design-system.service.gov.uk/components/accordion/
@@ -0,0 +1,143 @@
1
+ # Button
2
+
3
+ Button renders a native action control with independent disabled and loading
4
+ states. Its rendering contract follows the WAI-ARIA Button pattern and targets
5
+ WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use a button to perform an action on the current page. Use a link when activation
10
+ navigates to another location. Button labels should start with a clear verb and
11
+ describe the result of activation.
12
+
13
+ ## Props
14
+
15
+ | PropName | Type | Required | Default | Description |
16
+ |---|---|---:|---:|---|
17
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
18
+ | `className` | `string` | no | `""` | Adds classes to the native button. The legacy `is-loading` class continues to enable loading behavior. |
19
+ | `disabled` | `boolean` | no | - | Makes the action unavailable. |
20
+ | `id` | `string` | no | generated | Supplies the button ID. Generated IDs use the `button` prefix. |
21
+ | `loading` | `boolean` | no | - | Marks the action busy, suppresses its click handler, and displays the loading slot without disabling the native button. |
22
+ | `onClick` | `(event: unknown) => void` | no | noop | Handles an available button activation. Renderers may narrow the event type. |
23
+ | `tabIndex` | `number` | no | `0` | Controls sequential keyboard focus order. Prefer the default document order. |
24
+ | `type` | `'button' \| 'submit' \| 'reset'` | no | `'button'` | Selects native button behavior. |
25
+
26
+ ## Mediator state
27
+
28
+ | Property | Type | Description |
29
+ |---|---|---|
30
+ | `isDisabled` | `true \| undefined` | Is `true` when the button or its Buttons context is disabled; otherwise omitted. Renderers use this value directly for `.is-disabled`, `disabled`, and `aria-disabled`. |
31
+ | `isLoading` | `boolean` | Indicates loading independently from `isDisabled`. Loading suppresses the click handler without disabling the native button. |
32
+
33
+ ## Mediator context
34
+
35
+ Button does not publish its own context. When rendered inside `Buttons`, its
36
+ mediator optionally consumes the parent `ButtonsContext` value below and
37
+ combines it with the local `disabled` prop.
38
+
39
+ | Context property | Type | Description |
40
+ |---|---|---|
41
+ | `disabled` | `true \| undefined` | Normalized disabled state inherited from the containing Buttons component. |
42
+
43
+ ## Slots
44
+
45
+ | SlotName | Description | Default content |
46
+ |---|---|---|
47
+ | default | Supplies the button's accessible label and optional icon content. | Empty. |
48
+ | `loading` | Supplies status content while `loading` is active. | `Loading...` |
49
+
50
+ ## Utility classes
51
+
52
+ Apply these classes through `className`.
53
+
54
+ | Class | Description |
55
+ |---|---|
56
+ | `is-rounded` | Uses the rounded button radius token. |
57
+ | `is-full-width` | Expands the button to the available width. |
58
+
59
+ ## CSS Variables
60
+
61
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
62
+ |---|---|---|---|---|---|---|
63
+ | `--pw_Button_root_default_background-color` | `root` | `default` | `background-color` | `var(--pw_control_background-color)` | Button background. | Web, Capacitor, NativeScript |
64
+ | `--pw_Button_root_default_color` | `root` | `default` | `color` | `var(--pw_text_strong_color)` | Button text colour. | Web, Capacitor, NativeScript |
65
+ | `--pw_Button_root_default_padding` | `root` | `default` | `padding` | `var(--pw_padding)` | Inner spacing. | Web, Capacitor, NativeScript |
66
+ | `--pw_Button_root_default_margin` | `root` | `default` | `margin` | `var(--pw_margin)` | Outer spacing. | Web, Capacitor, NativeScript |
67
+ | `--pw_Button_root_default_border-color` | `root` | `default` | `border-color` | `var(--pw_border-color)` | Default border colour. | Web, Capacitor, NativeScript |
68
+ | `--pw_Button_root_default_border-width` | `root` | `default` | `border-width` | `var(--pw_border-width)` | Border width. | Web, Capacitor, NativeScript |
69
+ | `--pw_Button_root_default_border-style` | `root` | `default` | `border-style` | `var(--pw_border-style)` | Border style. | Web, Capacitor, NativeScript |
70
+ | `--pw_Button_root_default_border-radius` | `root` | `default` | `border-radius` | `var(--pw_border-radius)` | Default corner radius. | Web, Capacitor, NativeScript |
71
+ | `--pw_Button_root_rounded_border-radius` | `root` | `rounded` | `border-radius` | `var(--pw_rounded_border-radius)` | Corner radius for `is-rounded`. | Web, Capacitor, NativeScript |
72
+ | `--pw_Button_root_default_transition-duration` | `root` | `default` | `transition-duration` | `var(--pw_transition-duration)` | Visual state transition duration. | Web, Capacitor; NativeScript adapter |
73
+ | `--pw_Button_color_default_box-shadow` | `color` | `default` | `box-shadow` | `var(--pw_color_box-shadow)` | Focus shadow colour. | Web, Capacitor |
74
+ | `--pw_Button_inset-color_default_box-shadow` | `inset-color` | `default` | `box-shadow` | `var(--pw_inset_color_box-shadow)` | Hover inset shadow colour. | Web, Capacitor |
75
+ | `--pw_Button_size_default_box-shadow` | `size` | `default` | `box-shadow` | `var(--pw_size_box-shadow)` | Shadow size. | Web, Capacitor |
76
+ | `--pw_Button_root_disabled_opacity` | `root` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Disabled opacity. | Web, Capacitor, NativeScript |
77
+ | `--pw_Button_root_default_font-size` | `root` | `default` | `font-size` | `var(--pw_font-size)` | Label font size. | Web, Capacitor, NativeScript |
78
+ | `--pw_Button_root_default_font-weight` | `root` | `default` | `font-weight` | `var(--pw_semibold_font-weight)` | Label font weight. | Web, Capacitor, NativeScript |
79
+ | `--pw_Button_root_default_line-height` | `root` | `default` | `line-height` | `var(--pw_line-height)` | Label line height. | Web, Capacitor, NativeScript |
80
+ | `--pw_Button_item_default_gap` | `item` | `default` | `gap` | `var(--pw_horizontal_padding)` | Spacing between rendered items. | Web, Capacitor |
81
+ | `--pw_Button_icon_default_color` | `icon` | `default` | `color` | button colour | Icon colour. | Web, Capacitor, NativeScript |
82
+ | `--pw_Button_icon_default_stroke` | `icon` | `default` | `stroke` | icon colour | Icon stroke. | Web, Capacitor |
83
+ | `--pw_Button_icon_default_size` | `icon` | `default` | `size` | `var(--pw_icon_size)` | Font-icon size. | Web, Capacitor, NativeScript |
84
+
85
+ ```xts
86
+ import { useBeforeUnmount, useMediator, withDefaultProps } from '@pastweb/x';
87
+ import { buttonMediator } from '@pastweb/components-core/Button';
88
+ import { DEFAULT_PROPS } from './constants';
89
+
90
+ export function Button(props: ButtonProps) {
91
+ const p = withDefaultProps<ButtonDefaultProps>(props, DEFAULT_PROPS);
92
+ const m = useMediator<ButtonMediator>(buttonMediator, p);
93
+ const s = m.state;
94
+
95
+ useBeforeUnmount(m.useBeforeUnmount);
96
+
97
+ return (
98
+ <button
99
+ id={m.id}
100
+ type={p.type}
101
+ class={p.cls('Button', p.className, { 'is-disabled': s.isDisabled, 'is-loading': s.isLoading })}
102
+ @click={m.onClick}
103
+ disabled={s.isDisabled}
104
+ aria-disabled={s.isDisabled}
105
+ aria-busy={s.isLoading || undefined}
106
+ tabIndex={p.tabIndex}
107
+ >
108
+ <slot
109
+ map={(child, index) => (
110
+ <span key={index} class={p.cls('item')}>{child}</span>
111
+ )}
112
+ />
113
+ <span #if={s.isLoading} class={p.cls('loading')} role="status" aria-live="polite">
114
+ <slot name="loading">Loading...</slot>
115
+ </span>
116
+ </button>
117
+ );
118
+ }
119
+ ```
120
+
121
+ ## Accessibility notes
122
+
123
+ - The renderer must use a native button and preserve its keyboard activation.
124
+ - Disabled buttons use the native `disabled` property, `aria-disabled="true"`,
125
+ and the `is-disabled` class.
126
+ - Loading buttons remain independently enabled, expose `aria-busy="true"` and
127
+ the `is-loading` class, and suppress the component click handler. Status
128
+ content is announced politely.
129
+ - A button may be both disabled and loading when both states are set.
130
+ - The accessible name must remain meaningful without relying on a decorative icon.
131
+ - Shared web styles provide at least a 44 by 44 CSS-pixel target, visible focus,
132
+ forced-colours treatment, and reduced-motion behavior.
133
+
134
+ ## Keyboard interaction
135
+
136
+ - `Tab` and `Shift+Tab` move focus to and from the native button in document order.
137
+ - `Enter` or `Space` activates the button through native button behavior.
138
+
139
+ ## Standards
140
+
141
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
142
+ - WAI-ARIA Button pattern: https://www.w3.org/WAI/ARIA/apg/patterns/button/
143
+ - GOV.UK Button guidance: https://design-system.service.gov.uk/components/button/
@@ -0,0 +1,108 @@
1
+ # Buttons
2
+
3
+ Buttons groups related action controls and gives the collection a shared
4
+ accessible name. Its rendering contract follows the WAI-ARIA Button pattern
5
+ for the grouped native buttons and targets WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use Buttons when two or more actions belong to the same decision or workflow.
10
+ Keep the primary action first in the expected reading direction and avoid large
11
+ groups that make choices difficult to scan.
12
+
13
+ ## Props
14
+
15
+ | PropName | Type | Required | Default | Description |
16
+ |---|---|---:|---:|---|
17
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
18
+ | `ariaLabel` | `string` | no | `"Button group"` | Supplies the group's accessible name. Applications should provide a task-specific label; pass an empty string to omit the group semantics. |
19
+ | `className` | `string` | no | `""` | Adds classes to the group wrapper. |
20
+ | `disabled` | `boolean` | no | - | Disables descendant Pastweb Button components and marks the group unavailable. |
21
+ | `id` | `string` | no | generated | Supplies the group ID. Generated IDs use the `buttons` prefix. |
22
+
23
+ ## Mediator state
24
+
25
+ | Property | Type | Description |
26
+ |---|---|---|
27
+ | `ariaLabel` | `string \| undefined` | Normalized accessible label for the group. Empty labels are omitted. |
28
+ | `isDisabled` | `true \| undefined` | Is `true` when the group is disabled; otherwise omitted. Renderers use this value directly for `.is-disabled` and `aria-disabled`, and the Buttons context shares it with descendant Button mediators. |
29
+ | `role` | `'group' \| undefined` | Is `group` when `ariaLabel` supplies an accessible group name; otherwise omitted. |
30
+
31
+ ## Mediator context
32
+
33
+ `buttonsMediator.getContext()` exposes a live value consumed by descendant
34
+ Button mediators.
35
+
36
+ | Property | Type | Description |
37
+ |---|---|---|
38
+ | `disabled` | `true \| undefined` | Normalized group disabled state combined with each Button's local `disabled` prop. |
39
+
40
+ ## Slots
41
+
42
+ | SlotName | Description | Default content |
43
+ |---|---|---|
44
+ | default | Supplies the related Button controls in their intended reading and action order. | Empty. |
45
+
46
+ ## Utility classes
47
+
48
+ Apply these classes through `className`.
49
+
50
+ | Class | Description |
51
+ |---|---|
52
+ | `is-rounded` | Rounds the group's exposed first and last button corners. |
53
+ | `is-full-width` | Expands the group to the available width and distributes its buttons evenly. |
54
+
55
+ ## CSS Variables
56
+
57
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
58
+ |---|---|---|---|---|---|---|
59
+ | `--pw_Buttons_root_default_margin` | `root` | `default` | `margin` | `var(--pw_margin)` | Group outer spacing. | Web, Capacitor, NativeScript |
60
+ | `--pw_Buttons_root_default_border-radius` | `root` | `default` | `border-radius` | `var(--pw_border-radius)` | Outer button corner radius. | Web, Capacitor, NativeScript |
61
+ | `--pw_Buttons_root_rounded_border-radius` | `root` | `rounded` | `border-radius` | `var(--pw_rounded_border-radius)` | Outer radius for rounded groups. | Web, Capacitor, NativeScript |
62
+ | `--pw_Buttons_color_default_box-shadow` | `color` | `default` | `box-shadow` | `var(--pw_color_box-shadow)` | First-button focus shadow colour. | Web, Capacitor |
63
+ | `--pw_Buttons_inset-color_default_box-shadow` | `inset-color` | `default` | `box-shadow` | `var(--pw_inset_color_box-shadow)` | First-button hover inset shadow colour. | Web, Capacitor |
64
+ | `--pw_Buttons_size_default_box-shadow` | `size` | `default` | `box-shadow` | `var(--pw_size_box-shadow)` | First-button shadow size. | Web, Capacitor |
65
+
66
+ ```xts
67
+ import { setContext, useBeforeUnmount, useMediator, withDefaultProps } from '@pastweb/x';
68
+ import { BUTTONS_CONTEXT_KEY, buttonsMediator } from '@pastweb/components-core/Buttons';
69
+ import { DEFAULT_PROPS } from './constants';
70
+
71
+ export function Buttons(props: ButtonsProps) {
72
+ const p = withDefaultProps<ButtonsDefaultProps>(props, DEFAULT_PROPS);
73
+ const m = useMediator<ButtonsMediator>(buttonsMediator, p);
74
+ const s = m.state;
75
+ const c = m.getContext();
76
+ setContext(BUTTONS_CONTEXT_KEY, c);
77
+ useBeforeUnmount(m.useBeforeUnmount);
78
+
79
+ return (
80
+ <div
81
+ id={m.id}
82
+ class={p.cls('Buttons', p.className, { 'is-disabled': s.isDisabled })}
83
+ role={s.role}
84
+ aria-label={s.ariaLabel}
85
+ aria-disabled={s.isDisabled}
86
+ >
87
+ <slot />
88
+ </div>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ## Accessibility notes
94
+
95
+ - The wrapper uses `role="group"` only when `ariaLabel` provides a meaningful
96
+ accessible name. The default is `"Button group"`; both attributes are omitted
97
+ when an empty string is provided explicitly.
98
+ - Buttons retain their normal tab order and native keyboard behavior.
99
+ - Use a toolbar component with documented arrow-key behavior instead when the
100
+ collection represents a persistent application toolbar.
101
+ - Keep one primary call to action in a group. Additional default buttons make
102
+ the next step harder to identify.
103
+
104
+ ## Standards
105
+
106
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
107
+ - WAI-ARIA Button pattern: https://www.w3.org/WAI/ARIA/apg/patterns/button/
108
+ - GOV.UK Button guidance: https://design-system.service.gov.uk/components/button/