@usableapp/cardds 0.2.2 → 0.3.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.
@@ -1,38 +1,55 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
- export interface DropdownProps extends ComponentPropsWithoutRef<'div'> {
3
+ export interface DropdownProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue'> {
4
4
  /** unique id — the popover menu's id the trigger targets */
5
5
  id: string;
6
- /** the current value's label shown in the pill */
7
- label: ReactNode;
6
+ /** CONTROLLED: the chosen MenuItem's `value`. The app owns it — pair it with `onChange`. */
7
+ value?: string;
8
+ /** UNCONTROLLED: the value it starts with; the dropdown then keeps its own */
9
+ defaultValue?: string;
10
+ /** a MenuItem was picked */
11
+ onChange?: (value: string) => void;
12
+ /** what the pill says; default = the chosen MenuItem's own label */
13
+ label?: ReactNode;
8
14
  /** right-aligned trigger: the panel grows leftwards (in TopBar actions) */
9
15
  end?: boolean;
10
- /** MenuItems */
16
+ /** MenuItems, each with a `value` */
11
17
  children?: ReactNode;
12
18
  }
13
19
  /**
14
- * Dropdown — THE select: a pill trigger + a Menu popover anchored under it (native popover: light dismiss, Esc).
15
- * Never a native `<select>`. Children are MenuItems; cardds.js copies the picked item's label into the pill
16
- * and fires "change" on the Dropdown. In a FilterRow dropdowns share the width.
20
+ * Dropdown — THE select: a pill trigger + its Menu popover anchored under it (native popover: light dismiss, Esc). Never a native
21
+ * `<select>`. ONE component, like `<select>` is — the pill and the panel are its own anatomy. It holds a VALUE, controlled like an
22
+ * input: `value` + `onChange`, or `defaultValue`. Children are MenuItems with a `value`; the chosen one is marked and names the pill.
23
+ * In a FilterRow dropdowns share the width.
17
24
  */
18
- export declare function Dropdown({ id, label, end, className, children, ...rest }: DropdownProps): import("react").JSX.Element;
19
- export interface MenuProps extends Omit<ComponentPropsWithoutRef<'div'>, 'popover'> {
20
- /** render as a popover panel (needs an id and an invoker with popoverTarget); omit for an inline menu card */
25
+ export declare function Dropdown({ id, value, defaultValue, onChange, label, end, className, children, ...rest }: DropdownProps): import("react").JSX.Element;
26
+ export interface MenuProps extends Omit<ComponentPropsWithoutRef<'div'>, 'popover' | 'onChange' | 'defaultValue'> {
27
+ /** render as a popover panel: give it an `id`, and ANY button opens it with the platform's own `popoverTarget={id}` — the Menu does
28
+ * not know which. Omit for an inline menu card */
21
29
  popover?: boolean;
22
- /** an ACTIONS menu — things to do, not a value to hold: no item stays current */
30
+ /** an ACTIONS menu — things to do, not a value to hold: no item stays current; each MenuItem has the app's `onClick` */
23
31
  actions?: boolean;
32
+ /** a VALUE menu, controlled: the chosen MenuItem's `value` (+ `onChange`) */
33
+ value?: string;
34
+ /** …or the value it starts with */
35
+ defaultValue?: string;
36
+ onChange?: (value: string) => void;
24
37
  /** the trigger sits at the far right: the panel grows leftwards */
25
38
  end?: boolean;
26
39
  }
27
- /** Menu — a card of MenuItems: a popover anchored to its button, or inline rows of icon + label. `actions` for a to-do list (IconBtn's), plain for a value (Dropdown's). */
28
- export declare function Menu({ popover, actions, end, className, ...rest }: MenuProps): import("react").JSX.Element;
40
+ /**
41
+ * Menu — a card of MenuItems: a popover, or inline rows of icon + label. A COMPONENT on its own — what opens a popover Menu is any
42
+ * button carrying `popoverTarget={its id}` (HTML's own link; the panel anchors to that button). `actions` = a to-do list (plain
43
+ * `onClick`s); otherwise it holds a value (`value` + `onChange`, or `defaultValue`). Its one behaviour: picking an item closes it.
44
+ */
45
+ export declare function Menu({ popover, actions, value, defaultValue, onChange, end, className, onClick, ...rest }: MenuProps): import("react").JSX.Element;
29
46
  export interface MenuItemProps extends ComponentPropsWithoutRef<'button'> {
30
- /** the value cardds.js reports on change */
47
+ /** in a value Menu / Dropdown: what picking this row reports */
31
48
  value?: string;
32
- /** the current choice */
49
+ /** mark it current yourself (default: it is, when its `value` is the menu's) */
33
50
  current?: boolean;
34
51
  /** a leading icon */
35
52
  icon?: IconName;
36
53
  }
37
- /** MenuItem — one row in a Menu (48px tall): optional icon, label; the current one is filled. */
38
- export declare function MenuItem({ value, current, icon, className, children, ...rest }: MenuItemProps): import("react").JSX.Element;
54
+ /** MenuItem — one row in a Menu (48px tall): optional icon, label; the current one is filled. In an `actions` Menu give it an `onClick`. */
55
+ export declare function MenuItem({ value, current, icon, className, children, onClick, ...rest }: MenuItemProps): import("react").JSX.Element;
@@ -1,19 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, createContext, isValidElement, useContext, useRef, useState } from 'react';
2
3
  import { cx } from '../cx.js';
3
4
  import { Icon } from '../type/Icon.js';
5
+ /* what a Menu tells its items: the value it holds (so the matching item marks itself current) and how to pick one */
6
+ const MenuContext = createContext(null);
7
+ /* a menu's value is controlled (`value` + `onChange`) or its own (`defaultValue`) — like an input */
8
+ function useValue(value, defaultValue, onChange) {
9
+ const [inner, setInner] = useState(defaultValue);
10
+ const current = value ?? inner;
11
+ const pick = (next) => { if (value === undefined)
12
+ setInner(next); if (next !== current)
13
+ onChange?.(next); };
14
+ return [current, pick];
15
+ }
4
16
  /**
5
- * Dropdown — THE select: a pill trigger + a Menu popover anchored under it (native popover: light dismiss, Esc).
6
- * Never a native `<select>`. Children are MenuItems; cardds.js copies the picked item's label into the pill
7
- * and fires "change" on the Dropdown. In a FilterRow dropdowns share the width.
17
+ * Dropdown — THE select: a pill trigger + its Menu popover anchored under it (native popover: light dismiss, Esc). Never a native
18
+ * `<select>`. ONE component, like `<select>` is — the pill and the panel are its own anatomy. It holds a VALUE, controlled like an
19
+ * input: `value` + `onChange`, or `defaultValue`. Children are MenuItems with a `value`; the chosen one is marked and names the pill.
20
+ * In a FilterRow dropdowns share the width.
8
21
  */
9
- export function Dropdown({ id, label, end, className, children, ...rest }) {
10
- return (_jsxs("div", { className: cx('dropdown', end && 'dropdown--end', className), ...rest, children: [_jsxs("button", { type: "button", className: "dropdown__btn", popoverTarget: id, children: [_jsx("span", { className: "dropdown__label", children: label }), _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx("div", { className: "menu", id: id, popover: "auto", children: children })] }));
22
+ export function Dropdown({ id, value, defaultValue, onChange, label, end, className, children, ...rest }) {
23
+ const [current, pick] = useValue(value, defaultValue, onChange);
24
+ const chosen = Children.toArray(children).find((c) => isValidElement(c) && c.props.value === current);
25
+ return (_jsxs("div", { className: cx('dropdown', end && 'dropdown--end', className), "data-value": current, ...rest, children: [_jsxs("button", { type: "button", className: "dropdown__btn", popoverTarget: id, children: [_jsx("span", { className: "dropdown__label", children: label ?? (isValidElement(chosen) ? chosen.props.children : null) }), _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx(Menu, { popover: true, id: id, value: current, onChange: pick, children: children })] }));
11
26
  }
12
- /** Menu — a card of MenuItems: a popover anchored to its button, or inline rows of icon + label. `actions` for a to-do list (IconBtn's), plain for a value (Dropdown's). */
13
- export function Menu({ popover, actions, end, className, ...rest }) {
14
- return _jsx("div", { className: cx('menu', actions && 'menu--actions', end && 'menu--end', className), popover: popover ? 'auto' : undefined, ...rest });
27
+ /**
28
+ * Menu — a card of MenuItems: a popover, or inline rows of icon + label. A COMPONENT on its own — what opens a popover Menu is any
29
+ * button carrying `popoverTarget={its id}` (HTML's own link; the panel anchors to that button). `actions` = a to-do list (plain
30
+ * `onClick`s); otherwise it holds a value (`value` + `onChange`, or `defaultValue`). Its one behaviour: picking an item closes it.
31
+ */
32
+ export function Menu({ popover, actions, value, defaultValue, onChange, end, className, onClick, ...rest }) {
33
+ const ref = useRef(null);
34
+ const [current, pick] = useValue(value, defaultValue, onChange);
35
+ const click = (e) => {
36
+ onClick?.(e);
37
+ const el = ref.current;
38
+ if (el && e.target.closest('.menu__item') && el.matches(':popover-open'))
39
+ el.hidePopover();
40
+ };
41
+ return (_jsx(MenuContext.Provider, { value: actions ? null : { value: current, pick }, children: _jsx("div", { ref: ref, className: cx('menu', actions && 'menu--actions', end && 'menu--end', className), popover: popover ? 'auto' : undefined, onClick: click, ...rest }) }));
15
42
  }
16
- /** MenuItem — one row in a Menu (48px tall): optional icon, label; the current one is filled. */
17
- export function MenuItem({ value, current, icon, className, children, ...rest }) {
18
- return (_jsxs("button", { type: "button", className: cx('menu__item', className), "data-value": value, "aria-current": current ? 'true' : undefined, ...rest, children: [icon && _jsx(Icon, { name: icon }), children] }));
43
+ /** MenuItem — one row in a Menu (48px tall): optional icon, label; the current one is filled. In an `actions` Menu give it an `onClick`. */
44
+ export function MenuItem({ value, current, icon, className, children, onClick, ...rest }) {
45
+ const menu = useContext(MenuContext);
46
+ const on = current ?? (menu?.value !== undefined && value !== undefined && menu.value === value);
47
+ return (_jsxs("button", { type: "button", className: cx('menu__item', className), "data-value": value, "aria-current": on ? 'true' : undefined, onClick: (e) => { onClick?.(e); if (!e.defaultPrevented && value !== undefined)
48
+ menu?.pick?.(value); }, ...rest, children: [icon && _jsx(Icon, { name: icon }), children] }));
19
49
  }
@@ -1,4 +1,4 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
1
+ import { type ComponentPropsWithoutRef } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
3
  export interface IconBtnProps extends ComponentPropsWithoutRef<'button'> {
4
4
  /** the accessible name — required, the button has no text */
@@ -11,13 +11,300 @@ export interface IconBtnProps extends ComponentPropsWithoutRef<'button'> {
11
11
  invert?: boolean;
12
12
  /** a link styled as a round button */
13
13
  href?: string;
14
- /** an actions menu the button opens (MenuItems) — the popover anchored under it, growing leftwards (`menuStart` for a left-side button); needs `menuId` */
15
- menu?: ReactNode;
16
- /** the menu popover's id (unique on the page) */
17
- menuId?: string;
18
- /** the button sits at the left: the panel grows rightwards */
19
- menuStart?: boolean;
20
14
  }
21
15
  /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move.
22
- * With `menu` it gives the member options the way a Dropdown does — an actions Menu (edit, share, remove…) for the card or sheet it sits in. */
23
- export declare function IconBtn({ label, icon, sm, invert, href, menu, menuId, menuStart, className, type, children, ...rest }: IconBtnProps): import("react").JSX.Element;
16
+ * It is a button and nothing more: to open a Menu, give it the platform's `popoverTarget={the Menu's id}` and render that `<Menu popover actions id>`
17
+ * wherever you like — the panel anchors to the button that opened it. To open anything else, `onClick`. */
18
+ export declare function IconBtn({ label, icon, sm, invert, href, className, type, children, ...rest }: IconBtnProps): import("react").DetailedReactHTMLElement<{
19
+ form?: string | undefined | undefined;
20
+ slot?: string | undefined | undefined;
21
+ style?: import("react").CSSProperties | undefined;
22
+ title?: string | undefined | undefined;
23
+ suppressHydrationWarning?: boolean | undefined | undefined;
24
+ color?: string | undefined | undefined;
25
+ id?: string | undefined | undefined;
26
+ lang?: string | undefined | undefined;
27
+ name?: string | undefined | undefined;
28
+ nonce?: string | undefined | undefined;
29
+ part?: string | undefined | undefined;
30
+ role?: import("react").AriaRole | undefined;
31
+ tabIndex?: number | undefined | undefined;
32
+ "aria-activedescendant"?: string | undefined | undefined;
33
+ "aria-atomic"?: (boolean | "true" | "false") | undefined;
34
+ "aria-autocomplete"?: "none" | "inline" | "list" | "both" | undefined | undefined;
35
+ "aria-braillelabel"?: string | undefined | undefined;
36
+ "aria-brailleroledescription"?: string | undefined | undefined;
37
+ "aria-busy"?: (boolean | "true" | "false") | undefined;
38
+ "aria-checked"?: boolean | "false" | "mixed" | "true" | undefined | undefined;
39
+ "aria-colcount"?: number | undefined | undefined;
40
+ "aria-colindex"?: number | undefined | undefined;
41
+ "aria-colindextext"?: string | undefined | undefined;
42
+ "aria-colspan"?: number | undefined | undefined;
43
+ "aria-controls"?: string | undefined | undefined;
44
+ "aria-current"?: boolean | "false" | "true" | "page" | "step" | "location" | "date" | "time" | undefined | undefined;
45
+ "aria-describedby"?: string | undefined | undefined;
46
+ "aria-description"?: string | undefined | undefined;
47
+ "aria-details"?: string | undefined | undefined;
48
+ "aria-disabled"?: (boolean | "true" | "false") | undefined;
49
+ "aria-dropeffect"?: "none" | "copy" | "execute" | "link" | "move" | "popup" | undefined | undefined;
50
+ "aria-errormessage"?: string | undefined | undefined;
51
+ "aria-expanded"?: (boolean | "true" | "false") | undefined;
52
+ "aria-flowto"?: string | undefined | undefined;
53
+ "aria-grabbed"?: (boolean | "true" | "false") | undefined;
54
+ "aria-haspopup"?: boolean | "false" | "true" | "menu" | "listbox" | "tree" | "grid" | "dialog" | undefined | undefined;
55
+ "aria-hidden"?: (boolean | "true" | "false") | undefined;
56
+ "aria-invalid"?: boolean | "false" | "true" | "grammar" | "spelling" | undefined | undefined;
57
+ "aria-keyshortcuts"?: string | undefined | undefined;
58
+ "aria-label": string;
59
+ "aria-labelledby"?: string | undefined | undefined;
60
+ "aria-level"?: number | undefined | undefined;
61
+ "aria-live"?: "off" | "assertive" | "polite" | undefined | undefined;
62
+ "aria-modal"?: (boolean | "true" | "false") | undefined;
63
+ "aria-multiline"?: (boolean | "true" | "false") | undefined;
64
+ "aria-multiselectable"?: (boolean | "true" | "false") | undefined;
65
+ "aria-orientation"?: "horizontal" | "vertical" | undefined | undefined;
66
+ "aria-owns"?: string | undefined | undefined;
67
+ "aria-placeholder"?: string | undefined | undefined;
68
+ "aria-posinset"?: number | undefined | undefined;
69
+ "aria-pressed"?: boolean | "false" | "mixed" | "true" | undefined | undefined;
70
+ "aria-readonly"?: (boolean | "true" | "false") | undefined;
71
+ "aria-relevant"?: "additions" | "additions removals" | "additions text" | "all" | "removals" | "removals additions" | "removals text" | "text" | "text additions" | "text removals" | undefined | undefined;
72
+ "aria-required"?: (boolean | "true" | "false") | undefined;
73
+ "aria-roledescription"?: string | undefined | undefined;
74
+ "aria-rowcount"?: number | undefined | undefined;
75
+ "aria-rowindex"?: number | undefined | undefined;
76
+ "aria-rowindextext"?: string | undefined | undefined;
77
+ "aria-rowspan"?: number | undefined | undefined;
78
+ "aria-selected"?: (boolean | "true" | "false") | undefined;
79
+ "aria-setsize"?: number | undefined | undefined;
80
+ "aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined | undefined;
81
+ "aria-valuemax"?: number | undefined | undefined;
82
+ "aria-valuemin"?: number | undefined | undefined;
83
+ "aria-valuenow"?: number | undefined | undefined;
84
+ "aria-valuetext"?: string | undefined | undefined;
85
+ dangerouslySetInnerHTML?: {
86
+ __html: string | TrustedHTML;
87
+ } | undefined | undefined;
88
+ onCopy?: import("react").ClipboardEventHandler<HTMLButtonElement> | undefined;
89
+ onCopyCapture?: import("react").ClipboardEventHandler<HTMLButtonElement> | undefined;
90
+ onCut?: import("react").ClipboardEventHandler<HTMLButtonElement> | undefined;
91
+ onCutCapture?: import("react").ClipboardEventHandler<HTMLButtonElement> | undefined;
92
+ onPaste?: import("react").ClipboardEventHandler<HTMLButtonElement> | undefined;
93
+ onPasteCapture?: import("react").ClipboardEventHandler<HTMLButtonElement> | undefined;
94
+ onCompositionEnd?: import("react").CompositionEventHandler<HTMLButtonElement> | undefined;
95
+ onCompositionEndCapture?: import("react").CompositionEventHandler<HTMLButtonElement> | undefined;
96
+ onCompositionStart?: import("react").CompositionEventHandler<HTMLButtonElement> | undefined;
97
+ onCompositionStartCapture?: import("react").CompositionEventHandler<HTMLButtonElement> | undefined;
98
+ onCompositionUpdate?: import("react").CompositionEventHandler<HTMLButtonElement> | undefined;
99
+ onCompositionUpdateCapture?: import("react").CompositionEventHandler<HTMLButtonElement> | undefined;
100
+ onFocus?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
101
+ onFocusCapture?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
102
+ onBlur?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
103
+ onBlurCapture?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
104
+ onChange?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
105
+ onChangeCapture?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
106
+ onBeforeInput?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
107
+ onBeforeInputCapture?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
108
+ onInput?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
109
+ onInputCapture?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
110
+ onReset?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
111
+ onResetCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
112
+ onSubmit?: import("react").SubmitEventHandler<HTMLButtonElement> | undefined;
113
+ onSubmitCapture?: import("react").SubmitEventHandler<HTMLButtonElement> | undefined;
114
+ onInvalid?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
115
+ onInvalidCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
116
+ onLoad?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
117
+ onLoadCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
118
+ onError?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
119
+ onErrorCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
120
+ onKeyDown?: import("react").KeyboardEventHandler<HTMLButtonElement> | undefined;
121
+ onKeyDownCapture?: import("react").KeyboardEventHandler<HTMLButtonElement> | undefined;
122
+ onKeyPress?: import("react").KeyboardEventHandler<HTMLButtonElement> | undefined;
123
+ onKeyPressCapture?: import("react").KeyboardEventHandler<HTMLButtonElement> | undefined;
124
+ onKeyUp?: import("react").KeyboardEventHandler<HTMLButtonElement> | undefined;
125
+ onKeyUpCapture?: import("react").KeyboardEventHandler<HTMLButtonElement> | undefined;
126
+ onAbort?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
127
+ onAbortCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
128
+ onCanPlay?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
129
+ onCanPlayCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
130
+ onCanPlayThrough?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
131
+ onCanPlayThroughCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
132
+ onDurationChange?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
133
+ onDurationChangeCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
134
+ onEmptied?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
135
+ onEmptiedCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
136
+ onEncrypted?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
137
+ onEncryptedCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
138
+ onEnded?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
139
+ onEndedCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
140
+ onLoadedData?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
141
+ onLoadedDataCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
142
+ onLoadedMetadata?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
143
+ onLoadedMetadataCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
144
+ onLoadStart?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
145
+ onLoadStartCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
146
+ onPause?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
147
+ onPauseCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
148
+ onPlay?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
149
+ onPlayCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
150
+ onPlaying?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
151
+ onPlayingCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
152
+ onProgress?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
153
+ onProgressCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
154
+ onRateChange?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
155
+ onRateChangeCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
156
+ onSeeked?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
157
+ onSeekedCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
158
+ onSeeking?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
159
+ onSeekingCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
160
+ onStalled?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
161
+ onStalledCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
162
+ onSuspend?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
163
+ onSuspendCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
164
+ onTimeUpdate?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
165
+ onTimeUpdateCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
166
+ onVolumeChange?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
167
+ onVolumeChangeCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
168
+ onWaiting?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
169
+ onWaitingCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
170
+ onAuxClick?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
171
+ onAuxClickCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
172
+ onClick?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
173
+ onClickCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
174
+ onContextMenu?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
175
+ onContextMenuCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
176
+ onDoubleClick?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
177
+ onDoubleClickCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
178
+ onDrag?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
179
+ onDragCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
180
+ onDragEnd?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
181
+ onDragEndCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
182
+ onDragEnter?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
183
+ onDragEnterCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
184
+ onDragExit?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
185
+ onDragExitCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
186
+ onDragLeave?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
187
+ onDragLeaveCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
188
+ onDragOver?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
189
+ onDragOverCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
190
+ onDragStart?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
191
+ onDragStartCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
192
+ onDrop?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
193
+ onDropCapture?: import("react").DragEventHandler<HTMLButtonElement> | undefined;
194
+ onMouseDown?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
195
+ onMouseDownCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
196
+ onMouseEnter?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
197
+ onMouseLeave?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
198
+ onMouseMove?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
199
+ onMouseMoveCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
200
+ onMouseOut?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
201
+ onMouseOutCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
202
+ onMouseOver?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
203
+ onMouseOverCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
204
+ onMouseUp?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
205
+ onMouseUpCapture?: import("react").MouseEventHandler<HTMLButtonElement> | undefined;
206
+ onSelect?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
207
+ onSelectCapture?: import("react").ReactEventHandler<HTMLButtonElement> | undefined;
208
+ onTouchCancel?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
209
+ onTouchCancelCapture?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
210
+ onTouchEnd?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
211
+ onTouchEndCapture?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
212
+ onTouchMove?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
213
+ onTouchMoveCapture?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
214
+ onTouchStart?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
215
+ onTouchStartCapture?: import("react").TouchEventHandler<HTMLButtonElement> | undefined;
216
+ onPointerDown?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
217
+ onPointerDownCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
218
+ onPointerMove?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
219
+ onPointerMoveCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
220
+ onPointerUp?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
221
+ onPointerUpCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
222
+ onPointerCancel?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
223
+ onPointerCancelCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
224
+ onPointerEnter?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
225
+ onPointerLeave?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
226
+ onPointerOver?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
227
+ onPointerOverCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
228
+ onPointerOut?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
229
+ onPointerOutCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
230
+ onGotPointerCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
231
+ onGotPointerCaptureCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
232
+ onLostPointerCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
233
+ onLostPointerCaptureCapture?: import("react").PointerEventHandler<HTMLButtonElement> | undefined;
234
+ onScroll?: import("react").UIEventHandler<HTMLButtonElement> | undefined;
235
+ onScrollCapture?: import("react").UIEventHandler<HTMLButtonElement> | undefined;
236
+ onScrollEnd?: import("react").UIEventHandler<HTMLButtonElement> | undefined;
237
+ onScrollEndCapture?: import("react").UIEventHandler<HTMLButtonElement> | undefined;
238
+ onWheel?: import("react").WheelEventHandler<HTMLButtonElement> | undefined;
239
+ onWheelCapture?: import("react").WheelEventHandler<HTMLButtonElement> | undefined;
240
+ onAnimationStart?: import("react").AnimationEventHandler<HTMLButtonElement> | undefined;
241
+ onAnimationStartCapture?: import("react").AnimationEventHandler<HTMLButtonElement> | undefined;
242
+ onAnimationEnd?: import("react").AnimationEventHandler<HTMLButtonElement> | undefined;
243
+ onAnimationEndCapture?: import("react").AnimationEventHandler<HTMLButtonElement> | undefined;
244
+ onAnimationIteration?: import("react").AnimationEventHandler<HTMLButtonElement> | undefined;
245
+ onAnimationIterationCapture?: import("react").AnimationEventHandler<HTMLButtonElement> | undefined;
246
+ onToggle?: import("react").ToggleEventHandler<HTMLButtonElement> | undefined;
247
+ onBeforeToggle?: import("react").ToggleEventHandler<HTMLButtonElement> | undefined;
248
+ onTransitionCancel?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
249
+ onTransitionCancelCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
250
+ onTransitionEnd?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
251
+ onTransitionEndCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
252
+ onTransitionRun?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
253
+ onTransitionRunCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
254
+ onTransitionStart?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
255
+ onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
256
+ key?: import("react").Key | null | undefined;
257
+ defaultChecked?: boolean | undefined | undefined;
258
+ defaultValue?: string | number | readonly string[] | undefined;
259
+ suppressContentEditableWarning?: boolean | undefined | undefined;
260
+ accessKey?: string | undefined | undefined;
261
+ autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {}) | undefined;
262
+ autoFocus?: boolean | undefined | undefined;
263
+ contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
264
+ contextMenu?: string | undefined | undefined;
265
+ dir?: string | undefined | undefined;
266
+ draggable?: (boolean | "true" | "false") | undefined;
267
+ enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
268
+ hidden?: boolean | undefined | undefined;
269
+ spellCheck?: (boolean | "true" | "false") | undefined;
270
+ translate?: "yes" | "no" | undefined | undefined;
271
+ radioGroup?: string | undefined | undefined;
272
+ about?: string | undefined | undefined;
273
+ content?: string | undefined | undefined;
274
+ datatype?: string | undefined | undefined;
275
+ inlist?: any;
276
+ prefix?: string | undefined | undefined;
277
+ property?: string | undefined | undefined;
278
+ rel?: string | undefined | undefined;
279
+ resource?: string | undefined | undefined;
280
+ rev?: string | undefined | undefined;
281
+ typeof?: string | undefined | undefined;
282
+ vocab?: string | undefined | undefined;
283
+ autoCorrect?: string | undefined | undefined;
284
+ autoSave?: string | undefined | undefined;
285
+ itemProp?: string | undefined | undefined;
286
+ itemScope?: boolean | undefined | undefined;
287
+ itemType?: string | undefined | undefined;
288
+ itemID?: string | undefined | undefined;
289
+ itemRef?: string | undefined | undefined;
290
+ results?: number | undefined | undefined;
291
+ security?: string | undefined | undefined;
292
+ unselectable?: "on" | "off" | undefined | undefined;
293
+ popover?: "" | "auto" | "manual" | "hint" | undefined | undefined;
294
+ popoverTargetAction?: "toggle" | "show" | "hide" | undefined | undefined;
295
+ popoverTarget?: string | undefined | undefined;
296
+ inert?: boolean | undefined | undefined;
297
+ inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined | undefined;
298
+ is?: string | undefined | undefined;
299
+ exportparts?: string | undefined | undefined;
300
+ disabled?: boolean | undefined | undefined;
301
+ formAction?: string | ((formData: FormData) => void | Promise<void>) | undefined;
302
+ formEncType?: string | undefined | undefined;
303
+ formMethod?: string | undefined | undefined;
304
+ formNoValidate?: boolean | undefined | undefined;
305
+ formTarget?: string | undefined | undefined;
306
+ value?: string | number | readonly string[] | undefined;
307
+ href: string | undefined;
308
+ type: "button" | "reset" | "submit" | undefined;
309
+ className: string;
310
+ }, HTMLElement>;
@@ -1,13 +1,11 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createElement } from 'react';
3
- import { Menu } from './Dropdown.js';
4
3
  import { cx } from '../cx.js';
5
4
  import { Icon } from '../type/Icon.js';
6
5
  /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move.
7
- * With `menu` it gives the member options the way a Dropdown does — an actions Menu (edit, share, remove…) for the card or sheet it sits in. */
8
- export function IconBtn({ label, icon, sm, invert, href, menu, menuId, menuStart, className, type, children, ...rest }) {
9
- const btn = createElement(href ? 'a' : 'button', { href, type: href ? undefined : type ?? 'button', 'aria-label': label, popoverTarget: menu ? menuId : undefined, className: cx('icon-btn', sm && 'icon-btn--sm', invert && 'icon-btn--invert', className), ...rest }, icon ? _jsx(Icon, { name: icon, size: sm ? 'sm' : 'md' }) : children);
10
- if (!menu)
11
- return btn;
12
- return (_jsxs(_Fragment, { children: [btn, _jsx(Menu, { popover: true, actions: true, end: !menuStart, id: menuId, children: menu })] }));
6
+ * It is a button and nothing more: to open a Menu, give it the platform's `popoverTarget={the Menu's id}` and render that `<Menu popover actions id>`
7
+ * wherever you like — the panel anchors to the button that opened it. To open anything else, `onClick`. */
8
+ export function IconBtn({ label, icon, sm, invert, href, className, type, children, ...rest }) {
9
+ const btn = createElement(href ? 'a' : 'button', { href, type: href ? undefined : type ?? 'button', 'aria-label': label, className: cx('icon-btn', sm && 'icon-btn--sm', invert && 'icon-btn--invert', className), ...rest }, icon ? _jsx(Icon, { name: icon, size: sm ? 'sm' : 'md' }) : children);
10
+ return btn;
13
11
  }
package/dist/cardds.css CHANGED
@@ -90,6 +90,8 @@
90
90
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
91
91
  --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
92
92
  --header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
93
+ --card-pad-rest: var(--sp-5); /* 20 a card's padding before any variant — .card publishes it as --card-pad; the sheet's peek strip is built from it where no card is in scope */
94
+ --card-gap-rest: var(--sp-4); /* 16 …and its gap */
93
95
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
94
96
  --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
95
97
  --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
@@ -521,8 +523,8 @@ button { font: inherit; cursor: pointer; }
521
523
  .card {
522
524
  --card-bg: var(--card-1-bg); /* the slot, readable by children (avatar rings, fold) */
523
525
  --card-ink: var(--card-1-ink);
524
- --card-pad: var(--sp-5); /* published so .bleed can cancel it exactly */
525
- --card-gap: var(--sp-4); /* published so a sheet's peek strip (sheet.css) is exact */
526
+ --card-pad: var(--card-pad-rest); /* published so .bleed can cancel it exactly */
527
+ --card-gap: var(--card-gap-rest); /* published so a sheet's peek strip (sheet.css) is exact */
526
528
  background: var(--card-bg);
527
529
  color: var(--card-ink);
528
530
  border: var(--border-w) solid var(--card-border);
@@ -1105,7 +1107,7 @@ button { font: inherit; cursor: pointer; }
1105
1107
  page. Many sheets stacked as the app's menu are the other system,
1106
1108
  .sheet-stack (stack.css): the same material, its own class and file.
1107
1109
 
1108
- <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
1110
+ <article class="card sheet sheet--half">
1109
1111
  A tap on the handle TOGGLES the sheet between its own state and peek
1110
1112
  (Lh 2026-09-16) — never a step-by-step climb; a drag snaps to any of
1111
1113
  the listed states, and the state it lands on becomes the one a tap
@@ -1123,14 +1125,12 @@ button { font: inherit; cursor: pointer; }
1123
1125
  ============================================================ */
1124
1126
 
1125
1127
  .sheet {
1126
- --_handle: var(--sp-1);
1127
1128
  /* the strip a peeked sheet shows: the card's own top padding, the handle,
1128
1129
  the gap, one title row (--tap — the head rule below makes every head at
1129
1130
  least that tall) and the gap under it, so the cut lands exactly where
1130
1131
  the first control starts and nothing of it leaks through. Published
1131
1132
  (and registered in tokens.css) so a sheet whose head is taller can
1132
1133
  retune it: .my-sheet { --sheet-peek: … } */
1133
- --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
1134
1134
  --sheet-half: 0.5; /* a share of the sheet's own height */
1135
1135
  --sheet-3q: 0.25; /* three quarters showing: slid by a quarter */
1136
1136
  /* the sheet's own height, seen from inside it: the box it slides in is the
@@ -1165,14 +1165,16 @@ button { font: inherit; cursor: pointer; }
1165
1165
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1166
1166
  .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
1167
1167
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1168
+ /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
1169
+ have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
1170
+ inherits it. A sheet whose head is taller retunes it on the box. */
1171
+ :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--tap)); }
1172
+
1168
1173
  /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1169
1174
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
1170
1175
  changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
1171
- with `inert`. A button names its sheet — <button aria-controls="when"> … <article id="when" class="card
1172
- sheet sheet--away" inert data-sheet-states="peek full"> — and cardds.js does the rest: a tap slides that
1173
- sheet up to full and sends any other one away; the handle takes it down to peek, never further, so the
1174
- words and the other buttons are back in reach; the same button again raises it from peek to full.
1175
- Nothing waits at peek before the first call. No bar on such a screen (actions.css).
1176
+ with `inert` (the React Sheet does). WHO brings it onto the screen is the app's business — a button, a route:
1177
+ cardds ties no component to another. Nothing waits at peek before the first call.
1176
1178
  WHAT IT IS FOR: the base offers WAYS IN — sign in · new phone · join — and each button calls a sheet
1177
1179
  that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
1178
1180
  carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
@@ -1497,8 +1499,9 @@ button { font: inherit; cursor: pointer; }
1497
1499
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1498
1500
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
1499
1501
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
1500
- (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. Should a bar be written
1501
- beside a sheet anyway it stands down (actions.css + cardds.js) — a net for a mistake, not a layout. */
1502
+ (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
1503
+ own screens are composed (the guide); the framework does not police it — a component never changes
1504
+ because another one is there. */
1502
1505
 
1503
1506
  /* the reading block: a .base-content (base.css) — the same words-on-the-base
1504
1507
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
@@ -1517,11 +1520,10 @@ button { font: inherit; cursor: pointer; }
1517
1520
  color: var(--on-surface);
1518
1521
  }
1519
1522
  /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
1520
- (card.css). --sheet-room: the strip a CALLED sheet keeps at peek (sheet.css, "called sheets"), published
1521
- on the step by cardds.js while one is on the screen, so the buttons under it can scroll clear of it;
1522
- nothing without one. */
1523
+ (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
1524
+ (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
1525
+ .step:has(> .sheet--peek) > :is(.base-content, .pages) { padding-bottom: var(--sheet-peek); }
1523
1526
  .step > :is(.base-content, .pages) {
1524
- padding-bottom: var(--sheet-room, 0rem);
1525
1527
  flex: 0 1 auto;
1526
1528
  }
1527
1529
  .step > :is(.base-content, .pages) {
@@ -1896,14 +1898,9 @@ button { font: inherit; cursor: pointer; }
1896
1898
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1897
1899
  </div> — or the same two in an .action-bar__tier--pager under a control */
1898
1900
  .action-bar--pager { align-items: center; }
1899
- /* The bar belongs to the BASE (Lh, 2026-09-20): it is the move of a page of words — a pager turning
1900
- them, or one wide button. It is never in front of a sheet: a sheet that is on the screen covers it
1901
- (step.css, "layers"), a peeking one included, and the button that finishes a sheet is the sheet's own
1902
- foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
1903
- past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
1904
- is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
1905
- the eye cannot see. */
1906
- :has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
1901
+ /* The bar belongs to the BASE: the move of a page of words — a pager turning them, or one wide button. A screen
1902
+ with a sheet finishes on the sheet's own foot instead (sheet.css). Guidance for composing, not a rule the
1903
+ CSS enforces: a component never changes because another one is there. */
1907
1904
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1908
1905
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1909
1906
  .pager__at > * {