@scalewing/react 1.9.0 → 1.10.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +29 -1
  3. package/dist/anchored-position.d.ts +17 -5
  4. package/dist/anchored-position.js +19 -4
  5. package/dist/calendar-button-labels.d.ts +19 -0
  6. package/dist/calendar-button-labels.js +10 -0
  7. package/dist/calendar-labels.d.ts +7 -0
  8. package/dist/calendar-labels.js +9 -0
  9. package/dist/calendar-month.d.ts +2 -0
  10. package/dist/calendar-month.js +5 -0
  11. package/dist/components/ActionMenu.js +2 -58
  12. package/dist/components/CalendarButton.d.ts +43 -0
  13. package/dist/components/CalendarButton.js +47 -0
  14. package/dist/components/DateField.js +12 -23
  15. package/dist/components/action-menu/ActionMenuList.d.ts +21 -0
  16. package/dist/components/action-menu/ActionMenuList.js +27 -0
  17. package/dist/components/date-field/CalendarDialog.js +1 -1
  18. package/dist/components/date-field/DateFieldGlyph.d.ts +3 -3
  19. package/dist/components/date-field/DateFieldGlyph.js +3 -3
  20. package/dist/components/date-field/use-calendar-popup.d.ts +36 -0
  21. package/dist/components/date-field/use-calendar-popup.js +43 -0
  22. package/dist/components/use-anchored-popover.d.ts +14 -0
  23. package/dist/components/{date-field/use-anchored-popover.js → use-anchored-popover.js} +31 -9
  24. package/dist/css/css-action-menu.js +25 -6
  25. package/dist/css/css-calendar-button.d.ts +2 -0
  26. package/dist/css/css-calendar-button.js +32 -0
  27. package/dist/css/css-components.js +5 -25
  28. package/dist/css/css-date-field.js +2 -1
  29. package/dist/css/css-palettes.js +11 -1
  30. package/dist/css/css-text.d.ts +1 -0
  31. package/dist/css/css-text.js +42 -0
  32. package/dist/css/stacking.js +6 -3
  33. package/dist/css/stylesheet.js +2 -0
  34. package/dist/css/touch-target.d.ts +2 -0
  35. package/dist/css/touch-target.js +7 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/palette/amber.css +2 -0
  39. package/dist/palette/capri.css +2 -0
  40. package/dist/palette/cerulean.css +2 -0
  41. package/dist/palette/cornflower.css +2 -0
  42. package/dist/palette/fuchsia.css +2 -0
  43. package/dist/palette/harvest.css +2 -0
  44. package/dist/palette/ink.css +2 -0
  45. package/dist/palette/mocha.css +2 -0
  46. package/dist/palette/navy.css +2 -0
  47. package/dist/palette/postcard.css +2 -0
  48. package/dist/palette/raspberry.css +2 -0
  49. package/dist/palette/signal.css +2 -0
  50. package/dist/palette/sky.css +2 -0
  51. package/dist/palette/soft-blue.css +2 -0
  52. package/dist/palette/sunburst.css +2 -0
  53. package/dist/palette/synthwave.css +2 -0
  54. package/dist/palette/tangerine.css +2 -0
  55. package/dist/styles.css +92 -13
  56. package/package.json +1 -1
  57. package/dist/components/date-field/use-anchored-popover.d.ts +0 -8
@@ -0,0 +1,36 @@
1
+ import { type RefObject } from 'react';
2
+ export type CalendarPopupOptions = {
3
+ value: string;
4
+ disabled: boolean;
5
+ onChange: (value: string) => void;
6
+ /** Runs before a chosen date closes the calendar, such as a text reset. */
7
+ beforeSelect?: () => void;
8
+ };
9
+ export type CalendarPopup = {
10
+ /** The button that toggles the calendar and takes focus back. */
11
+ buttonRef: RefObject<HTMLButtonElement | null>;
12
+ shown: boolean;
13
+ toggle: () => void;
14
+ /** ARIA for the toggling button, as the WAI-ARIA date picker asks. */
15
+ trigger: {
16
+ 'aria-controls': string | undefined;
17
+ 'aria-expanded': boolean;
18
+ 'aria-haspopup': 'dialog';
19
+ };
20
+ /**
21
+ * CalendarDialog props that belong to the open and close cycle; pass
22
+ * `buttonRef` beside them.
23
+ */
24
+ dialog: {
25
+ id: string;
26
+ onClose: (restoreFocus: boolean) => void;
27
+ onSelect: (value: string) => void;
28
+ today: string;
29
+ };
30
+ };
31
+ /**
32
+ * The open state of a calendar dialog toggled by one button, shared by
33
+ * DateField and CalendarButton. Disabling closes it for good; choosing a
34
+ * date closes it, returns focus to the button, and reports a changed date.
35
+ */
36
+ export declare function useCalendarPopup({ value, disabled, onChange, beforeSelect, }: CalendarPopupOptions): CalendarPopup;
@@ -0,0 +1,43 @@
1
+ import { useId, useRef, useState } from 'react';
2
+ import { todayDateOnly } from '../../date-only.js';
3
+ /**
4
+ * The open state of a calendar dialog toggled by one button, shared by
5
+ * DateField and CalendarButton. Disabling closes it for good; choosing a
6
+ * date closes it, returns focus to the button, and reports a changed date.
7
+ */
8
+ export function useCalendarPopup({ value, disabled, onChange, beforeSelect, }) {
9
+ const buttonRef = useRef(null);
10
+ const [open, setOpen] = useState(false);
11
+ // Disabling closes the calendar for good, not just while disabled.
12
+ if (disabled && open)
13
+ setOpen(false);
14
+ const shown = open && !disabled;
15
+ const id = useId();
16
+ function close(restoreFocus) {
17
+ setOpen(false);
18
+ if (restoreFocus)
19
+ buttonRef.current?.focus();
20
+ }
21
+ function select(next) {
22
+ beforeSelect?.();
23
+ close(true);
24
+ if (next !== value)
25
+ onChange(next);
26
+ }
27
+ return {
28
+ buttonRef,
29
+ shown,
30
+ toggle: () => (shown ? close(true) : setOpen(true)),
31
+ trigger: {
32
+ 'aria-controls': shown ? id : undefined,
33
+ 'aria-expanded': shown,
34
+ 'aria-haspopup': 'dialog',
35
+ },
36
+ dialog: {
37
+ id,
38
+ onClose: close,
39
+ onSelect: select,
40
+ today: todayDateOnly(),
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,14 @@
1
+ import { type RefObject } from 'react';
2
+ export type AnchoredPopoverPlacement = {
3
+ /** Line up with the anchor's end when the start-aligned popover overflows. */
4
+ flipInline?: boolean;
5
+ };
6
+ /**
7
+ * Shows a mounted element on the top layer as a manual popover, keeps it
8
+ * beside `anchorRef` through scroll, window resize, and a change in its own
9
+ * or the anchor's size, a `space-1` gap from it and a
10
+ * `space-2` inset from the viewport edges, and calls `onDismiss` for a press
11
+ * outside it and outside `ignoreRef` (the button that toggles it).
12
+ * Browsers without the popover API get the same fixed placement.
13
+ */
14
+ export declare function useAnchoredPopover(popoverRef: RefObject<HTMLElement | null>, anchorRef: RefObject<HTMLElement | null>, ignoreRef: RefObject<HTMLElement | null>, onDismiss: () => void, { flipInline }?: AnchoredPopoverPlacement): void;
@@ -1,22 +1,37 @@
1
1
  import { spacingScale } from '@scalewing/tokens';
2
2
  import { useLayoutEffect, useRef } from 'react';
3
- import { anchoredPosition } from '../../anchored-position.js';
4
- function placePopover(node, anchor) {
5
- const position = anchoredPosition(anchor.getBoundingClientRect(), { width: node.offsetWidth, height: node.offsetHeight }, { width: window.innerWidth, height: window.innerHeight }, {
3
+ import { anchoredPosition } from '../anchored-position.js';
4
+ /**
5
+ * The viewport a fixed popover can use: the root's client box, which leaves
6
+ * out a classic scrollbar (`innerWidth` and `100vw` include it). Falls back
7
+ * to the window size where nothing is laid out, as in jsdom.
8
+ */
9
+ function layoutViewport() {
10
+ const root = document.documentElement;
11
+ return {
12
+ width: root.clientWidth || window.innerWidth,
13
+ height: root.clientHeight || window.innerHeight,
14
+ };
15
+ }
16
+ function placePopover(node, anchor, { flipInline = false }) {
17
+ const position = anchoredPosition(anchor.getBoundingClientRect(), { width: node.offsetWidth, height: node.offsetHeight }, layoutViewport(), {
6
18
  gap: spacingScale[1],
7
19
  inset: spacingScale[2],
8
20
  rtl: getComputedStyle(anchor).direction === 'rtl',
21
+ flipInline,
9
22
  });
10
23
  node.style.left = `${position.left}px`;
11
24
  node.style.top = `${position.top}px`;
12
25
  }
13
26
  /**
14
27
  * Shows a mounted element on the top layer as a manual popover, keeps it
15
- * beside `anchorRef` through scroll and resize, and calls `onDismiss` for a
16
- * press outside it and outside `ignoreRef` (the button that toggles it).
28
+ * beside `anchorRef` through scroll, window resize, and a change in its own
29
+ * or the anchor's size, a `space-1` gap from it and a
30
+ * `space-2` inset from the viewport edges, and calls `onDismiss` for a press
31
+ * outside it and outside `ignoreRef` (the button that toggles it).
17
32
  * Browsers without the popover API get the same fixed placement.
18
33
  */
19
- export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss) {
34
+ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss, { flipInline = false } = {}) {
20
35
  const dismissRef = useRef(onDismiss);
21
36
  useLayoutEffect(() => {
22
37
  dismissRef.current = onDismiss;
@@ -34,7 +49,7 @@ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss)
34
49
  if (!node.matches(':popover-open'))
35
50
  node.showPopover();
36
51
  }
37
- placePopover(node, anchor);
52
+ placePopover(node, anchor, { flipInline });
38
53
  function onPointerDown(event) {
39
54
  const target = event.target;
40
55
  if (!(target instanceof Node))
@@ -45,17 +60,24 @@ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss)
45
60
  }
46
61
  function onMove() {
47
62
  if (node && anchor)
48
- placePopover(node, anchor);
63
+ placePopover(node, anchor, { flipInline });
49
64
  }
65
+ // A popover or anchor that changes size while open (new commands, a
66
+ // month with another row of weeks, a relabelled trigger) moves too.
67
+ // ResizeObserver is missing in some runtimes, such as jsdom.
68
+ const resizes = typeof ResizeObserver === 'function' ? new ResizeObserver(onMove) : null;
69
+ resizes?.observe(node);
70
+ resizes?.observe(anchor);
50
71
  document.addEventListener('pointerdown', onPointerDown);
51
72
  window.addEventListener('resize', onMove);
52
73
  window.addEventListener('scroll', onMove, true);
53
74
  return () => {
75
+ resizes?.disconnect();
54
76
  document.removeEventListener('pointerdown', onPointerDown);
55
77
  window.removeEventListener('resize', onMove);
56
78
  window.removeEventListener('scroll', onMove, true);
57
79
  if (supportsPopover && node.matches(':popover-open'))
58
80
  node.hidePopover();
59
81
  };
60
- }, [anchorRef, ignoreRef, popoverRef]);
82
+ }, [anchorRef, flipInline, ignoreRef, popoverRef]);
61
83
  }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { zIndex } from './stacking.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
3
4
  const label = typographyVariants.label;
4
5
  export function cssActionMenuClasses() {
5
6
  return `.sw-action-menu {
@@ -39,20 +40,22 @@ export function cssActionMenuClasses() {
39
40
  -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
40
41
  box-sizing: border-box;
41
42
  color: var(--sw-color-text);
43
+ /* The popover layer's inset: 0 would ignore the placed left in right-to-left. */
44
+ inset: auto;
42
45
  margin: 0;
43
46
  max-height: var(--sw-select-max);
44
- max-width: calc(100vw - var(--sw-space-2));
45
- min-width: max-content;
47
+ /* On the top layer 100% is the viewport without a classic scrollbar. */
48
+ max-width: calc(100% - var(--sw-space-2) - var(--sw-space-2));
49
+ /* Never narrower than a touch target. */
50
+ min-width: var(--sw-control-md-min-height);
46
51
  overflow: auto;
47
52
  padding: var(--sw-space-1);
48
53
  position: fixed;
54
+ /* As wide as its longest command, until max-width wraps it. */
55
+ width: max-content;
49
56
  ${zIndex('popup')}
50
57
  }
51
58
 
52
- .sw-action-menu-list[hidden] {
53
- display: none;
54
- }
55
-
56
59
  .sw-action-menu-item {
57
60
  align-items: center;
58
61
  appearance: none;
@@ -65,7 +68,11 @@ export function cssActionMenuClasses() {
65
68
  font-family: var(--sw-font-sans);
66
69
  font-size: ${label.fontSize}px;
67
70
  gap: var(--sw-space-2);
71
+ line-height: ${label.lineHeight}px;
68
72
  min-height: var(--sw-control-xs-min-height);
73
+ /* A long command wraps inside the capped menu, a word at a time if it must. */
74
+ overflow-wrap: anywhere;
75
+ padding-block: var(--sw-space-1);
69
76
  padding-inline: var(--sw-control-xs-padding-inline);
70
77
  text-align: start;
71
78
  width: 100%;
@@ -83,6 +90,18 @@ export function cssActionMenuClasses() {
83
90
 
84
91
  .sw-action-menu-item-danger {
85
92
  color: var(--sw-color-danger);
93
+ }
94
+
95
+ /* A coarse pointer gets the full touch target: the trigger at least square, and every command as tall. */
96
+ @media ${coarsePointerQuery} {
97
+ .sw-action-menu-trigger {
98
+ min-height: ${touchTarget};
99
+ min-width: ${touchTarget};
100
+ }
101
+
102
+ .sw-action-menu-item {
103
+ min-height: ${touchTarget};
104
+ }
86
105
  }`;
87
106
  }
88
107
  export function actionMenuClassCatalog() {
@@ -0,0 +1,2 @@
1
+ export declare function cssCalendarButtonClasses(): string;
2
+ export declare function calendarButtonClassCatalog(): string[];
@@ -0,0 +1,32 @@
1
+ import { buttonSizes } from '@scalewing/tokens';
2
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
3
+ /* Square at every Button size: as wide as the size's control height. */
4
+ function squareRules() {
5
+ return buttonSizes
6
+ .map((size) => `.sw-button-${size}.sw-calendar-button { min-width: var(--sw-control-${size}-min-height); }`)
7
+ .join('\n');
8
+ }
9
+ export function cssCalendarButtonClasses() {
10
+ return `.sw-button.sw-calendar-button {
11
+ flex: none;
12
+ padding-inline: 0;
13
+ }
14
+
15
+ ${squareRules()}
16
+
17
+ .sw-calendar-button:not(.sw-button-md) .sw-date-field-glyph {
18
+ height: var(--sw-space-4);
19
+ width: var(--sw-space-4);
20
+ }
21
+
22
+ /* A coarse pointer gets the full touch target at the smaller sizes too. */
23
+ @media ${coarsePointerQuery} {
24
+ .sw-button.sw-calendar-button {
25
+ min-height: ${touchTarget};
26
+ min-width: ${touchTarget};
27
+ }
28
+ }`;
29
+ }
30
+ export function calendarButtonClassCatalog() {
31
+ return ['sw-calendar-button'];
32
+ }
@@ -1,9 +1,8 @@
1
- import { compactTypographyVariants, typographyVariants, } from '@scalewing/tokens';
2
- import { breakpointQuery } from './breakpoints.js';
3
1
  import { cssActionBarClasses } from './css-action-bar.js';
4
2
  import { cssActionMenuClasses } from './css-action-menu.js';
5
3
  import { cssAccordionClasses } from './css-accordion.js';
6
4
  import { cssButtonClasses } from './css-button.js';
5
+ import { cssCalendarButtonClasses } from './css-calendar-button.js';
7
6
  import { cssChartClasses } from './css-chart.js';
8
7
  import { cssCheckboxClasses } from './css-checkbox.js';
9
8
  import { cssRadioGroupClasses } from './css-radio-group.js';
@@ -14,6 +13,7 @@ import { cssDialogClasses } from './css-dialog.js';
14
13
  import { cssSelectClasses } from './css-select.js';
15
14
  import { cssSplitClasses } from './css-split.js';
16
15
  import { cssSwitchClasses } from './css-switch.js';
16
+ import { cssTextClasses } from './css-text.js';
17
17
  import { cssSpinnerClasses } from './css-spinner.js';
18
18
  import { cssProgressClasses } from './css-progress.js';
19
19
  import { cssTooltipClasses } from './css-tooltip.js';
@@ -57,22 +57,6 @@ function reducedTransparencyRules() {
57
57
  }
58
58
  }`;
59
59
  }
60
- function textVariantRules() {
61
- return Object.entries(typographyVariants)
62
- .map(([name, variant]) => {
63
- const tabular = 'tabularNums' in variant && variant.tabularNums
64
- ? ' font-variant-numeric: tabular-nums;'
65
- : '';
66
- return `.sw-text-${name} { font-family: var(--sw-font-sans); font-size: ${variant.fontSize}px; line-height: ${variant.lineHeight}px; font-weight: ${variant.fontWeight}; letter-spacing: ${variant.letterSpacing}px;${tabular} }`;
67
- })
68
- .join('\n');
69
- }
70
- function compactTextVariantRules() {
71
- const rules = Object.entries(compactTypographyVariants)
72
- .map(([name, variant]) => ` .sw-text-${name} { font-size: ${variant.fontSize}px; line-height: ${variant.lineHeight}px; letter-spacing: ${variant.letterSpacing}px; }`)
73
- .join('\n');
74
- return `@media ${breakpointQuery('below', 'md')} {\n${rules}\n}`;
75
- }
76
60
  export function cssComponentClasses() {
77
61
  return `.sw-card {
78
62
  border-radius: var(--sw-radius-lg);
@@ -105,10 +89,6 @@ ${cssPopupHostRules()}
105
89
  background: var(--sw-color-subtle);
106
90
  }
107
91
 
108
- .sw-text-align-start { text-align: start; }
109
- .sw-text-align-center { text-align: center; }
110
- .sw-text-align-end { text-align: end; }
111
-
112
92
  .sw-truncate {
113
93
  overflow: hidden;
114
94
  text-overflow: ellipsis;
@@ -119,9 +99,7 @@ ${cssPopupHostRules()}
119
99
  font-variant-numeric: tabular-nums;
120
100
  }
121
101
 
122
- ${textVariantRules()}
123
-
124
- ${compactTextVariantRules()}
102
+ ${cssTextClasses()}
125
103
 
126
104
  ${cssButtonClasses()}
127
105
 
@@ -133,6 +111,8 @@ ${cssDataClasses()}
133
111
 
134
112
  ${cssDateFieldClasses()}
135
113
 
114
+ ${cssCalendarButtonClasses()}
115
+
136
116
  ${cssChartClasses()}
137
117
 
138
118
  ${cssChromeClasses()}
@@ -142,7 +142,8 @@ function calendarRules() {
142
142
  height: auto;
143
143
  inset: auto;
144
144
  margin: 0;
145
- max-width: calc(100vw - var(--sw-space-2) - var(--sw-space-2));
145
+ /* On the top layer 100% is the viewport without a classic scrollbar. */
146
+ max-width: calc(100% - var(--sw-space-2) - var(--sw-space-2));
146
147
  overflow: visible;
147
148
  padding: var(--sw-space-3);
148
149
  position: fixed;
@@ -1,10 +1,20 @@
1
1
  import { colorsForPalette, createTheme, paletteHasStylesheet, palettesWithStylesheets, secondaryActionBorder, semanticColorKeys, } from '@scalewing/tokens';
2
+ /**
3
+ * A palette sets the colours it overlays, plus the accent tint whenever it
4
+ * moves the accent or surface the tint is mixed from.
5
+ */
6
+ function overlaidColorKey(key, overlay) {
7
+ if (key === 'accentSubtle') {
8
+ return Boolean(overlay.accentSubtle || overlay.accent || overlay.surface);
9
+ }
10
+ return Boolean(overlay[key]);
11
+ }
2
12
  function paletteCustomProperties(id, scheme) {
3
13
  const overlay = colorsForPalette(id, scheme);
4
14
  const theme = createTheme({ colorScheme: scheme, palette: id });
5
15
  const lines = [];
6
16
  for (const key of semanticColorKeys) {
7
- if (overlay[key]) {
17
+ if (overlaidColorKey(key, overlay)) {
8
18
  lines.push(` --sw-color-${key}: ${theme.colors[key]};`);
9
19
  }
10
20
  }
@@ -0,0 +1 @@
1
+ export declare function cssTextClasses(): string;
@@ -0,0 +1,42 @@
1
+ import { compactTypographyVariants, typographyVariants, } from '@scalewing/tokens';
2
+ import { breakpointQuery } from './breakpoints.js';
3
+ function textClassNames() {
4
+ return Object.keys(typographyVariants).map((name) => `.sw-text-${name}`);
5
+ }
6
+ /*
7
+ * Text renders `p` and `h1`–`h4` by default. The browser gives those a
8
+ * block margin, which a flex Stack or Inline does not collapse, so it would
9
+ * add to every gap. `:where()` keeps the reset at zero specificity: it beats
10
+ * the browser's margins (author styles win over the user agent's) and loses
11
+ * to any authored margin, such as `sw-sr-only` or a consumer's class.
12
+ */
13
+ function textMarginReset() {
14
+ return `:where(${textClassNames().join(', ')}) { margin: 0; }`;
15
+ }
16
+ function textVariantRules() {
17
+ return Object.entries(typographyVariants)
18
+ .map(([name, variant]) => {
19
+ const tabular = 'tabularNums' in variant && variant.tabularNums
20
+ ? ' font-variant-numeric: tabular-nums;'
21
+ : '';
22
+ return `.sw-text-${name} { font-family: var(--sw-font-sans); font-size: ${variant.fontSize}px; line-height: ${variant.lineHeight}px; font-weight: ${variant.fontWeight}; letter-spacing: ${variant.letterSpacing}px;${tabular} }`;
23
+ })
24
+ .join('\n');
25
+ }
26
+ function compactTextVariantRules() {
27
+ const rules = Object.entries(compactTypographyVariants)
28
+ .map(([name, variant]) => ` .sw-text-${name} { font-size: ${variant.fontSize}px; line-height: ${variant.lineHeight}px; letter-spacing: ${variant.letterSpacing}px; }`)
29
+ .join('\n');
30
+ return `@media ${breakpointQuery('below', 'md')} {\n${rules}\n}`;
31
+ }
32
+ export function cssTextClasses() {
33
+ return `.sw-text-align-start { text-align: start; }
34
+ .sw-text-align-center { text-align: center; }
35
+ .sw-text-align-end { text-align: end; }
36
+
37
+ ${textMarginReset()}
38
+
39
+ ${textVariantRules()}
40
+
41
+ ${compactTextVariantRules()}`;
42
+ }
@@ -13,7 +13,8 @@
13
13
  * 4. `topChrome`: the sticky AppHeader, over a lifted surface that scrolls up
14
14
  * under it (the header is the page's way back, and a popup opens downward).
15
15
  * 5. `popup`: a popup outside any lifted surface (Select list, ActionMenu
16
- * list, Tooltip, DateField calendar) over everything in the page.
16
+ * list, Tooltip, the DateField and CalendarButton calendar) over
17
+ * everything in the page.
17
18
  *
18
19
  * Dialog and Toast sit on the browser's top layer, above all of these.
19
20
  */
@@ -29,9 +30,11 @@ export function zIndex(layer) {
29
30
  }
30
31
  const openPopups = [
31
32
  '.sw-select-list',
32
- '.sw-action-menu-list:not([hidden])',
33
- '.sw-tooltip:not([hidden])',
34
33
  // Rendered only while open; on the top layer where the popover API exists.
34
+ '.sw-action-menu-list',
35
+ '.sw-tooltip:not([hidden])',
36
+ // DateField and CalendarButton share it. Rendered only while open; on the
37
+ // top layer where the popover API exists.
35
38
  '.sw-date-field-calendar',
36
39
  ].join(', ');
37
40
  const popupHosts = ['.sw-card-glass', '.sw-accordion'];
@@ -4,6 +4,7 @@ import { actionMenuClassCatalog } from './css-action-menu.js';
4
4
  import { accordionClassCatalog } from './css-accordion.js';
5
5
  import { switchClassCatalog } from './css-switch.js';
6
6
  import { dateFieldClassCatalog } from './css-date-field.js';
7
+ import { calendarButtonClassCatalog } from './css-calendar-button.js';
7
8
  import { checkboxClassCatalog } from './css-checkbox.js';
8
9
  import { radioGroupClassCatalog } from './css-radio-group.js';
9
10
  import { spinnerClassCatalog } from './css-spinner.js';
@@ -57,6 +58,7 @@ export function utilityClassCatalog() {
57
58
  ...actionBarClassCatalog(),
58
59
  ...switchClassCatalog(),
59
60
  ...dateFieldClassCatalog(),
61
+ ...calendarButtonClassCatalog(),
60
62
  ...checkboxClassCatalog(),
61
63
  ...radioGroupClassCatalog(),
62
64
  ...spinnerClassCatalog(),
@@ -0,0 +1,2 @@
1
+ export declare const touchTarget = "var(--sw-control-md-min-height)";
2
+ export declare const coarsePointerQuery = "(pointer: coarse)";
@@ -0,0 +1,7 @@
1
+ /*
2
+ * The comfortable touch target on a coarse pointer: the md control height,
3
+ * 44 px, both ways. Controls drawn smaller for a fine pointer (CalendarButton
4
+ * at sm and xs, the ActionMenu trigger and commands) grow to it on touch.
5
+ */
6
+ export const touchTarget = 'var(--sw-control-md-min-height)';
7
+ export const coarsePointerQuery = '(pointer: coarse)';
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { Accordion } from './components/Accordion.js';
4
4
  export type { AccordionProps, AccordionSize } from './components/Accordion.js';
5
5
  export { Switch, type SwitchProps } from './components/Switch.js';
6
6
  export { DateField, type DateFieldLabels, type DateFieldProps, type WeekStart, } from './components/DateField.js';
7
+ export { CalendarButton, type CalendarButtonLabels, type CalendarButtonProps, } from './components/CalendarButton.js';
7
8
  export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
8
9
  export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
9
10
  export { Progress, type ProgressProps, type ProgressTone, } from './components/Progress.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { ActionBar } from './components/ActionBar.js';
3
3
  export { Accordion } from './components/Accordion.js';
4
4
  export { Switch } from './components/Switch.js';
5
5
  export { DateField, } from './components/DateField.js';
6
+ export { CalendarButton, } from './components/CalendarButton.js';
6
7
  export { Checkbox } from './components/Checkbox.js';
7
8
  export { Spinner, } from './components/Spinner.js';
8
9
  export { Progress, } from './components/Progress.js';
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #B45309;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #FAF3EE;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #FCD34D;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #322E23;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(68, 61, 38, 0.72);
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #0077B6;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #F2F8FB;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #48CAE4;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #202D32;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(36, 59, 66, 0.72);
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #0066CC;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #E6F0FA;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #5AC8FA;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #222D34;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(39, 59, 70, 0.72);
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #4F6BED;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #FFFFFF;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #93A4FF;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #282A35;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(49, 52, 71, 0.72);
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #C026D3;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #FEFBFE;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #E879F9;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #302534;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(65, 45, 69, 0.72);
@@ -7,6 +7,7 @@
7
7
  --sw-color-muted: #5C5346;
8
8
  --sw-color-accent: #3E3E24;
9
9
  --sw-color-onAccent: #FFFFFF;
10
+ --sw-color-accentSubtle: #DAD5C4;
10
11
  --sw-color-border: #C9C2AE;
11
12
  --sw-button-secondary-border: #C9C2AE;
12
13
  --sw-glass-blur: 24px;
@@ -23,6 +24,7 @@
23
24
  --sw-color-muted: #B8B090;
24
25
  --sw-color-accent: #CEA453;
25
26
  --sw-color-onAccent: #101214;
27
+ --sw-color-accentSubtle: #2F4A2C;
26
28
  --sw-color-border: #3D5238;
27
29
  --sw-button-secondary-border: #3D5238;
28
30
  --sw-glass-blur: 24px;
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #1D1D1F;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #E8E8E9;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #F5F5F7;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #323234;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(67, 67, 69, 0.72);
@@ -7,6 +7,7 @@
7
7
  --sw-color-muted: #6B5348;
8
8
  --sw-color-accent: #473333;
9
9
  --sw-color-onAccent: #FFFFFF;
10
+ --sw-color-accentSubtle: #E6DBCF;
10
11
  --sw-color-border: #E0CBA8;
11
12
  --sw-button-secondary-border: #E0CBA8;
12
13
  --sw-glass-blur: 24px;
@@ -23,6 +24,7 @@
23
24
  --sw-color-muted: #D4C4A8;
24
25
  --sw-color-accent: #E8C69F;
25
26
  --sw-color-onAccent: #101214;
27
+ --sw-color-accentSubtle: #684F4B;
26
28
  --sw-color-border: #6B5353;
27
29
  --sw-button-secondary-border: #6B5353;
28
30
  --sw-glass-blur: 24px;
@@ -3,6 +3,7 @@
3
3
  [data-theme='light'] {
4
4
  --sw-color-accent: #0A2540;
5
5
  --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-accentSubtle: #E7E9EC;
6
7
  --sw-glass-blur: 24px;
7
8
  --sw-glass-saturate: 1;
8
9
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -13,6 +14,7 @@
13
14
  [data-theme='dark'] {
14
15
  --sw-color-accent: #7EB8FF;
15
16
  --sw-color-onAccent: #101214;
17
+ --sw-color-accentSubtle: #262C35;
16
18
  --sw-glass-blur: 24px;
17
19
  --sw-glass-saturate: 1.8;
18
20
  --sw-glass-fill: rgba(46, 56, 71, 0.72);