@usableapp/cardds 0.7.103 → 0.7.104

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 (60) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/css/actions.css +102 -36
  3. package/css/base.css +139 -15
  4. package/css/card.css +308 -79
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +28 -27
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +4 -4
  9. package/css/layover.css +26 -24
  10. package/css/lists.css +8 -6
  11. package/css/media.css +13 -74
  12. package/css/numbers.css +37 -32
  13. package/css/palette.css +1 -1
  14. package/css/people.css +3 -8
  15. package/css/post.css +23 -19
  16. package/css/sheet.css +84 -56
  17. package/css/stack.css +39 -215
  18. package/css/stamp.css +41 -29
  19. package/css/step.css +1 -107
  20. package/css/sticky.css +43 -41
  21. package/css/theme.template.css +12 -0
  22. package/css/tokens.css +100 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/cardds.css +1053 -868
  26. package/dist/cards/Card.d.ts +3 -3
  27. package/dist/cards/Card.js +1 -1
  28. package/dist/cards/CardFeed.js +7 -9
  29. package/dist/catalog.json +1 -1
  30. package/dist/layover/Snackbar.js +2 -2
  31. package/dist/lists/Bubble.d.ts +1 -1
  32. package/dist/lists/Bubble.js +1 -1
  33. package/dist/lists/Row.js +5 -7
  34. package/dist/numbers/Picker.js +11 -18
  35. package/dist/people/Avatar.d.ts +1 -1
  36. package/dist/people/Avatar.js +3 -2
  37. package/dist/post/PostDesk.js +6 -9
  38. package/dist/post/PostLetter.js +4 -4
  39. package/dist/pressable.d.ts +9 -0
  40. package/dist/pressable.js +6 -0
  41. package/dist/room.d.ts +5 -0
  42. package/dist/room.js +9 -0
  43. package/dist/scaffold/Panes.js +8 -14
  44. package/dist/sheets/Sheet.d.ts +1 -1
  45. package/dist/sheets/Sheet.js +2 -3
  46. package/dist/sheets/SheetSteps.js +3 -3
  47. package/dist/sheets/StackSheet.d.ts +1 -1
  48. package/dist/sheets/sheetStepsPile.js +2 -3
  49. package/dist/sheets/stackCard.js +67 -115
  50. package/dist/state.d.ts +21 -0
  51. package/dist/state.js +28 -1
  52. package/dist/sticky/Sticky.js +3 -7
  53. package/dist/templates/Home.d.ts +1 -1
  54. package/dist/templates/Splash.js +2 -2
  55. package/dist/templates/Walkthrough.js +4 -5
  56. package/docs/reference.md +71 -9
  57. package/lib/catalog.mjs +23 -7
  58. package/lint/rules.mjs +7 -5
  59. package/lint/values.mjs +144 -0
  60. package/package.json +6 -4
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Icon } from '../type/Icon.js';
5
- import { reducedMotion, useControlled, useLatest } from '../state.js';
5
+ import { reducedMotion, sheetMs, useControlled, useLatest } from '../state.js';
6
6
  /**
7
7
  * Snackbar — a quick GOOD-NEWS line laid over the screen: something went well, said in passing: a check and the words on the soft tone. It slides in from the screen's top
8
8
  * (or `bottom`), stays `duration`, slides out, never takes the cursor and never stops the member. Not for a warning or a question —
@@ -24,7 +24,7 @@ export function Snackbar({ open, defaultOpen = true, onOpenChange, duration = 40
24
24
  useEffect(() => {
25
25
  if (shown || !entered)
26
26
  return;
27
- const ms = reducedMotion() ? 0 : parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--motion-sheet')) * 1000 || 350;
27
+ const ms = reducedMotion() ? 0 : sheetMs();
28
28
  const t = setTimeout(() => closed.current?.(), ms);
29
29
  return () => clearTimeout(t);
30
30
  }, [shown, entered]);
@@ -1,7 +1,7 @@
1
1
  import type { ComponentProps, ReactNode } from 'react';
2
2
  export interface ThreadProps extends ComponentProps<'div'> {
3
3
  }
4
- /** Thread — a stack of Bubbles: a conversation. */
4
+ /** Thread — a stack of Bubbles: a chat, a conversation turn by turn. */
5
5
  export declare function Thread({ className, ...rest }: ThreadProps): import("react").JSX.Element;
6
6
  export interface BubbleProps extends ComponentProps<'div'> {
7
7
  /** whose: theirs on the left (default), mine on the right */
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** Thread — a stack of Bubbles: a conversation. */
3
+ /** Thread — a stack of Bubbles: a chat, a conversation turn by turn. */
4
4
  export function Thread({ className, ...rest }) {
5
5
  return _jsx("div", { className: cx('thread', className), ...rest });
6
6
  }
package/dist/lists/Row.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cloneElement, createElement, isValidElement, useId, useLayoutEffect, useRef } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Icon } from '../type/Icon.js';
5
- import { reducedMotion, setRefs, useDisclosure } from '../state.js';
5
+ import { glide, reducedMotion, setRefs, stopMoves, useDisclosure } from '../state.js';
6
6
  /**
7
7
  * Row — a frameless row inside a card: lead · title + meta · end. 48px+ tall, a tap target.
8
8
  * Several go in Rows (hairlines between). A row that IS a card is `Card row`.
@@ -32,22 +32,20 @@ export function Rows({ heading, className, children, ref, ...rest }) {
32
32
  if (!box)
33
33
  return;
34
34
  const rows = [...box.children].filter((el) => el.id !== id);
35
- rows.forEach((el) => el.getAnimations().forEach((a) => { if (a.id === 'rows-move')
36
- a.cancel(); })); // read the resting places
35
+ stopMoves(rows, 'rows-move'); // read the resting places
37
36
  const at = new Map(rows.map((el) => [el, el.offsetTop]));
38
37
  const before = order.current, moved = before.length === rows.length && rows.some((el, i) => el !== before[i]) && rows.every((el) => was.current.has(el));
39
38
  if (moved && !reducedMotion()) {
40
- const ms = (parseFloat(getComputedStyle(box).getPropertyValue('--motion-sheet')) || 0.35) * 1000;
39
+ const cs = getComputedStyle(box);
41
40
  for (const el of rows) {
42
41
  const dy = (was.current.get(el) ?? 0) - (at.get(el) ?? 0);
43
42
  if (!dy)
44
43
  continue;
45
44
  const up = dy > 0; // it goes UP: over the others, on the card's ground so they pass under it
46
- const a = el.animate([{ translate: `0 ${dy}px` }, { translate: '0 0' }], { duration: ms, easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)' });
47
- a.id = 'rows-move';
45
+ const a = glide(el, dy, cs, 'rows-move');
48
46
  if (up) {
49
47
  el.style.position = 'relative';
50
- el.style.zIndex = '1';
48
+ el.style.zIndex = 'var(--z-over)';
51
49
  const clear = () => { el.style.position = ''; el.style.zIndex = ''; };
52
50
  a.finished.then(clear, clear);
53
51
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useLayoutEffect, useRef } from 'react';
3
- import { useControlled } from '../state.js';
2
+ import { createContext, useContext, useLayoutEffect, useRef } from 'react';
3
+ import { useControlled, useSettled } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  const PickerContext = createContext(null);
6
6
  /**
@@ -13,7 +13,6 @@ export function Picker({ value, defaultValue, onChange, unit, label, className,
13
13
  const [current, pick] = useControlled(value, defaultValue, onChange);
14
14
  const rail = useRef(null);
15
15
  const mounted = useRef(false);
16
- const settle = useRef(null);
17
16
  const items = () => [...(rail.current?.querySelectorAll(':scope > .picker__item') ?? [])];
18
17
  const column = () => (rail.current ? rail.current.clientWidth / 3 : 0); // the rail is three columns wide; item i stands in the centre at i × one column
19
18
  /* the chosen value stands in the centre — at once on arrival, sliding after */
@@ -23,22 +22,16 @@ export function Picker({ value, defaultValue, onChange, unit, label, className,
23
22
  r.scrollTo({ left: i * column(), behavior: mounted.current ? 'smooth' : 'auto' });
24
23
  mounted.current = true;
25
24
  });
26
- useEffect(() => () => { if (settle.current)
27
- clearTimeout(settle.current); }, []);
28
25
  /* the rail came to rest: whatever stands in the centre is what the member chose */
29
- const scrolled = () => {
30
- if (settle.current)
31
- clearTimeout(settle.current);
32
- settle.current = setTimeout(() => {
33
- const r = rail.current, col = column();
34
- if (!r || !col)
35
- return;
36
- const at = items()[Math.round(r.scrollLeft / col)]?.dataset.value;
37
- if (at !== undefined)
38
- pick(at);
39
- }, 120);
40
- };
41
- return (_jsx(PickerContext.Provider, { value: { value: current, pick }, children: _jsxs("div", { className: cx('picker', className), "aria-label": label, "data-value": current, ...rest, children: [_jsx("div", { className: "picker__rail", role: "listbox", ref: rail, onScroll: scrolled, children: children }), unit != null && _jsx("span", { className: "picker__unit", children: unit })] }) }));
26
+ const { settled } = useSettled(() => {
27
+ const r = rail.current, col = column();
28
+ if (!r || !col)
29
+ return;
30
+ const at = items()[Math.round(r.scrollLeft / col)]?.dataset.value;
31
+ if (at !== undefined)
32
+ pick(at);
33
+ });
34
+ return (_jsx(PickerContext.Provider, { value: { value: current, pick }, children: _jsxs("div", { className: cx('picker', className), "aria-label": label, "data-value": current, ...rest, children: [_jsx("div", { className: "picker__rail", role: "listbox", ref: rail, onScroll: settled, children: children }), unit != null && _jsx("span", { className: "picker__unit", children: unit })] }) }));
42
35
  }
43
36
  /** PickerItem — one value on the Picker rail; a tap brings it to the centre. */
44
37
  export function PickerItem({ value, className, children, onClick, ...rest }) {
@@ -308,9 +308,9 @@ export declare function Avatar({ size, outline, add, on, halo, center, src, alt,
308
308
  onTransitionRunCapture?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
309
309
  onTransitionStart?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
310
310
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
311
+ className: string;
311
312
  href: string | undefined;
312
313
  type: string | undefined;
313
- className: string;
314
314
  }, HTMLSpanElement>;
315
315
  export interface AvatarStackProps extends ComponentProps<'span'> {
316
316
  /** the count that trails the stack ("+9"): plain text, no round, no fill, in title type — it reads at arm's length. Use it for a stack of Stamps or when the count is the point; a count that should look like one more head is `<Avatar size="sm" on>+7</Avatar>` as the last child */
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
+ import { tagged } from '../pressable.js';
4
5
  import { Icon } from '../type/Icon.js';
5
6
  /** Avatar — initials by default, a photo or an Icon when there is one; sizes are the tokens. */
6
7
  export function Avatar({ size, outline, add, on, halo, center, src, alt = '', icon, as, href, className, children, ...rest }) {
7
- const el = as ?? (href ? 'a' : 'span');
8
- return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('avatar', size && `avatar--${size}`, outline && 'avatar--outline', add && 'avatar--add', on && 'avatar--on', halo && 'avatar--halo', center && 'avatar--center', className), ...rest }, src ? _jsx("img", { src: src, alt: alt }) : icon ? _jsx(Icon, { name: icon }) : children);
8
+ const { el, props } = tagged(as, href);
9
+ return createElement(el, { ...props, className: cx('avatar', size && `avatar--${size}`, outline && 'avatar--outline', add && 'avatar--add', on && 'avatar--on', halo && 'avatar--halo', center && 'avatar--center', className), ...rest }, src ? _jsx("img", { src: src, alt: alt }) : icon ? _jsx(Icon, { name: icon }) : children);
9
10
  }
10
11
  /** AvatarStack — overlapping small Avatars, or small Stamps (`bare`), side by side; a trailing count is `more` (or an `on` Avatar as the last child). */
11
12
  export function AvatarStack({ more, className, children, ...rest }) {
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef } from 'react';
3
3
  import { cx } from '../cx.js';
4
- import { reducedMotion, useControlled, useLatest, watchSize } from '../state.js';
5
- import { holdRoom } from '../room.js';
4
+ import { reducedMotion, swipeOf, useControlled, useLatest } from '../state.js';
5
+ import { holdRoomWhile } from '../room.js';
6
6
  import { word } from '../words.js';
7
7
  import { StampPlace } from '../stamp/Stamp.js';
8
8
  const STRIKE = 700; // ms the struck card is seen before it asks to leave
@@ -53,10 +53,7 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
53
53
  useLayoutEffect(() => { measure(); room.current?.sync(); }, [text]);
54
54
  useEffect(() => {
55
55
  const ta = paper.current;
56
- room.current = holdRoom(ta.parentElement, () => paper.current, () => ta.scrollHeight > ta.clientHeight + 1, (refused) => refused && full.current?.());
57
- document.fonts?.ready.then(measure);
58
- const stop = watchSize([ta], measure);
59
- return () => { room.current?.stop(); room.current = null; stop(); };
56
+ return holdRoomWhile(room, [ta], measure, ta.parentElement, () => paper.current, () => ta.scrollHeight > ta.clientHeight + 1, (refused) => refused && full.current?.());
60
57
  }, []);
61
58
  useEffect(() => {
62
59
  if (at !== 'struck')
@@ -66,12 +63,12 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
66
63
  }, [at, askState]);
67
64
  const from = useRef(null);
68
65
  const drop = (e) => {
69
- const dx = from.current == null ? 0 : e.clientX - from.current;
66
+ const dx = from.current == null ? 0 : swipeOf(from.current, e, 'x', SWIPE);
70
67
  from.current = null;
71
- if (writing && Math.abs(dx) >= SWIPE)
68
+ if (writing && dx)
72
69
  askSide(dx < 0 ? 'address' : 'message');
73
70
  };
74
- return (_jsxs("div", { className: cx('post-desk', `post-desk--${side}`, `post-desk--${at}`, className), ...rest, children: [_jsxs("div", { className: "post-desk__window", onPointerDown: (e) => { from.current = e.clientX; }, onPointerUp: drop, children: [_jsxs("div", { className: "post-desk__card post-paper", children: [_jsx("section", { className: "post-desk__half post-desk__half--message", "aria-label": word('message'), inert: !writing || side !== 'message', children: _jsx("textarea", { ref: paper, className: "post-desk__write", "aria-label": word('message'), placeholder: placeholder, value: text, onChange: (e) => askText(e.currentTarget.value) }) }), _jsxs("section", { className: "post-desk__half post-desk__half--address", "aria-label": word('address'), inert: !writing || side !== 'address', children: [_jsx("div", { className: "post-desk__corner", children: _jsxs("div", { className: "post-corner", children: [!writing && postmark, _jsx("button", { className: "post-corner__stamp post-desk__slot", type: "button", "aria-pressed": chosen != null, "aria-label": word(chosen ? 'stampOff' : 'stampOn'), onClick: () => askStamp(chosen ? null : stamps[0]?.id ?? null), children: chosen ? chosen.stamp : _jsx(StampPlace, { "aria-hidden": "true", children: word('stampOn') }) })] }) }), _jsx("div", { className: "post-desk__lines", children: address })] })] }), writing && _jsx("button", { className: `post-desk__reach post-desk__reach--${side === 'message' ? 'address' : 'message'}`, type: "button", "aria-label": word(side === 'message' ? 'toAddress' : 'toMessage'), onClick: () => askSide(side === 'message' ? 'address' : 'message') }), at === 'sent' && _jsx("div", { className: "post-desk__gone", children: sent })] }), _jsx("p", { className: "post-desk__note", "aria-live": "polite", children: note }), writing && side === 'address' && stamps.length > 0 && (_jsx("div", { className: "post-desk__stamps", role: "group", "aria-label": word('stamps'), children: stamps.map((s) => (_jsx("button", { className: "post-desk__pick", type: "button", "aria-pressed": s.id === stuck, "aria-label": s.label, onClick: () => askStamp(s.id), children: s.stamp }, s.id))) }))] }));
71
+ return (_jsxs("div", { className: cx('post-desk', `post-desk--${side}`, `post-desk--${at}`, className), ...rest, children: [_jsxs("div", { className: "post-desk__window", onPointerDown: (e) => { from.current = { x: e.clientX, y: e.clientY }; }, onPointerUp: drop, children: [_jsxs("div", { className: "post-desk__card post-paper", children: [_jsx("section", { className: "post-desk__half post-desk__half--message", "aria-label": word('message'), inert: !writing || side !== 'message', children: _jsx("textarea", { ref: paper, className: "post-desk__write", "aria-label": word('message'), placeholder: placeholder, value: text, onChange: (e) => askText(e.currentTarget.value) }) }), _jsxs("section", { className: "post-desk__half post-desk__half--address", "aria-label": word('address'), inert: !writing || side !== 'address', children: [_jsx("div", { className: "post-desk__corner", children: _jsxs("div", { className: "post-corner", children: [!writing && postmark, _jsx("button", { className: "post-corner__stamp post-desk__slot", type: "button", "aria-pressed": chosen != null, "aria-label": word(chosen ? 'stampOff' : 'stampOn'), onClick: () => askStamp(chosen ? null : stamps[0]?.id ?? null), children: chosen ? chosen.stamp : _jsx(StampPlace, { "aria-hidden": "true", children: word('stampOn') }) })] }) }), _jsx("div", { className: "post-desk__lines", children: address })] })] }), writing && _jsx("button", { className: `post-desk__reach post-desk__reach--${side === 'message' ? 'address' : 'message'}`, type: "button", "aria-label": word(side === 'message' ? 'toAddress' : 'toMessage'), onClick: () => askSide(side === 'message' ? 'address' : 'message') }), at === 'sent' && _jsx("div", { className: "post-desk__gone", children: sent })] }), _jsx("p", { className: "post-desk__note", "aria-live": "polite", children: note }), writing && side === 'address' && stamps.length > 0 && (_jsx("div", { className: "post-desk__stamps", role: "group", "aria-label": word('stamps'), children: stamps.map((s) => (_jsx("button", { className: "post-desk__pick", type: "button", "aria-pressed": s.id === stuck, "aria-label": s.label, onClick: () => askStamp(s.id), children: s.stamp }, s.id))) }))] }));
75
72
  }
76
73
  /** PostDeskLine — one address line of a PostDesk (`address`): a small word and a bare input on a rule. The value is the app's, like any input's. */
77
74
  export function PostDeskLine({ label, type = 'text', autoComplete = 'off', className, ...rest }) {
@@ -8,9 +8,9 @@ import { IconBtn } from '../actions/IconBtn.js';
8
8
  import { PostCorner } from './postCorner.js';
9
9
  const OTHER = { picture: 'back', back: 'picture' };
10
10
  /* half a turn: the sheet swings on its upright middle between flat (0) and edge-on (±90). `hold` keeps it edge-on until the other sheet is rendered */
11
- function swing(el, from, to, easing, hold = false) {
11
+ function swing(el, from, to, ease, hold = false) {
12
12
  const lens = `perspective(${el.offsetWidth * 3}px)`;
13
- const flip = parseFloat(getComputedStyle(el).getPropertyValue('--motion-flip')) || 0.45;
13
+ const cs = getComputedStyle(el), flip = parseFloat(cs.getPropertyValue('--motion-flip')) || 0.45, easing = cs.getPropertyValue(ease).trim() || 'ease';
14
14
  return el.animate([{ transform: `${lens} rotateY(${from}deg)` }, { transform: `${lens} rotateY(${to}deg)` }], { duration: flip * 500, easing, fill: hold ? 'forwards' : 'none' });
15
15
  }
16
16
  /**
@@ -35,7 +35,7 @@ export function PostLetter({ face, defaultFace = 'picture', onFaceChange, pictur
35
35
  setLying(asked);
36
36
  return;
37
37
  }
38
- const out = swing(el, 0, 90, 'ease-in', true);
38
+ const out = swing(el, 0, 90, '--ease-in', true);
39
39
  out.onfinish = () => setLying(asked);
40
40
  return () => out.cancel();
41
41
  }, [asked, lying]);
@@ -45,7 +45,7 @@ export function PostLetter({ face, defaultFace = 'picture', onFaceChange, pictur
45
45
  landed.current = lying;
46
46
  const el = sheets.current[lying];
47
47
  if (el?.animate)
48
- swing(el, -90, 0, 'ease-out');
48
+ swing(el, -90, 0, '--ease-out');
49
49
  }, [lying]);
50
50
  const turn = () => ask(OTHER[asked]);
51
51
  const [name, ...lines] = to;
@@ -6,3 +6,12 @@ export declare function pressable<E extends 'span' | 'div'>(onClick: (() => void
6
6
  Box: E;
7
7
  boxProps: ComponentProps<E>;
8
8
  };
9
+ /** a tag-like part (`as` span · a · button): `as`, else a link when it has an `href`, else a span — with what the tag needs before the
10
+ * part's own props: the `href`, and `type="button"` on a button so it never submits a form */
11
+ export declare function tagged(as: 'span' | 'a' | 'button' | undefined, href: string | undefined): {
12
+ el: "a" | "button" | "span";
13
+ props: {
14
+ href: string | undefined;
15
+ type: string | undefined;
16
+ };
17
+ };
package/dist/pressable.js CHANGED
@@ -4,3 +4,9 @@ export function pressable(onClick, still, rest, extra) {
4
4
  const tap = onClick != null;
5
5
  return { tap, Box: (tap ? 'button' : still), boxProps: (tap ? { type: 'button', onClick, ...extra, ...rest } : rest) };
6
6
  }
7
+ /** a tag-like part (`as` span · a · button): `as`, else a link when it has an `href`, else a span — with what the tag needs before the
8
+ * part's own props: the `href`, and `type="button"` on a button so it never submits a form */
9
+ export function tagged(as, href) {
10
+ const el = as ?? (href ? 'a' : 'span');
11
+ return { el, props: { href, type: el === 'button' ? 'button' : undefined } };
12
+ }
package/dist/room.d.ts CHANGED
@@ -10,3 +10,8 @@ export interface Room {
10
10
  stop(): void;
11
11
  }
12
12
  export declare function holdRoom(paper: HTMLElement, fieldOf: () => HTMLTextAreaElement | null, over: () => boolean, after?: (refused: boolean) => void): Room;
13
+ /** an effect's room: held in `room` while the effect runs, `measure` again whenever `watched` change size and once the fonts are in;
14
+ * returns the effect's cleanup */
15
+ export declare function holdRoomWhile(room: {
16
+ current: Room | null;
17
+ }, watched: Array<Element | null | undefined>, measure: () => void, ...hold: Parameters<typeof holdRoom>): () => void;
package/dist/room.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { watchSize } from './state.js';
1
2
  export function holdRoom(paper, fieldOf, over, after = () => { }) {
2
3
  let last = fieldOf()?.value ?? '', composing = false;
3
4
  const refuse = (field, dropped) => {
@@ -36,3 +37,11 @@ export function holdRoom(paper, fieldOf, over, after = () => { }) {
36
37
  stop() { paper.removeEventListener('input', onInput); paper.removeEventListener('compositionstart', onComposing); paper.removeEventListener('compositionend', onComposed); },
37
38
  };
38
39
  }
40
+ /** an effect's room: held in `room` while the effect runs, `measure` again whenever `watched` change size and once the fonts are in;
41
+ * returns the effect's cleanup */
42
+ export function holdRoomWhile(room, watched, measure, ...hold) {
43
+ room.current = holdRoom(...hold);
44
+ const stop = watchSize(watched, measure);
45
+ document.fonts?.ready.then(measure);
46
+ return () => { room.current?.stop(); room.current = null; stop(); };
47
+ }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, isValidElement, useEffect, useLayoutEffect, useRef } from 'react';
3
- import { reducedMotion, useControlled, watchSize } from '../state.js';
3
+ import { reducedMotion, useControlled, useSettled, watchSize } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  import { Pager } from './Pager.js';
6
6
  /**
@@ -13,7 +13,6 @@ export function Panes({ at, defaultAt = 0, onAtChange, dots = true, label, class
13
13
  const [current, request] = useControlled(at, defaultAt, onAtChange);
14
14
  const rail = useRef(null);
15
15
  const mounted = useRef(false);
16
- const settle = useRef(null);
17
16
  const panes = Children.toArray(children).filter(isValidElement);
18
17
  /* the chosen pane stands in sight — at once on arrival, sliding after */
19
18
  const align = (smooth) => {
@@ -24,20 +23,15 @@ export function Panes({ at, defaultAt = 0, onAtChange, dots = true, label, class
24
23
  useLayoutEffect(() => { align(mounted.current); mounted.current = true; });
25
24
  useEffect(() => {
26
25
  const stop = watchSize([rail.current], () => align(false)); // the box changed width (a turn of the phone): the same pane stays in sight
27
- return () => { stop(); if (settle.current)
28
- clearTimeout(settle.current); };
26
+ return () => { stop(); atRest.cancel(); };
29
27
  });
30
28
  /* the rail came to rest: the pane standing in it is where the member went */
31
- const scrolled = () => {
32
- if (settle.current)
33
- clearTimeout(settle.current);
34
- settle.current = setTimeout(() => {
35
- const r = rail.current;
36
- if (r && r.clientWidth)
37
- request(Math.min(panes.length - 1, Math.max(0, Math.round(r.scrollLeft / r.clientWidth))));
38
- }, 120);
39
- };
40
- return (_jsxs("div", { className: cx('panes', className), role: "group", "aria-label": label, ...rest, children: [_jsx("div", { className: "panes__rail", ref: rail, onScroll: scrolled, children: panes.map((p, i) => cloneElement(p, { inert: i !== current || undefined })) }), dots && panes.length > 1 && _jsx(Pager, { at: current + 1, of: panes.length })] }));
29
+ const atRest = useSettled(() => {
30
+ const r = rail.current;
31
+ if (r && r.clientWidth)
32
+ request(Math.min(panes.length - 1, Math.max(0, Math.round(r.scrollLeft / r.clientWidth))));
33
+ });
34
+ return (_jsxs("div", { className: cx('panes', className), role: "group", "aria-label": label, ...rest, children: [_jsx("div", { className: "panes__rail", ref: rail, onScroll: atRest.settled, children: panes.map((p, i) => cloneElement(p, { inert: i !== current || undefined })) }), dots && panes.length > 1 && _jsx(Pager, { at: current + 1, of: panes.length })] }));
41
35
  }
42
36
  /** Pane — one pane of a `Panes`: components one after another, as in a card. */
43
37
  export function Pane({ className, ...rest }) {
@@ -27,7 +27,7 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
27
27
  refreshing?: boolean;
28
28
  /** the card tone slot, as `Card tone` — 2 and up add `card--N`. The head shows it; with `paper` the content does not */
29
29
  tone?: 1 | 2 | 3 | 4 | 5;
30
- /** the content is white paper and the head keeps the sheet's colour. The same prop on a `StackSheet` or a `Card` in a stack. Omit it and the content is the same colour as the head */
30
+ /** the content is the theme's paper (`--action-bg`, the ground of its fields) and the head keeps the sheet's colour. The same prop on a `StackSheet` or a `Card` in a stack. Omit it and the content is the same colour as the head */
31
31
  paper?: boolean;
32
32
  }
33
33
  /**
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, isValidElement, useCallback, useEffect, useRef, useState } from 'react';
3
- import { focusWhenActive, useControlled, watchSize } from '../state.js';
3
+ import { focusWhenActive, useControlled, visibleBottom, watchSize } from '../state.js';
4
4
  import { CardHead } from '../cards/CardHead.js';
5
5
  import { SheetHead } from './SheetHead.js';
6
6
  import { cx } from '../cx.js';
@@ -81,9 +81,8 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
81
81
  const restTop = rect.top - slid(el);
82
82
  const bodyTop = (el.querySelector(':scope > .sheet__body') ?? el).getBoundingClientRect().top - rect.top;
83
83
  const box = el.parentElement.getBoundingClientRect(); // what clips it: the Step or the SheetStage
84
- const vv = window.visualViewport;
85
84
  const edge = Math.min(box.bottom, restTop + rect.height); // the sheet's own bottom edge at rest — above the box's on a stage that gave the keyboard its room
86
- const cut = Math.min(edge, vv ? vv.offsetTop + vv.height : Infinity);
85
+ const cut = Math.min(edge, visibleBottom());
87
86
  const button = el.querySelector(':scope > .card__foot')?.firstElementChild; // the foot's button stands on that edge at every state: the control has to clear its strip too
88
87
  const need = control.getBoundingClientRect().height + (button ? edge - button.getBoundingClientRect().top : 0);
89
88
  let to = list[list.length - 1];
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
- import { focusWhenActive, useControlled } from '../state.js';
3
+ import { focusWhenActive, swipeOf, useControlled } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  import { Icon } from '../type/Icon.js';
6
6
  import { Link } from '../actions/Link.js';
@@ -130,8 +130,8 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
130
130
  press.current = null;
131
131
  if (!p)
132
132
  return;
133
- const dx = e.clientX - p.x, dy = e.clientY - p.y;
134
- if (Math.abs(dy) < SWIPE_REM * parseFloat(getComputedStyle(document.documentElement).fontSize) || Math.abs(dy) < 2 * Math.abs(dx))
133
+ const dy = swipeOf(p, e, 'y', SWIPE_REM * parseFloat(getComputedStyle(document.documentElement).fontSize), 2);
134
+ if (!dy)
135
135
  return;
136
136
  const body = e.target.closest('.sheet-step__body');
137
137
  if (body && (dy < 0 ? body.scrollTop + body.clientHeight < body.scrollHeight - 1 : body.scrollTop > 0))
@@ -15,7 +15,7 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
15
15
  title: ReactNode;
16
16
  /** the tone slot — see `Tone`. The head shows it */
17
17
  tone?: Tone;
18
- /** the content is white paper and the head keeps the sheet's colour. The same look as `Sheet paper`. Omit it and the content matches the head */
18
+ /** the content is the theme's paper (`--action-bg`, the ground of its fields) and the head keeps the sheet's colour. The same look as `Sheet paper`. Omit it and the content matches the head */
19
19
  paper?: boolean;
20
20
  /** the head's end when the sheet has no tabs: an Icon, an IconRow */
21
21
  icon?: ReactNode;
@@ -1,7 +1,7 @@
1
1
  /* the PILE of a SheetSteps — its own behaviour, not exported from the package: where the body is scrolled when a step opens or a
2
2
  field takes the keyboard, and the quick slide that replays every jump of that scroll so the eye can follow it. */
3
3
  import { useEffect, useRef } from 'react';
4
- import { reducedMotion } from '../state.js';
4
+ import { reducedMotion, visibleBottom } from '../state.js';
5
5
  export const GLIDE_MS = 220; // the pile's slide out of the keyboard's way and back: quick, but the eye follows it
6
6
  const GLIDE_FOR = 900; // how long after a focus move (or a step change) a jump of the scroll is replayed as a slide
7
7
  /** `settle()` scrolls the pile to its place · `glideNow()` = the next jumps are played as slides. `moving` = until when a step
@@ -26,8 +26,7 @@ export function usePile(ref, moving) {
26
26
  but never past the LATEST done row, which always stays in sight */
27
27
  const focused = document.activeElement instanceof HTMLElement && now.contains(document.activeElement) ? document.activeElement : null;
28
28
  if (focused) {
29
- const vv = window.visualViewport;
30
- const cut = Math.min(body.getBoundingClientRect().bottom - (parseFloat(getComputedStyle(body).scrollPaddingBottom) || 0), vv ? vv.offsetTop + vv.height : Infinity);
29
+ const cut = Math.min(body.getBoundingClientRect().bottom - (parseFloat(getComputedStyle(body).scrollPaddingBottom) || 0), visibleBottom());
31
30
  const under = (focused.closest('.field, .pin') ?? focused).getBoundingClientRect().bottom + (body.scrollTop - to) - cut; // where its bottom will be once scrolled to `to`
32
31
  const most = above.length ? at(above[above.length - 1]) : 0;
33
32
  if (under > 0)