@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.
- package/CHANGELOG.md +39 -0
- package/README.md +38 -22
- package/css/card.css +7 -4
- package/css/people.css +2 -2
- package/css/stamp.css +6 -3
- package/css/sticky.css +34 -5
- package/dist/cardds.css +49 -14
- package/dist/cards/CardFeed.d.ts +22 -12
- package/dist/cards/CardFeed.js +51 -24
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sticky/Sticky.d.ts +5 -3
- package/dist/sticky/Sticky.js +64 -3
- package/dist/sticky/StickyField.d.ts +5 -1
- package/dist/sticky/StickyField.js +5 -1
- package/dist/sticky/StickyHeading.d.ts +309 -0
- package/dist/sticky/StickyHeading.js +21 -0
- package/dist/sticky/fit.d.ts +6 -0
- package/dist/sticky/fit.js +17 -0
- package/package.json +1 -1
package/dist/cards/CardFeed.js
CHANGED
|
@@ -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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
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
|
|
21
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
34
|
-
const
|
|
35
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
}
|