@usableapp/cardds 0.7.87 → 0.7.89
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 +20 -0
- package/css/card.css +9 -0
- package/css/finish.css +2 -2
- package/css/post.css +1 -3
- package/css/stamp.css +3 -3
- package/css/step.css +8 -3
- package/dist/cardds.css +23 -11
- package/dist/cards/Card.d.ts +10 -2
- package/dist/cards/Card.js +18 -4
- package/dist/catalog.json +1 -1
- package/dist/lists/Row.d.ts +2 -0
- package/dist/scaffold/EmptyState.d.ts +2 -1
- package/dist/scaffold/EmptyState.js +2 -1
- package/dist/stamp/Stamp.d.ts +8 -1
- package/dist/stamp/Stamp.js +7 -2
- package/dist/stamp/StampOn.d.ts +1 -1
- package/dist/stamp/StampOn.js +3 -17
- package/dist/stamp/useStrike.d.ts +7 -0
- package/dist/stamp/useStrike.js +20 -0
- package/lint/rules.mjs +7 -0
- package/package.json +1 -1
package/dist/lists/Row.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface RowProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
23
23
|
done?: boolean;
|
|
24
24
|
/** div (default), a link, a label wrapping a Check/Toggle, li */
|
|
25
25
|
as?: 'div' | 'a' | 'label' | 'li';
|
|
26
|
+
/** with `as="label"`: the `id` of the control the row presses (its `Check`). Needed when the `end` holds a button BEFORE the check (a picked row's own move, "open the map"): a label presses the first control inside it, so without this a touch on the row's words would press that button */
|
|
27
|
+
htmlFor?: string;
|
|
26
28
|
/** makes the row a link (an `a`); not with `more`, whose row is a summary */
|
|
27
29
|
href?: string;
|
|
28
30
|
/** what the row EXPANDS to show under itself (who liked, the rest of a list): the row becomes a `<details>`' summary */
|
|
@@ -10,5 +10,6 @@ export interface EmptyStateProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
10
10
|
level?: 'h2' | 'h3';
|
|
11
11
|
}
|
|
12
12
|
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
|
|
13
|
-
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
13
|
+
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
14
|
+
* Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a `SheetBody` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
|
|
14
15
|
export declare function EmptyState({ icon, title, action, level, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
|
|
4
|
-
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
4
|
+
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
5
|
+
* Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a `SheetBody` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
|
|
5
6
|
export function EmptyState({ icon, title, action, level = 'h2', className, children, ...rest }) {
|
|
6
7
|
const H = level;
|
|
7
8
|
return (_jsxs("div", { className: cx('empty-state', className), ...rest, children: [icon, title != null && _jsx(H, { className: "t-h2", children: title }), typeof children === 'string' ? _jsx("p", { className: "t-body", children: children }) : children, action != null && _jsx("div", { className: "empty-state__action", children: action })] }));
|
package/dist/stamp/Stamp.d.ts
CHANGED
|
@@ -15,14 +15,21 @@ export interface StampProps extends Omit<ComponentProps<'span'>, 'onClick'> {
|
|
|
15
15
|
tilt?: 'left' | 'right';
|
|
16
16
|
/** the whole stamp is a button: it renders a `<button>` */
|
|
17
17
|
onClick?: () => void;
|
|
18
|
+
/** the member presses send and the stamp that already stands in the card's head is STRUCK onto its place by itself (shown large and faint, pressed down to its size) — as it comes on, or as this turns true. Use it when a stamp stands where the layout put it (`Card end`, a centre, the album) and sending should strike it; the stamp does not move and it keeps its `tilt`. A stamp laid ON a surface at a place the member chose is `StampOn arrive`. A stamp without `arrive` is not struck. With `onArrived` */
|
|
19
|
+
arrive?: boolean;
|
|
20
|
+
/** an arriving stamp has LANDED (its strike ended; at once where motion is off, and once per arrival) — the app does what follows: sends, closes, sets the card's `leave` */
|
|
21
|
+
onArrived?: () => void;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Stamp — a perforated paper stamp, 4:5: the pictures fill the inner frame on a tone, the words sit on the picture's foot.
|
|
21
25
|
* Every stamp is ONE size wherever it stands (`--stamp-size`, two taps wide: 96 × 120) — what holds it only places it; a holder narrower than that shows it whole, smaller.
|
|
22
26
|
* The one small stamp is a stamp stuck on something (`StampOn`). Every inner length follows the stamp's width.
|
|
23
27
|
* The outer box holds the size and the shadow; the paper inside is the one whose edge is punched (a mask would cut the shadow too).
|
|
28
|
+
* With `arrive` the stamp that stands in its place (the head of a card, `end={<Stamp arrive …/>}`) is struck onto it when the member sends, and `onArrived` tells
|
|
29
|
+
* the app it has landed, so it sends. The strike scales it down from twice its size, so it never moves the layout (it may cover its neighbours for a moment);
|
|
30
|
+
* a `tilt` is kept, the stamp ends at its turn. Afterwards the same stamp stands in the same place with no strike: drop `arrive` or keep it, nothing is struck twice.
|
|
24
31
|
*/
|
|
25
|
-
export declare function Stamp({ pictures, tone, name, words, bare, tilt, onClick, className, ...rest }: StampProps): import("react").JSX.Element;
|
|
32
|
+
export declare function Stamp({ pictures, tone, name, words, bare, tilt, onClick, arrive, onArrived, className, onAnimationEnd, ...rest }: StampProps): import("react").JSX.Element;
|
|
26
33
|
export interface StampPlaceProps extends Omit<ComponentProps<'span'>, 'onClick'> {
|
|
27
34
|
/** a gap nobody is asked to fill: a paler edge (an album's free places) */
|
|
28
35
|
faint?: boolean;
|
package/dist/stamp/Stamp.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
import { pressable } from '../pressable.js';
|
|
4
|
+
import { useStrike } from './useStrike.js';
|
|
4
5
|
/**
|
|
5
6
|
* Stamp — a perforated paper stamp, 4:5: the pictures fill the inner frame on a tone, the words sit on the picture's foot.
|
|
6
7
|
* Every stamp is ONE size wherever it stands (`--stamp-size`, two taps wide: 96 × 120) — what holds it only places it; a holder narrower than that shows it whole, smaller.
|
|
7
8
|
* The one small stamp is a stamp stuck on something (`StampOn`). Every inner length follows the stamp's width.
|
|
8
9
|
* The outer box holds the size and the shadow; the paper inside is the one whose edge is punched (a mask would cut the shadow too).
|
|
10
|
+
* With `arrive` the stamp that stands in its place (the head of a card, `end={<Stamp arrive …/>}`) is struck onto it when the member sends, and `onArrived` tells
|
|
11
|
+
* the app it has landed, so it sends. The strike scales it down from twice its size, so it never moves the layout (it may cover its neighbours for a moment);
|
|
12
|
+
* a `tilt` is kept, the stamp ends at its turn. Afterwards the same stamp stands in the same place with no strike: drop `arrive` or keep it, nothing is struck twice.
|
|
9
13
|
*/
|
|
10
|
-
export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onClick, className, ...rest }) {
|
|
14
|
+
export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onClick, arrive, onArrived, className, onAnimationEnd, ...rest }) {
|
|
11
15
|
const shown = pictures.slice(0, 4);
|
|
16
|
+
const { struck } = useStrike(arrive, onArrived, onAnimationEnd);
|
|
12
17
|
const { tap, Box, boxProps } = pressable(onClick, 'span', rest);
|
|
13
|
-
return (_jsx(Box, { className: cx('stamp', `stamp--${shown.length}`, bare && 'stamp--bare', tilt && `stamp--tilt-${tilt}`, tap && 'stamp--tap', className), ...boxProps, children: _jsx("span", { className: "stamp__paper", children: _jsxs("span", { className: cx('stamp__pic', `sticky--${tone}`), children: [shown.map((picture, i) => _jsx("span", { className: "stamp__art", "aria-hidden": "true", children: picture }, i)), (name != null || words != null) && (_jsxs("span", { className: "stamp__cap", children: [words != null && _jsx("span", { className: "stamp__words", children: words }), name != null && _jsx("span", { className: "stamp__name", children: name })] }))] }) }) }));
|
|
18
|
+
return (_jsx(Box, { className: cx('stamp', `stamp--${shown.length}`, bare && 'stamp--bare', tilt && `stamp--tilt-${tilt}`, tap && 'stamp--tap', arrive && 'stamp--arrive', className), onAnimationEnd: struck, ...boxProps, children: _jsx("span", { className: "stamp__paper", children: _jsxs("span", { className: cx('stamp__pic', `sticky--${tone}`), children: [shown.map((picture, i) => _jsx("span", { className: "stamp__art", "aria-hidden": "true", children: picture }, i)), (name != null || words != null) && (_jsxs("span", { className: "stamp__cap", children: [words != null && _jsx("span", { className: "stamp__words", children: words }), name != null && _jsx("span", { className: "stamp__name", children: name })] }))] }) }) }));
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp is (`--stamp-size`), a word or an Icon inside.
|
package/dist/stamp/StampOn.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface StampOnProps extends ComponentProps<'div'> {
|
|
|
24
24
|
/** the stamp at the stamp's MAIN size, 96 × 120 (`--stamp-size`), to cover what it lies on; the default is the small size, 48 × 60 (`--stamp-sm-size`), which leaves a note's words readable */
|
|
25
25
|
main?: boolean;
|
|
26
26
|
/** the stamp sticks ITSELF: as it comes onto the screen it is struck onto its place (shown large, pressed down to its size). Nobody
|
|
27
|
-
* picked it or pressed to stick it — the app shows it when its own move (send) begins. With `onArrived` */
|
|
27
|
+
* picked it or pressed to stick it — the app shows it when its own move (send) begins. With `onArrived`. A stamp that already stands in a card's head or a centre, where the layout put it, is `Stamp arrive` instead */
|
|
28
28
|
arrive?: boolean;
|
|
29
29
|
/** an arriving stamp has LANDED (its strike ended; at once where motion is off) — the app does what follows: sends, closes */
|
|
30
30
|
onArrived?: () => void;
|
package/dist/stamp/StampOn.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useContext, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
|
-
import {
|
|
4
|
+
import { useControlled } from '../state.js';
|
|
5
5
|
import { Icon } from '../type/Icon.js';
|
|
6
6
|
import { word } from '../words.js';
|
|
7
7
|
import { UnderNotes } from './underNotes.js';
|
|
8
|
+
import { useStrike } from './useStrike.js';
|
|
8
9
|
const MIDDLE = { x: 0.5, y: 0.5 };
|
|
9
10
|
const STEP = 0.02; // an arrow key's move: a share of the surface
|
|
10
11
|
const MOVES = { ArrowLeft: [-STEP, 0], ArrowRight: [STEP, 0], ArrowUp: [0, -STEP], ArrowDown: [0, STEP] };
|
|
@@ -18,7 +19,6 @@ function within(el, x, y) {
|
|
|
18
19
|
const mx = el.offsetWidth / 2 / box.clientWidth, my = el.offsetHeight / 2 / box.clientHeight;
|
|
19
20
|
return { x: round(clamp(x, mx, 1 - mx)), y: round(clamp(y, my, 1 - my)) };
|
|
20
21
|
}
|
|
21
|
-
const STRIKE = 'strike'; // css/tokens.css — the strike every mark struck onto paper reads
|
|
22
22
|
const write = (el, at) => { el.style.setProperty('--stamp-on-x', String(at.x)); el.style.setProperty('--stamp-on-y', String(at.y)); };
|
|
23
23
|
/**
|
|
24
24
|
* StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
|
|
@@ -46,7 +46,7 @@ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, o
|
|
|
46
46
|
const grip = useRef(null);
|
|
47
47
|
const timer = useRef(undefined);
|
|
48
48
|
const moves = onAtChange != null && !under;
|
|
49
|
-
const
|
|
49
|
+
const { arrived, struck } = useStrike(arrive, onArrived, onAnimationEnd);
|
|
50
50
|
const removes = onRemove != null && !under && !(arrive && !arrived);
|
|
51
51
|
const ask = (next) => { if (next.x !== shown.x || next.y !== shown.y)
|
|
52
52
|
request(next); };
|
|
@@ -115,20 +115,6 @@ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, o
|
|
|
115
115
|
if (!moves)
|
|
116
116
|
show();
|
|
117
117
|
};
|
|
118
|
-
const landed = useRef(onArrived);
|
|
119
|
-
landed.current = onArrived;
|
|
120
|
-
const land = () => { setArrived(true); landed.current?.(); };
|
|
121
|
-
useEffect(() => { if (!arrive)
|
|
122
|
-
setArrived(false);
|
|
123
|
-
else if (reducedMotion()) {
|
|
124
|
-
setArrived(true);
|
|
125
|
-
landed.current?.();
|
|
126
|
-
} }, [arrive]); // no strike to wait for
|
|
127
|
-
const struck = (e) => {
|
|
128
|
-
onAnimationEnd?.(e);
|
|
129
|
-
if (arrive && e.target === e.currentTarget && e.animationName === STRIKE)
|
|
130
|
-
land();
|
|
131
|
-
};
|
|
132
118
|
const place = { '--stamp-on-x': shown.x, '--stamp-on-y': shown.y, '--stamp-on-turn': turn, ...style };
|
|
133
119
|
const own = moves || removes ? { tabIndex: 0, role: 'group', 'aria-label': label, onKeyDown: key } : null;
|
|
134
120
|
const drag = moves ? { onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up } : null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AnimationEvent } from 'react';
|
|
2
|
+
/** a holder that `arrive`s: has its strike ended? Returns `arrived` (false again once `arrive` turns off) and `struck`, the holder's `onAnimationEnd`
|
|
3
|
+
* (it calls `onAnimationEnd` first). `onArrived` comes once per arrival: when the strike ends, at once where motion is off */
|
|
4
|
+
export declare function useStrike<E extends Element>(arrive: boolean | undefined, onArrived: (() => void) | undefined, onAnimationEnd?: (e: AnimationEvent<E>) => void): {
|
|
5
|
+
arrived: boolean;
|
|
6
|
+
struck: (e: AnimationEvent<E>) => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { reducedMotion, useLatest } from '../state.js';
|
|
3
|
+
const STRIKE = 'strike'; // css/tokens.css — the strike every mark struck onto paper reads
|
|
4
|
+
/** a holder that `arrive`s: has its strike ended? Returns `arrived` (false again once `arrive` turns off) and `struck`, the holder's `onAnimationEnd`
|
|
5
|
+
* (it calls `onAnimationEnd` first). `onArrived` comes once per arrival: when the strike ends, at once where motion is off */
|
|
6
|
+
export function useStrike(arrive, onArrived, onAnimationEnd) {
|
|
7
|
+
const told = useLatest(onArrived);
|
|
8
|
+
const [arrived, setArrived] = useState(false);
|
|
9
|
+
const land = () => { setArrived(true); told.current?.(); };
|
|
10
|
+
useEffect(() => { if (!arrive)
|
|
11
|
+
setArrived(false);
|
|
12
|
+
else if (reducedMotion())
|
|
13
|
+
land(); }, [arrive]); // no strike to wait for
|
|
14
|
+
const struck = (e) => {
|
|
15
|
+
onAnimationEnd?.(e);
|
|
16
|
+
if (arrive && e.target === e.currentTarget && e.animationName === STRIKE)
|
|
17
|
+
land();
|
|
18
|
+
};
|
|
19
|
+
return { arrived, struck };
|
|
20
|
+
}
|
package/lint/rules.mjs
CHANGED
|
@@ -209,5 +209,12 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// 8 · a label presses the FIRST control inside it: a button before the row's Check takes the press meant for the row
|
|
213
|
+
for (const m of text.matchAll(/<Row\b/g)) {
|
|
214
|
+
const tag = tagAt(text, m.index);
|
|
215
|
+
if (/\sas\s*=\s*\{?\s*["']label["']/.test(tag) && /<(?:IconBtn|Btn|Link)\b/.test(tag) && !/\shtmlFor\s*=/.test(tag))
|
|
216
|
+
add('row-label-button', 'warn', m.index, '<Row as="label"> holding a button and no htmlFor: a label presses the first control inside it, so a touch on the row\'s words presses that button, not its Check. Give the Check an id and the row htmlFor={id}.');
|
|
217
|
+
}
|
|
218
|
+
|
|
212
219
|
return findings.sort((a, b) => a.line - b.line);
|
|
213
220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.89",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",
|