@usableapp/cardds 0.7.89 → 0.7.92

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,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createContext, isValidElement, useContext, useId, useLayoutEffect, useRef } from 'react';
2
+ import { Children, createContext, isValidElement, useContext, useEffect, useId, useLayoutEffect, useRef } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { reducedMotion, setRefs, useControlled, warnOnce } from '../state.js';
5
5
  import { Card } from './Card.js';
@@ -10,39 +10,48 @@ import { Icon } from '../type/Icon.js';
10
10
  import { Text } from '../type/Text.js';
11
11
  const FeedCtx = createContext(null);
12
12
  /**
13
- * CardFeed — a QUEUE of cards the system hands the member one after another; at most two are on the screen: one BIG (heading, content,
14
- * its button) and one SMALL (the same card cut down to its head). A touch anywhere on the small one makes it the big one and the other
15
- * the small one; neither changes place. The queue is the app's: when it changes (a card left, a new one came) what is left slides up,
16
- * the next comes in as the lower card, and the feed asks for the UPPER card to be big, whichever was big before. One card alone is big;
17
- * no card, nothing is drawn. Which card is big is controlled like an input (`big` + `onBigChange`, or `defaultBig`).
13
+ * CardFeed — a QUEUE of cards the system hands the member one after another, all of them on the screen: one BIG (heading, content, its
14
+ * button) and every other SMALL (the same card cut down to its head). A touch anywhere on a small one makes it the big one and the big
15
+ * one small; none changes place. The queue is the app's, and so is its length (5, then 10…): near its end the feed asks for more
16
+ * (`onMore`, at the card with `ahead` cards under it) and the app appends; the appended cards fade in and the member stays where they
17
+ * were. When a card LEAVES or the order changes, what is left slides up and the feed asks for the UPPER card to be big. When the
18
+ * app says the queue is complete (`end`) the feed draws those words after the last card and stops asking. One card alone is big; no
19
+ * card, nothing is drawn. Which card is big is controlled like an input (`big` + `onBigChange`, or `defaultBig`).
18
20
  */
19
- export function CardFeed({ big: bigProp, defaultBig, onBigChange, className, children, ref, ...rest }) {
20
- const queue = Children.toArray(children).filter((c) => isValidElement(c) && typeof c.props.id === 'string');
21
- if (queue.length !== Children.count(children))
21
+ export function CardFeed({ big: bigProp, defaultBig, onBigChange, onMore, ahead = 2, end, className, children, ref, ...rest }) {
22
+ const cards = Children.toArray(children).filter((c) => isValidElement(c) && typeof c.props.id === 'string');
23
+ if (cards.length !== Children.count(children))
22
24
  warnOnce('card-feed-child', 'cardds CardFeed: every child is a <FeedCard id="…"> — a child without a string `id` is not drawn');
23
- const shown = queue.slice(0, 2);
24
- const ids = shown.map((c) => c.props.id);
25
+ const ids = cards.map((c) => c.props.id);
25
26
  const [asked, request] = useControlled(bigProp, defaultBig, onBigChange);
26
- const big = asked !== undefined && ids.includes(asked) ? asked : ids[0];
27
+ const feed = {
28
+ ids,
29
+ big: asked !== undefined && ids.includes(asked) ? asked : (ids[0] ?? ''),
30
+ asker: ids[Math.max(0, ids.length - 1 - ahead)],
31
+ };
27
32
  const own = useRef(null);
28
33
  const tops = useRef(new Map());
29
34
  const was = useRef(null);
35
+ const more = useRef(onMore);
30
36
  const key = ids.join('\n');
31
37
  useLayoutEffect(() => {
38
+ more.current = onMore;
32
39
  const box = own.current;
33
- const cards = box ? [...box.children] : [];
34
- const changed = was.current !== null && was.current !== key;
35
- was.current = key;
40
+ const els = box ? [...box.querySelectorAll(':scope > .feed-card')] : [];
41
+ const before = was.current;
42
+ const changed = before !== null && before.join('\n') !== key;
43
+ const kept = before !== null && before.length > 0 && before.every((id, i) => ids[i] === id); // an append leaves the old ids a prefix: the member stays where they scrolled to
44
+ was.current = ids;
36
45
  if (changed) {
37
- if (ids.length)
46
+ if (!kept && ids.length)
38
47
  request(ids[0], 'queue');
39
- cards.forEach((el) => el.getAnimations().forEach((a) => { if (a.id === 'feed-move')
48
+ els.forEach((el) => el.getAnimations().forEach((a) => { if (a.id === 'feed-move')
40
49
  a.cancel(); })); // read the resting places
41
50
  }
42
- const at = new Map(cards.map((el) => [el, el.offsetTop]));
51
+ const at = new Map(els.map((el) => [el, el.offsetTop]));
43
52
  if (changed && box && !reducedMotion()) {
44
53
  const ms = (parseFloat(getComputedStyle(box).getPropertyValue('--motion-sheet')) || 0.35) * 1000;
45
- for (const el of cards) {
54
+ for (const el of els) {
46
55
  const from = tops.current.get(el);
47
56
  const move = from === undefined
48
57
  ? el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: ms, easing: 'ease-out' }) // it came in: no place to come from
@@ -53,9 +62,25 @@ export function CardFeed({ big: bigProp, defaultBig, onBigChange, className, chi
53
62
  }
54
63
  tops.current = at;
55
64
  });
56
- if (!shown.length)
65
+ const asking = useRef(null); // the queue that has asked already
66
+ const canAsk = onMore != null && !end;
67
+ useEffect(() => {
68
+ const el = feed.asker === undefined ? undefined : own.current?.querySelectorAll(':scope > .feed-card')[ids.indexOf(feed.asker)];
69
+ if (!canAsk || !el)
70
+ return;
71
+ const watch = new IntersectionObserver((seen) => {
72
+ if (!seen.some((s) => s.isIntersecting) || asking.current === key)
73
+ return;
74
+ asking.current = key;
75
+ watch.disconnect();
76
+ more.current?.();
77
+ });
78
+ watch.observe(el);
79
+ return () => watch.disconnect();
80
+ }, [key, feed.asker, canAsk]);
81
+ if (!cards.length)
57
82
  return null;
58
- return (_jsx(FeedCtx.Provider, { value: { big, grow: (id) => request(id, 'tap') }, children: _jsx("div", { ref: setRefs(own, ref), className: cx('card-feed', className), ...rest, children: shown }) }));
83
+ return (_jsx(FeedCtx.Provider, { value: { big: feed.big, grow: (id) => request(id, 'tap') }, children: _jsxs("div", { ref: setRefs(own, ref), className: cx('card-feed', className), ...rest, children: [cards, end ? _jsx(Text, { level: "caption", muted: true, as: "p", role: "note", className: "card-feed__end", children: end }) : null] }) }));
59
84
  }
60
85
  const onSmallKey = (e) => {
61
86
  if (e.target !== e.currentTarget || (e.key !== 'Enter' && e.key !== ' '))
@@ -64,15 +89,17 @@ const onSmallKey = (e) => {
64
89
  e.currentTarget.click();
65
90
  };
66
91
  /**
67
- * FeedCard — ONE card of a `CardFeed`'s queue, the same markup in both sizes: its head (thumb · heading · caption) always shows, its
92
+ * FeedCard — ONE card of a `CardFeed`'s queue, the same markup in both sizes: its head (thumb or lead · heading · caption) always shows, its
68
93
  * content and its button are one part that opens while the card is big. The SIZE IS THE FEED'S, never a prop. Small, the card itself
69
94
  * is the control (a button named by its heading; Enter and Space) and its closed part cannot be pressed, focused or read. Big, it is a
70
95
  * plain card: with one button in its foot it is pressed whole; with two or more only the buttons act.
71
96
  */
72
- export function FeedCard({ id, heading, caption, icon, tone, foot, className, children, onClick, onKeyDown, ref, ...rest }) {
97
+ export function FeedCard({ id, heading, caption, icon, lead, tone, foot, className, children, onClick, onKeyDown, ref, ...rest }) {
73
98
  const feed = useContext(FeedCtx);
74
99
  if (!feed)
75
100
  warnOnce('feed-card-alone', 'cardds FeedCard: outside a <CardFeed> it has nobody to say its size — it is drawn big');
101
+ if (lead != null && icon)
102
+ warnOnce('feed-card-lead', 'cardds FeedCard: `lead` and `icon` are both given — the lead is drawn, the icon is not');
76
103
  const small = feed != null && feed.big !== id;
77
104
  const name = useId();
78
105
  const own = useRef(null);
@@ -94,5 +121,5 @@ export function FeedCard({ id, heading, caption, icon, tone, foot, className, ch
94
121
  byKey.current = e.detail === 0;
95
122
  feed.grow(id);
96
123
  } }, onKeyDown: (e) => { onKeyDown?.(e); if (small && !e.defaultPrevented)
97
- onSmallKey(e); }, children: [_jsxs(CardHead, { className: "feed-card__head", children: [icon && _jsx(CardThumb, { children: _jsx(Icon, { name: icon, size: "sm" }) }), _jsx(Text, { level: "h2", id: name, children: heading }), caption != null && _jsx(Text, { level: "caption", muted: true, children: caption })] }), _jsx("div", { className: "feed-card__more", inert: small, children: _jsxs("div", { className: "feed-card__rest", children: [children, foot != null && _jsx(CardFoot, { children: foot })] }) })] }));
124
+ onSmallKey(e); }, children: [_jsxs(CardHead, { className: "feed-card__head", children: [lead != null ? _jsx("span", { className: "feed-card__lead", children: lead }) : icon && _jsx(CardThumb, { children: _jsx(Icon, { name: icon, size: "sm" }) }), _jsx(Text, { level: "h2", id: name, children: heading }), caption != null && _jsx(Text, { level: "caption", muted: true, children: caption })] }), _jsx("div", { className: "feed-card__more", inert: small, children: _jsxs("div", { className: "feed-card__rest", children: [children, foot != null && _jsx(CardFoot, { children: foot })] }) })] }));
98
125
  }