@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,352 @@
1
+ # Tabs
2
+
3
+ Tabs switches between related sections of content in the same view. Its
4
+ rendering contract follows the WAI-ARIA Tabs pattern with automatic activation
5
+ and targets WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use Tabs when users need to switch among a small set of related panels without
10
+ leaving the page. Do not use Tabs for site navigation, for a sequence of form
11
+ questions, or when users must compare two sections at once. Prefer separate
12
+ pages, headings with in-page links, or an accordion when those are simpler.
13
+ Keep labels short. Put a heading that repeats the tab label at the start of
14
+ each panel.
15
+
16
+ ## Tabs props
17
+
18
+ | PropName | Type | Required | Default | Description |
19
+ |---|---|---:|---:|---|
20
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component, part, state, and utility classes. |
21
+ | `activeTab` | `number` | no | `0` | Controls the active zero-based index. |
22
+ | `ariaLabel` | `string` | no | `"Tabs"` | Names the tab list. An explicit empty string omits the label. |
23
+ | `className` | `string` | no | `""` | Adds root classes, including utilities that determine layout and inherited descendant styling. |
24
+ | `disabled` | `boolean` | no | - | Disables all Tab controls and applies `is-disabled` to the root. |
25
+ | `onChange` | `(index: number) => void` | no | noop | Reports requested selection changes. |
26
+
27
+ ## Tab props
28
+
29
+ | PropName | Type | Required | Default | Description |
30
+ |---|---|---:|---:|---|
31
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves the Tab part, state, and inherited utility classes. |
32
+ | `ariaLabel` | `string` | no | - | Supplies an accessible name when the visible label is insufficient. |
33
+ | `className` | `string` | no | `""` | Adds classes to this Tab list item. |
34
+ | `disabled` | `boolean` | no | - | Makes this Tab unavailable for activation and keyboard selection. |
35
+ | `onClick` | `() => void` | no | noop | Reports successful activation after the Tabs mediator updates selection. |
36
+
37
+ ## Mediator state
38
+
39
+ ### Tabs state
40
+
41
+ | Property | Type | Description |
42
+ |---|---|---|
43
+ | `activeTab` | `number` | Current normalized zero-based active index. |
44
+ | `ariaLabel` | `string \| undefined` | Normalized tab-list accessible label. |
45
+ | `className` | `string` | Utility classes inherited by Tab mediators and applied to generated panels. |
46
+ | `isDisabled` | `true \| undefined` | Normalized root disabled state. |
47
+ | `orientation` | `'horizontal' \| 'vertical'` | Orientation derived from positioning utility classes. |
48
+
49
+ ### Tab state
50
+
51
+ | Property | Type | Description |
52
+ |---|---|---|
53
+ | `ariaLabel` | `string \| undefined` | Optional normalized accessible name. |
54
+ | `className` | `string` | Complete Tab class name, including active, disabled, local, and inherited utility classes. |
55
+ | `controlsId` | `string` | ID of the corresponding tab panel. |
56
+ | `id` | `string` | Stable ID referenced by the corresponding panel. |
57
+ | `index` | `number` | Stable zero-based registration index. |
58
+ | `isActive` | `boolean` | Whether this Tab is selected. |
59
+ | `isDisabled` | `true \| undefined` | Normalized local or inherited disabled state. |
60
+ | `tabIndex` | `0 \| -1` | Roving tab-order value. |
61
+
62
+ ## Tabs mediator context
63
+
64
+ `tabsMediator.getContext()` exposes live selection, the registered Tab props
65
+ array, relationship IDs, and navigation methods consumed by Tab mediators, the
66
+ Tabs renderer, and extensions through `useTabsContext`.
67
+
68
+ | Property or method | Type | Description |
69
+ |---|---|---|
70
+ | `activeTab` | `number` | Live active zero-based index. |
71
+ | `state` | `TabsState` | Live Tabs mediator state, including inherited class names and disabled state. |
72
+ | `tabs` | `TabDefaultProps[]` | Registered Tab props in visual order. |
73
+ | `consumeTabFocus` | `(index: number) => boolean` | Consumes a pending keyboard-focus request for a Tab index. |
74
+ | `getContentId` | `(index: number) => string` | Returns the stable panel relationship ID for an index. |
75
+ | `getTabId` | `(index: number) => string` | Returns the stable Tab relationship ID for an index. |
76
+ | `onTabKeyDown` | `(event: TabsKeyEvent, index: number) => void` | Applies orientation-aware roving keyboard navigation. |
77
+ | `registerTab` | `(props: TabDefaultProps) => number` | Stores Tab props in order and returns the Tab index. The props object identity is the registration key. |
78
+ | `setActiveTab` | `(index: number) => void` | Requests selection of an available Tab index. |
79
+ | `unregisterTab` | `(props: TabDefaultProps) => void` | Removes a Tab registration. Remaining indexes follow array order. |
80
+
81
+ ## Slots
82
+
83
+ | Component | SlotName | Description | Default content |
84
+ |---|---|---|---|
85
+ | Tabs | `tabs` | Supplies ordered Tab controls inside the mediator-owned `tablist`. | Empty. |
86
+ | Tabs | `content` | Supplies matching panel children in the same order as the tabs. Tabs wraps each child in a `role="tabpanel"` container with the `tabContent` part class. | Empty. |
87
+ | Tab | default | Supplies the visible Tab label. | Empty. |
88
+
89
+ ## Keyboard interaction
90
+
91
+ - `Tab` and `Shift+Tab` move sequential focus. The tab list contains one
92
+ tab stop: the selected Tab. The next stop is the active panel when it has
93
+ `tabindex="0"`, then focusable content inside that panel.
94
+ - Left and Right activate the previous or next enabled Tab in a horizontal
95
+ list. Up and Down do the same in a vertical list. Navigation wraps.
96
+ - `Home` and `End` activate the first and last enabled Tab.
97
+ - `Enter` and `Space` activate the focused Tab through native button behavior.
98
+ - Disabled Tabs are skipped.
99
+
100
+ ## Utility classes
101
+
102
+ Positioning classes are applied to the `Tabs` root through `className`; the
103
+ root stylesheet coordinates the generated list, tabs, and panels. Orientation
104
+ is derived from these classes for layout, `aria-orientation`, and arrow-key
105
+ navigation.
106
+
107
+ | Class | Result |
108
+ |---|---|
109
+ | `is-top`, `is-top-left` | List above the content, aligned to the start. |
110
+ | `is-top-right` | List above the content, aligned to the end. |
111
+ | `is-bottom`, `is-bottom-left` | List below the content, aligned to the start. |
112
+ | `is-bottom-right` | List below the content, aligned to the end. |
113
+ | `is-left`, `is-left-top` | List to the left of the content, aligned to the top. |
114
+ | `is-left-bottom` | List to the left of the content, aligned to the bottom. |
115
+ | `is-right`, `is-right-top` | List to the right of the content, aligned to the top. |
116
+ | `is-right-bottom` | List to the right of the content, aligned to the bottom. |
117
+ | `is-full-width` | Makes horizontal Tab labels share the full list width and vertical Tab labels share the full list height. |
118
+ | `has-vertical-labels` | On Tabs, gives every Tab a vertical writing mode when combined with an `is-left*` or `is-right*` utility. |
119
+ | `has-vertical-label` | On one Tab, gives only that label a vertical writing mode when combined with inherited `is-left*` or `is-right*` positioning. |
120
+ | `no-content-padding`, `no-padding` | Removes padding from every generated `tabContent` panel. |
121
+ | `no-content-border`, `no-border` | Removes borders from every generated `tabContent` panel. |
122
+
123
+ Left and right positioning utilities produce vertical Tabs using Up/Down arrow
124
+ keys. Top, bottom, and the default layout produce horizontal Tabs using
125
+ Left/Right arrow keys. Consumers do not set orientation separately. Tab buttons
126
+ remain horizontal unless `has-vertical-labels` is applied to Tabs or
127
+ `has-vertical-label` is applied to an individual Tab.
128
+
129
+ ## CSS Variables
130
+
131
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
132
+ |---|---|---|---|---|---|---|
133
+ | `--pw_Tabs_root_disabled_opacity` | root | disabled | `opacity` | `var(--pw_disabled_opacity)` | Opacity applied once when the complete Tabs component is disabled. | Browser, Capacitor |
134
+ | `--pw_Tabs_tab_default_background-color` | tab | default | `background-color` | `var(--pw_control_background-color)` | Background of an inactive tab. | Browser, Capacitor |
135
+ | `--pw_Tabs_tab_default_border-color` | tab | default | `border-color` | `var(--pw_border-color)` | Border colour of a tab. | Browser, Capacitor |
136
+ | `--pw_Tabs_tab_default_border-width` | tab | default | `border-width` | `var(--pw_border-width)` | Border width of a tab. | Browser, Capacitor |
137
+ | `--pw_Tabs_tab_default_border-style` | tab | default | `border-style` | `var(--pw_border-style)` | Border style of a tab. | Browser, Capacitor |
138
+ | `--pw_Tabs_tab_default_border-radius` | tab | default | `border-radius` | `var(--pw_border-radius)` | Corner radius of a tab. | Browser, Capacitor |
139
+ | `--pw_Tabs_tab_default_padding` | tab | default | `padding` | `var(--pw_padding)` | Inner spacing of a tab. | Browser, Capacitor |
140
+ | `--pw_Tabs_tab_default_color` | tab | default | `color` | `var(--pw_text_strong_color)` | Foreground colour of a tab. | Browser, Capacitor |
141
+ | `--pw_Tabs_tab_default_font-size` | tab | default | `font-size` | `var(--pw_font-size)` | Font size of a tab. | Browser, Capacitor |
142
+ | `--pw_Tabs_tab_default_transition-duration` | tab | default | `transition-duration` | `var(--pw_transition-duration)` | Duration of visual tab-state transitions. | Browser, Capacitor |
143
+ | `--pw_Tabs_tab_active_background-color` | tab | active | `background-color` | `var(--pw_background-color)` | Background of the active tab. | Browser, Capacitor |
144
+ | `--pw_Tabs_tab_hover-size_box-shadow` | tab | hover-size | `box-shadow` | `var(--pw_size_box-shadow)` | Size portion of the inactive-tab hover shadow. | Browser, Capacitor |
145
+ | `--pw_Tabs_tab_hover-color_box-shadow` | tab | hover-color | `box-shadow` | `var(--pw_color_box-shadow)` | Colour portion of the inactive-tab hover shadow. | Browser, Capacitor |
146
+ | `--pw_Tabs_content_default_border-color` | content | default | `border-color` | `var(--pw_border-color)` | Border colour of a tab panel. | Browser, Capacitor |
147
+ | `--pw_Tabs_content_default_border-radius` | content | default | `border-radius` | `var(--pw_border-radius)` | Radius applied to the exposed outer corners of a tab panel. | Browser, Capacitor |
148
+ | `--pw_Tabs_content_default_border-style` | content | default | `border-style` | `var(--pw_border-style)` | Border style of a tab panel. | Browser, Capacitor |
149
+ | `--pw_Tabs_content_default_border-width` | content | default | `border-width` | `var(--pw_border-width)` | Border width of a tab panel. | Browser, Capacitor |
150
+ | `--pw_Tabs_content_default_background-color` | content | default | `background-color` | `var(--pw_background-color)` | Background of a tab panel. | Browser, Capacitor |
151
+ | `--pw_Tabs_content_default_padding` | content | default | `padding` | `var(--pw_padding)` | Inner spacing of a tab panel. | Browser, Capacitor |
152
+
153
+ ```xts
154
+ import { setContext, useBeforeUnmount, useMediator, withDefaultProps } from '@pastweb/x';
155
+ import { tabsMediator, TABS_CONTEXT_KEY } from '@pastweb/components-core/Tabs';
156
+ import { DEFAULT_PROPS } from './constants';
157
+
158
+ export function Tabs(props) {
159
+ const p = withDefaultProps(props, DEFAULT_PROPS);
160
+ const m = useMediator(tabsMediator, p);
161
+ const s = m.state;
162
+ const c = m.getContext();
163
+
164
+ setContext(TABS_CONTEXT_KEY, c);
165
+ useBeforeUnmount(m.useBeforeUnmount);
166
+
167
+ return (
168
+ <div
169
+ class={p.cls(
170
+ 'Tabs',
171
+ s.isDisabled && 'is-disabled',
172
+ p.className,
173
+ )}
174
+ aria-disabled={s.isDisabled}
175
+ >
176
+ <ul
177
+ class={p.cls(
178
+ 'tabsList',
179
+ s.className,
180
+ )}
181
+ role="tablist"
182
+ aria-label={s.ariaLabel}
183
+ aria-orientation={s.orientation}
184
+ >
185
+ <slot name="tabs" props={{ cls: p.cls }} />
186
+ </ul>
187
+ <slot
188
+ name="content"
189
+ map={(content, index) => {
190
+ const isActive = s.activeTab === index;
191
+
192
+ return (
193
+ <div
194
+ id={c.getContentId(index)}
195
+ class={p.cls(
196
+ 'tabContent',
197
+ s.className,
198
+ isActive && 'is-active',
199
+ )}
200
+ role="tabpanel"
201
+ aria-labelledby={c.getTabId(index)}
202
+ tabindex={isActive ? 0 : undefined}
203
+ hidden={isActive ? undefined : true}
204
+ >
205
+ {content}
206
+ </div>
207
+ );
208
+ }}
209
+ />
210
+ </div>
211
+ );
212
+ }
213
+ ```
214
+
215
+ The standard composition supplies `Tab` controls through the named `tabs`
216
+ template and matching panel children through the named `content` template.
217
+ `Tabs` wraps each `content` child in a `role="tabpanel"` container:
218
+
219
+ ```tsx
220
+ <Tabs>
221
+ <Template name="tabs">
222
+ <Tab>Overview</Tab>
223
+ <Tab>Accessibility</Tab>
224
+ </Template>
225
+
226
+ <Template name="content">
227
+ <div>Overview content.</div>
228
+ <div>Accessibility content.</div>
229
+ </Template>
230
+ </Tabs>
231
+ ```
232
+
233
+ `Tabs` owns the complete tab-list markup and the panel wrappers. Supply one
234
+ `content` child per Tab, in the same order. Generated panels use the
235
+ `tabContent` part class.
236
+
237
+ ## useTabsContext
238
+
239
+ React renderers expose `useTabsContext()` for descendants of Tabs. It returns
240
+ the live Tabs context, including `activeTab`, `state`, `tabs` (registered Tab
241
+ props in visual order), and the navigation helpers.
242
+
243
+ Call it inside the Tabs tree. The wrapper that renders `<Tabs>` is outside the
244
+ provider and cannot read the context.
245
+
246
+ ```tsx
247
+ import { useTabsContext } from '@pastweb/react-components/Tabs';
248
+
249
+ function TabScrollArea({ index, children }) {
250
+ const { tabs } = useTabsContext();
251
+ const scrollArea = tabs[index]?.scrollArea;
252
+
253
+ if (!scrollArea) return children;
254
+
255
+ return <ScrollArea {...scrollArea}>{children}</ScrollArea>;
256
+ }
257
+ ```
258
+
259
+ `registerTab` stores each Tab's props object in `context.tabs`. Extra props on
260
+ an extended Tab, such as `scrollArea`, are available at the matching index
261
+ when wrapping `content` slot children.
262
+
263
+ `Tab` delegates registration, relationship IDs, active and disabled state,
264
+ inherited positioning classes, class-name composition, activation, keyboard
265
+ navigation, and mounted-reference focus coordination to `tabMediator`:
266
+
267
+ ```xts
268
+ import { useMediator, useMounted, useRef, withDefaultProps } from '@pastweb/x';
269
+ import { tabMediator } from '@pastweb/components-core/Tabs';
270
+ import { DEFAULT_TAB_PROPS } from './constants';
271
+
272
+ export function Tab(props) {
273
+ const p = withDefaultProps(props, DEFAULT_TAB_PROPS);
274
+ const m = useMediator(tabMediator, p);
275
+ const s = m.state;
276
+ const buttonReference = useRef(null);
277
+
278
+ useMounted(() => {
279
+ if (!buttonReference.value) return;
280
+
281
+ m.useMounted(buttonReference.value);
282
+ });
283
+
284
+ return (
285
+ <li
286
+ class={s.className}
287
+ role="presentation"
288
+ >
289
+ <button
290
+ ref={buttonReference}
291
+ id={s.id}
292
+ type="button"
293
+ class={p.cls('tabButton')}
294
+ role="tab"
295
+ aria-label={s.ariaLabel}
296
+ aria-selected={s.isActive}
297
+ aria-controls={s.controlsId}
298
+ aria-disabled={s.isDisabled}
299
+ disabled={s.isDisabled}
300
+ tabindex={s.tabIndex}
301
+ @click={m.onActivate}
302
+ @keydown={m.onKeyDown}
303
+ >
304
+ <span class={p.cls('tabContent')}>
305
+ <slot />
306
+ </span>
307
+ </button>
308
+ </li>
309
+ );
310
+ }
311
+ ```
312
+
313
+ The web template renders a `role="tablist"`, native buttons with `role="tab"`,
314
+ and matching `role="tabpanel"` containers linked by IDs. Native renderers map
315
+ the same state to an accessible selectable collection and content views, using
316
+ platform focus APIs rather than ARIA.
317
+
318
+ ## Accessibility notes
319
+
320
+ - The selected Tab has `aria-selected="true"` and `tabindex="0"`. Other Tabs
321
+ have `aria-selected="false"` and `tabindex="-1"`.
322
+ - Each Tab uses `aria-controls` to point at its panel. Each panel uses
323
+ `aria-labelledby` to point at its Tab. Panels stay mounted; inactive panels
324
+ use `hidden`.
325
+ - The tab list is named with `aria-label`. Pass a task-specific label when
326
+ possible; an empty string omits the name.
327
+ - Left and right positioning utilities set `aria-orientation="vertical"`.
328
+ - Disabled Tabs use native `disabled` and `aria-disabled`.
329
+ - Shared web styles keep a 24 by 24 CSS-pixel minimum target, visible
330
+ `:focus-visible` indication, forced-colours treatment, and reduced-motion
331
+ behavior.
332
+
333
+ ## Native renderer mapping
334
+
335
+ Native renderers map selection to a platform segmented control or equivalent
336
+ accessible collection. Do not copy ARIA attributes into native markup.
337
+
338
+ ## Known limitations
339
+
340
+ - GOV.UK treats Tabs as experimental and prefers a table of contents plus
341
+ stacked sections when JavaScript is unavailable. This package does not
342
+ provide that no-JS fallback.
343
+ - Do not put a second tab list inside a panel. Do not use Tabs as primary
344
+ navigation.
345
+ - Automated accessibility checks do not by themselves establish WCAG
346
+ conformance.
347
+
348
+ ## Standards
349
+
350
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
351
+ - WAI-ARIA Tabs pattern: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
352
+ - GOV.UK Tabs: https://design-system.service.gov.uk/components/tabs/
@@ -6,8 +6,9 @@ import { getOrientation } from "./getOrientation.cjs";
6
6
  import { getRelationshipId } from "./getRelationshipId.cjs";
7
7
  import { onTabKeyDown } from "./onTabKeyDown.cjs";
8
8
  import { registerTab } from "./registerTab.cjs";
9
+ import { unregisterTab } from "./unregisterTab.cjs";
9
10
  import { setActiveTab } from "./setActiveTab.cjs";
10
11
  import { setActiveTabFromKeyboard } from "./setActiveTabFromKeyboard.cjs";
11
12
  import { useBeforeUnmount } from "./useBeforeUnmount.cjs";
12
13
  import { consumeTabFocus } from "./consumeTabFocus.cjs";
13
- export { consumeTabFocus, getActiveTab, getAriaLabel, getContext, getIsDisabled, getOrientation, getRelationshipId, onTabKeyDown, registerTab, setActiveTab, setActiveTabFromKeyboard, useBeforeUnmount };
14
+ export { consumeTabFocus, getActiveTab, getAriaLabel, getContext, getIsDisabled, getOrientation, getRelationshipId, onTabKeyDown, registerTab, setActiveTab, setActiveTabFromKeyboard, unregisterTab, useBeforeUnmount };
@@ -0,0 +1,90 @@
1
+ # Tag
2
+
3
+ Tag displays a short, non-interactive category or status label. Its rendering
4
+ contract targets WCAG 2.2 Level AA.
5
+
6
+ ## When to use
7
+
8
+ Use Tag for a compact category or status. Visible text must describe the
9
+ status. Colour may reinforce meaning but must not be the only indicator. Use
10
+ `closeButton` only to dismiss the tag itself, not to start another action.
11
+
12
+ ## Props
13
+
14
+ | PropName | Type | Required | Default | Description |
15
+ |---|---|---:|---:|---|
16
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
17
+ | `ariaLabel` | `string` | no | `""` | Accessible name when visible content is insufficient. Pass an empty string to omit it. |
18
+ | `className` | `string` | no | `""` | Additional classes. |
19
+ | `closeButton` | `boolean` | no | - | Displays the native close button. |
20
+ | `closeLabel` | `string` | no | `"Close tag"` | Names the close button. |
21
+ | `onClose` | `() => void` | no | noop | Reports close intent. |
22
+ | `rounded` | `boolean` | no | - | Applies `is-rounded`. |
23
+
24
+ ## CSS Variables
25
+
26
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
27
+ |---|---|---|---|---|---|---|
28
+ | `--pw_Tag_root_default_color` | `root` | `default` | `color` | `var(--pw_text_color)` | Tag foreground. | Web, Capacitor, NativeScript |
29
+ | `--pw_Tag_root_default_background-color` | `root` | `default` | `background-color` | `var(--pw_control_background-color)` | Tag background. | Web, Capacitor, NativeScript |
30
+ | `--pw_Tag_root_default_border-color` | `root` | `default` | `border-color` | `var(--pw_border-color)` | Border colour. | Web, Capacitor, NativeScript |
31
+ | `--pw_Tag_root_default_border-width` | `root` | `default` | `border-width` | `var(--pw_border-width)` | Border width. | Web, Capacitor, NativeScript |
32
+ | `--pw_Tag_root_default_border-style` | `root` | `default` | `border-style` | `var(--pw_border-style)` | Border style. | Web, Capacitor, NativeScript |
33
+ | `--pw_Tag_root_default_border-radius` | `root` | `default` | `border-radius` | `var(--pw_border-radius)` | Corner radius. | Web, Capacitor, NativeScript |
34
+ | `--pw_Tag_root_rounded_border-radius` | `root` | `rounded` | `border-radius` | `var(--pw_rounded_border-radius)` | Rounded variant radius. | Web, Capacitor, NativeScript |
35
+ | `--pw_Tag_root_default_padding` | `root` | `default` | `padding` | `0.125em 0.625em` | Inner spacing. | Web, Capacitor, NativeScript |
36
+ | `--pw_Tag_root_default_font-size` | `root` | `default` | `font-size` | `var(--pw_small_font-size)` | Text size. | Web, Capacitor, NativeScript |
37
+ | `--pw_Tag_root_default_font-weight` | `root` | `default` | `font-weight` | `var(--pw_bold_font-weight)` | Text weight. | Web, Capacitor, NativeScript |
38
+ | `--pw_Tag_root_default_letter-spacing` | `root` | `default` | `letter-spacing` | `0.04em` | Uppercase tracking. | Web, Capacitor, NativeScript |
39
+ | `--pw_Tag_close-button_default_min-width` | `close-button` | `default` | `min-width` | `1.563em` | Close control width, scaled to Tag size. | Web, Capacitor, NativeScript |
40
+ | `--pw_Tag_close-button_default_height` | `close-button` | `default` | `height` | `1.563em` | Close control height, scaled to Tag size. | Web, Capacitor, NativeScript |
41
+
42
+ ```xts
43
+ import { withDefaultProps } from '@pastweb/x';
44
+ import { DEFAULT_PROPS } from './constants';
45
+
46
+ export function Tag(props: TagProps) {
47
+ const p = withDefaultProps<TagDefaultProps>(props, DEFAULT_PROPS);
48
+
49
+ return (
50
+ <div class={p.cls('Tag', { 'is-rounded': p.rounded, 'has-close': p.closeButton }, p.className)} aria-label={p.ariaLabel || undefined}>
51
+ <span class={p.cls('content')}><slot /></span>
52
+ <button #if={p.closeButton} type="button" class={p.cls('closeButton')} @click={p.onClose} aria-label={p.closeLabel}>
53
+ <span class={p.cls('closeBar')} aria-hidden="true" />
54
+ <span class={p.cls('closeBar')} aria-hidden="true" />
55
+ </button>
56
+ </div>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ## Keyboard interaction
62
+
63
+ - Without `closeButton`, Tag is not interactive and does not enter the tab order.
64
+ - The close action is a native button operable with `Enter` or `Space`.
65
+
66
+ ## Accessibility notes
67
+
68
+ - Visible content is the default accessible name. `ariaLabel` supplements it
69
+ when the visible text is insufficient.
70
+ - The close action is a native button named by `closeLabel`.
71
+ - Status meaning is carried by the text. Colour is decorative reinforcement.
72
+ - Shared web styles use uppercase status text, a Panel-style close control
73
+ scaled to the Tag font size, visible `:focus-visible` indication, and
74
+ forced-colours treatment.
75
+
76
+ ## Known limitations
77
+
78
+ - Automated accessibility checks do not by themselves establish WCAG
79
+ conformance.
80
+
81
+ ## Native renderer mapping
82
+
83
+ Native renderers map Tag to a non-interactive accessible text container. Do not
84
+ copy ARIA attributes into native markup.
85
+
86
+ ## Standards
87
+
88
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
89
+ - WAI-ARIA APG patterns: https://www.w3.org/WAI/ARIA/apg/patterns/
90
+ - GOV.UK Tag: https://design-system.service.gov.uk/components/tag/
@@ -0,0 +1,105 @@
1
+ # TaskBar
2
+
3
+ TaskBar renders one control for each window registered with an application
4
+ `WindowManager`. It provides desktop-style selection, restore, and minimization
5
+ through the manager's `manageWindow` command.
6
+
7
+ ## When to use
8
+
9
+ Use TaskBar with one application-level `WindowManager` when several managed
10
+ windows can remain open at once. Do not use it for unrelated page navigation or
11
+ as a replacement for a toolbar.
12
+
13
+ ## Props
14
+
15
+ | PropName | Type | Required | Default | Description |
16
+ |---|---|---:|---:|---|
17
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
18
+ | `manager` | `WindowManager` | yes | - | The application-level manager whose registered windows are displayed. |
19
+ | `className` | `string` | no | `""` | Adds classes to the taskbar root. |
20
+ | `ariaLabel` | `string` | no | `"Window taskbar"` | Names the navigation landmark. |
21
+ | `getLabel` | `(window: ManagedWindowState) => string` | no | `window.meta.label ?? window.meta.title ?? window.id` | Resolves the accessible and visible task button label. |
22
+
23
+ ## Mediator state and API
24
+
25
+ | Property | Type | Description |
26
+ |---|---|---|
27
+ | `state.activeId` | `string \| null` | ID of the manager's active window. |
28
+ | `state.windows` | `readonly ManagedWindowState[]` | Registered windows in stable taskbar order. |
29
+ | `manageWindow` | `(window: ManagedWindowState) => boolean` | Delegates task-button selection to the manager. It activates an inactive window, minimizes an active visible window, and restores a minimized window. |
30
+
31
+ ## CSS Variables
32
+
33
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
34
+ |---|---|---|---|---|---|---|
35
+ | `--pw_TaskBar_root_default_gap` | root | default | `gap` | `0.04em` | Gap between task controls. | Web, Capacitor |
36
+ | `--pw_TaskBar_root_default_min-height` | root | default | `min-height` | `3.5em` | Minimum taskbar height. | Web, Capacitor |
37
+ | `--pw_TaskBar_root_default_padding` | root | default | `padding` | `0.125em` | Inner taskbar spacing. | Web, Capacitor |
38
+ | `--pw_TaskBar_root_default_border-color` | root | default | `border-color` | `var(--pw_border-color)` | Taskbar border colour. | Web, Capacitor |
39
+ | `--pw_TaskBar_root_default_border-style` | root | default | `border-style` | `var(--pw_border-style)` | Taskbar border style. | Web, Capacitor |
40
+ | `--pw_TaskBar_root_default_border-width` | root | default | `border-width` | `var(--pw_border-width)` | Taskbar border width. | Web, Capacitor |
41
+ | `--pw_TaskBar_root_default_border-radius` | root | default | `border-radius` | `var(--pw_border-radius)` | Taskbar corner radius. | Web, Capacitor |
42
+ | `--pw_TaskBar_root_default_background-color` | root | default | `background-color` | `var(--pw_control_background-color)` | Taskbar background. | Web, Capacitor |
43
+ | `--pw_TaskBar_taskButton_default_min-width` | taskButton | default | `min-width` | `2.75em` | Minimum task-button width. | Web, Capacitor |
44
+ | `--pw_TaskBar_taskButton_default_min-height` | taskButton | default | `min-height` | `2.75em` | Minimum task-button height. | Web, Capacitor |
45
+ | `--pw_TaskBar_taskButton_default_max-width` | taskButton | default | `max-width` | `16em` | Maximum task-button width before text truncates. | Web, Capacitor |
46
+ | `--pw_TaskBar_taskButton_default_padding` | taskButton | default | `padding` | `var(--pw_padding)` | Task-button inner spacing. | Web, Capacitor |
47
+ | `--pw_TaskBar_taskButton_default_margin` | taskButton | default | `margin` | `0.25em` | Task-button spacing. | Web, Capacitor |
48
+ | `--pw_TaskBar_taskButton_default_border-color` | taskButton | default | `border-color` | `var(--pw_border-color)` | Task-button border colour. | Web, Capacitor |
49
+ | `--pw_TaskBar_taskButton_default_border-style` | taskButton | default | `border-style` | `var(--pw_border-style)` | Task-button border style. | Web, Capacitor |
50
+ | `--pw_TaskBar_taskButton_default_border-width` | taskButton | default | `border-width` | `var(--pw_border-width)` | Task-button border width. | Web, Capacitor |
51
+ | `--pw_TaskBar_taskButton_default_border-radius` | taskButton | default | `border-radius` | `var(--pw_border-radius)` | Task-button corner radius. | Web, Capacitor |
52
+ | `--pw_TaskBar_taskButton_default_background-color` | taskButton | default | `background-color` | `var(--pw_control_background-color)` | Task-button background. | Web, Capacitor |
53
+ | `--pw_TaskBar_taskButton_default_color` | taskButton | default | `color` | `inherit` | Task-button text colour. | Web, Capacitor |
54
+ | `--pw_TaskBar_taskButton_default_font-size` | taskButton | default | `font-size` | `inherit` | Task-button font size. | Web, Capacitor |
55
+ | `--pw_TaskBar_taskButton_default_font-weight` | taskButton | default | `font-weight` | `var(--pw_semibold_font-weight)` | Task-button font weight. | Web, Capacitor |
56
+ | `--pw_TaskBar_taskButton_default_line-height` | taskButton | default | `line-height` | `var(--pw_line-height)` | Task-button line height. | Web, Capacitor |
57
+ | `--pw_TaskBar_taskButton_default_transition-duration` | taskButton | default | `transition-duration` | `var(--pw_transition-duration)` | Task-button transition duration. | Web, Capacitor |
58
+ | `--pw_TaskBar_taskButton_minimised_box-shadow` | taskButton | minimised | `box-shadow` | inset shared shadow | Inset shadow that indicates a minimized window. | Web, Capacitor |
59
+
60
+ ```xts
61
+ import { useMediator, withDefaultProps } from '@pastweb/x';
62
+ import { taskBarMediator } from '@pastweb/components-core/TaskBar';
63
+ import { DEFAULT_PROPS } from './constants';
64
+
65
+ export function TaskBar(props: TaskBarProps) {
66
+ const p = withDefaultProps<TaskBarDefaultProps>(props, DEFAULT_PROPS);
67
+ const m = useMediator<TaskBarMediator>(taskBarMediator, p);
68
+ const s = m.state;
69
+
70
+ return (
71
+ <nav class={p.cls('TaskBar', p.className)} aria-label={p.ariaLabel}>
72
+ {s.windows.map(window => (
73
+ <button
74
+ key={window.id}
75
+ type='button'
76
+ class={p.cls('taskButton', {
77
+ 'is-active': window.id === s.activeId,
78
+ 'is-minimised': window.status === 'minimized',
79
+ })}
80
+ aria-pressed={window.id === s.activeId}
81
+ @click={() => m.manageWindow(window)}
82
+ >
83
+ {p.getLabel(window)}
84
+ </button>
85
+ ))}
86
+ </nav>
87
+ );
88
+ }
89
+ ```
90
+
91
+ ## Keyboard and accessibility behaviour
92
+
93
+ - The root is a named `nav` landmark and each window is a native button.
94
+ - `aria-pressed` identifies the active task. Native button keyboard activation
95
+ runs the same manager command as a pointer click.
96
+ - Horizontal overflow remains available when more windows are open than fit in
97
+ the taskbar.
98
+ - The taskbar uses the `--pw_z-index_window-max` layer so its controls remain
99
+ reachable when floating managed windows overlap the same workspace.
100
+
101
+ ## Native renderer mapping
102
+
103
+ Native renderers map the taskbar to an accessible horizontal collection of
104
+ native buttons and use the same `manageWindow` command for activation,
105
+ minimization, and restoration.