@scalewing/react 1.50.0 → 1.52.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.
- package/CHANGELOG.md +16 -0
- package/README.md +54 -2
- package/dist/components/SiteHeader.d.ts +6 -3
- package/dist/components/SiteHeader.js +8 -4
- package/dist/components/TaskList.d.ts +22 -0
- package/dist/components/TaskList.js +27 -0
- package/dist/components/site-header/types.d.ts +16 -1
- package/dist/components/site-header/validate.d.ts +4 -3
- package/dist/components/site-header/validate.js +12 -3
- package/dist/components/task-list/assert-task-list.d.ts +8 -0
- package/dist/components/task-list/assert-task-list.js +31 -0
- package/dist/components/task-list/task-list-item.d.ts +25 -0
- package/dist/components/task-list/task-list-item.js +1 -0
- package/dist/components/task-list/task-list-status.d.ts +11 -0
- package/dist/components/task-list/task-list-status.js +24 -0
- package/dist/css/css-button.js +14 -1
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-site-footer.js +11 -1
- package/dist/css/css-site-header-anchors.d.ts +1 -0
- package/dist/css/css-site-header-anchors.js +22 -0
- package/dist/css/css-site-header-skip-link.d.ts +1 -0
- package/dist/css/css-site-header-skip-link.js +49 -0
- package/dist/css/css-site-header.js +26 -11
- package/dist/css/css-task-list.d.ts +10 -0
- package/dist/css/css-task-list.js +182 -0
- package/dist/css/site-header-geometry.d.ts +21 -0
- package/dist/css/site-header-geometry.js +28 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/styles.css +196 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.52.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 03f2e78: Site chrome follow-ups from Teisoro's marketing-site UX review. `SiteHeader` takes an optional `skipLink` (`{ label, href }`, exported as `SiteHeaderSkipLink`): a "skip to content" link that is the header's first focusable element, visually hidden until it has focus, then a pill on the surface with the accent ring laid over the bar's start (`sw-site-header-skip-link`); a blank label or an `href` that is not a `#fragment` throws a `RangeError`. While a sticky `SiteHeader` is on the page, the document's `scroll-padding-top` clears the bar (56 px, 72 px from `lg`), its hairline and a small gap, so `#fragment` links, `:target` and `scrollIntoView` land below the bar; a header with `sticky={false}` changes nothing. Below `lg`, the bar's `compactActions` (now in a `sw-site-header-compact-actions` box with `display: contents`) step aside while the menu is open, since the menu holds its own `menuActions`, leaving the brand and the close button. On a coarse pointer, a plain link that is all of a `SiteFooter` bottom-row slot (`legal` or `end`) gets the 44 px touch target, as the group links have. No new tokens or dependencies. Request: `docs/requests/teisoro-site-chrome-follow-ups.md`.
|
|
8
|
+
|
|
9
|
+
## 1.51.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- cf5de2a: `TaskList`: a setup checklist, an `ol` (`role="list"`) named by `label` with one row per `items` entry (`{ id, title, detail?, status, action? }`): a 24 px status marker hidden from assistive technology (`done` a check on the success fill, `next` an accent ring round a dot, `todo` a grey ring, `waiting` a muted lock), the title, a muted detail line and an optional action at the row's end that wraps under the words on a narrow column. Each title is read with its status word ("Season, Done") as one hidden line; `labels` (`Partial<TaskListLabels>`) replaces the English words (`defaultTaskListLabels`), a word left out or `undefined` staying English. `action` may be a function given `{ titleId }` to describe the action by its task. Exports `TaskList`, `TaskListProps`, `TaskListItem`, `TaskListActionContext`, `TaskListLabels`, `TaskListStatus`, `taskListStatuses`, `defaultTaskListLabels`. New classes `sw-task-list*`; a blank label, word, id, title or detail, a repeated id, or an unknown status throws a `RangeError`. Request: `docs/requests/futmas-task-list.md`.
|
|
14
|
+
|
|
15
|
+
`Button` (and `ButtonLink` and the controls built on them): on a coarse pointer every size is at least 44 × 44 px, the md control height, so an `xs` or `sm` Button in a table row or a checklist is a comfortable touch target. Padding and type keep the size's; only the minimum box grows, so rows and popovers holding small Buttons are taller on phones and tablets (DateField's calendar footer, Toast's action, a Step's Edit). Two layouts keep `min-width: 0` so they can share a row: a full-width attached `ButtonGroup` below md and a `width="full"` Button in an `Inline`; both keep the 44 px height. A fine pointer is unchanged. Request: `docs/requests/futmas-button-touch-target.md`.
|
|
16
|
+
|
|
17
|
+
No new tokens or dependencies.
|
|
18
|
+
|
|
3
19
|
## 1.50.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -123,6 +123,55 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
123
123
|
</Steps>
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
`TaskList` is a setup checklist: an `ol` (with `role="list"`, which Safari drops from a list without bullets) named by `label` holding one row per item of `items`, in order, a hairline between rows. Each item is `{ id, title, detail?, status, action? }` (`TaskListItem`): `status` (`taskListStatuses`: `'done' | 'next' | 'todo' | 'waiting'`, type `TaskListStatus`) draws a 24 px marker hidden from assistive technology, a different shape for each so colour is never the only cue: `done` a check on the success fill, `next` an accent ring round an accent dot, `todo` the grey hairline ring of a step ahead, and `waiting` a lock in the muted colour (waiting is not work that needs the person, so it is grey, not the warning amber). Then the `title` (label type, a waiting task's muted at the body weight), a muted caption `detail` under it, and `action` at the row's end, such as a small `Button`; the marker and the action sit on the row's middle, and on a narrow column an action that would leave the words less than half the row wraps to a line of its own at the row's end. Each title is read with its status word after a comma, as one visually hidden line ("Season, Done"; the drawn title is `aria-hidden`, as in a folded `Step`), then the detail, then the action. The words are English by default (`defaultTaskListLabels`: Done, Next step, To do, Waiting); `labels` (`Partial<TaskListLabels>`) takes yours, any you leave out or pass as `undefined` staying English. A row's text content holds the title twice (drawn, then spoken), so find a task in tests by its spoken line or by role. `action` may be a function given `{ titleId }`, the id of the drawn title, for the action's `aria-describedby` (or its `aria-labelledby` with its own id), so two "Change" buttons are told apart by their tasks out of the list. Scalewing holds no order rules and adds no `aria-current`: you set every status, and which action is the page's primary (the next task's, by convention) and which are `neutral`. On a coarse pointer the row's Buttons are 44 px like every Button, so rows grow. Forced colours draw every ring as a `CanvasText` border (the next one `Highlight`) and keep the check, dot and lock, which are borders. A blank `label`, status word, id, title or `detail`, a repeated id, or an unknown status throws a `RangeError`; an empty `items` draws an empty list. Other `ol` attributes (`className`, `id`, `data-*`, `aria-describedby`) pass through.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<TaskList
|
|
130
|
+
label="Get the spring survey ready"
|
|
131
|
+
labels={{
|
|
132
|
+
done: 'Listo',
|
|
133
|
+
next: 'Siguiente paso',
|
|
134
|
+
todo: 'Pendiente',
|
|
135
|
+
waiting: 'En espera',
|
|
136
|
+
}}
|
|
137
|
+
items={[
|
|
138
|
+
{
|
|
139
|
+
id: 'site',
|
|
140
|
+
title: 'The site',
|
|
141
|
+
detail: 'North marsh',
|
|
142
|
+
status: 'done',
|
|
143
|
+
action: ({ titleId }) => (
|
|
144
|
+
<Button
|
|
145
|
+
size="sm"
|
|
146
|
+
variant="neutral"
|
|
147
|
+
aria-describedby={titleId}
|
|
148
|
+
onPress={change}
|
|
149
|
+
>
|
|
150
|
+
Change
|
|
151
|
+
</Button>
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'count',
|
|
156
|
+
title: 'The count',
|
|
157
|
+
detail: 'Count the herons at dawn',
|
|
158
|
+
status: 'next',
|
|
159
|
+
action: (
|
|
160
|
+
<Button size="sm" onPress={count}>
|
|
161
|
+
Start counting
|
|
162
|
+
</Button>
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'report',
|
|
167
|
+
title: 'Send the report',
|
|
168
|
+
detail: 'Needs the count',
|
|
169
|
+
status: 'waiting',
|
|
170
|
+
},
|
|
171
|
+
]}
|
|
172
|
+
/>
|
|
173
|
+
```
|
|
174
|
+
|
|
126
175
|
`Select` is a labeled listbox menu when the open list must match the canvas.
|
|
127
176
|
|
|
128
177
|
`Field` supports optional `description`, `error`, and `required` on one native input, select, or textarea child. It preserves an existing `aria-describedby` and appends a stable ID for the hint, or for the error, which replaces the hint on screen while it is supplied. `error` sets `aria-invalid` and token-owned invalid styling. The error sits in a polite live region (`aria-live="polite"`) that is always rendered, empty while there is no error, so an error that appears while the person types, or after a submit, is announced once, politely, when its text is swapped in; it is never `role="alert"`, so a submit that finds several errors does not fire several assertive alerts at once. Empty, the region takes no room in the layout. The consumer owns validation and localized messages, and after a refused submit it moves focus to the first invalid control (or to one form-level summary). `Checkbox`, `RadioGroup` and `DateField` expose their `error` the same way. `invalid` marks the control invalid the same way (`aria-invalid` and the danger border, `Mark` in forced colors) without a message of its own, for fields whose one error is shown elsewhere, such as a count that adds up to nothing under a group of count fields: point each control's `aria-describedby` at that message so it is read with the field. The field's own polite region stays empty and its `description` stays on screen; `error` implies `invalid`. Passing `invalid`, even `invalid={false}` but not `invalid={undefined}`, needs one native control child and throws a `TypeError` on the first render otherwise, so a form learns it while the fields are valid, not on its first failed submit. `error` keeps its rule: it needs the native control only while it holds a message. `invalid` is `Field`'s only: `DateField`, `Select` and the other controls with `error` do not have it yet.
|
|
@@ -327,6 +376,8 @@ On the web, `variant="neutral"` is a quiet grey fill (the muted color at 16 %, t
|
|
|
327
376
|
|
|
328
377
|
`Button` `width` is `'content'` (the default: as wide as its words) or `'full'`: the button fills its container's inline size in a `Stack`, a `Grid` cell or any column, and in an `Inline` row takes the space its siblings leave, as a full `Select` does, so an Edit beside a ••• `ActionMenu` fills the row and the menu keeps its own square at the end. The row rule holds for a `Button` that is a direct child of the `Inline`; wrapped in another box it fills that box instead. In a wrapping `Inline` (`wrap`) a full button grows from its words' width and moves to a line of its own when the row runs out, rather than shrinking past its words. Inside a `ButtonGroup` it is not needed: the group already stacks its buttons full width below `md`. The words stay centred. An unknown width throws a `RangeError`. The class is `sw-button-full`.
|
|
329
378
|
|
|
379
|
+
On a coarse pointer (a phone or a tablet, `(pointer: coarse)`) every `Button`, `ButtonLink` and control built on them is at least 44 × 44 px (the md control height), whatever its `size`: an `xs` or `sm` Button keeps its padding and type but its box grows to the touch target, so a small row action in a table or a checklist is still easy to tap. A fine pointer keeps each size's height (28, 32 and 44 px). Rows holding small Buttons are taller on touch; two layouts keep `min-width: 0` so their Buttons can share a row, a full-width attached `ButtonGroup` below `md` and a `width="full"` Button in an `Inline`, and both keep the 44 px height. Popovers holding small Buttons grow too, such as `DateField`'s calendar with its footer.
|
|
380
|
+
|
|
330
381
|
`ButtonLink` is a link drawn as a `Button`, for a destination that reads as an action, such as a site's Contact us or Sign in: an `<a href>` with the Button's `variant` (default `primary`), `size` (default `md`) and `width`, so it opens like any link (a new tab, a page rendered with no script) and keeps its variant's words over the canvas link colour, hover included. `aria-current="page"` marks the one that is the current page with the pressed button's ring (a Highlight border in forced colours). A link has no disabled state: render none instead. Use `Button` for a press that does something on the page. An unknown variant, size or width throws a `RangeError`; the class is `sw-button-link` beside the Button's own.
|
|
331
382
|
|
|
332
383
|
```tsx
|
|
@@ -1028,10 +1079,11 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
1028
1079
|
</Band>
|
|
1029
1080
|
```
|
|
1030
1081
|
|
|
1031
|
-
`SiteHeader` is a public site's header. Supply `brand` (such as a logo linked to the home page), `navLabel` and `items` (`{ id, label, href, current? }`, `current` as SectionNav's: `page`, `location` or `true` sets `aria-current`), `labels` (`openMenu`, `closeMenu`) and the decorative `menuIcon` and `closeIcon` glyphs. From `lg` the bar shows the brand, the destinations as a row of links (the current one at label weight over a bar in the words' colour, a thick underline in forced colours) and `actions` at its end, such as a language switch, Sign in (`Box as="a" link="quiet"`) and a `ButtonLink`. Below `lg` it shows the brand, `compactActions` and a menu button that opens, under the bar over the rest of the window, the destinations as large rows (the current one in the accent) and `menuActions` at the panel's foot, such as full-width `ButtonLink`s. The menu is a native `details`, so it opens and closes by touch, click and keyboard with no script, as a page rendered on the server needs; its `summary` is the button, named `openMenu` or `closeMenu` by its state. Script adds Escape (focus back on the button), and closes it when a link in it is chosen, focus leaves it, a press lands outside it, or the window widens to `lg`; nothing traps focus. A page rendered on the server has the no-script menu until it hydrates; a site that never hydrates keeps it, which still opens and closes. The bar is 56 px tall below `lg` and 72 px from it, on the solid surface with a hairline under it; `sticky` (default `true`) keeps it at the window's top. No items, blank or repeated ids, blank labels or hrefs,
|
|
1082
|
+
`SiteHeader` is a public site's header. Supply `brand` (such as a logo linked to the home page), `navLabel` and `items` (`{ id, label, href, current? }`, `current` as SectionNav's: `page`, `location` or `true` sets `aria-current`), `labels` (`openMenu`, `closeMenu`) and the decorative `menuIcon` and `closeIcon` glyphs. From `lg` the bar shows the brand, the destinations as a row of links (the current one at label weight over a bar in the words' colour, a thick underline in forced colours) and `actions` at its end, such as a language switch, Sign in (`Box as="a" link="quiet"`) and a `ButtonLink`. Below `lg` it shows the brand, `compactActions` and a menu button that opens, under the bar over the rest of the window, the destinations as large rows (the current one in the accent) and `menuActions` at the panel's foot, such as full-width `ButtonLink`s. The menu is a native `details`, so it opens and closes by touch, click and keyboard with no script, as a page rendered on the server needs; its `summary` is the button, named `openMenu` or `closeMenu` by its state. Script adds Escape (focus back on the button), and closes it when a link in it is chosen, focus leaves it, a press lands outside it, or the window widens to `lg`; nothing traps focus. A page rendered on the server has the no-script menu until it hydrates; a site that never hydrates keeps it, which still opens and closes. While the menu is open the bar's `compactActions` step aside (the menu holds its own `menuActions`), so the bar shows only the brand and the close button. The bar is 56 px tall below `lg` and 72 px from it, on the solid surface with a hairline under it; `sticky` (default `true`) keeps it at the window's top. While a sticky header is on the page the document's `scroll-padding-top` clears the bar, its hairline and a small gap, so a `#fragment` link, `:target` and `scrollIntoView` land below the bar instead of under it; a header with `sticky={false}` leaves the scroll padding alone. An optional `skipLink` (`{ label, href }`, such as `{ label: 'Skip to content', href: '#main' }`) is the header's first focusable element: visually hidden until it has focus, then a pill on the surface with the focus ring over the bar's start. Give the target (usually `main`) the matching `id`, and `tabIndex={-1}` so every browser moves focus there with the jump. No items, blank or repeated ids, blank labels or hrefs, a blank `navLabel` or menu label, or a skip link with a blank label or an `href` that is not a `#fragment` throw a `RangeError`.
|
|
1032
1083
|
|
|
1033
1084
|
```tsx
|
|
1034
1085
|
<SiteHeader
|
|
1086
|
+
skipLink={{ label: 'Skip to content', href: '#main' }}
|
|
1035
1087
|
brand={<a href="/">Wetland Trust</a>}
|
|
1036
1088
|
navLabel="Main"
|
|
1037
1089
|
items={[{ id: 'marsh', label: 'Marsh', href: '/marsh', current: 'page' }]}
|
|
@@ -1081,7 +1133,7 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
1081
1133
|
</ScreenPreview>
|
|
1082
1134
|
```
|
|
1083
1135
|
|
|
1084
|
-
`SiteFooter` is a public site's footer on the dark inverse band (`Band tone="inverse"`, so a brand moves it with a theme overlay of `inverse`). Supply `brand` (such as a logo for a dark background), an optional `tagline`, and `groups`: each `{ id, label, links }` is a `nav` named by its label, which shows above its links (hidden from assistive technology, since the `nav` already carries it). Each link is `{ id, label, href, current?, lang?, hrefLang? }`; `lang` and `hrefLang` pass through to the link unchecked; `current` sets `aria-current="page"` and draws it in the band's words at label weight, the others in its muted words, underlined on hover and focus. `legal` and `end` fill the bottom row past a hairline, such as the copyright and a language switch. Below `md` the brand takes the full width and the groups two columns; from `md` the brand and the groups share one spread row. Links get the 44 px target on a coarse pointer. Put it last in the page's `body` (outside a `section` or `article`) so it is the page's `contentinfo` landmark. No groups, a group with no links, blank or repeated ids, blank labels or a blank `href` throw a `RangeError`.
|
|
1136
|
+
`SiteFooter` is a public site's footer on the dark inverse band (`Band tone="inverse"`, so a brand moves it with a theme overlay of `inverse`). Supply `brand` (such as a logo for a dark background), an optional `tagline`, and `groups`: each `{ id, label, links }` is a `nav` named by its label, which shows above its links (hidden from assistive technology, since the `nav` already carries it). Each link is `{ id, label, href, current?, lang?, hrefLang? }`; `lang` and `hrefLang` pass through to the link unchecked; `current` sets `aria-current="page"` and draws it in the band's words at label weight, the others in its muted words, underlined on hover and focus. `legal` and `end` fill the bottom row past a hairline, such as the copyright and a language switch; a plain link that is all of `legal` or `end` gets the 44 px target on a coarse pointer too (a link inside running legal words stays inline). Below `md` the brand takes the full width and the groups two columns; from `md` the brand and the groups share one spread row. Links get the 44 px target on a coarse pointer. Put it last in the page's `body` (outside a `section` or `article`) so it is the page's `contentinfo` landmark. No groups, a group with no links, blank or repeated ids, blank labels or a blank `href` throw a `RangeError`.
|
|
1085
1137
|
|
|
1086
1138
|
```tsx
|
|
1087
1139
|
<SiteFooter
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SiteHeaderProps } from './site-header/types.js';
|
|
2
|
-
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './site-header/types.js';
|
|
2
|
+
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, SiteHeaderSkipLink, } from './site-header/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* A public site's header: the brand at the start; from `lg` the main
|
|
5
5
|
* destinations as a row of links (the current one marked by weight and a
|
|
@@ -9,6 +9,9 @@ export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './site-
|
|
|
9
9
|
* it opens and closes by touch, click and keyboard with no script, as a
|
|
10
10
|
* page rendered on the server needs; script adds Escape (focus back on the
|
|
11
11
|
* button) and closes it when a link is chosen or focus leaves it. It traps
|
|
12
|
-
* no focus.
|
|
12
|
+
* no focus. While the menu is open the compact actions step aside, so the
|
|
13
|
+
* bar shows only the brand and the close button. An optional skip link
|
|
14
|
+
* comes first, hidden until it has focus. Invalid items or words throw a
|
|
15
|
+
* `RangeError`.
|
|
13
16
|
*/
|
|
14
|
-
export declare function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, sticky, }: SiteHeaderProps): import("react").JSX.Element;
|
|
17
|
+
export declare function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, skipLink, sticky, }: SiteHeaderProps): import("react").JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useRef } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
+
import { tabStop } from '../tab-stop.js';
|
|
5
6
|
import { SiteHeaderLinks } from './site-header/SiteHeaderLinks.js';
|
|
6
7
|
import { useMenuDismiss } from './site-header/use-menu-dismiss.js';
|
|
7
8
|
import { assertSiteHeaderProps } from './site-header/validate.js';
|
|
@@ -14,11 +15,14 @@ import { assertSiteHeaderProps } from './site-header/validate.js';
|
|
|
14
15
|
* it opens and closes by touch, click and keyboard with no script, as a
|
|
15
16
|
* page rendered on the server needs; script adds Escape (focus back on the
|
|
16
17
|
* button) and closes it when a link is chosen or focus leaves it. It traps
|
|
17
|
-
* no focus.
|
|
18
|
+
* no focus. While the menu is open the compact actions step aside, so the
|
|
19
|
+
* bar shows only the brand and the close button. An optional skip link
|
|
20
|
+
* comes first, hidden until it has focus. Invalid items or words throw a
|
|
21
|
+
* `RangeError`.
|
|
18
22
|
*/
|
|
19
|
-
export function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, sticky = true, }) {
|
|
20
|
-
assertSiteHeaderProps({ items, labels, navLabel });
|
|
23
|
+
export function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, skipLink, sticky = true, }) {
|
|
24
|
+
assertSiteHeaderProps({ items, labels, navLabel, skipLink });
|
|
21
25
|
const menuRef = useRef(null);
|
|
22
26
|
useMenuDismiss(menuRef);
|
|
23
|
-
return (
|
|
27
|
+
return (_jsxs("header", { className: cx('sw-site-header', sticky && 'sw-site-header-sticky'), children: [skipLink ? (_jsx("a", { className: "sw-site-header-skip-link", href: skipLink.href, tabIndex: tabStop, children: skipLink.label })) : null, _jsxs("div", { className: "sw-site-header-bar", children: [_jsx("div", { className: "sw-site-header-brand", children: brand }), _jsx(SiteHeaderLinks, { className: "sw-site-header-nav", items: items, navLabel: navLabel }), actions ? (_jsx("div", { className: "sw-site-header-actions", children: actions })) : null, _jsxs("div", { className: "sw-site-header-compact", children: [compactActions ? (_jsx("div", { className: "sw-site-header-compact-actions", children: compactActions })) : null, _jsxs("details", { className: "sw-site-header-menu", ref: menuRef, children: [_jsxs("summary", { className: "sw-site-header-menu-button", children: [_jsxs("span", { className: "sw-site-header-menu-open", children: [_jsx("span", { "aria-hidden": "true", className: "sw-site-header-glyph", children: menuIcon }), _jsx("span", { className: "sw-sr-only", children: labels.openMenu })] }), _jsxs("span", { className: "sw-site-header-menu-close", children: [_jsx("span", { "aria-hidden": "true", className: "sw-site-header-glyph", children: closeIcon }), _jsx("span", { className: "sw-sr-only", children: labels.closeMenu })] })] }), _jsxs("div", { className: "sw-site-header-panel", children: [_jsx(SiteHeaderLinks, { className: "sw-site-header-menu-nav", items: items, navLabel: navLabel }), menuActions ? (_jsx("div", { className: "sw-site-header-menu-actions", children: menuActions })) : null] })] })] })] })] }));
|
|
24
28
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import type { TaskListItem } from './task-list/task-list-item.js';
|
|
3
|
+
import { type TaskListLabels } from './task-list/task-list-status.js';
|
|
4
|
+
export type { TaskListActionContext, TaskListItem, } from './task-list/task-list-item.js';
|
|
5
|
+
export { defaultTaskListLabels, type TaskListLabels, type TaskListStatus, taskListStatuses, } from './task-list/task-list-status.js';
|
|
6
|
+
export type TaskListProps = Omit<HTMLAttributes<HTMLOListElement>, 'aria-label' | 'children'> & {
|
|
7
|
+
/** The list's name, such as "Get Spring 2027 ready". */
|
|
8
|
+
label: string;
|
|
9
|
+
/** The tasks, in order. */
|
|
10
|
+
items: readonly TaskListItem[];
|
|
11
|
+
/** The status words heard after each title. English by default. */
|
|
12
|
+
labels?: Partial<TaskListLabels>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A setup checklist: an ordered list of tasks, each with a status marker
|
|
16
|
+
* (done: a check on the success fill; next: an accent ring round a dot;
|
|
17
|
+
* todo: a grey ring; waiting: a muted lock), its title, a muted detail
|
|
18
|
+
* line and an optional action at the row's end. Each title is followed by
|
|
19
|
+
* its status word for assistive technology. Scalewing holds no order
|
|
20
|
+
* rules: the consumer sets every task's status and action.
|
|
21
|
+
*/
|
|
22
|
+
export declare function TaskList({ label, items, labels, className, ...rest }: TaskListProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { assertTaskList } from './task-list/assert-task-list.js';
|
|
6
|
+
import { taskListWords, taskSpokenTitle, } from './task-list/task-list-status.js';
|
|
7
|
+
export { defaultTaskListLabels, taskListStatuses, } from './task-list/task-list-status.js';
|
|
8
|
+
function TaskRow({ item, labels, titleId, }) {
|
|
9
|
+
const action = typeof item.action === 'function' ? item.action({ titleId }) : item.action;
|
|
10
|
+
const hasAction = action !== undefined && action !== null && action !== false;
|
|
11
|
+
return (_jsxs("li", { className: cx('sw-task-list-item', `sw-task-list-item-${item.status}`), children: [_jsx("span", { "aria-hidden": "true", className: "sw-task-list-marker" }), _jsxs("span", { className: "sw-task-list-body", children: [_jsxs("span", { className: "sw-task-list-title", children: [_jsx("span", { "aria-hidden": "true", id: titleId, children: item.title }), _jsx("span", { className: "sw-sr-only", children: taskSpokenTitle(item.title, item.status, labels) })] }), item.detail !== undefined ? (_jsx("span", { className: "sw-task-list-detail", children: item.detail })) : null] }), hasAction ? _jsx("span", { className: "sw-task-list-action", children: action }) : null] }));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A setup checklist: an ordered list of tasks, each with a status marker
|
|
15
|
+
* (done: a check on the success fill; next: an accent ring round a dot;
|
|
16
|
+
* todo: a grey ring; waiting: a muted lock), its title, a muted detail
|
|
17
|
+
* line and an optional action at the row's end. Each title is followed by
|
|
18
|
+
* its status word for assistive technology. Scalewing holds no order
|
|
19
|
+
* rules: the consumer sets every task's status and action.
|
|
20
|
+
*/
|
|
21
|
+
export function TaskList({ label, items, labels, className, ...rest }) {
|
|
22
|
+
const words = taskListWords(labels);
|
|
23
|
+
assertTaskList(label, items, words);
|
|
24
|
+
const baseId = useId();
|
|
25
|
+
// role="list" keeps the list a list where list-style: none drops it (Safari).
|
|
26
|
+
return (_jsx("ol", { "aria-label": label, className: cx('sw-task-list', className), role: "list", ...rest, children: items.map((item, index) => (_jsx(TaskRow, { item: item, labels: words, titleId: `${baseId}-${index}` }, item.id))) }));
|
|
27
|
+
}
|
|
@@ -15,6 +15,16 @@ export type SiteHeaderLabels = {
|
|
|
15
15
|
/** The open menu button's name, such as "Close the menu". */
|
|
16
16
|
closeMenu: string;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* A "skip to content" link, the header's first focusable element: hidden
|
|
20
|
+
* until it has focus, then shown at the bar's start.
|
|
21
|
+
*/
|
|
22
|
+
export type SiteHeaderSkipLink = {
|
|
23
|
+
/** The link's words, such as "Skip to content". */
|
|
24
|
+
label: string;
|
|
25
|
+
/** The main content's fragment, such as `#main`. */
|
|
26
|
+
href: string;
|
|
27
|
+
};
|
|
18
28
|
export type SiteHeaderProps = {
|
|
19
29
|
/** The site's mark at the start, such as a logo linked to the home page. */
|
|
20
30
|
brand: ReactNode;
|
|
@@ -24,7 +34,10 @@ export type SiteHeaderProps = {
|
|
|
24
34
|
items: readonly SiteHeaderItem[];
|
|
25
35
|
/** The bar's end from `lg`, such as a language switch, Sign in and a ButtonLink. */
|
|
26
36
|
actions?: ReactNode;
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Beside the menu button below `lg`, such as the language switch. They
|
|
39
|
+
* step aside while the menu is open, which holds its own `menuActions`.
|
|
40
|
+
*/
|
|
28
41
|
compactActions?: ReactNode;
|
|
29
42
|
/** In the open menu under its links, such as full-width ButtonLinks. */
|
|
30
43
|
menuActions?: ReactNode;
|
|
@@ -35,4 +48,6 @@ export type SiteHeaderProps = {
|
|
|
35
48
|
closeIcon: ReactNode;
|
|
36
49
|
/** Whether the bar sticks to the window's top as the page scrolls (default `true`). */
|
|
37
50
|
sticky?: boolean;
|
|
51
|
+
/** A "skip to content" link before everything else in the header. */
|
|
52
|
+
skipLink?: SiteHeaderSkipLink;
|
|
38
53
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type SiteHeaderProps } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fails closed on the header's words and destinations: no items, blank or
|
|
4
|
-
* repeated ids, blank labels or hrefs,
|
|
5
|
-
* button name
|
|
4
|
+
* repeated ids, blank labels or hrefs, a blank navigation name or menu
|
|
5
|
+
* button name, and a skip link with blank words or an address that is not
|
|
6
|
+
* a fragment on the page.
|
|
6
7
|
*/
|
|
7
|
-
export declare function assertSiteHeaderProps({ items, labels, navLabel, }: Pick<SiteHeaderProps, 'items' | 'labels' | 'navLabel'>): void;
|
|
8
|
+
export declare function assertSiteHeaderProps({ items, labels, navLabel, skipLink, }: Pick<SiteHeaderProps, 'items' | 'labels' | 'navLabel' | 'skipLink'>): void;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { assertNavItems } from '../nav-items.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fails closed on the header's words and destinations: no items, blank or
|
|
4
|
-
* repeated ids, blank labels or hrefs,
|
|
5
|
-
* button name
|
|
4
|
+
* repeated ids, blank labels or hrefs, a blank navigation name or menu
|
|
5
|
+
* button name, and a skip link with blank words or an address that is not
|
|
6
|
+
* a fragment on the page.
|
|
6
7
|
*/
|
|
7
|
-
export function assertSiteHeaderProps({ items, labels, navLabel, }) {
|
|
8
|
+
export function assertSiteHeaderProps({ items, labels, navLabel, skipLink, }) {
|
|
8
9
|
if (items.length === 0)
|
|
9
10
|
throw new RangeError('SiteHeader needs at least one item');
|
|
10
11
|
assertNavItems('SiteHeader', items);
|
|
@@ -14,4 +15,12 @@ export function assertSiteHeaderProps({ items, labels, navLabel, }) {
|
|
|
14
15
|
throw new RangeError('SiteHeader needs a nonempty navLabel');
|
|
15
16
|
if (!labels.openMenu.trim() || !labels.closeMenu.trim())
|
|
16
17
|
throw new RangeError('SiteHeader labels need nonempty openMenu and closeMenu');
|
|
18
|
+
if (skipLink)
|
|
19
|
+
assertSkipLink(skipLink);
|
|
20
|
+
}
|
|
21
|
+
function assertSkipLink({ href, label }) {
|
|
22
|
+
if (!label.trim())
|
|
23
|
+
throw new RangeError('SiteHeader skipLink needs a nonempty label');
|
|
24
|
+
if (!/^#\S+$/.test(href))
|
|
25
|
+
throw new RangeError('SiteHeader skipLink href must be a fragment such as #main');
|
|
17
26
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TaskListLabels } from './task-list-status.js';
|
|
2
|
+
import type { TaskListItem } from './task-list-item.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fails closed on what would leave a task unnamed or unread: a blank list
|
|
5
|
+
* label or status word, a blank or repeated id, a blank title or detail,
|
|
6
|
+
* or an unknown status.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertTaskList(label: string, items: readonly TaskListItem[], labels: TaskListLabels): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isTaskListStatus, taskListStatuses, } from './task-list-status.js';
|
|
2
|
+
function isBlank(value) {
|
|
3
|
+
return typeof value !== 'string' || !value.trim();
|
|
4
|
+
}
|
|
5
|
+
function assertItem(item, ids) {
|
|
6
|
+
if (isBlank(item.id))
|
|
7
|
+
throw new RangeError('TaskList item id must not be empty');
|
|
8
|
+
if (ids.has(item.id))
|
|
9
|
+
throw new RangeError(`TaskList item ids must be unique: ${item.id}`);
|
|
10
|
+
ids.add(item.id);
|
|
11
|
+
if (isBlank(item.title))
|
|
12
|
+
throw new RangeError('TaskList item title must not be empty');
|
|
13
|
+
if (item.detail !== undefined && isBlank(item.detail))
|
|
14
|
+
throw new RangeError('TaskList item detail must not be empty');
|
|
15
|
+
if (!isTaskListStatus(item.status))
|
|
16
|
+
throw new RangeError(`Unknown TaskList status: ${String(item.status)} (use ${taskListStatuses.join(', ')})`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fails closed on what would leave a task unnamed or unread: a blank list
|
|
20
|
+
* label or status word, a blank or repeated id, a blank title or detail,
|
|
21
|
+
* or an unknown status.
|
|
22
|
+
*/
|
|
23
|
+
export function assertTaskList(label, items, labels) {
|
|
24
|
+
if (isBlank(label))
|
|
25
|
+
throw new RangeError('TaskList label must not be empty');
|
|
26
|
+
if (taskListStatuses.some((status) => isBlank(labels[status])))
|
|
27
|
+
throw new RangeError('TaskList labels must not be empty');
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
for (const item of items)
|
|
30
|
+
assertItem(item, ids);
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { TaskListStatus } from './task-list-status.js';
|
|
3
|
+
/** What a task's action is given: the id of its drawn title. */
|
|
4
|
+
export type TaskListActionContext = {
|
|
5
|
+
/**
|
|
6
|
+
* The id of the task's drawn title, for the action's `aria-describedby`
|
|
7
|
+
* (or `aria-labelledby` with the action's own id), so a "Change" button
|
|
8
|
+
* is heard as "Change" for "Season" out of the list too.
|
|
9
|
+
*/
|
|
10
|
+
titleId: string;
|
|
11
|
+
};
|
|
12
|
+
/** One task of a `TaskList`. */
|
|
13
|
+
export type TaskListItem = {
|
|
14
|
+
/** Unique within the list; the row's React key. */
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
/** One muted line under the title: what is done, or what is left. */
|
|
18
|
+
detail?: string;
|
|
19
|
+
status: TaskListStatus;
|
|
20
|
+
/**
|
|
21
|
+
* A control at the row's end, such as a small `Button`. A function gets
|
|
22
|
+
* the title's id to name or describe the control by its task.
|
|
23
|
+
*/
|
|
24
|
+
action?: ReactNode | ((context: TaskListActionContext) => ReactNode);
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Where a task stands in a checklist. */
|
|
2
|
+
export declare const taskListStatuses: readonly ["done", "next", "todo", "waiting"];
|
|
3
|
+
export type TaskListStatus = (typeof taskListStatuses)[number];
|
|
4
|
+
/** The status words assistive technology hears after each task's title. */
|
|
5
|
+
export type TaskListLabels = Record<TaskListStatus, string>;
|
|
6
|
+
export declare const defaultTaskListLabels: TaskListLabels;
|
|
7
|
+
export declare function isTaskListStatus(value: unknown): value is TaskListStatus;
|
|
8
|
+
/** The words read for one task: its title, a comma, and its status word. */
|
|
9
|
+
export declare function taskSpokenTitle(title: string, status: TaskListStatus, labels: TaskListLabels): string;
|
|
10
|
+
/** The consumer's words over the English ones; a word left out or `undefined` stays English. */
|
|
11
|
+
export declare function taskListWords(labels: Partial<TaskListLabels> | undefined): TaskListLabels;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Where a task stands in a checklist. */
|
|
2
|
+
export const taskListStatuses = ['done', 'next', 'todo', 'waiting'];
|
|
3
|
+
export const defaultTaskListLabels = {
|
|
4
|
+
done: 'Done',
|
|
5
|
+
next: 'Next step',
|
|
6
|
+
todo: 'To do',
|
|
7
|
+
waiting: 'Waiting',
|
|
8
|
+
};
|
|
9
|
+
export function isTaskListStatus(value) {
|
|
10
|
+
return taskListStatuses.includes(value);
|
|
11
|
+
}
|
|
12
|
+
/** The words read for one task: its title, a comma, and its status word. */
|
|
13
|
+
export function taskSpokenTitle(title, status, labels) {
|
|
14
|
+
return `${title}, ${labels[status]}`;
|
|
15
|
+
}
|
|
16
|
+
/** The consumer's words over the English ones; a word left out or `undefined` stays English. */
|
|
17
|
+
export function taskListWords(labels) {
|
|
18
|
+
return {
|
|
19
|
+
done: labels?.done ?? defaultTaskListLabels.done,
|
|
20
|
+
next: labels?.next ?? defaultTaskListLabels.next,
|
|
21
|
+
todo: labels?.todo ?? defaultTaskListLabels.todo,
|
|
22
|
+
waiting: labels?.waiting ?? defaultTaskListLabels.waiting,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/css/css-button.js
CHANGED
|
@@ -3,6 +3,7 @@ import { breakpointQuery } from './breakpoints.js';
|
|
|
3
3
|
import { buttonGroupAttachedClassCatalog, cssButtonGroupAttachedClasses, } from './css-button-group-attached.js';
|
|
4
4
|
import { buttonWidthClassCatalog, cssButtonWidthClasses, } from './css-button-width.js';
|
|
5
5
|
import { quietFill } from './quiet-fill.js';
|
|
6
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
6
7
|
const label = typographyVariants.label;
|
|
7
8
|
/**
|
|
8
9
|
* The web Button's variants: the shared tiers from `@scalewing/tokens`, plus
|
|
@@ -82,13 +83,25 @@ function ariaDisabledForcedRules() {
|
|
|
82
83
|
.sw-button-disabled-neutral[aria-disabled='true']:not(.sw-button-ghost) { border-color: GrayText; color: GrayText; }
|
|
83
84
|
}`;
|
|
84
85
|
}
|
|
86
|
+
/*
|
|
87
|
+
* Each size's height and side padding for a fine pointer, then the
|
|
88
|
+
* comfortable touch target (44 px both ways, the md control height) for
|
|
89
|
+
* every size on a coarse pointer, so an xs or sm Button in a table row or a
|
|
90
|
+
* checklist is still easy to tap. Its look stays the size's (padding and
|
|
91
|
+
* type); only the minimum box grows. The coarse rule follows the sizes,
|
|
92
|
+
* which it outranks only by order.
|
|
93
|
+
*/
|
|
85
94
|
function sizeRules() {
|
|
86
|
-
|
|
95
|
+
const sizes = buttonSizes
|
|
87
96
|
.map((size) => {
|
|
88
97
|
const control = controlScale[size];
|
|
89
98
|
return `.sw-button-${size} { min-height: ${control.minHeight}px; padding-inline: ${control.paddingInline}px; }`;
|
|
90
99
|
})
|
|
91
100
|
.join('\n');
|
|
101
|
+
return `${sizes}
|
|
102
|
+
@media ${coarsePointerQuery} {
|
|
103
|
+
.sw-button { min-height: ${touchTarget}; min-width: ${touchTarget}; }
|
|
104
|
+
}`;
|
|
92
105
|
}
|
|
93
106
|
/*
|
|
94
107
|
* The button's own children are its flex items, so a glyph and its label
|
|
@@ -67,6 +67,7 @@ import { cssDenominationGridClasses } from './css-denomination-grid.js';
|
|
|
67
67
|
import { cssFieldClasses } from './css-field.js';
|
|
68
68
|
import { cssStepperClasses } from './css-stepper.js';
|
|
69
69
|
import { cssStepsClasses } from './css-steps.js';
|
|
70
|
+
import { cssTaskListClasses } from './css-task-list.js';
|
|
70
71
|
import { cssToastClasses } from './css-toast.js';
|
|
71
72
|
import { cssToastCardClasses } from './css-toast-card.js';
|
|
72
73
|
import { cssBoxBorderClasses } from './css-box-border.js';
|
|
@@ -276,6 +277,8 @@ ${cssStepperClasses()}
|
|
|
276
277
|
|
|
277
278
|
${cssStepsClasses()}
|
|
278
279
|
|
|
280
|
+
${cssTaskListClasses()}
|
|
281
|
+
|
|
279
282
|
${cssSplitClasses()}
|
|
280
283
|
|
|
281
284
|
${cssToastClasses()}
|
|
@@ -21,7 +21,10 @@ function type(variant, weight = variant.fontWeight) {
|
|
|
21
21
|
* win by coming after it (`cssSiteFooterClasses` follows `cssBandClasses`;
|
|
22
22
|
* a test holds the order).
|
|
23
23
|
* Below `md` the brand spans two columns of groups; from `md` the brand
|
|
24
|
-
* and the groups share one wrapping row, spread apart.
|
|
24
|
+
* and the groups share one wrapping row, spread apart. On a coarse pointer
|
|
25
|
+
* the group links and a link that is all of a bottom-row slot (`legal` or
|
|
26
|
+
* `end`, such as a language switch) are the 44 px touch target; a link
|
|
27
|
+
* inside running legal words stays inline.
|
|
25
28
|
*/
|
|
26
29
|
export function cssSiteFooterClasses() {
|
|
27
30
|
return `.sw-site-footer-top {
|
|
@@ -120,6 +123,13 @@ export function cssSiteFooterClasses() {
|
|
|
120
123
|
min-height: ${touchTarget};
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
[data-theme] .sw-band-inverse :is(.sw-site-footer-legal, .sw-site-footer-end) > a:not(.sw-button) {
|
|
127
|
+
align-items: center;
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
min-height: ${touchTarget};
|
|
131
|
+
}
|
|
132
|
+
|
|
123
133
|
.sw-site-footer-links {
|
|
124
134
|
gap: 0;
|
|
125
135
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cssSiteHeaderAnchorRules(): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { siteHeaderCompactHeight, siteHeaderWideHeight, siteHeaderWideQuery, } from './site-header-geometry.js';
|
|
2
|
+
/** The room an in-page jump leaves above its target: the bar, its hairline and a gap. */
|
|
3
|
+
function scrollPadding(barHeight) {
|
|
4
|
+
return `calc(${barHeight} + 1px + var(--sw-space-3))`;
|
|
5
|
+
}
|
|
6
|
+
/*
|
|
7
|
+
* While a sticky SiteHeader is on the page, the document's scroll padding
|
|
8
|
+
* clears the bar (its height at each width, its hairline) and a small gap,
|
|
9
|
+
* so a #fragment link, :target and scrollIntoView land below the bar
|
|
10
|
+
* instead of under it. A header in the page flow changes nothing.
|
|
11
|
+
*/
|
|
12
|
+
export function cssSiteHeaderAnchorRules() {
|
|
13
|
+
return `html:has(.sw-site-header-sticky) {
|
|
14
|
+
scroll-padding-top: ${scrollPadding(siteHeaderCompactHeight)};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media ${siteHeaderWideQuery} {
|
|
18
|
+
html:has(.sw-site-header-sticky) {
|
|
19
|
+
scroll-padding-top: ${scrollPadding(siteHeaderWideHeight)};
|
|
20
|
+
}
|
|
21
|
+
}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cssSiteHeaderSkipLinkRules(): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
3
|
+
import { siteHeaderContentStart, siteHeaderGutter, siteHeaderGutterQuery, } from './site-header-geometry.js';
|
|
4
|
+
const label = typographyVariants.label;
|
|
5
|
+
/*
|
|
6
|
+
* The header's skip link, its first focusable element: visually hidden
|
|
7
|
+
* until it has focus, then a pill on the surface in the text colour with
|
|
8
|
+
* the accent ring, laid over the bar's start (centred in its height and
|
|
9
|
+
* lined up with the brand), the control height tall only then. Positioned,
|
|
10
|
+
* it paints over the bar's content, which is not, so it needs no layer of
|
|
11
|
+
* its own.
|
|
12
|
+
*/
|
|
13
|
+
export function cssSiteHeaderSkipLinkRules() {
|
|
14
|
+
return `.sw-site-header-skip-link {
|
|
15
|
+
align-items: center;
|
|
16
|
+
background: var(--sw-color-surface);
|
|
17
|
+
border-radius: var(--sw-radius-pill);
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
font-family: var(--sw-font-sans);
|
|
21
|
+
font-size: ${label.fontSize}px;
|
|
22
|
+
font-weight: ${label.fontWeight};
|
|
23
|
+
inset-block-start: 50%;
|
|
24
|
+
inset-inline-start: ${siteHeaderContentStart(siteHeaderGutter.compact)};
|
|
25
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
26
|
+
line-height: ${label.lineHeight}px;
|
|
27
|
+
padding-inline: var(--sw-space-4);
|
|
28
|
+
position: absolute;
|
|
29
|
+
transform: translateY(-50%);
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[data-theme] .sw-site-header-skip-link {
|
|
34
|
+
color: var(--sw-color-text);
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-theme] .sw-site-header-skip-link:focus {
|
|
39
|
+
min-height: var(--sw-control-md-min-height);
|
|
40
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
41
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sw-site-header-skip-link:not(:focus) { ${visuallyHiddenDeclarations} }
|
|
45
|
+
|
|
46
|
+
@media ${siteHeaderGutterQuery} {
|
|
47
|
+
.sw-site-header-skip-link { inset-inline-start: ${siteHeaderContentStart(siteHeaderGutter.wide)}; }
|
|
48
|
+
}`;
|
|
49
|
+
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
-
import {
|
|
2
|
+
import { cssSiteHeaderAnchorRules } from './css-site-header-anchors.js';
|
|
3
|
+
import { cssSiteHeaderSkipLinkRules } from './css-site-header-skip-link.js';
|
|
3
4
|
import { quietFill } from './quiet-fill.js';
|
|
5
|
+
import { siteHeaderBarMaxWidth, siteHeaderCompactHeight as compactHeight, siteHeaderGutter, siteHeaderGutterQuery, siteHeaderWideHeight as wideHeight, siteHeaderWideQuery as wide, } from './site-header-geometry.js';
|
|
4
6
|
import { zIndex } from './stacking.js';
|
|
5
7
|
const label = typographyVariants.label;
|
|
6
8
|
const title = typographyVariants.title;
|
|
7
|
-
/** The bar's height: 56 px on a phone, 72 px from `lg`. */
|
|
8
|
-
const compactHeight = 'calc(var(--sw-control-md-min-height) + var(--sw-space-3))';
|
|
9
|
-
const wideHeight = 'calc(var(--sw-control-md-min-height) + var(--sw-space-5) + var(--sw-space-1))';
|
|
10
|
-
const wide = breakpointQuery('from', 'lg');
|
|
11
9
|
/*
|
|
12
10
|
* The bar: the solid surface with a hairline under it, its content in the
|
|
13
11
|
* page container with the page gutter. Below `lg` it holds the brand, the
|
|
14
12
|
* compact actions and the menu button; from `lg` the brand, the
|
|
15
13
|
* destinations (the current one at label weight over a bar in the words'
|
|
16
|
-
* colour, as tall as the bar) and the actions, spread apart.
|
|
14
|
+
* colour, as tall as the bar) and the actions, spread apart. The compact
|
|
15
|
+
* actions' box adds no layout of its own (`display: contents`).
|
|
17
16
|
*/
|
|
18
17
|
function barRules() {
|
|
19
18
|
return `.sw-site-header {
|
|
@@ -38,9 +37,9 @@ function barRules() {
|
|
|
38
37
|
gap: var(--sw-space-5);
|
|
39
38
|
justify-content: space-between;
|
|
40
39
|
margin-inline: auto;
|
|
41
|
-
max-width:
|
|
40
|
+
max-width: ${siteHeaderBarMaxWidth};
|
|
42
41
|
min-height: var(--sw-site-header-height);
|
|
43
|
-
padding-inline:
|
|
42
|
+
padding-inline: ${siteHeaderGutter.compact};
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
.sw-site-header-brand {
|
|
@@ -60,6 +59,10 @@ function barRules() {
|
|
|
60
59
|
gap: var(--sw-space-1);
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
.sw-site-header-compact-actions {
|
|
63
|
+
display: contents;
|
|
64
|
+
}
|
|
65
|
+
|
|
63
66
|
.sw-site-header-list {
|
|
64
67
|
list-style: none;
|
|
65
68
|
margin: 0;
|
|
@@ -101,8 +104,8 @@ function barRules() {
|
|
|
101
104
|
gap: var(--sw-space-4);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
@media ${
|
|
105
|
-
.sw-site-header-bar { padding-inline:
|
|
107
|
+
@media ${siteHeaderGutterQuery} {
|
|
108
|
+
.sw-site-header-bar { padding-inline: ${siteHeaderGutter.wide}; }
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
@media ${wide} {
|
|
@@ -118,13 +121,19 @@ function barRules() {
|
|
|
118
121
|
* open, the other pair hidden so only one is read), and its panel lies
|
|
119
122
|
* under the bar over the rest of the window, the destinations as large
|
|
120
123
|
* rows with hairlines and the menu's actions at the panel's foot. It
|
|
121
|
-
* scrolls on its own when the window is short.
|
|
124
|
+
* scrolls on its own when the window is short. While it is open the bar's
|
|
125
|
+
* compact actions step aside, since the menu holds its own actions, so the
|
|
126
|
+
* bar shows only the brand and the close button.
|
|
122
127
|
*/
|
|
123
128
|
function menuRules() {
|
|
124
129
|
return `.sw-site-header-menu > summary {
|
|
125
130
|
list-style: none;
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
.sw-site-header-compact:has(> .sw-site-header-menu[open]) > .sw-site-header-compact-actions {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
.sw-site-header-menu > summary::-webkit-details-marker {
|
|
129
138
|
display: none;
|
|
130
139
|
}
|
|
@@ -214,6 +223,10 @@ export function cssSiteHeaderClasses() {
|
|
|
214
223
|
|
|
215
224
|
${menuRules()}
|
|
216
225
|
|
|
226
|
+
${cssSiteHeaderSkipLinkRules()}
|
|
227
|
+
|
|
228
|
+
${cssSiteHeaderAnchorRules()}
|
|
229
|
+
|
|
217
230
|
/*
|
|
218
231
|
* Forced colours drop box shadows, so the current destination's bar is a
|
|
219
232
|
* thick underline there.
|
|
@@ -248,6 +261,8 @@ export function siteHeaderClassCatalog() {
|
|
|
248
261
|
'sw-site-header-nav',
|
|
249
262
|
'sw-site-header-actions',
|
|
250
263
|
'sw-site-header-compact',
|
|
264
|
+
'sw-site-header-compact-actions',
|
|
265
|
+
'sw-site-header-skip-link',
|
|
251
266
|
'sw-site-header-list',
|
|
252
267
|
'sw-site-header-link',
|
|
253
268
|
'sw-site-header-menu',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A setup checklist: an ordered list of rows a hairline apart, each a
|
|
3
|
+
* marker, the title (label type) over a muted caption detail, and an
|
|
4
|
+
* action at the row's end. The marker and the action sit on the row's
|
|
5
|
+
* middle. On a narrow column, an action that would leave the words less
|
|
6
|
+
* than half the row wraps to a line of its own at the row's end. A
|
|
7
|
+
* waiting task's title is muted at the body weight.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cssTaskListClasses(): string;
|
|
10
|
+
export declare function taskListClassCatalog(): string[];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { checkStrokeIn } from './chevron.js';
|
|
3
|
+
const body = typographyVariants.body;
|
|
4
|
+
const caption = typographyVariants.caption;
|
|
5
|
+
const label = typographyVariants.label;
|
|
6
|
+
/** The marker's side: spacing step 5 (24 px). */
|
|
7
|
+
const marker = 'var(--sw-space-5)';
|
|
8
|
+
/** The width of a ring, and of the check's and the lock's strokes. */
|
|
9
|
+
const stroke = '2px';
|
|
10
|
+
/*
|
|
11
|
+
* Each status's marker is private chrome drawn with borders and an inset
|
|
12
|
+
* ring, so the shapes differ (a check, a ring round a dot, an empty ring,
|
|
13
|
+
* a lock) and colour is never the only cue: done is the success fill with
|
|
14
|
+
* a check in the surface colour, as Steps' done marker; next an accent
|
|
15
|
+
* ring round an accent dot (a zero-size box with a step-1 border, so forced
|
|
16
|
+
* colours keep it); todo the hairline ring of a step ahead; waiting a lock
|
|
17
|
+
* in the muted colour with no ring (a shackle of three borders, 8 px
|
|
18
|
+
* square, over a 12 × 8 px body that is all border).
|
|
19
|
+
*/
|
|
20
|
+
function markerRules() {
|
|
21
|
+
return `.sw-task-list-item-todo .sw-task-list-marker {
|
|
22
|
+
box-shadow: inset 0 0 0 ${stroke} var(--sw-color-border);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.sw-task-list-item-next .sw-task-list-marker {
|
|
26
|
+
box-shadow: inset 0 0 0 ${stroke} var(--sw-color-accent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sw-task-list-item-next .sw-task-list-marker::after {
|
|
30
|
+
border: var(--sw-space-1) solid var(--sw-color-accent);
|
|
31
|
+
border-radius: var(--sw-radius-pill);
|
|
32
|
+
content: '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.sw-task-list-item-done .sw-task-list-marker {
|
|
36
|
+
background: var(--sw-color-success);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.sw-task-list-item-done .sw-task-list-marker::after {
|
|
40
|
+
${checkStrokeIn('var(--sw-color-surface)')}
|
|
41
|
+
content: '';
|
|
42
|
+
height: var(--sw-space-3);
|
|
43
|
+
margin-top: calc(-1 * var(--sw-space-1) / 2);
|
|
44
|
+
transform: rotate(45deg);
|
|
45
|
+
width: calc(var(--sw-space-3) / 2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sw-task-list-item-waiting .sw-task-list-marker {
|
|
49
|
+
color: var(--sw-color-muted);
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sw-task-list-item-waiting .sw-task-list-marker::before {
|
|
54
|
+
border: ${stroke} solid currentColor;
|
|
55
|
+
border-bottom: 0;
|
|
56
|
+
border-radius: var(--sw-space-1) var(--sw-space-1) 0 0;
|
|
57
|
+
content: '';
|
|
58
|
+
height: calc(var(--sw-space-2) - ${stroke});
|
|
59
|
+
width: calc(var(--sw-space-2) - 2 * ${stroke});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.sw-task-list-item-waiting .sw-task-list-marker::after {
|
|
63
|
+
border: var(--sw-space-1) solid currentColor;
|
|
64
|
+
border-radius: calc(var(--sw-space-1) / 2);
|
|
65
|
+
content: '';
|
|
66
|
+
width: calc(var(--sw-space-3) - 2 * var(--sw-space-1));
|
|
67
|
+
}`;
|
|
68
|
+
}
|
|
69
|
+
/* Forced colours drop fills and inset shadows: every ring becomes a
|
|
70
|
+
border, the next one Highlight, and the strokes take CanvasText. */
|
|
71
|
+
function forcedColorRules() {
|
|
72
|
+
return `@media (forced-colors: active) {
|
|
73
|
+
.sw-task-list-item:not(.sw-task-list-item-waiting) .sw-task-list-marker {
|
|
74
|
+
border: ${stroke} solid CanvasText;
|
|
75
|
+
box-shadow: none;
|
|
76
|
+
}
|
|
77
|
+
.sw-task-list-item-next .sw-task-list-marker { border-color: Highlight; }
|
|
78
|
+
.sw-task-list-item-next .sw-task-list-marker::after { border-color: Highlight; }
|
|
79
|
+
.sw-task-list-item-done .sw-task-list-marker::after { border-color: CanvasText; }
|
|
80
|
+
.sw-task-list-item-waiting .sw-task-list-marker { color: CanvasText; }
|
|
81
|
+
.sw-task-list-item-waiting .sw-task-list-title { color: GrayText; }
|
|
82
|
+
}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A setup checklist: an ordered list of rows a hairline apart, each a
|
|
86
|
+
* marker, the title (label type) over a muted caption detail, and an
|
|
87
|
+
* action at the row's end. The marker and the action sit on the row's
|
|
88
|
+
* middle. On a narrow column, an action that would leave the words less
|
|
89
|
+
* than half the row wraps to a line of its own at the row's end. A
|
|
90
|
+
* waiting task's title is muted at the body weight.
|
|
91
|
+
*/
|
|
92
|
+
export function cssTaskListClasses() {
|
|
93
|
+
return `.sw-task-list {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
list-style: none;
|
|
97
|
+
margin: 0;
|
|
98
|
+
min-width: 0;
|
|
99
|
+
padding: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.sw-task-list-item {
|
|
103
|
+
align-items: center;
|
|
104
|
+
color: var(--sw-color-text);
|
|
105
|
+
column-gap: var(--sw-space-3);
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
font-family: var(--sw-font-sans);
|
|
109
|
+
min-width: 0;
|
|
110
|
+
padding-block: var(--sw-space-2);
|
|
111
|
+
row-gap: var(--sw-space-2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sw-task-list-item + .sw-task-list-item {
|
|
115
|
+
border-top: 1px solid var(--sw-color-border);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.sw-task-list-marker {
|
|
119
|
+
align-items: center;
|
|
120
|
+
border-radius: var(--sw-radius-pill);
|
|
121
|
+
box-sizing: border-box;
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
flex: none;
|
|
124
|
+
height: ${marker};
|
|
125
|
+
justify-content: center;
|
|
126
|
+
width: ${marker};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.sw-task-list-body {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex: 1 1 0;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
min-width: 50%;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sw-task-list-title {
|
|
137
|
+
font-size: ${label.fontSize}px;
|
|
138
|
+
font-weight: ${label.fontWeight};
|
|
139
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
140
|
+
line-height: ${label.lineHeight}px;
|
|
141
|
+
overflow-wrap: anywhere;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.sw-task-list-item-waiting .sw-task-list-title {
|
|
145
|
+
color: var(--sw-color-muted);
|
|
146
|
+
font-weight: ${body.fontWeight};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.sw-task-list-detail {
|
|
150
|
+
color: var(--sw-color-muted);
|
|
151
|
+
font-size: ${caption.fontSize}px;
|
|
152
|
+
letter-spacing: ${caption.letterSpacing}px;
|
|
153
|
+
line-height: ${caption.lineHeight}px;
|
|
154
|
+
overflow-wrap: anywhere;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.sw-task-list-action {
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
flex: none;
|
|
160
|
+
margin-inline-start: auto;
|
|
161
|
+
max-width: 100%;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
${markerRules()}
|
|
165
|
+
|
|
166
|
+
${forcedColorRules()}`;
|
|
167
|
+
}
|
|
168
|
+
export function taskListClassCatalog() {
|
|
169
|
+
return [
|
|
170
|
+
'sw-task-list',
|
|
171
|
+
'sw-task-list-item',
|
|
172
|
+
'sw-task-list-item-done',
|
|
173
|
+
'sw-task-list-item-next',
|
|
174
|
+
'sw-task-list-item-todo',
|
|
175
|
+
'sw-task-list-item-waiting',
|
|
176
|
+
'sw-task-list-marker',
|
|
177
|
+
'sw-task-list-body',
|
|
178
|
+
'sw-task-list-title',
|
|
179
|
+
'sw-task-list-detail',
|
|
180
|
+
'sw-task-list-action',
|
|
181
|
+
];
|
|
182
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The bar's height below `lg`: 56 px. */
|
|
2
|
+
export declare const siteHeaderCompactHeight = "calc(var(--sw-control-md-min-height) + var(--sw-space-3))";
|
|
3
|
+
/** The bar's height from `lg`: 72 px. */
|
|
4
|
+
export declare const siteHeaderWideHeight = "calc(var(--sw-control-md-min-height) + var(--sw-space-5) + var(--sw-space-1))";
|
|
5
|
+
/** The bar's content: the page container plus the page gutter each side. */
|
|
6
|
+
export declare const siteHeaderBarMaxWidth = "calc(var(--sw-container-max) + 2 * var(--sw-space-5))";
|
|
7
|
+
/** From here the bar is the wide one with its row of destinations. */
|
|
8
|
+
export declare const siteHeaderWideQuery: string;
|
|
9
|
+
/** From here the bar's gutter is the wider one. */
|
|
10
|
+
export declare const siteHeaderGutterQuery: string;
|
|
11
|
+
/** The bar's gutter below `md` (`compact`) and from it (`wide`). */
|
|
12
|
+
export declare const siteHeaderGutter: {
|
|
13
|
+
readonly compact: "var(--sw-space-4)";
|
|
14
|
+
readonly wide: "var(--sw-space-5)";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Where the bar's content starts, measured from the header's start: the
|
|
18
|
+
* centred bar's margin (none on a window narrower than the bar) plus its
|
|
19
|
+
* gutter, so something laid over the bar lines up with the brand.
|
|
20
|
+
*/
|
|
21
|
+
export declare function siteHeaderContentStart(gutter: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
2
|
+
/*
|
|
3
|
+
* The site header's measures, shared by the bar, its skip link and the
|
|
4
|
+
* document's scroll padding so all three agree on where the bar ends.
|
|
5
|
+
*/
|
|
6
|
+
/** The bar's height below `lg`: 56 px. */
|
|
7
|
+
export const siteHeaderCompactHeight = 'calc(var(--sw-control-md-min-height) + var(--sw-space-3))';
|
|
8
|
+
/** The bar's height from `lg`: 72 px. */
|
|
9
|
+
export const siteHeaderWideHeight = 'calc(var(--sw-control-md-min-height) + var(--sw-space-5) + var(--sw-space-1))';
|
|
10
|
+
/** The bar's content: the page container plus the page gutter each side. */
|
|
11
|
+
export const siteHeaderBarMaxWidth = 'calc(var(--sw-container-max) + 2 * var(--sw-space-5))';
|
|
12
|
+
/** From here the bar is the wide one with its row of destinations. */
|
|
13
|
+
export const siteHeaderWideQuery = breakpointQuery('from', 'lg');
|
|
14
|
+
/** From here the bar's gutter is the wider one. */
|
|
15
|
+
export const siteHeaderGutterQuery = breakpointQuery('from', 'md');
|
|
16
|
+
/** The bar's gutter below `md` (`compact`) and from it (`wide`). */
|
|
17
|
+
export const siteHeaderGutter = {
|
|
18
|
+
compact: 'var(--sw-space-4)',
|
|
19
|
+
wide: 'var(--sw-space-5)',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Where the bar's content starts, measured from the header's start: the
|
|
23
|
+
* centred bar's margin (none on a window narrower than the bar) plus its
|
|
24
|
+
* gutter, so something laid over the bar lines up with the brand.
|
|
25
|
+
*/
|
|
26
|
+
export function siteHeaderContentStart(gutter) {
|
|
27
|
+
return `calc(max(0px, (100% - ${siteHeaderBarMaxWidth}) / 2) + ${gutter})`;
|
|
28
|
+
}
|
package/dist/css/stylesheet.js
CHANGED
|
@@ -45,6 +45,7 @@ import { scrollRegionClassCatalog } from './css-scroll-region.js';
|
|
|
45
45
|
import { fieldClassCatalog } from './css-field.js';
|
|
46
46
|
import { stepperClassCatalog } from './css-stepper.js';
|
|
47
47
|
import { stepsClassCatalog } from './css-steps.js';
|
|
48
|
+
import { taskListClassCatalog } from './css-task-list.js';
|
|
48
49
|
import { boxBorderClassCatalog } from './css-box-border.js';
|
|
49
50
|
import { boxLinkClassCatalog } from './css-box-link.js';
|
|
50
51
|
import { cssComponentClasses } from './css-components.js';
|
|
@@ -158,6 +159,7 @@ export function utilityClassCatalog() {
|
|
|
158
159
|
...fieldClassCatalog(),
|
|
159
160
|
...stepperClassCatalog(),
|
|
160
161
|
...stepsClassCatalog(),
|
|
162
|
+
...taskListClassCatalog(),
|
|
161
163
|
...splitClassCatalog(),
|
|
162
164
|
...toastClassCatalog(),
|
|
163
165
|
...toastCardClassCatalog(),
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { Field, fieldVariants, type FieldProps, type FieldSize, type FieldVarian
|
|
|
49
49
|
export { fieldWidths, type FieldWidth } from './css/css-field-width.js';
|
|
50
50
|
export { requiredMarks, type RequiredMark, } from './components/FieldLabelText.js';
|
|
51
51
|
export { Step, type StepHeadingLevel, type StepProps, Steps, type StepsLabels, type StepsProps, type StepStatus, } from './components/Steps.js';
|
|
52
|
+
export { defaultTaskListLabels, TaskList, type TaskListActionContext, type TaskListItem, type TaskListLabels, type TaskListProps, type TaskListStatus, taskListStatuses, } from './components/TaskList.js';
|
|
52
53
|
export { Stepper, stepperLimitAppearances, type StepperLimitAppearance, type StepperProps, type StepperWidth, } from './components/Stepper.js';
|
|
53
54
|
export { Inline, type InlineProps } from './components/Inline.js';
|
|
54
55
|
export { Split } from './components/Split.js';
|
|
@@ -80,7 +81,7 @@ export { breakpointQuery } from './css/breakpoints.js';
|
|
|
80
81
|
export { Band, bandElements, bandTones, bandWidths, } from './components/Band.js';
|
|
81
82
|
export type { BandElement, BandProps, BandTone, BandWidth, } from './components/Band.js';
|
|
82
83
|
export { SiteHeader } from './components/SiteHeader.js';
|
|
83
|
-
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './components/SiteHeader.js';
|
|
84
|
+
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, SiteHeaderSkipLink, } from './components/SiteHeader.js';
|
|
84
85
|
export { ScreenPreview, screenPreviewFrames, } from './components/ScreenPreview.js';
|
|
85
86
|
export type { ScreenPreviewFrame, ScreenPreviewProps, } from './components/ScreenPreview.js';
|
|
86
87
|
export { PreviewGroup } from './components/screen-preview/PreviewGroup.js';
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export { Field, fieldVariants, } from './components/Field.js';
|
|
|
47
47
|
export { fieldWidths } from './css/css-field-width.js';
|
|
48
48
|
export { requiredMarks, } from './components/FieldLabelText.js';
|
|
49
49
|
export { Step, Steps, } from './components/Steps.js';
|
|
50
|
+
export { defaultTaskListLabels, TaskList, taskListStatuses, } from './components/TaskList.js';
|
|
50
51
|
export { Stepper, stepperLimitAppearances, } from './components/Stepper.js';
|
|
51
52
|
export { Inline } from './components/Inline.js';
|
|
52
53
|
export { Split } from './components/Split.js';
|
package/dist/styles.css
CHANGED
|
@@ -986,6 +986,9 @@ body {
|
|
|
986
986
|
.sw-button-xs { min-height: 28px; padding-inline: 10px; }
|
|
987
987
|
.sw-button-sm { min-height: 32px; padding-inline: 12px; }
|
|
988
988
|
.sw-button-md { min-height: 44px; padding-inline: 16px; }
|
|
989
|
+
@media (pointer: coarse) {
|
|
990
|
+
.sw-button { min-height: var(--sw-control-md-min-height); min-width: var(--sw-control-md-min-height); }
|
|
991
|
+
}
|
|
989
992
|
.sw-button-full {
|
|
990
993
|
width: 100%;
|
|
991
994
|
}
|
|
@@ -4936,6 +4939,13 @@ body {
|
|
|
4936
4939
|
min-height: var(--sw-control-md-min-height);
|
|
4937
4940
|
}
|
|
4938
4941
|
|
|
4942
|
+
[data-theme] .sw-band-inverse :is(.sw-site-footer-legal, .sw-site-footer-end) > a:not(.sw-button) {
|
|
4943
|
+
align-items: center;
|
|
4944
|
+
display: inline-flex;
|
|
4945
|
+
justify-content: center;
|
|
4946
|
+
min-height: var(--sw-control-md-min-height);
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4939
4949
|
.sw-site-footer-links {
|
|
4940
4950
|
gap: 0;
|
|
4941
4951
|
}
|
|
@@ -4995,6 +5005,10 @@ body {
|
|
|
4995
5005
|
gap: var(--sw-space-1);
|
|
4996
5006
|
}
|
|
4997
5007
|
|
|
5008
|
+
.sw-site-header-compact-actions {
|
|
5009
|
+
display: contents;
|
|
5010
|
+
}
|
|
5011
|
+
|
|
4998
5012
|
.sw-site-header-list {
|
|
4999
5013
|
list-style: none;
|
|
5000
5014
|
margin: 0;
|
|
@@ -5051,6 +5065,10 @@ body {
|
|
|
5051
5065
|
list-style: none;
|
|
5052
5066
|
}
|
|
5053
5067
|
|
|
5068
|
+
.sw-site-header-compact:has(> .sw-site-header-menu[open]) > .sw-site-header-compact-actions {
|
|
5069
|
+
display: none;
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5054
5072
|
.sw-site-header-menu > summary::-webkit-details-marker {
|
|
5055
5073
|
display: none;
|
|
5056
5074
|
}
|
|
@@ -5135,6 +5153,52 @@ body {
|
|
|
5135
5153
|
padding-top: var(--sw-space-5);
|
|
5136
5154
|
}
|
|
5137
5155
|
|
|
5156
|
+
.sw-site-header-skip-link {
|
|
5157
|
+
align-items: center;
|
|
5158
|
+
background: var(--sw-color-surface);
|
|
5159
|
+
border-radius: var(--sw-radius-pill);
|
|
5160
|
+
box-sizing: border-box;
|
|
5161
|
+
display: inline-flex;
|
|
5162
|
+
font-family: var(--sw-font-sans);
|
|
5163
|
+
font-size: 15px;
|
|
5164
|
+
font-weight: 600;
|
|
5165
|
+
inset-block-start: 50%;
|
|
5166
|
+
inset-inline-start: calc(max(0px, (100% - calc(var(--sw-container-max) + 2 * var(--sw-space-5))) / 2) + var(--sw-space-4));
|
|
5167
|
+
letter-spacing: -0.2px;
|
|
5168
|
+
line-height: 20px;
|
|
5169
|
+
padding-inline: var(--sw-space-4);
|
|
5170
|
+
position: absolute;
|
|
5171
|
+
transform: translateY(-50%);
|
|
5172
|
+
white-space: nowrap;
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
[data-theme] .sw-site-header-skip-link {
|
|
5176
|
+
color: var(--sw-color-text);
|
|
5177
|
+
text-decoration: none;
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
[data-theme] .sw-site-header-skip-link:focus {
|
|
5181
|
+
min-height: var(--sw-control-md-min-height);
|
|
5182
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
5183
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
.sw-site-header-skip-link:not(:focus) { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
5187
|
+
|
|
5188
|
+
@media (min-width: 48rem) {
|
|
5189
|
+
.sw-site-header-skip-link { inset-inline-start: calc(max(0px, (100% - calc(var(--sw-container-max) + 2 * var(--sw-space-5))) / 2) + var(--sw-space-5)); }
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
html:has(.sw-site-header-sticky) {
|
|
5193
|
+
scroll-padding-top: calc(calc(var(--sw-control-md-min-height) + var(--sw-space-3)) + 1px + var(--sw-space-3));
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
@media (min-width: 64rem) {
|
|
5197
|
+
html:has(.sw-site-header-sticky) {
|
|
5198
|
+
scroll-padding-top: calc(calc(var(--sw-control-md-min-height) + var(--sw-space-5) + var(--sw-space-1)) + 1px + var(--sw-space-3));
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5138
5202
|
/*
|
|
5139
5203
|
* Forced colours drop box shadows, so the current destination's bar is a
|
|
5140
5204
|
* thick underline there.
|
|
@@ -9493,6 +9557,138 @@ body {
|
|
|
9493
9557
|
.sw-steps-item-skipped .sw-steps-title { color: GrayText; }
|
|
9494
9558
|
}
|
|
9495
9559
|
|
|
9560
|
+
.sw-task-list {
|
|
9561
|
+
display: flex;
|
|
9562
|
+
flex-direction: column;
|
|
9563
|
+
list-style: none;
|
|
9564
|
+
margin: 0;
|
|
9565
|
+
min-width: 0;
|
|
9566
|
+
padding: 0;
|
|
9567
|
+
}
|
|
9568
|
+
|
|
9569
|
+
.sw-task-list-item {
|
|
9570
|
+
align-items: center;
|
|
9571
|
+
color: var(--sw-color-text);
|
|
9572
|
+
column-gap: var(--sw-space-3);
|
|
9573
|
+
display: flex;
|
|
9574
|
+
flex-wrap: wrap;
|
|
9575
|
+
font-family: var(--sw-font-sans);
|
|
9576
|
+
min-width: 0;
|
|
9577
|
+
padding-block: var(--sw-space-2);
|
|
9578
|
+
row-gap: var(--sw-space-2);
|
|
9579
|
+
}
|
|
9580
|
+
|
|
9581
|
+
.sw-task-list-item + .sw-task-list-item {
|
|
9582
|
+
border-top: 1px solid var(--sw-color-border);
|
|
9583
|
+
}
|
|
9584
|
+
|
|
9585
|
+
.sw-task-list-marker {
|
|
9586
|
+
align-items: center;
|
|
9587
|
+
border-radius: var(--sw-radius-pill);
|
|
9588
|
+
box-sizing: border-box;
|
|
9589
|
+
display: inline-flex;
|
|
9590
|
+
flex: none;
|
|
9591
|
+
height: var(--sw-space-5);
|
|
9592
|
+
justify-content: center;
|
|
9593
|
+
width: var(--sw-space-5);
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9596
|
+
.sw-task-list-body {
|
|
9597
|
+
display: flex;
|
|
9598
|
+
flex: 1 1 0;
|
|
9599
|
+
flex-direction: column;
|
|
9600
|
+
min-width: 50%;
|
|
9601
|
+
}
|
|
9602
|
+
|
|
9603
|
+
.sw-task-list-title {
|
|
9604
|
+
font-size: 15px;
|
|
9605
|
+
font-weight: 600;
|
|
9606
|
+
letter-spacing: -0.2px;
|
|
9607
|
+
line-height: 20px;
|
|
9608
|
+
overflow-wrap: anywhere;
|
|
9609
|
+
}
|
|
9610
|
+
|
|
9611
|
+
.sw-task-list-item-waiting .sw-task-list-title {
|
|
9612
|
+
color: var(--sw-color-muted);
|
|
9613
|
+
font-weight: 400;
|
|
9614
|
+
}
|
|
9615
|
+
|
|
9616
|
+
.sw-task-list-detail {
|
|
9617
|
+
color: var(--sw-color-muted);
|
|
9618
|
+
font-size: 13px;
|
|
9619
|
+
letter-spacing: -0.08px;
|
|
9620
|
+
line-height: 18px;
|
|
9621
|
+
overflow-wrap: anywhere;
|
|
9622
|
+
}
|
|
9623
|
+
|
|
9624
|
+
.sw-task-list-action {
|
|
9625
|
+
display: inline-flex;
|
|
9626
|
+
flex: none;
|
|
9627
|
+
margin-inline-start: auto;
|
|
9628
|
+
max-width: 100%;
|
|
9629
|
+
}
|
|
9630
|
+
|
|
9631
|
+
.sw-task-list-item-todo .sw-task-list-marker {
|
|
9632
|
+
box-shadow: inset 0 0 0 2px var(--sw-color-border);
|
|
9633
|
+
}
|
|
9634
|
+
|
|
9635
|
+
.sw-task-list-item-next .sw-task-list-marker {
|
|
9636
|
+
box-shadow: inset 0 0 0 2px var(--sw-color-accent);
|
|
9637
|
+
}
|
|
9638
|
+
|
|
9639
|
+
.sw-task-list-item-next .sw-task-list-marker::after {
|
|
9640
|
+
border: var(--sw-space-1) solid var(--sw-color-accent);
|
|
9641
|
+
border-radius: var(--sw-radius-pill);
|
|
9642
|
+
content: '';
|
|
9643
|
+
}
|
|
9644
|
+
|
|
9645
|
+
.sw-task-list-item-done .sw-task-list-marker {
|
|
9646
|
+
background: var(--sw-color-success);
|
|
9647
|
+
}
|
|
9648
|
+
|
|
9649
|
+
.sw-task-list-item-done .sw-task-list-marker::after {
|
|
9650
|
+
border-bottom: 2px solid var(--sw-color-surface);
|
|
9651
|
+
border-right: 2px solid var(--sw-color-surface);
|
|
9652
|
+
content: '';
|
|
9653
|
+
height: var(--sw-space-3);
|
|
9654
|
+
margin-top: calc(-1 * var(--sw-space-1) / 2);
|
|
9655
|
+
transform: rotate(45deg);
|
|
9656
|
+
width: calc(var(--sw-space-3) / 2);
|
|
9657
|
+
}
|
|
9658
|
+
|
|
9659
|
+
.sw-task-list-item-waiting .sw-task-list-marker {
|
|
9660
|
+
color: var(--sw-color-muted);
|
|
9661
|
+
flex-direction: column;
|
|
9662
|
+
}
|
|
9663
|
+
|
|
9664
|
+
.sw-task-list-item-waiting .sw-task-list-marker::before {
|
|
9665
|
+
border: 2px solid currentColor;
|
|
9666
|
+
border-bottom: 0;
|
|
9667
|
+
border-radius: var(--sw-space-1) var(--sw-space-1) 0 0;
|
|
9668
|
+
content: '';
|
|
9669
|
+
height: calc(var(--sw-space-2) - 2px);
|
|
9670
|
+
width: calc(var(--sw-space-2) - 2 * 2px);
|
|
9671
|
+
}
|
|
9672
|
+
|
|
9673
|
+
.sw-task-list-item-waiting .sw-task-list-marker::after {
|
|
9674
|
+
border: var(--sw-space-1) solid currentColor;
|
|
9675
|
+
border-radius: calc(var(--sw-space-1) / 2);
|
|
9676
|
+
content: '';
|
|
9677
|
+
width: calc(var(--sw-space-3) - 2 * var(--sw-space-1));
|
|
9678
|
+
}
|
|
9679
|
+
|
|
9680
|
+
@media (forced-colors: active) {
|
|
9681
|
+
.sw-task-list-item:not(.sw-task-list-item-waiting) .sw-task-list-marker {
|
|
9682
|
+
border: 2px solid CanvasText;
|
|
9683
|
+
box-shadow: none;
|
|
9684
|
+
}
|
|
9685
|
+
.sw-task-list-item-next .sw-task-list-marker { border-color: Highlight; }
|
|
9686
|
+
.sw-task-list-item-next .sw-task-list-marker::after { border-color: Highlight; }
|
|
9687
|
+
.sw-task-list-item-done .sw-task-list-marker::after { border-color: CanvasText; }
|
|
9688
|
+
.sw-task-list-item-waiting .sw-task-list-marker { color: CanvasText; }
|
|
9689
|
+
.sw-task-list-item-waiting .sw-task-list-title { color: GrayText; }
|
|
9690
|
+
}
|
|
9691
|
+
|
|
9496
9692
|
.sw-split {
|
|
9497
9693
|
align-items: stretch;
|
|
9498
9694
|
align-self: start;
|