@scalewing/react 1.39.0 → 1.40.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 +9 -0
- package/README.md +102 -0
- package/dist/calendar-month.d.ts +6 -0
- package/dist/calendar-month.js +15 -0
- package/dist/components/DateRangeButton.d.ts +65 -0
- package/dist/components/DateRangeButton.js +54 -0
- package/dist/components/ListGroup.d.ts +33 -0
- package/dist/components/ListGroup.js +19 -0
- package/dist/components/ListRow.d.ts +51 -0
- package/dist/components/ListRow.js +59 -0
- package/dist/components/NavCount.d.ts +21 -0
- package/dist/components/NavCount.js +23 -0
- package/dist/components/SectionNav.js +1 -8
- package/dist/components/StatTile.d.ts +11 -4
- package/dist/components/StatTile.js +11 -4
- package/dist/components/{date-field → calendar}/CalendarGrid.d.ts +17 -2
- package/dist/components/{date-field → calendar}/CalendarGrid.js +11 -3
- package/dist/components/calendar/CalendarPopover.d.ts +25 -0
- package/dist/components/calendar/CalendarPopover.js +22 -0
- package/dist/components/calendar/MonthStep.d.ts +16 -0
- package/dist/components/calendar/MonthStep.js +17 -0
- package/dist/components/calendar/use-grid-focus.d.ts +18 -0
- package/dist/components/calendar/use-grid-focus.js +35 -0
- package/dist/components/calendar/wrap-tab.d.ts +7 -0
- package/dist/components/calendar/wrap-tab.js +29 -0
- package/dist/components/date-field/CalendarDialog.js +9 -67
- package/dist/components/date-field/CalendarHeader.js +2 -11
- package/dist/components/date-range/DateRangePopover.d.ts +7 -0
- package/dist/components/date-range/DateRangePopover.js +17 -0
- package/dist/components/date-range/DateRangeSheet.d.ts +7 -0
- package/dist/components/date-range/DateRangeSheet.js +15 -0
- package/dist/components/date-range/RangeMonths.d.ts +28 -0
- package/dist/components/date-range/RangeMonths.js +50 -0
- package/dist/components/date-range/RangePresets.d.ts +16 -0
- package/dist/components/date-range/RangePresets.js +10 -0
- package/dist/components/date-range/RangeSummary.d.ts +15 -0
- package/dist/components/date-range/RangeSummary.js +11 -0
- package/dist/components/date-range/range-panel-props.d.ts +31 -0
- package/dist/components/date-range/range-panel-props.js +1 -0
- package/dist/components/date-range/use-range-popup.d.ts +21 -0
- package/dist/components/date-range/use-range-popup.js +53 -0
- package/dist/components/link-press.d.ts +7 -0
- package/dist/components/link-press.js +13 -0
- package/dist/components/list/ListRowAccessoryMark.d.ts +5 -0
- package/dist/components/list/ListRowAccessoryMark.js +7 -0
- package/dist/components/list/ListRowContent.d.ts +20 -0
- package/dist/components/list/ListRowContent.js +10 -0
- package/dist/components/list/list-row-container.d.ts +5 -0
- package/dist/components/list/list-row-container.js +6 -0
- package/dist/components/use-media-query.d.ts +5 -0
- package/dist/components/use-media-query.js +18 -0
- package/dist/css/chevron.d.ts +6 -0
- package/dist/css/chevron.js +14 -0
- package/dist/css/css-button-group-attached.js +8 -4
- package/dist/css/css-button-group-track.js +1 -1
- package/dist/css/css-checkbox.js +2 -2
- package/dist/css/css-components.js +9 -0
- package/dist/css/css-count-pill.d.ts +2 -0
- package/dist/css/css-count-pill.js +54 -0
- package/dist/css/css-date-field.js +3 -2
- package/dist/css/css-date-range-presets.d.ts +2 -0
- package/dist/css/css-date-range-presets.js +109 -0
- package/dist/css/css-date-range.d.ts +2 -0
- package/dist/css/css-date-range.js +197 -0
- package/dist/css/css-list-row.d.ts +2 -0
- package/dist/css/css-list-row.js +193 -0
- package/dist/css/css-list.d.ts +3 -0
- package/dist/css/css-list.js +85 -0
- package/dist/css/css-scroll-region.d.ts +18 -0
- package/dist/css/css-scroll-region.js +41 -0
- package/dist/css/css-section-nav-filled.js +2 -25
- package/dist/css/css-stat-tile.d.ts +7 -0
- package/dist/css/css-stat-tile.js +16 -0
- package/dist/css/css-steps.js +2 -2
- package/dist/css/css-table-bounded-edges.js +4 -27
- package/dist/css/range-band.d.ts +2 -0
- package/dist/css/range-band.js +7 -0
- package/dist/css/stacking.js +2 -2
- package/dist/css/stylesheet.js +6 -0
- package/dist/date-range-draft.d.ts +26 -0
- package/dist/date-range-draft.js +36 -0
- package/dist/date-range-input.d.ts +26 -0
- package/dist/date-range-input.js +58 -0
- package/dist/date-range-labels.d.ts +22 -0
- package/dist/date-range-labels.js +26 -0
- package/dist/date-range-view.d.ts +12 -0
- package/dist/date-range-view.js +29 -0
- package/dist/date-range-words.d.ts +11 -0
- package/dist/date-range-words.js +40 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/list-options.d.ts +70 -0
- package/dist/list-options.js +77 -0
- package/dist/styles.css +507 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 935ff2a: `DateRangeButton`: a button showing a range of days (`value` `{ start, end }`, `YYYY-MM-DD`) that opens a calendar to pick a first and a last day. From `md` up it is a popover with the `presets` (`DateRangePreset`: `id`, `label`, `range`; a press applies it at once) beside two months (above them below `lg`), the range being picked in a polite status, and Cancel and Apply; below `md` it is a Dialog bottom sheet with the presets in a row and one month. The first press sets the start and the second the end; days after `max` are disabled, and once a start is pressed so are the days past `maxDays`. Apply calls `onChange` with a changed range; Cancel, Escape, a press outside and the sheet's close keep the value. Its name is `label`, `labels.nameSeparator` and the range it shows. `labels` (`DateRangeButtonLabels`) holds every word, `dayCount` included; `min`, `today`, `weekStartsOn`, `locale`, `disabled`, `id`, `size` and `variant` (default `neutral`) as on CalendarButton. Input it cannot draw throws a `RangeError`. New exports `DateRangeButton`, `DateRangeButtonProps`, `DateRangeButtonValue`, `DateRangePreset`, `DateRangeButtonLabels`; new `sw-date-range-*` classes. It shares CalendarButton's private calendar grid, popover and month steps, whose public behaviour is unchanged. `ButtonGroup attached` now finds its buttons by type, so an open calendar beside one keeps the group's dividers and corners. No new tokens, no new dependency. Request: `docs/requests/teisoro-date-range-button.md`.
|
|
8
|
+
- 3192e17: Web `ListGroup` and `ListRow`, the counterpart of react-native's. `ListGroup` is a list (`ul`, role kept) of rows with hairlines between them, a bordered `panel` or `plain` rows inside a surface, named by `aria-label` or `aria-labelledby`; `maxHeight` (`{ rows }`) bounds it to rows of a title and one detail line that scroll inside a named, focusable region shaded at an edge with more past it, and `footer` stays pinned under the rows, such as a total. `ListRow` has a `title` (`titleAppearance` `strong` or `plain`, `tone` `danger`), a muted `detail`, a `value` with `valueDetail` and `valueTone` (a table cell's tones) and a hidden `leading` mark; `onPress` or `href` makes the whole row a button or link with a chevron, named by its title and value and described by its details (`href` with `onPress` takes a plain press for a client router); `selected` makes it a choice with a check (`aria-pressed` or `aria-current`); `accessory="none"` hides the chevron; `disabled` blocks presses. Input it cannot draw throws a `RangeError`. New exports `ListGroup`, `ListGroupProps`, `ListRow`, `ListRowProps`, `listGroupVariants`, `listRowTones`, `listRowTitleAppearances`, `listRowValueTones`, `ListGroupMaxHeight`, `ListGroupVariant`, `ListRowTone`, `ListRowTitleAppearance`, `ListRowValueTone`; new `sw-list*` classes; no new tokens, no new dependency. Request: `docs/requests/teisoro-list-group.md`.
|
|
9
|
+
- 1d3fafd: `NavCount`: a count after a Nav destination's words, placed inside its link or Button (`<a href="/tasks">Tasks<NavCount count={8} /></a>`), such as the tasks waiting in an area. It is SectionNav's count pill, now one shared rule, and it is read in the destination's name ("Tasks 8"); `countLabel` is read in place of the bare number ("Tasks 8 tasks to do") with the pill hidden from assistive technology; a `count` of 0 shows nothing. A count that is not a whole number from 0 or a blank `countLabel` throws a `RangeError`. New exports `NavCount`, `NavCountProps`; new class `sw-nav-count`; no new tokens, no new dependency. Request: `docs/requests/teisoro-nav-count.md`.
|
|
10
|
+
- 62c33b1: `StatTile` `appearance="plain"` (`StatTileAppearance`, `statTileAppearances`; default `card`): the figure without the tile's surface, edge, radius or padding, top-aligned, for figures inside a card, such as four answers in a row divided by a composed `Separator`. A plain tile cannot be `emphasis="primary"`; that or an unknown appearance throws a `RangeError`. New class `sw-stat-tile-plain`; no new tokens, no new dependency. Request: `docs/requests/teisoro-stat-tile-plain.md`.
|
|
11
|
+
|
|
3
12
|
## 1.39.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Box,
|
|
18
18
|
Button,
|
|
19
19
|
CalendarButton,
|
|
20
|
+
DateRangeButton,
|
|
20
21
|
Card,
|
|
21
22
|
Checkbox,
|
|
22
23
|
RadioGroup,
|
|
@@ -29,6 +30,8 @@ import {
|
|
|
29
30
|
Separator,
|
|
30
31
|
DescriptionList,
|
|
31
32
|
DescriptionItem,
|
|
33
|
+
ListGroup,
|
|
34
|
+
ListRow,
|
|
32
35
|
DenominationGrid,
|
|
33
36
|
FilterChips,
|
|
34
37
|
Grid,
|
|
@@ -375,6 +378,20 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
375
378
|
|
|
376
379
|
`appearance="words"` (opt-in; default `"default"`, `navAppearances`) draws every destination, a link or a `Button` of any variant, as plain words: no fill, border or underline, label type, the others in the muted color at medium weight and the current one (`aria-current`, not `"false"`) in the text color at label weight. A Button keeps its size, padding, touch target and focus ring; hover lifts a destination to the text color where the pointer can hover, never a disabled one. The current mark is color and weight, so each destination's words must be visible: a glyph-only destination has no weight to change (keep `default` for a glyph rail). Forced colors draw the current one's border in Highlight and underline its words. An unknown value throws a `RangeError`.
|
|
377
380
|
|
|
381
|
+
`NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. A count that is not a whole number from 0, or a blank `countLabel`, throws a `RangeError`.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
<Nav appearance="words" aria-label="Areas">
|
|
385
|
+
<Box as="a" href="/sightings">
|
|
386
|
+
Sightings
|
|
387
|
+
</Box>
|
|
388
|
+
<Box as="a" href="/tasks" aria-current="page">
|
|
389
|
+
Tasks
|
|
390
|
+
<NavCount count={8} countLabel="8 tasks to do" />
|
|
391
|
+
</Box>
|
|
392
|
+
</Nav>
|
|
393
|
+
```
|
|
394
|
+
|
|
378
395
|
`SectionNav` is the navigation between the sections of one area, such as a portal's pages: a labelled `nav` (`aria-label` or `aria-labelledby`) of links from `items` (`{ id, label, href, current?, icon?, count? }`), quieter than a workspace's `Nav`. `current` is `'page'`, `'location'` (a page inside that section, such as a detail page under its list) or a boolean, written as `aria-current`; the current item is in the text color over an accent underline, or, from `verticalFrom` (a breakpoint) up, where the items stack as a side list, a bar at its start. `icon` is a decorative glyph before the label. `onNavigate` takes a plain press after preventing the browser's navigation, for a client router; a press with a modifier key or the middle button keeps the browser's behavior, and without `onNavigate` the links carry no handler, so a server component can render the nav. `count` (a whole number) is how many things wait in a section, such as tasks: a filled pill after the label, in the danger color with its on-color figures, read as part of the link's name ("Tasks 3"); 0 shows none, and there is no cap. `countLabel` says what the count means in your words ("3 tasks to do"): it is read in place of the bare number and the pill is hidden from assistive technology. `appearance="filled"` (default `"underline"`) is a quiet row of page buttons for an area's sub-navigation beside its name: no hairline and no underline or start bar, every link the `sm` control with the small radius at weight 500, the current one on a soft muted fill in the text color at 600 (a 2 px `Highlight` edge in forced colors), in the row or the side list. A coarse pointer gets 44 px targets. Empty or duplicate ids, empty labels, a `count` that is not a whole number from 0, a blank `countLabel` or one without a `count`, and an unknown `verticalFrom` or `appearance` throw a `RangeError`. Use `Tabs` for panels on one page.
|
|
379
396
|
|
|
380
397
|
```tsx
|
|
@@ -620,6 +637,65 @@ A bar in its own column reads as empty cells to assistive technology, which is f
|
|
|
620
637
|
</DescriptionList>
|
|
621
638
|
```
|
|
622
639
|
|
|
640
|
+
`StatTile` is one prominent figure with its label: a `label`, a consumer-formatted `value` (tabular figures; `tone` `accent`, `success`, `danger` or `warning` colours it), an optional `caption` under it and an optional `glyph` in a tinted circle. `emphasis="primary"` fills the one figure a page leads with in the accent. `appearance="plain"` (default `"card"`, `statTileAppearances`) drops the tile's surface, edge, radius and padding, top-aligned, for figures inside a card that already has them, such as four answers in a row divided by hairlines: compose a `Grid` and a vertical `Separator` before every tile but the first, hiding the one that would start a row below `md` (`Inline hideBelow="md"` around it). A plain tile cannot be `primary` (there is no surface to fill), and an unknown appearance throws a `RangeError`.
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
<Card padding={5}>
|
|
644
|
+
<Grid columns={4} columnsBelow={{ md: 2 }} gap={4}>
|
|
645
|
+
{answers.map((answer, index) => (
|
|
646
|
+
<Inline align="stretch" gap={4} key={answer.label}>
|
|
647
|
+
{index > 0 ? (
|
|
648
|
+
// 2 is columnsBelow md: an answer that starts a row there has no hairline.
|
|
649
|
+
<Inline
|
|
650
|
+
align="stretch"
|
|
651
|
+
hideBelow={index % 2 === 0 ? 'md' : undefined}
|
|
652
|
+
>
|
|
653
|
+
<Separator orientation="vertical" decorative />
|
|
654
|
+
</Inline>
|
|
655
|
+
) : null}
|
|
656
|
+
<StatTile appearance="plain" {...answer} />
|
|
657
|
+
</Inline>
|
|
658
|
+
))}
|
|
659
|
+
</Grid>
|
|
660
|
+
</Card>
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
`ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted` or `accent`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant or tone, a `valueDetail` without a value, or a bad row count throws a `RangeError`.
|
|
664
|
+
|
|
665
|
+
```tsx
|
|
666
|
+
<Card padding={4}>
|
|
667
|
+
<Text as="h3" id="look" variant="label">
|
|
668
|
+
Worth a look
|
|
669
|
+
</Text>
|
|
670
|
+
<ListGroup aria-labelledby="look" variant="plain">
|
|
671
|
+
<ListRow
|
|
672
|
+
leading={<Circle size={8} fill="currentColor" />}
|
|
673
|
+
title="Wed, Oct 7 · North marsh counted 6 fewer herons"
|
|
674
|
+
detail="Recounted Sat, Oct 10 by Field team A"
|
|
675
|
+
href="/nights/2026-10-07"
|
|
676
|
+
onPress={() => router.push('/nights/2026-10-07')}
|
|
677
|
+
/>
|
|
678
|
+
</ListGroup>
|
|
679
|
+
</Card>
|
|
680
|
+
|
|
681
|
+
<ListGroup
|
|
682
|
+
aria-label="Night counts"
|
|
683
|
+
maxHeight={{ rows: 5 }}
|
|
684
|
+
footer={<ListRow title="8 nights" value="+0" />}
|
|
685
|
+
>
|
|
686
|
+
{nights.map((night) => (
|
|
687
|
+
<ListRow
|
|
688
|
+
key={night.id}
|
|
689
|
+
title={night.name}
|
|
690
|
+
detail={night.detail}
|
|
691
|
+
value={night.change}
|
|
692
|
+
valueTone={night.tone}
|
|
693
|
+
onPress={() => openNight(night)}
|
|
694
|
+
/>
|
|
695
|
+
))}
|
|
696
|
+
</ListGroup>
|
|
697
|
+
```
|
|
698
|
+
|
|
623
699
|
`Card` defaults to `glass`; `outlined` and `elevated` are solid surfaces, and `filled` is a quiet `--sw-color-subtle` fill with no border for plain information apart from pressable rows.
|
|
624
700
|
|
|
625
701
|
`--sw-color-accentSubtle` is the accent tint from `@scalewing/tokens`: a quiet fill behind accent text or glyphs that keeps 4.5:1. The stylesheet and every palette file set it, and a palette that moves the accent or surface retints it.
|
|
@@ -736,6 +812,32 @@ People type the date into a text entry in the locale's numeric order (`MM/DD/YYY
|
|
|
736
812
|
</Inline>
|
|
737
813
|
```
|
|
738
814
|
|
|
815
|
+
`DateRangeButton` shows a range of days and opens a calendar to pick a first and a last day, such as a report's period between its own ‹ and › steps. Supply `label` (what pressing it does, such as "Choose survey period"), `value` (`{ start, end }`, both `YYYY-MM-DD` and included) and `onChange`; optional `min`, `max`, `today`, `disabled`, `locale`, `weekStartsOn`, `id` and a `ref` behave as on `CalendarButton`. The button shows a calendar glyph and the range in the locale, sharing what both ends have ("Oct 6 – 12, 2026"; one day as one date); its name is `label`, `labels.nameSeparator`, then that range. The first press on a day sets the start and the second the end; a press before the start starts over from it, and Apply with only a start applies that one day. Days after `max` are disabled, and once a start is pressed so are the days past `maxDays` (a whole number of days, both ends counted, such as 366). The range being picked and its length (`labels.dayCount`) are a polite status. **Apply** calls `onChange` when the range changed, closes and returns focus to the button; **Cancel**, Escape and a press outside close without a change. `presets` (`{ id, label, range }[]`) are ranges chosen in one press: a press applies its range at once and closes, and the one matching the range shown is pressed; a preset outside `min`/`max` or longer than `maxDays` throws, so compute presets from the same bounds and leave out one that does not fit (such as Last year for a station opened this spring). From `md` up the calendar is a popover with the presets beside two months (above them below `lg`, where both do not fit side by side), the range and the hint, and Cancel and Apply; below `md` it is a `Dialog` bottom sheet titled by `label` with the range under the title, the presets in a row that scrolls sideways, one month and the hint over Cancel and Apply. The keyboard is the calendar's: arrows, Home, End, PageUp and PageDown (with Shift by year) move one focus day across both months, and Enter or Space presses it. `labels` takes `previousMonth`, `nextMonth`, `nameSeparator`, `presets` (the presets group's name), `apply`, `cancel`, `close`, `hint` and `dayCount(days)`, in English by default. `size` (default `md`, at least 44 px tall on a coarse pointer) and `variant` (default `neutral`) follow `Button`. A value outside `min`/`max` or longer than `maxDays` is kept; an empty or malformed date, a start after its end, a `maxDays` under 1 or fractional, repeated preset ids, or an empty word throws a `RangeError`.
|
|
816
|
+
|
|
817
|
+
```tsx
|
|
818
|
+
<ButtonGroup aria-label="Survey period" attached>
|
|
819
|
+
<Button aria-label="The period before" variant="neutral" onPress={previous}>
|
|
820
|
+
‹
|
|
821
|
+
</Button>
|
|
822
|
+
<DateRangeButton
|
|
823
|
+
label="Choose survey period"
|
|
824
|
+
value={period}
|
|
825
|
+
onChange={setPeriod}
|
|
826
|
+
presets={[
|
|
827
|
+
{ id: 'week', label: 'Last 7 days', range: lastSevenDays },
|
|
828
|
+
{ id: 'month', label: 'This month', range: thisMonth },
|
|
829
|
+
]}
|
|
830
|
+
max={stationDay}
|
|
831
|
+
maxDays={366}
|
|
832
|
+
today={stationDay}
|
|
833
|
+
labels={{ apply: 'Show' }}
|
|
834
|
+
/>
|
|
835
|
+
<Button aria-label="The period after" variant="neutral" onPress={next}>
|
|
836
|
+
›
|
|
837
|
+
</Button>
|
|
838
|
+
</ButtonGroup>
|
|
839
|
+
```
|
|
840
|
+
|
|
739
841
|
```tsx
|
|
740
842
|
<Switch
|
|
741
843
|
label="Active habitats only"
|
package/dist/calendar-month.d.ts
CHANGED
|
@@ -17,8 +17,14 @@ export declare function weekdayOf(value: string): number;
|
|
|
17
17
|
export declare function addDays(value: string, delta: number): string;
|
|
18
18
|
/** Same day of the month `delta` months away, capped at that month's length. */
|
|
19
19
|
export declare function addMonths(value: string, delta: number): string;
|
|
20
|
+
/** Days from `start` to `end`: 0 for the same day, negative when `end` is earlier. */
|
|
21
|
+
export declare function daysBetween(start: string, end: string): number;
|
|
20
22
|
export declare function monthOf(value: string): CalendarMonth;
|
|
21
23
|
export declare function shiftMonth(month: CalendarMonth, delta: number): CalendarMonth;
|
|
24
|
+
/** Months from `from` to `to`: 0 for the same month, negative when `to` is earlier. */
|
|
25
|
+
export declare function monthsBetween(from: CalendarMonth, to: CalendarMonth): number;
|
|
26
|
+
/** `value`'s day of the month in `month`, capped at that month's length. */
|
|
27
|
+
export declare function sameDayIn(month: CalendarMonth, value: string): string;
|
|
22
28
|
export declare function firstOfMonth(month: CalendarMonth): string;
|
|
23
29
|
export declare function lastOfMonth(month: CalendarMonth): string;
|
|
24
30
|
/** Whether any day of the month falls inside `[min, max]`. */
|
package/dist/calendar-month.js
CHANGED
|
@@ -61,6 +61,10 @@ export function addMonths(value, delta) {
|
|
|
61
61
|
day: Math.min(parts.day, daysInMonth(month.year, month.month)),
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
+
/** Days from `start` to `end`: 0 for the same day, negative when `end` is earlier. */
|
|
65
|
+
export function daysBetween(start, end) {
|
|
66
|
+
return toDayNumber(requireDate(end)) - toDayNumber(requireDate(start));
|
|
67
|
+
}
|
|
64
68
|
export function monthOf(value) {
|
|
65
69
|
const parts = requireDate(value);
|
|
66
70
|
return { year: parts.year, month: parts.month };
|
|
@@ -69,6 +73,17 @@ export function shiftMonth(month, delta) {
|
|
|
69
73
|
const index = month.year * 12 + (month.month - 1) + delta;
|
|
70
74
|
return { year: Math.floor(index / 12), month: (index % 12) + 1 };
|
|
71
75
|
}
|
|
76
|
+
/** Months from `from` to `to`: 0 for the same month, negative when `to` is earlier. */
|
|
77
|
+
export function monthsBetween(from, to) {
|
|
78
|
+
return to.year * 12 + to.month - (from.year * 12 + from.month);
|
|
79
|
+
}
|
|
80
|
+
/** `value`'s day of the month in `month`, capped at that month's length. */
|
|
81
|
+
export function sameDayIn(month, value) {
|
|
82
|
+
return formatDateOnly({
|
|
83
|
+
...month,
|
|
84
|
+
day: Math.min(requireDate(value).day, daysInMonth(month.year, month.month)),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
72
87
|
export function firstOfMonth(month) {
|
|
73
88
|
return formatDateOnly({ ...month, day: 1 });
|
|
74
89
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type WeekStart } from '../calendar-month.js';
|
|
2
|
+
import { type DateRangePreset } from '../date-range-input.js';
|
|
3
|
+
import { type DateRangeButtonLabels } from '../date-range-labels.js';
|
|
4
|
+
import { type DateOnlyRange } from '../date-only.js';
|
|
5
|
+
import { type ButtonSize, type ButtonVariant } from './Button.js';
|
|
6
|
+
export type { DateRangeButtonLabels, DateRangePreset };
|
|
7
|
+
/** A span of calendar days `YYYY-MM-DD`, both ends included. */
|
|
8
|
+
export type DateRangeButtonValue = DateOnlyRange;
|
|
9
|
+
export type DateRangeButtonProps = {
|
|
10
|
+
/**
|
|
11
|
+
* What pressing it does, such as "Choose a period". The button's name is
|
|
12
|
+
* this label, `labels.nameSeparator`, then the range it shows; the
|
|
13
|
+
* popover and the sheet are named by it.
|
|
14
|
+
*/
|
|
15
|
+
label: string;
|
|
16
|
+
/**
|
|
17
|
+
* The range shown, `{ start, end }` as `YYYY-MM-DD`, start not after end.
|
|
18
|
+
* A range outside `min`/`max` or longer than `maxDays` stays the value.
|
|
19
|
+
*/
|
|
20
|
+
value: DateRangeButtonValue;
|
|
21
|
+
/** Receives a new range on Apply or a preset, never converted through UTC. */
|
|
22
|
+
onChange: (value: DateRangeButtonValue) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Ranges chosen in one press, such as "Last 7 days": a list beside the
|
|
25
|
+
* months (a row above the month on a phone). A press applies its range
|
|
26
|
+
* and closes. Each must lie inside `min`/`max` and `maxDays`.
|
|
27
|
+
*/
|
|
28
|
+
presets?: readonly DateRangePreset[];
|
|
29
|
+
min?: string;
|
|
30
|
+
/** The last day that can be picked, such as today. */
|
|
31
|
+
max?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The longest range, in days with both ends counted: once a first day is
|
|
34
|
+
* pressed, the days after this many are disabled.
|
|
35
|
+
*/
|
|
36
|
+
maxDays?: number;
|
|
37
|
+
/**
|
|
38
|
+
* The calendar's today `YYYY-MM-DD`, the day it marks. Defaults to the
|
|
39
|
+
* device's local date; pass the business's day when it keeps its own.
|
|
40
|
+
*/
|
|
41
|
+
today?: string;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
/** The `<button>` element's id. */
|
|
44
|
+
id?: string;
|
|
45
|
+
/**
|
|
46
|
+
* BCP 47 tag for the dates and the calendar's month and weekday names.
|
|
47
|
+
* Defaults to the nearest `lang` attribute, then en-US.
|
|
48
|
+
*/
|
|
49
|
+
locale?: string;
|
|
50
|
+
/** First column of the calendar: 0 Sunday (default) or 1 Monday. */
|
|
51
|
+
weekStartsOn?: WeekStart;
|
|
52
|
+
/** The calendar's words; English by default. */
|
|
53
|
+
labels?: Partial<DateRangeButtonLabels>;
|
|
54
|
+
/** A Button size; a coarse pointer gets a 44 px target. */
|
|
55
|
+
size?: ButtonSize;
|
|
56
|
+
/** Default `neutral`, the quiet grey of a page's period controls. */
|
|
57
|
+
variant?: ButtonVariant;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A button showing a range of days that opens a calendar to pick a first
|
|
61
|
+
* and a last day: from `md` up a popover with the presets and two months,
|
|
62
|
+
* below it a bottom sheet with one month. Apply gives the range; Cancel,
|
|
63
|
+
* Escape and a press outside keep the value. The ref reaches the `<button>`.
|
|
64
|
+
*/
|
|
65
|
+
export declare const DateRangeButton: import("react").ForwardRefExoticComponent<DateRangeButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback } from 'react';
|
|
4
|
+
import { breakpointQuery } from '../css/breakpoints.js';
|
|
5
|
+
import { latestPick, rangeDayCount, sameRange } from '../date-range-draft.js';
|
|
6
|
+
import { assertDateRangeInput, } from '../date-range-input.js';
|
|
7
|
+
import { resolveDateRangeButtonLabels, } from '../date-range-labels.js';
|
|
8
|
+
import { formatRangeLong, formatRangeShort } from '../date-range-words.js';
|
|
9
|
+
import { todayDateOnly } from '../date-only.js';
|
|
10
|
+
import { Button } from './Button.js';
|
|
11
|
+
import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
|
|
12
|
+
import { useLangLocale } from './date-field/use-lang-locale.js';
|
|
13
|
+
import { DateRangePopover } from './date-range/DateRangePopover.js';
|
|
14
|
+
import { DateRangeSheet } from './date-range/DateRangeSheet.js';
|
|
15
|
+
import { useRangePopup } from './date-range/use-range-popup.js';
|
|
16
|
+
import { useMediaQuery } from './use-media-query.js';
|
|
17
|
+
/** Sets a forwarded ref, callback or object, to the same node. */
|
|
18
|
+
function setForwardedRef(ref, node) {
|
|
19
|
+
if (typeof ref === 'function')
|
|
20
|
+
ref(node);
|
|
21
|
+
else if (ref)
|
|
22
|
+
ref.current = node;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A button showing a range of days that opens a calendar to pick a first
|
|
26
|
+
* and a last day: from `md` up a popover with the presets and two months,
|
|
27
|
+
* below it a bottom sheet with one month. Apply gives the range; Cancel,
|
|
28
|
+
* Escape and a press outside keep the value. The ref reaches the `<button>`.
|
|
29
|
+
*/
|
|
30
|
+
export const DateRangeButton = forwardRef(function DateRangeButton({ label, value, onChange, presets = [], min, max, maxDays, today, disabled = false, id, locale, weekStartsOn = 0, labels, size = 'md', variant = 'neutral', }, ref) {
|
|
31
|
+
assertDateRangeInput({
|
|
32
|
+
label,
|
|
33
|
+
value,
|
|
34
|
+
presets,
|
|
35
|
+
min,
|
|
36
|
+
max,
|
|
37
|
+
maxDays,
|
|
38
|
+
today,
|
|
39
|
+
weekStartsOn,
|
|
40
|
+
});
|
|
41
|
+
const words = resolveDateRangeButtonLabels(labels);
|
|
42
|
+
const popup = useRangePopup(value, disabled, onChange);
|
|
43
|
+
const resolvedLocale = useLangLocale(locale, popup.buttonRef);
|
|
44
|
+
const wide = useMediaQuery(breakpointQuery('from', 'md'), true);
|
|
45
|
+
const { buttonRef } = popup;
|
|
46
|
+
const setButton = useCallback((node) => {
|
|
47
|
+
buttonRef.current = node;
|
|
48
|
+
setForwardedRef(ref, node);
|
|
49
|
+
}, [buttonRef, ref]);
|
|
50
|
+
const { range } = popup.draft;
|
|
51
|
+
const shown = formatRangeShort(value, resolvedLocale);
|
|
52
|
+
const Panel = wide ? DateRangePopover : DateRangeSheet;
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { "aria-controls": popup.shown ? popup.id : undefined, "aria-expanded": popup.shown, "aria-haspopup": "dialog", className: "sw-date-range-button", disabled: disabled, id: id, onPress: popup.toggle, ref: setButton, size: size, variant: variant, children: [_jsx("span", { className: "sw-sr-only", children: `${label}${words.nameSeparator}${shown}` }), _jsx(DateFieldGlyph, { name: "calendar" }), _jsx("span", { "aria-hidden": "true", children: shown })] }), popup.shown ? (_jsx(Panel, { buttonRef: buttonRef, count: words.dayCount(rangeDayCount(range)), draft: popup.draft, id: popup.id, label: label, labels: words, latest: latestPick(popup.draft, max, maxDays), line: formatRangeLong(range, resolvedLocale), locale: resolvedLocale, max: max, min: min, onApply: () => popup.apply(range), onCancel: popup.cancel, onChoosePreset: (preset) => popup.apply(preset.range), onPress: popup.press, presets: presets, pressedPresetId: presets.find((preset) => sameRange(preset.range, range))?.id, today: today ?? todayDateOnly(), weekStartsOn: weekStartsOn })) : null] }));
|
|
54
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ListGroupMaxHeight, type ListGroupVariant } from '../list-options.js';
|
|
3
|
+
export type ListGroupProps = {
|
|
4
|
+
/** `ListRow` elements; the group draws a hairline between each pair. */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Names the list, such as its card's heading. Required with `maxHeight`. */
|
|
7
|
+
'aria-label'?: string;
|
|
8
|
+
'aria-labelledby'?: string;
|
|
9
|
+
/**
|
|
10
|
+
* `panel` (the default) is one bordered panel; `plain` draws no panel
|
|
11
|
+
* and a hairline above the first row too, for rows inside a Card.
|
|
12
|
+
*/
|
|
13
|
+
variant?: ListGroupVariant;
|
|
14
|
+
/**
|
|
15
|
+
* Bounds the list to a row count of its own region, `{ rows }` rows of a
|
|
16
|
+
* title and one detail line, where the rest scroll; the region is a named
|
|
17
|
+
* keyboard stop and shades an edge with more rows past it.
|
|
18
|
+
*/
|
|
19
|
+
maxHeight?: ListGroupMaxHeight;
|
|
20
|
+
/**
|
|
21
|
+
* Pinned under the rows, outside the scroll, under a hairline: a total
|
|
22
|
+
* the rows add up to, such as a `ListRow` with the count and the sum.
|
|
23
|
+
*/
|
|
24
|
+
footer?: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Rows in one list (`ul`, its list role kept) with hairlines between them:
|
|
30
|
+
* a panel of its own, or plain rows inside a surface. The ref reaches the
|
|
31
|
+
* outer box.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ListGroup: import("react").ForwardRefExoticComponent<ListGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { assertListGroup, listMaxHeightStyle, } from '../list-options.js';
|
|
6
|
+
import { ListRowContainer } from './list/list-row-container.js';
|
|
7
|
+
import { ScrollRegion } from './ScrollRegion.js';
|
|
8
|
+
/**
|
|
9
|
+
* Rows in one list (`ul`, its list role kept) with hairlines between them:
|
|
10
|
+
* a panel of its own, or plain rows inside a surface. The ref reaches the
|
|
11
|
+
* outer box.
|
|
12
|
+
*/
|
|
13
|
+
export const ListGroup = forwardRef(function ListGroup({ children, 'aria-label': label, 'aria-labelledby': labelledBy, variant = 'panel', maxHeight, footer, className, id, }, ref) {
|
|
14
|
+
assertListGroup({ label, labelledBy, variant, maxHeight });
|
|
15
|
+
const list = (_jsx("ul", { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list",
|
|
16
|
+
// A list styled without bullets loses its role in Safari.
|
|
17
|
+
role: "list", children: _jsx(ListRowContainer.Provider, { value: "li", children: children }) }));
|
|
18
|
+
return (_jsxs("div", { className: cx('sw-list-group', `sw-list-group-${variant}`, className), id: id, ref: ref, children: [maxHeight === undefined ? (list) : (_jsx(ScrollRegion, { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list-group-scroll", style: listMaxHeightStyle(maxHeight), children: list })), footer === undefined ? null : (_jsx("div", { className: "sw-list-group-footer", children: _jsx(ListRowContainer.Provider, { value: "div", children: footer }) }))] }));
|
|
19
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ListRowTitleAppearance, type ListRowTone, type ListRowValueTone } from '../list-options.js';
|
|
3
|
+
export type ListRowProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
/** A muted line under the title. */
|
|
6
|
+
detail?: string;
|
|
7
|
+
/** A figure or word at the row's end, such as "+$1.48". */
|
|
8
|
+
value?: string;
|
|
9
|
+
/** A muted line under the value, such as "checks $3,427". */
|
|
10
|
+
valueDetail?: string;
|
|
11
|
+
/** Colours the value only, as a table cell's tone; the words say why. */
|
|
12
|
+
valueTone?: ListRowValueTone;
|
|
13
|
+
/**
|
|
14
|
+
* A consumer mark at the row's start, such as a status dot or a Lucide
|
|
15
|
+
* glyph (ADR 0008). It is hidden from assistive technology, so the words
|
|
16
|
+
* must say what it means.
|
|
17
|
+
*/
|
|
18
|
+
leading?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Makes the whole row a button (or, with `href`, a link's plain press
|
|
21
|
+
* for a client router) and adds the chevron.
|
|
22
|
+
*/
|
|
23
|
+
onPress?: () => void;
|
|
24
|
+
/** Makes the whole row a link to `href`, with the chevron. */
|
|
25
|
+
href?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Makes the row a choice: a check while true, nothing while false, and
|
|
28
|
+
* the state (`aria-pressed` on a button, `aria-current` on a link). Only
|
|
29
|
+
* on a row with `onPress` or `href`.
|
|
30
|
+
*/
|
|
31
|
+
selected?: boolean;
|
|
32
|
+
/** Dims the row and blocks presses; a link row drops its `href`. */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/** `danger` draws the title in the danger colour, for a row that ends something. */
|
|
35
|
+
tone?: ListRowTone;
|
|
36
|
+
/** `plain` sets the title at the body weight, for a list of plain statements. */
|
|
37
|
+
titleAppearance?: ListRowTitleAppearance;
|
|
38
|
+
/** `none` hides the chevron of a pressable row that acts in place. */
|
|
39
|
+
accessory?: 'chevron' | 'none';
|
|
40
|
+
/**
|
|
41
|
+
* Replaces a pressable row's name, which is its title and value; its
|
|
42
|
+
* detail lines are its description.
|
|
43
|
+
*/
|
|
44
|
+
'aria-label'?: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* One row of a `ListGroup`: it opens something (`onPress` or `href`, with
|
|
48
|
+
* a chevron, the whole row pressing), picks one option in place
|
|
49
|
+
* (`selected`), or only states something (neither).
|
|
50
|
+
*/
|
|
51
|
+
export declare function ListRow({ title, detail, value, valueDetail, valueTone, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useContext, useId } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { assertListRow, listRowAccessory, } from '../list-options.js';
|
|
6
|
+
import { tabStop } from '../tab-stop.js';
|
|
7
|
+
import { plainPress } from './link-press.js';
|
|
8
|
+
import { ListRowContent } from './list/ListRowContent.js';
|
|
9
|
+
import { ListRowContainer } from './list/list-row-container.js';
|
|
10
|
+
function ids(...parts) {
|
|
11
|
+
const joined = parts.filter(Boolean).join(' ');
|
|
12
|
+
return joined === '' ? undefined : joined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One row of a `ListGroup`: it opens something (`onPress` or `href`, with
|
|
16
|
+
* a chevron, the whole row pressing), picks one option in place
|
|
17
|
+
* (`selected`), or only states something (neither).
|
|
18
|
+
*/
|
|
19
|
+
export function ListRow({ title, detail, value, valueDetail, valueTone, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', 'aria-label': ariaLabel, }) {
|
|
20
|
+
const pressable = onPress !== undefined || href !== undefined;
|
|
21
|
+
assertListRow({
|
|
22
|
+
title,
|
|
23
|
+
tone,
|
|
24
|
+
titleAppearance,
|
|
25
|
+
valueTone,
|
|
26
|
+
accessory,
|
|
27
|
+
value,
|
|
28
|
+
valueDetail,
|
|
29
|
+
selected,
|
|
30
|
+
pressable,
|
|
31
|
+
});
|
|
32
|
+
const container = useContext(ListRowContainer);
|
|
33
|
+
const id = useId();
|
|
34
|
+
const kind = listRowAccessory({ pressable, selected, accessory });
|
|
35
|
+
const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, title: title, titleAppearance: titleAppearance, tone: tone, value: value, valueDetail: valueDetail, valueTone: valueTone }));
|
|
36
|
+
const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled');
|
|
37
|
+
const naming = {
|
|
38
|
+
'aria-label': ariaLabel,
|
|
39
|
+
'aria-labelledby': ariaLabel
|
|
40
|
+
? undefined
|
|
41
|
+
: ids(`${id}-title`, value !== undefined && `${id}-value`),
|
|
42
|
+
'aria-describedby': ids(Boolean(detail) && `${id}-detail`, Boolean(valueDetail) && `${id}-value-detail`),
|
|
43
|
+
};
|
|
44
|
+
let row;
|
|
45
|
+
if (href !== undefined)
|
|
46
|
+
row = (_jsx("a", { ...naming, "aria-current": selected ? 'true' : undefined, "aria-disabled": disabled || undefined, className: className, href: disabled ? undefined : href, onClick: onPress && !disabled
|
|
47
|
+
? (event) => {
|
|
48
|
+
if (!plainPress(event))
|
|
49
|
+
return;
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
onPress();
|
|
52
|
+
}
|
|
53
|
+
: undefined, role: disabled ? 'link' : undefined, tabIndex: disabled ? undefined : tabStop, children: content }));
|
|
54
|
+
else if (onPress !== undefined)
|
|
55
|
+
row = (_jsx("button", { ...naming, "aria-pressed": selected, className: className, disabled: disabled, onClick: onPress, tabIndex: tabStop, type: "button", children: content }));
|
|
56
|
+
else
|
|
57
|
+
row = (_jsx("div", { "aria-label": ariaLabel, className: className, role: ariaLabel ? 'group' : undefined, children: content }));
|
|
58
|
+
return container === 'li' ? (_jsx("li", { className: "sw-list-item", children: row })) : (_jsx("div", { className: "sw-list-item", children: row }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type NavCountProps = {
|
|
2
|
+
/**
|
|
3
|
+
* How many things wait behind the destination, such as tasks to do: a
|
|
4
|
+
* filled pill after its words, read in the link's name ("Tasks 8"). A
|
|
5
|
+
* whole number; 0 shows nothing.
|
|
6
|
+
*/
|
|
7
|
+
count: number;
|
|
8
|
+
/**
|
|
9
|
+
* What the count means, in the consumer's words ("8 tasks to do"): read
|
|
10
|
+
* in the name in place of the bare number ("Tasks 8 tasks to do"), the
|
|
11
|
+
* pill then hidden from assistive technology.
|
|
12
|
+
*/
|
|
13
|
+
countLabel?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A count after a Nav destination's words, placed inside its link or
|
|
17
|
+
* Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
|
|
18
|
+
* pill SectionNav's item count draws, and it is part of the destination's
|
|
19
|
+
* accessible name, as there.
|
|
20
|
+
*/
|
|
21
|
+
export declare function NavCount({ count, countLabel }: NavCountProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Fails closed on a count that is not a whole number or an empty label. */
|
|
3
|
+
function assertNavCount({ count, countLabel }) {
|
|
4
|
+
if (!Number.isInteger(count) || count < 0)
|
|
5
|
+
throw new RangeError(`NavCount count must be a whole number from 0: ${String(count)}`);
|
|
6
|
+
if (countLabel !== undefined &&
|
|
7
|
+
(typeof countLabel !== 'string' || !countLabel.trim()))
|
|
8
|
+
throw new RangeError('NavCount countLabel must be non-empty text');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A count after a Nav destination's words, placed inside its link or
|
|
12
|
+
* Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
|
|
13
|
+
* pill SectionNav's item count draws, and it is part of the destination's
|
|
14
|
+
* accessible name, as there.
|
|
15
|
+
*/
|
|
16
|
+
export function NavCount({ count, countLabel }) {
|
|
17
|
+
assertNavCount({ count, countLabel });
|
|
18
|
+
if (count === 0)
|
|
19
|
+
return null;
|
|
20
|
+
// The space is its own text node, so every browser and the name's
|
|
21
|
+
// computation keep it between the words and the count.
|
|
22
|
+
return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: "sw-nav-count", children: count })] }));
|
|
23
|
+
}
|
|
@@ -4,15 +4,8 @@ import { breakpoints } from '../css/breakpoints.js';
|
|
|
4
4
|
import { sectionNavVerticalClass } from '../css/css-section-nav.js';
|
|
5
5
|
import { assertSectionNavAppearance, } from '../css/css-section-nav-filled.js';
|
|
6
6
|
import { tabStop } from '../tab-stop.js';
|
|
7
|
+
import { plainPress } from './link-press.js';
|
|
7
8
|
export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
|
|
8
|
-
function plainPress(event) {
|
|
9
|
-
return (event.button === 0 &&
|
|
10
|
-
!event.defaultPrevented &&
|
|
11
|
-
!event.metaKey &&
|
|
12
|
-
!event.ctrlKey &&
|
|
13
|
-
!event.shiftKey &&
|
|
14
|
-
!event.altKey);
|
|
15
|
-
}
|
|
16
9
|
function ariaCurrent(current) {
|
|
17
10
|
if (current === true)
|
|
18
11
|
return 'true';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
-
import { type StatTileTone, statTileTones } from '../css/css-stat-tile.js';
|
|
3
|
-
export type { StatTileTone };
|
|
4
|
-
export { statTileTones };
|
|
2
|
+
import { type StatTileAppearance, statTileAppearances, type StatTileTone, statTileTones } from '../css/css-stat-tile.js';
|
|
3
|
+
export type { StatTileAppearance, StatTileTone };
|
|
4
|
+
export { statTileAppearances, statTileTones };
|
|
5
5
|
export type StatTileEmphasis = 'default' | 'primary';
|
|
6
6
|
export type StatTileProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
7
7
|
/** A short name for the figure, set in the label variant. */
|
|
@@ -16,10 +16,17 @@ export type StatTileProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
|
16
16
|
caption?: ReactNode;
|
|
17
17
|
/** `primary` fills the tile in the accent for the one figure a page leads with. */
|
|
18
18
|
emphasis?: StatTileEmphasis;
|
|
19
|
+
/**
|
|
20
|
+
* `plain` drops the tile's surface, edge and padding, so the figure sits
|
|
21
|
+
* inside a card that already has them, such as four answers in a row
|
|
22
|
+
* with hairlines between them. A plain tile cannot be `primary`, which
|
|
23
|
+
* is a fill. Default `card`.
|
|
24
|
+
*/
|
|
25
|
+
appearance?: StatTileAppearance;
|
|
19
26
|
};
|
|
20
27
|
/**
|
|
21
28
|
* One prominent figure with its label: the tiles a report or a list opens
|
|
22
29
|
* with. Presentation only; the consumer formats the number and picks the
|
|
23
30
|
* glyph.
|
|
24
31
|
*/
|
|
25
|
-
export declare function StatTile({ caption, className, emphasis, glyph, label, tone, value, ...rest }: StatTileProps): import("react").JSX.Element;
|
|
32
|
+
export declare function StatTile({ appearance, caption, className, emphasis, glyph, label, tone, value, ...rest }: StatTileProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../class-names.js';
|
|
3
|
-
import { statTileTones } from '../css/css-stat-tile.js';
|
|
3
|
+
import { statTileAppearances, statTileTones, } from '../css/css-stat-tile.js';
|
|
4
4
|
import { Text } from './Text.js';
|
|
5
|
-
export { statTileTones };
|
|
5
|
+
export { statTileAppearances, statTileTones };
|
|
6
|
+
function assertStatTile(appearance, emphasis) {
|
|
7
|
+
if (!statTileAppearances.includes(appearance))
|
|
8
|
+
throw new RangeError(`Unknown StatTile appearance: ${String(appearance)}`);
|
|
9
|
+
if (appearance === 'plain' && emphasis === 'primary')
|
|
10
|
+
throw new RangeError('A plain StatTile has no surface to fill; emphasis primary needs appearance card');
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
13
|
* One prominent figure with its label: the tiles a report or a list opens
|
|
8
14
|
* with. Presentation only; the consumer formats the number and picks the
|
|
9
15
|
* glyph.
|
|
10
16
|
*/
|
|
11
|
-
export function StatTile({ caption, className, emphasis = 'default', glyph, label, tone = 'default', value, ...rest }) {
|
|
17
|
+
export function StatTile({ appearance = 'card', caption, className, emphasis = 'default', glyph, label, tone = 'default', value, ...rest }) {
|
|
18
|
+
assertStatTile(appearance, emphasis);
|
|
12
19
|
const primary = emphasis === 'primary';
|
|
13
|
-
return (_jsxs("section", { className: cx('sw-stat-tile', primary && 'sw-stat-tile-primary', className), ...rest, children: [glyph ? (_jsx("span", { "aria-hidden": "true", className: "sw-stat-tile-glyph", children: glyph })) : null, _jsxs("span", { className: "sw-stat-tile-body", children: [_jsx(Text, { as: "span", className: "sw-stat-tile-label", color: primary ? 'onAccent' : 'muted', variant: "label", children: label }), _jsx(Text, { as: "strong", className: "sw-stat-tile-value", color: primary ? 'onAccent' : tone === 'default' ? 'text' : tone, variant: "heading", children: value }), caption ? (_jsx(Text, { as: "span", className: "sw-stat-tile-caption", color: primary ? 'onAccent' : 'muted', variant: "caption", children: caption })) : null] })] }));
|
|
20
|
+
return (_jsxs("section", { className: cx('sw-stat-tile', appearance === 'plain' && 'sw-stat-tile-plain', primary && 'sw-stat-tile-primary', className), ...rest, children: [glyph ? (_jsx("span", { "aria-hidden": "true", className: "sw-stat-tile-glyph", children: glyph })) : null, _jsxs("span", { className: "sw-stat-tile-body", children: [_jsx(Text, { as: "span", className: "sw-stat-tile-label", color: primary ? 'onAccent' : 'muted', variant: "label", children: label }), _jsx(Text, { as: "strong", className: "sw-stat-tile-value", color: primary ? 'onAccent' : tone === 'default' ? 'text' : tone, variant: "heading", children: value }), caption ? (_jsx(Text, { as: "span", className: "sw-stat-tile-caption", color: primary ? 'onAccent' : 'muted', variant: "caption", children: caption })) : null] })] }));
|
|
14
21
|
}
|