@usableapp/cardds 0.7.54 → 0.7.55
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 +13 -0
- package/README.md +2 -2
- package/dist/cards/Card.js +4 -4
- package/dist/cards/CardHead.d.ts +3 -1
- package/dist/cards/CardHead.js +10 -7
- package/dist/choice/Slider.js +6 -4
- package/dist/scaffold/LayerHistory.d.ts +9 -5
- package/dist/scaffold/LayerHistory.js +27 -20
- package/dist/sheets/Sheet.js +14 -3
- package/dist/sheets/SheetBody.d.ts +1 -1
- package/dist/sheets/SheetBody.js +4 -1
- package/dist/sheets/SheetHead.d.ts +3 -1
- package/dist/sheets/SheetHead.js +2 -2
- package/dist/sheets/SheetStack.js +20 -1
- package/dist/sheets/SheetSteps.js +4 -4
- package/dist/sheets/StackSheet.js +4 -3
- package/dist/sheets/sheetMeasure.d.ts +1 -1
- package/dist/sheets/sheetMeasure.js +10 -5
- package/dist/sheets/stackCard.d.ts +5 -3
- package/dist/sheets/stackCard.js +14 -7
- package/dist/templates/Home.d.ts +1 -1
- package/dist/templates/Home.js +26 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
> For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
|
|
4
4
|
|
|
5
|
+
## 0.7.55 — 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Back on Home closes the open sheet and stays on the page.** With `LayerHistory` above `Home` and no `open` prop, the open sheet is one history entry. Back folds it. The address does not change, so the screen does not slide. Opening keeps the page's own query, and only `arrive` drops a query. If your `push` and `replace` still call `navigate({ pathname }, { state })`, they drop that query until you pass the page's own search through, as the README snippet does now.
|
|
9
|
+
- **A character typed while the SheetSteps holder has the cursor lands in a Composer.** It already landed in a Field or a Pin. Nothing to change.
|
|
10
|
+
- **Closing a SheetStack, the sheets in front return one at a time.** Nothing to change.
|
|
11
|
+
- **A pull to refresh keeps going when the sheet starts reloading while the finger is still down.** Letting go clears the pull. Nothing to change.
|
|
12
|
+
- **`Slider` with an empty `value` and a `defaultValue` draws one number,** on the knob and on the track. Nothing to change.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **The navigation that opens the home screen replaces the page behind it,** so a closed stack has nothing of the app to go back to. `Home` does not do this itself. The route that arrives there uses replace.
|
|
16
|
+
- **A long title renders `is-long`, and a pull renders `is-refreshing`.** Do not stamp those classes onto the node after it has rendered.
|
|
17
|
+
|
|
5
18
|
## 0.7.54 — 2026-10-06
|
|
6
19
|
|
|
7
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -387,8 +387,8 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
387
387
|
sheet: read `?way=` once, no entry added, the query dropped). `useLayers().closeAll(then)` takes every layer off, then moves
|
|
388
388
|
(signing out). NOTHING INSIDE A LAYER TAKES AN ENTRY — a step, typed text, a sheet's rest, a tab: back at any step takes the
|
|
389
389
|
whole sheet down, and opened again it starts anew (a new `key` per opening). The project hands its router over once:
|
|
390
|
-
`<LayerHistory state push replace back>` around its screens — react-router: `
|
|
391
|
-
`push = (s) => navigate({ pathname }, { state: s })`, `replace = (s) => navigate({ pathname }, { replace: true, state: s })`,
|
|
390
|
+
`<LayerHistory state push replace back>` around its screens — react-router: `location = useLocation()`, `state = location.state`,
|
|
391
|
+
`push = (s, search) => navigate({ pathname: location.pathname, search: search === 'drop' ? '' : location.search }, { state: s })`, `replace = (s, search) => navigate({ pathname: location.pathname, search: search === 'drop' ? '' : location.search }, { replace: true, state: s })`,
|
|
392
392
|
`back = (n) => navigate(-n)`. cardds never calls the History API itself. Story `LayerHistory · Back`.
|
|
393
393
|
A FINISHED PROCESS IS NOT GONE BACK INTO: history cannot be deleted, so (a) inside a process every move on is a `replace`,
|
|
394
394
|
never a push — when it ends, back skips all of it; (b) a page that can no longer be returned to guards itself: it checks on
|
package/dist/cards/Card.js
CHANGED
|
@@ -4,7 +4,7 @@ import { cx } from '../cx.js';
|
|
|
4
4
|
import { warnOnce } from '../state.js';
|
|
5
5
|
import { CardHead } from './CardHead.js';
|
|
6
6
|
import { CardFoot } from './CardFoot.js';
|
|
7
|
-
import { StackHeadCtx, stackPaint, useStackCard } from '../sheets/stackCard.js';
|
|
7
|
+
import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/stackCard.js';
|
|
8
8
|
/**
|
|
9
9
|
* Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
|
|
10
10
|
* The head and the foot are SLOTS: `heading` · `caption` · `end` (the card writes its CardHead first) and `foot` · `footLink`
|
|
@@ -24,8 +24,8 @@ export function Card({ tone, sm, row, cover, fold, centre, band, as = 'article',
|
|
|
24
24
|
}
|
|
25
25
|
const own = useRef(null);
|
|
26
26
|
const swallow = useRef(false);
|
|
27
|
-
useStackCard(own, stack, swallow);
|
|
28
|
-
const painted = stackPaint(stack);
|
|
27
|
+
const pulling = useStackCard(own, stack, swallow);
|
|
28
|
+
const painted = stackPaint(stack, pulling);
|
|
29
29
|
const setRef = (node) => {
|
|
30
30
|
own.current = node;
|
|
31
31
|
const passed = ref;
|
|
@@ -50,7 +50,7 @@ export function Card({ tone, sm, row, cover, fold, centre, band, as = 'article',
|
|
|
50
50
|
if (!stack.open && !stack.before && !stack.after)
|
|
51
51
|
stack.onOpen();
|
|
52
52
|
};
|
|
53
|
-
const inner = (body) => (_jsx(StackHeadCtx.Provider, { value: stack ? (stack.before || stack.after ? -1 : 0) : null, children: body }));
|
|
53
|
+
const inner = (body) => (_jsx(StackHeadCtx.Provider, { value: stack ? (stack.before || stack.after ? -1 : 0) : null, children: _jsx(StackPullCtx.Provider, { value: pulling, children: body }) }));
|
|
54
54
|
const props = {
|
|
55
55
|
...rest,
|
|
56
56
|
ref: setRef,
|
package/dist/cards/CardHead.d.ts
CHANGED
|
@@ -10,5 +10,7 @@ export interface CardHeadProps extends ComponentProps<'div'> {
|
|
|
10
10
|
top?: boolean;
|
|
11
11
|
/** the heading level to render the string heading with (h2 default; h1 for the one-feature page) */
|
|
12
12
|
level?: 'h1' | 'h2' | 'display';
|
|
13
|
+
/** the sheet measured this title as wider than its line, so the heading runs sideways */
|
|
14
|
+
long?: boolean;
|
|
13
15
|
}
|
|
14
|
-
export declare function CardHead({ heading, caption, end, top, level, className, children, ...rest }: CardHeadProps): import("react").JSX.Element;
|
|
16
|
+
export declare function CardHead({ heading, caption, end, top, level, long, className, children, ...rest }: CardHeadProps): import("react").JSX.Element;
|
package/dist/cards/CardHead.js
CHANGED
|
@@ -14,8 +14,8 @@ const onTitleKey = (e) => {
|
|
|
14
14
|
e.preventDefault();
|
|
15
15
|
e.currentTarget.click();
|
|
16
16
|
};
|
|
17
|
-
/** the stack's title is the keyboard's way in. The heading renders the tab stop
|
|
18
|
-
function markTitle(node, tab) {
|
|
17
|
+
/** the stack's title is the keyboard's way in. The heading renders the tab stop and, when the sheet measured it long, `.is-long`. Nothing sets either on the node afterwards */
|
|
18
|
+
function markTitle(node, tab, long) {
|
|
19
19
|
if (!isValidElement(node))
|
|
20
20
|
return node;
|
|
21
21
|
const el = node;
|
|
@@ -24,13 +24,16 @@ function markTitle(node, tab) {
|
|
|
24
24
|
if (level !== 'h1' && level !== 'h2' && level !== 'display' && !intrinsic)
|
|
25
25
|
return node;
|
|
26
26
|
return cloneElement(el, {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
...(long ? { className: cx(el.props.className, 'is-long') } : {}),
|
|
28
|
+
...(tab != null ? {
|
|
29
|
+
tabIndex: tab,
|
|
30
|
+
onKeyDown: (e) => { el.props.onKeyDown?.(e); if (!e.defaultPrevented)
|
|
31
|
+
onTitleKey(e); },
|
|
32
|
+
} : {}),
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
|
-
export function CardHead({ heading, caption, end, top, level = 'h2', className, children, ...rest }) {
|
|
35
|
+
export function CardHead({ heading, caption, end, top, level = 'h2', long, className, children, ...rest }) {
|
|
33
36
|
const tab = useContext(StackHeadCtx);
|
|
34
37
|
const H = level === 'h1' ? 'h1' : 'h2';
|
|
35
|
-
return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className: `t-${level}`, tabIndex: tab ?? undefined, onKeyDown: tab != null ? onTitleKey : undefined, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), tab == null ? children : Children.map(children, (c) => markTitle(c, tab)), end] }));
|
|
38
|
+
return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className: cx(`t-${level}`, long && 'is-long'), tabIndex: tab ?? undefined, onKeyDown: tab != null ? onTitleKey : undefined, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), tab == null && !long ? children : Children.map(children, (c) => markTitle(c, tab, !!long)), end] }));
|
|
36
39
|
}
|
package/dist/choice/Slider.js
CHANGED
|
@@ -7,8 +7,10 @@ const share = (value, min, max) => (max > min ? Math.min(1, Math.max(0, (value -
|
|
|
7
7
|
export function Slider({ label, glyph, className, style, onInput, ...input }) {
|
|
8
8
|
const cursor = useStepCursor();
|
|
9
9
|
const min = Number(input.min) || 0, max = Number(input.max) || 100;
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const { value: given, defaultValue, ...rest } = input;
|
|
11
|
+
const held = given != null && given !== '';
|
|
12
|
+
const [live, setLive] = useState(Number((held ? given : defaultValue) ?? min));
|
|
13
|
+
const shown = held ? Number(given) : live;
|
|
14
|
+
return (_jsxs("label", { className: cx('slider', className), style: { ...style, '--slider-at': String(share(shown, min, max)) }, children: [label != null && _jsx("span", { className: "slider__label", children: label }), glyph != null && _jsx("span", { className: "slider__glyph", "aria-hidden": "true", children: glyph }), _jsx("input", { className: "slider__range", type: "range", onInput: (e) => { if (!held)
|
|
15
|
+
setLive(Number(e.currentTarget.value)); onInput?.(e); }, ...rest, ...(held ? { value: given } : { defaultValue }), ...cursor })] }));
|
|
14
16
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
export type LayerValue = string | number;
|
|
3
|
+
export type LayerSearch = 'keep' | 'drop';
|
|
3
4
|
type EntryState = Record<string, unknown>;
|
|
4
5
|
export interface LayerHistoryProps {
|
|
5
6
|
/** the CURRENT history entry's state, as a value of this render — the hook re-reads it on every back / forward / push
|
|
6
7
|
* (react-router: `useLocation().state`) */
|
|
7
8
|
state: unknown;
|
|
8
|
-
/** a NEW entry for the same page with this state
|
|
9
|
-
* `navigate({ pathname }, { state })`) */
|
|
10
|
-
push: (state: EntryState) => void;
|
|
11
|
-
/** the SAME entry, its state replaced
|
|
12
|
-
|
|
9
|
+
/** a NEW entry for the same page with this state. The path is kept. The query is kept unless the second argument is `'drop'` (react-router:
|
|
10
|
+
* `navigate({ pathname, search: search === 'drop' ? '' : location.search }, { state })`) */
|
|
11
|
+
push: (state: EntryState, search?: LayerSearch) => void;
|
|
12
|
+
/** the SAME entry, its state replaced. The path is kept. The query is kept unless the second argument is `'drop'`
|
|
13
|
+
* (`navigate({ pathname, search: search === 'drop' ? '' : location.search }, { replace: true, state })`) */
|
|
14
|
+
replace: (state: EntryState, search?: LayerSearch) => void;
|
|
13
15
|
/** back by this many entries (`navigate(-entries)`) */
|
|
14
16
|
back: (entries: number) => void;
|
|
15
17
|
children?: ReactNode;
|
|
@@ -40,6 +42,8 @@ export interface LayerApi {
|
|
|
40
42
|
* give what is inside a new `key` per opening.
|
|
41
43
|
*/
|
|
42
44
|
export declare function useLayer(name: string): LayerApi;
|
|
45
|
+
/** useOptionalLayer — the same layer as `useLayer`, or `null` when no `LayerHistory` is above. A missing provider does not warn. */
|
|
46
|
+
export declare function useOptionalLayer(name: string): LayerApi | null;
|
|
43
47
|
/**
|
|
44
48
|
* useLayers — every layer over this screen: `up` (how many), and `closeAll(then)` — take them ALL off, then run `then`: before a
|
|
45
49
|
* move that must leave nothing of this page's layers under it (signed out → replace with the sign-in page). The entries are
|
|
@@ -33,6 +33,26 @@ const useHistory = () => {
|
|
|
33
33
|
warnOnce('layer-history', 'cardds useLayer: no <LayerHistory> above — hand the router over once, around the screens; until then a layer never comes up.');
|
|
34
34
|
return h ?? NONE;
|
|
35
35
|
};
|
|
36
|
+
const layerApi = (name, handle) => {
|
|
37
|
+
const { state, push, replace, back } = handle;
|
|
38
|
+
const { rest, layers } = read(state);
|
|
39
|
+
const layer = layers[name] ?? null;
|
|
40
|
+
const others = () => Object.fromEntries(Object.entries(layers).filter(([key]) => key !== name));
|
|
41
|
+
return {
|
|
42
|
+
value: layer?.value ?? null,
|
|
43
|
+
open: (to) => (layer ? replace({ ...rest, layers: { ...layers, [name]: { value: to, pushed: layer.pushed } } }, 'keep') : push({ ...rest, layers: { ...layers, [name]: { value: to, pushed: true } } }, 'keep')),
|
|
44
|
+
// already up on an entry the app pushed (the link opened where the layer was): it stays a pushed one, or the close would leave that entry behind
|
|
45
|
+
arrive: (as) => replace({ ...rest, layers: { ...layers, [name]: { value: as, pushed: layer?.pushed ?? false } } }, 'drop'),
|
|
46
|
+
close: () => {
|
|
47
|
+
if (!layer)
|
|
48
|
+
return;
|
|
49
|
+
if (layer.pushed)
|
|
50
|
+
back(1);
|
|
51
|
+
else
|
|
52
|
+
replace({ ...rest, layers: others() }, 'keep');
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
36
56
|
/**
|
|
37
57
|
* useLayer — ONE layer of this screen, by its name: a whole bottom sheet, a modal. Back takes it off; the screen stays.
|
|
38
58
|
* `const way = useLayer('way')` → `<CallSheet open={way.value} onOpenChange={(up, i) => (up ? way.open(i!) : way.close())}>`.
|
|
@@ -40,26 +60,13 @@ const useHistory = () => {
|
|
|
40
60
|
* give what is inside a new `key` per opening.
|
|
41
61
|
*/
|
|
42
62
|
export function useLayer(name) {
|
|
43
|
-
const
|
|
44
|
-
return useMemo(() =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
open: (to) => (layer ? replace({ ...rest, layers: { ...layers, [name]: { value: to, pushed: layer.pushed } } }) : push({ ...rest, layers: { ...layers, [name]: { value: to, pushed: true } } })),
|
|
51
|
-
// already up on an entry the app pushed (the link opened where the layer was): it stays a pushed one, or the close would leave that entry behind
|
|
52
|
-
arrive: (as) => replace({ ...rest, layers: { ...layers, [name]: { value: as, pushed: layer?.pushed ?? false } } }),
|
|
53
|
-
close: () => {
|
|
54
|
-
if (!layer)
|
|
55
|
-
return;
|
|
56
|
-
if (layer.pushed)
|
|
57
|
-
back(1);
|
|
58
|
-
else
|
|
59
|
-
replace({ ...rest, layers: others() });
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}, [state, push, replace, back, name]);
|
|
63
|
+
const handle = useHistory();
|
|
64
|
+
return useMemo(() => layerApi(name, handle), [handle, name]);
|
|
65
|
+
}
|
|
66
|
+
/** useOptionalLayer — the same layer as `useLayer`, or `null` when no `LayerHistory` is above. A missing provider does not warn. */
|
|
67
|
+
export function useOptionalLayer(name) {
|
|
68
|
+
const handle = useContext(Ctx);
|
|
69
|
+
return useMemo(() => (handle ? layerApi(name, handle) : null), [handle, name]);
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
72
|
* useLayers — every layer over this screen: `up` (how many), and `closeAll(then)` — take them ALL off, then run `then`: before a
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Children, cloneElement, isValidElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { focusWhenActive, useControlled, watchSize } from '../state.js';
|
|
4
|
+
import { CardHead } from '../cards/CardHead.js';
|
|
5
|
+
import { SheetHead } from './SheetHead.js';
|
|
4
6
|
import { cx } from '../cx.js';
|
|
5
7
|
import { word } from '../words.js';
|
|
6
8
|
import { ORDER, slid, stopsOf, useFootRoom, usePeekStrip, useTitleRun } from './sheetMeasure.js';
|
|
7
9
|
import { useSheetSlide } from './sheetSlide.js';
|
|
8
10
|
import { useSheetDrag } from './sheetDrag.js';
|
|
9
11
|
const RANK = { away: 0, peek: 1, half: 2, '3q': 3, full: 4 };
|
|
12
|
+
/** the sheet's own head renders `.is-long` from the measure. A heading further down is not that title. */
|
|
13
|
+
function markLong(node, long) {
|
|
14
|
+
if (!long || !isValidElement(node))
|
|
15
|
+
return node;
|
|
16
|
+
if (node.type !== CardHead && node.type !== SheetHead)
|
|
17
|
+
return node;
|
|
18
|
+
return cloneElement(node, { long: true });
|
|
19
|
+
}
|
|
10
20
|
/**
|
|
11
21
|
* Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
|
|
12
22
|
* box — a Step or a SheetStage — a state is only how far it slid (translate; the material never changes size).
|
|
@@ -102,7 +112,8 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
102
112
|
});
|
|
103
113
|
}, [reveal]);
|
|
104
114
|
usePeekStrip(ref);
|
|
105
|
-
|
|
115
|
+
const [titleLong, setTitleLong] = useState(false);
|
|
116
|
+
useTitleRun(ref, setTitleLong);
|
|
106
117
|
useFootRoom(ref);
|
|
107
118
|
/* the handle held and moved: the sheet follows, and asks for the nearest state on release — sheetDrag.ts */
|
|
108
119
|
const { down, move, up, afterDrag } = useSheetDrag(ref, allowed, (to) => request(to, 'drag'));
|
|
@@ -120,5 +131,5 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
120
131
|
return; // the click that follows a drag is not a tap
|
|
121
132
|
request(current === 'peek' ? (allowed().includes(home.current) ? home.current : allowed().find((s) => s !== 'peek') ?? 'half') : 'peek', 'tap');
|
|
122
133
|
};
|
|
123
|
-
return (_jsxs("article", { ref: ref, className: cx('card sheet', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', `sheet--${shown}`, className), inert: current === 'away' || held || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: cx('sheet__handle', mark === 'dots' && 'sheet__handle--dots'), type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), children] }));
|
|
134
|
+
return (_jsxs("article", { ref: ref, className: cx('card sheet', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', `sheet--${shown}`, className), inert: current === 'away' || held || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: cx('sheet__handle', mark === 'dots' && 'sheet__handle--dots'), type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), Children.map(children, (node) => markLong(node, titleLong))] }));
|
|
124
135
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
2
|
export interface SheetBodyProps extends ComponentProps<'div'> {
|
|
3
3
|
}
|
|
4
4
|
/** SheetBody — everything under a Sheet's head: the one thing that scrolls; its scroll padding keeps a focused control above the cut. Content never goes straight into the Sheet. */
|
package/dist/sheets/SheetBody.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useContext } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
4
|
+
import { StackPullCtx } from './stackCard.js';
|
|
3
5
|
/** SheetBody — everything under a Sheet's head: the one thing that scrolls; its scroll padding keeps a focused control above the cut. Content never goes straight into the Sheet. */
|
|
4
6
|
export function SheetBody({ className, ...rest }) {
|
|
5
|
-
|
|
7
|
+
const pulling = useContext(StackPullCtx);
|
|
8
|
+
return _jsx("div", { className: cx('sheet__body', pulling && 'is-refreshing', className), ...rest });
|
|
6
9
|
}
|
|
@@ -8,6 +8,8 @@ export interface SheetHeadProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
8
8
|
sub?: ReactNode;
|
|
9
9
|
/** the round button on the right */
|
|
10
10
|
trail?: ReactNode;
|
|
11
|
+
/** the sheet measured this title as wider than its line, so it runs sideways */
|
|
12
|
+
long?: boolean;
|
|
11
13
|
}
|
|
12
14
|
/** SheetHead — the stage sheet's three-column head: round button · centred title + sub · round button. A step sheet uses a CardHead instead. */
|
|
13
|
-
export declare function SheetHead({ lead, title, sub, trail, className, ...rest }: SheetHeadProps): import("react").JSX.Element;
|
|
15
|
+
export declare function SheetHead({ lead, title, sub, trail, long, className, ...rest }: SheetHeadProps): import("react").JSX.Element;
|
package/dist/sheets/SheetHead.js
CHANGED
|
@@ -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
3
|
/** SheetHead — the stage sheet's three-column head: round button · centred title + sub · round button. A step sheet uses a CardHead instead. */
|
|
4
|
-
export function SheetHead({ lead, title, sub, trail, className, ...rest }) {
|
|
5
|
-
return (_jsxs("div", { className: cx('sheet__head', className), ...rest, children: [lead ?? _jsx("span", {}), _jsxs("div", { children: [_jsx("h2", { className:
|
|
4
|
+
export function SheetHead({ lead, title, sub, trail, long, className, ...rest }) {
|
|
5
|
+
return (_jsxs("div", { className: cx('sheet__head', className), ...rest, children: [lead ?? _jsx("span", {}), _jsxs("div", { children: [_jsx("h2", { className: cx('sheet__title', long && 'is-long'), children: title }), sub != null && _jsx("span", { className: "sheet__sub", children: sub })] }), trail ?? _jsx("span", {})] }));
|
|
6
6
|
}
|
|
@@ -5,6 +5,7 @@ import { cx } from '../cx.js';
|
|
|
5
5
|
import { Card } from '../cards/Card.js';
|
|
6
6
|
import { StackSheet } from './StackSheet.js';
|
|
7
7
|
const PHASE = 380; // ms before the before-pack un-tucks on close, so the after-pack is clear first
|
|
8
|
+
const UNTUCK = 700; // the stagger stays for the return: the delay is read when is-before leaves, and a later render must not take it away mid-move
|
|
8
9
|
const SHEET = new Set([Card, StackSheet]);
|
|
9
10
|
/* one wrapper around a sheet is fine (a host's own box). The props land on the Card or StackSheet inside it, so the card renders them. */
|
|
10
11
|
function stamp(child, place) {
|
|
@@ -40,6 +41,8 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
40
41
|
const [current, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
41
42
|
const prev = useRef(current);
|
|
42
43
|
const [closingFrom, setClosingFrom] = useState(null);
|
|
44
|
+
const back = useRef(null);
|
|
45
|
+
const [, tick] = useState(0);
|
|
43
46
|
if (prev.current !== current) {
|
|
44
47
|
const from = prev.current;
|
|
45
48
|
prev.current = current;
|
|
@@ -54,6 +57,22 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
54
57
|
const t = setTimeout(() => setClosingFrom(null), PHASE);
|
|
55
58
|
return () => clearTimeout(t);
|
|
56
59
|
}, [closingFrom]);
|
|
60
|
+
if (closingFrom != null)
|
|
61
|
+
back.current = { at: closingFrom, until: 0 };
|
|
62
|
+
else if (back.current && back.current.until === 0)
|
|
63
|
+
back.current = { at: back.current.at, until: performance.now() + UNTUCK };
|
|
64
|
+
const returning = closingFrom == null && back.current && performance.now() < back.current.until ? back.current.at : null;
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const held = back.current;
|
|
67
|
+
if (!held || held.until === 0)
|
|
68
|
+
return;
|
|
69
|
+
const t = setTimeout(() => {
|
|
70
|
+
if (back.current === held)
|
|
71
|
+
back.current = null;
|
|
72
|
+
tick((n) => n + 1);
|
|
73
|
+
}, Math.max(0, held.until - performance.now()));
|
|
74
|
+
return () => clearTimeout(t);
|
|
75
|
+
});
|
|
57
76
|
const closing = current == null && closingFrom != null;
|
|
58
77
|
const anchor = current ?? closingFrom;
|
|
59
78
|
const items = Children.toArray(children);
|
|
@@ -68,7 +87,7 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
68
87
|
refreshing: refreshing === i,
|
|
69
88
|
before,
|
|
70
89
|
after: !closing && current != null && i > current,
|
|
71
|
-
stagger: before && anchor != null ? anchor - 1 - i : 0,
|
|
90
|
+
stagger: before && anchor != null ? anchor - 1 - i : returning != null && i < returning ? returning - 1 - i : 0,
|
|
72
91
|
onOpen: () => request(i),
|
|
73
92
|
onClose: () => request(null),
|
|
74
93
|
onRefresh: onRefresh ? () => onRefresh(i) : undefined,
|
|
@@ -11,9 +11,8 @@ import { usePile } from './sheetStepsPile.js';
|
|
|
11
11
|
import { StepCursor } from './stepCursor.js';
|
|
12
12
|
const MOVE_MS = 350; // = --motion-sheet: the accordion's beat
|
|
13
13
|
const SWIPE_REM = 3; // an up/down move longer than this (and mostly up/down) is a swipe
|
|
14
|
-
/* the open step's first field — the one that takes the cursor
|
|
14
|
+
/* the open step's first field — the one that takes the cursor, and the one a typed character is handed to (an input or a textarea) */
|
|
15
15
|
const NOW_FIELD = '.sheet-step--now .sheet-step__inner :is(input:not([type=checkbox], [type=radio], [type=hidden]), textarea)';
|
|
16
|
-
const NOW_INPUT = '.sheet-step--now .sheet-step__inner input:not([type=checkbox], [type=radio], [type=hidden])';
|
|
17
16
|
const coarse = () => typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches; // a phone: its keyboard follows a finger, not a script
|
|
18
17
|
const StepCtx = createContext(null);
|
|
19
18
|
/**
|
|
@@ -150,11 +149,12 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
150
149
|
const handOver = (e) => {
|
|
151
150
|
const typed = e.currentTarget.value;
|
|
152
151
|
e.currentTarget.value = '';
|
|
153
|
-
const field = ref.current?.querySelector(
|
|
152
|
+
const field = ref.current?.querySelector(NOW_FIELD);
|
|
154
153
|
if (!typed || !field)
|
|
155
154
|
return;
|
|
156
155
|
field.focus({ preventScroll: true });
|
|
157
|
-
|
|
156
|
+
const proto = field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
157
|
+
Object.getOwnPropertyDescriptor(proto, 'value').set.call(field, field.value + typed);
|
|
158
158
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
159
159
|
};
|
|
160
160
|
return (_jsxs("div", { ref: ref, className: cx('sheet-steps', !reversible && 'sheet-steps--locked', className), style: style, onFocus: focusIn, onBlur: focusOut, onPointerDown: down, onPointerUp: up, onPointerCancel: (e) => { onPointerCancel?.(e); press.current = null; }, ...rest, children: [_jsx("input", { ref: holderRef, className: "sheet-steps__holder", "aria-hidden": true, tabIndex: -1, autoComplete: "off", onFocus: () => setParked(true), onBlur: () => setParked(false), onInput: handOver }), _jsx("ol", { className: "sheet-steps__track", children: steps.map((step, i) => (_jsx(StepCtx.Provider, { value: { numbered, i, at: current, furthest: reach, count, reversible, go, park, parked }, children: step }, step.key ?? i))) }), finish != null && ( // always in sight; it takes taps only once every step is done (or skipped)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useControlled, watchSize } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { Card } from '../cards/Card.js';
|
|
@@ -24,6 +24,7 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
|
|
|
24
24
|
// a title longer than its room does not wrap: it runs sideways, like a song title in a short slot — the room it overflows by
|
|
25
25
|
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
|
26
26
|
const titleRef = useRef(null);
|
|
27
|
+
const [long, setLong] = useState(false);
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
const h = titleRef.current, run = h?.firstElementChild;
|
|
29
30
|
if (!h || !run)
|
|
@@ -32,10 +33,10 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
|
|
|
32
33
|
const over = Math.max(0, Math.ceil(run.scrollWidth - h.clientWidth));
|
|
33
34
|
h.style.setProperty('--_over', `${over}px`);
|
|
34
35
|
h.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
35
|
-
|
|
36
|
+
setLong(over > 0);
|
|
36
37
|
};
|
|
37
38
|
measure();
|
|
38
39
|
return watchSize([h, run], measure);
|
|
39
40
|
}, [title]);
|
|
40
|
-
return (_jsxs(Card, { stack: stack, tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [backAt === 'start' && _jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className:
|
|
41
|
+
return (_jsxs(Card, { stack: stack, tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [backAt === 'start' && _jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: cx('stack-sheet__title', long && 'is-long'), children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon, backAt === 'end' && _jsx(StackBack, { end: true, label: backLabel })] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
|
|
41
42
|
}
|
|
@@ -5,6 +5,6 @@ type SheetRef = RefObject<HTMLElement | null>;
|
|
|
5
5
|
export declare const slid: (el: HTMLElement) => number;
|
|
6
6
|
export declare const stopsOf: (el: HTMLElement) => Record<Resting, number>;
|
|
7
7
|
export declare function usePeekStrip(ref: SheetRef): void;
|
|
8
|
-
export declare function useTitleRun(ref: SheetRef): void;
|
|
8
|
+
export declare function useTitleRun(ref: SheetRef, onLong: (long: boolean) => void): void;
|
|
9
9
|
export declare function useFootRoom(ref: SheetRef): void;
|
|
10
10
|
export {};
|
|
@@ -39,25 +39,30 @@ export function usePeekStrip(ref) {
|
|
|
39
39
|
}, []);
|
|
40
40
|
}
|
|
41
41
|
/* the TITLE is one line; too long for it, it runs sideways like a song title (Lh 2026-10-03): the sheet measures how far its
|
|
42
|
-
words overflow the line and hands
|
|
43
|
-
(a range), so the running indent never feeds back into the number. */
|
|
44
|
-
export function useTitleRun(ref) {
|
|
42
|
+
words overflow the line and hands the widths to the CSS (--_over, --_run). The heading renders `.is-long` from what this
|
|
43
|
+
reports. Measured on the words themselves (a range), so the running indent never feeds back into the number. */
|
|
44
|
+
export function useTitleRun(ref, onLong) {
|
|
45
45
|
useEffect(() => {
|
|
46
46
|
const el = ref.current, title = el?.querySelector(':scope > .card__head > :is(h1, h2, h3):first-child, :scope > .sheet__head .sheet__title');
|
|
47
47
|
if (!el || !title)
|
|
48
48
|
return;
|
|
49
|
+
let was = false;
|
|
49
50
|
const measure = () => {
|
|
50
51
|
const words = document.createRange();
|
|
51
52
|
words.selectNodeContents(title);
|
|
52
53
|
const over = Math.max(0, Math.ceil(words.getBoundingClientRect().width - title.clientWidth));
|
|
53
54
|
title.style.setProperty('--_over', `${over}px`);
|
|
54
55
|
title.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
55
|
-
|
|
56
|
+
const long = over > 0;
|
|
57
|
+
if (long !== was) {
|
|
58
|
+
was = long;
|
|
59
|
+
onLong(long);
|
|
60
|
+
}
|
|
56
61
|
};
|
|
57
62
|
const stop = watchSize([title], measure);
|
|
58
63
|
const mo = new MutationObserver(measure);
|
|
59
64
|
mo.observe(title, { childList: true, characterData: true, subtree: true }); // the app changed the words
|
|
60
|
-
return () => { stop(); mo.disconnect(); title.
|
|
65
|
+
return () => { stop(); mo.disconnect(); title.style.removeProperty('--_over'); title.style.removeProperty('--_run'); };
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
/* the FOOT's room follows what the foot really holds (timebank, Lh 2026-10-01): a button grows with the member's text size, a
|
|
@@ -9,7 +9,7 @@ export interface StackPlace {
|
|
|
9
9
|
before: boolean;
|
|
10
10
|
/** in front of the open sheet, leaving below */
|
|
11
11
|
after: boolean;
|
|
12
|
-
/** nearest-first
|
|
12
|
+
/** nearest-first delay, while tucked and while those sheets return */
|
|
13
13
|
stagger: number;
|
|
14
14
|
onOpen: () => void;
|
|
15
15
|
onClose: () => void;
|
|
@@ -17,7 +17,9 @@ export interface StackPlace {
|
|
|
17
17
|
}
|
|
18
18
|
/** the head's title is a tab stop while this sheet is the one Enter acts on: every title when none is open, only the open one otherwise */
|
|
19
19
|
export declare const StackHeadCtx: import("react").Context<0 | -1 | null>;
|
|
20
|
-
|
|
20
|
+
/** a finger is pulling this card's content. The body renders `is-refreshing` from it */
|
|
21
|
+
export declare const StackPullCtx: import("react").Context<boolean>;
|
|
22
|
+
export declare function stackPaint(place: StackPlace | undefined, pulling?: boolean): {
|
|
21
23
|
className?: string;
|
|
22
24
|
style?: CSSProperties;
|
|
23
25
|
busy?: true;
|
|
@@ -28,4 +30,4 @@ export declare function stackPaint(place: StackPlace | undefined): {
|
|
|
28
30
|
*/
|
|
29
31
|
export declare function useStackCard(ref: RefObject<HTMLElement | null>, place: StackPlace | undefined, swallow: {
|
|
30
32
|
current: boolean;
|
|
31
|
-
}):
|
|
33
|
+
}): boolean;
|
package/dist/sheets/stackCard.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/* one sheet of a tap stack — its own behaviour, not exported from the package.
|
|
2
2
|
The app says which sheet is open and whether its data is in (`SheetStack`'s `open` and `refreshing`).
|
|
3
3
|
This card renders what those two mean: the place classes, the grip, the pull. Nothing stamps them on afterwards. */
|
|
4
|
-
import { createContext, useEffect, useLayoutEffect, useRef } from 'react';
|
|
4
|
+
import { createContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
5
5
|
import { flushSync } from 'react-dom';
|
|
6
6
|
import { focusWhenActive } from '../state.js';
|
|
7
7
|
const SLOP_REM = 0.5;
|
|
8
8
|
const COMMIT_REM = 4.5;
|
|
9
9
|
/** the head's title is a tab stop while this sheet is the one Enter acts on: every title when none is open, only the open one otherwise */
|
|
10
10
|
export const StackHeadCtx = createContext(null);
|
|
11
|
-
|
|
11
|
+
/** a finger is pulling this card's content. The body renders `is-refreshing` from it */
|
|
12
|
+
export const StackPullCtx = createContext(false);
|
|
13
|
+
export function stackPaint(place, pulling = false) {
|
|
12
14
|
if (!place)
|
|
13
15
|
return {};
|
|
14
16
|
const loading = place.open && place.refreshing;
|
|
15
|
-
const className = ['stack-card', place.open && 'is-open', place.before && 'is-before', place.after && 'is-after', loading && 'is-loading'].filter(Boolean).join(' ');
|
|
17
|
+
const className = ['stack-card', place.open && 'is-open', place.before && 'is-before', place.after && 'is-after', loading && 'is-loading', pulling && 'is-refreshing'].filter(Boolean).join(' ');
|
|
16
18
|
return {
|
|
17
19
|
className,
|
|
18
|
-
style: place.
|
|
20
|
+
style: place.stagger > 0 ? { '--stagger': String(place.stagger) } : undefined,
|
|
19
21
|
busy: loading || undefined,
|
|
20
22
|
};
|
|
21
23
|
}
|
|
@@ -49,6 +51,7 @@ export function useStackCard(ref, place, swallow) {
|
|
|
49
51
|
const live = useRef(null);
|
|
50
52
|
const headPull = useRef(null);
|
|
51
53
|
const bodyPull = useRef(null);
|
|
54
|
+
const [pulling, setPulling] = useState(false);
|
|
52
55
|
useLayoutEffect(() => {
|
|
53
56
|
const el = ref.current;
|
|
54
57
|
if (!el || !place)
|
|
@@ -191,6 +194,8 @@ export function useStackCard(ref, place, swallow) {
|
|
|
191
194
|
return;
|
|
192
195
|
}
|
|
193
196
|
}
|
|
197
|
+
if (!pull.moved)
|
|
198
|
+
flushSync(() => setPulling(true));
|
|
194
199
|
pull.moved = true;
|
|
195
200
|
ev.preventDefault();
|
|
196
201
|
try {
|
|
@@ -198,12 +203,11 @@ export function useStackCard(ref, place, swallow) {
|
|
|
198
203
|
scroller.setPointerCapture(ev.pointerId);
|
|
199
204
|
}
|
|
200
205
|
catch { /* the pointer is already gone */ }
|
|
201
|
-
scroller.classList.add('is-refreshing');
|
|
202
206
|
scroller.style.setProperty('--_refresh', `${Math.min(dy, pull.commit)}px`);
|
|
203
207
|
};
|
|
204
208
|
const finish = () => {
|
|
205
|
-
scroller.classList.remove('is-refreshing');
|
|
206
209
|
scroller.style.removeProperty('--_refresh');
|
|
210
|
+
setPulling(false);
|
|
207
211
|
};
|
|
208
212
|
const onUp = (ev) => {
|
|
209
213
|
if (ev.pointerId !== pull.pointerId || bodyPull.current !== pull)
|
|
@@ -258,12 +262,14 @@ export function useStackCard(ref, place, swallow) {
|
|
|
258
262
|
stopDoc();
|
|
259
263
|
headPull.current = null;
|
|
260
264
|
bodyPull.current = null;
|
|
265
|
+
scroller.style.removeProperty('--_refresh');
|
|
266
|
+
setPulling(false);
|
|
261
267
|
if (live.current?.prop === '--_pull')
|
|
262
268
|
release('--_pull');
|
|
263
269
|
head?.removeEventListener('pointerdown', onHeadDown);
|
|
264
270
|
scroller.removeEventListener('pointerdown', onBodyDown);
|
|
265
271
|
};
|
|
266
|
-
}, [place?.open,
|
|
272
|
+
}, [place?.open, !!place?.onRefresh, ref, swallow]);
|
|
267
273
|
/* none open: a pull up on a peeked head follows the finger and, past the commit, opens this sheet */
|
|
268
274
|
useEffect(() => {
|
|
269
275
|
const card = ref.current;
|
|
@@ -386,4 +392,5 @@ export function useStackCard(ref, place, swallow) {
|
|
|
386
392
|
head.removeEventListener('pointerdown', onDown);
|
|
387
393
|
};
|
|
388
394
|
}, [place?.open, place?.before, place?.after, ref, swallow]);
|
|
395
|
+
return pulling;
|
|
389
396
|
}
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
49
49
|
* Home — TEMPLATE, a `Screen` with its parts filled — top: the AppBar · content: the `base` (title, welcome…) over a closed `SheetStack` · no bottom
|
|
50
50
|
* (the stack is content, never the bottom: open, a sheet fills everything under the top).
|
|
51
51
|
* Every section is a `StackSheet` of three parts: SHEET HEAD (title + icon tabs, a count over a tab; the back button once open) · SHEET BODY (scrolls) · SHEET FOOT (the CTA,
|
|
52
|
-
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. Only the open sheet's body is rendered — three
|
|
52
|
+
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. With a LayerHistory above and no `open` prop, that open sheet is one history entry named `home`. Only the open sheet's body is rendered — three
|
|
53
53
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
54
54
|
*/
|
|
55
55
|
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, onRefresh, refreshing, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
package/dist/templates/Home.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { useControlled } from '../state.js';
|
|
4
|
+
import { useOptionalLayer } from '../scaffold/LayerHistory.js';
|
|
4
5
|
import { Screen } from '../scaffold/Screen.js';
|
|
5
6
|
import { SheetStack } from '../sheets/SheetStack.js';
|
|
6
7
|
import { StackSheet } from '../sheets/StackSheet.js';
|
|
@@ -12,20 +13,34 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
12
13
|
* Home — TEMPLATE, a `Screen` with its parts filled — top: the AppBar · content: the `base` (title, welcome…) over a closed `SheetStack` · no bottom
|
|
13
14
|
* (the stack is content, never the bottom: open, a sheet fills everything under the top).
|
|
14
15
|
* Every section is a `StackSheet` of three parts: SHEET HEAD (title + icon tabs, a count over a tab; the back button once open) · SHEET BODY (scrolls) · SHEET FOOT (the CTA,
|
|
15
|
-
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. Only the open sheet's body is rendered — three
|
|
16
|
+
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. With a LayerHistory above and no `open` prop, that open sheet is one history entry named `home`. Only the open sheet's body is rendered — three
|
|
16
17
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
17
18
|
*/
|
|
18
19
|
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, onRefresh, refreshing, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
19
|
-
const
|
|
20
|
+
const layer = useOptionalLayer('home');
|
|
21
|
+
const [local, change] = useControlled(openProp, defaultOpen, onOpenChange);
|
|
22
|
+
// A passed `open` is the parent's. With no LayerHistory the index stays local, so a story without a router still opens sheets.
|
|
23
|
+
const shown = openProp !== undefined ? local : layer == null ? local : typeof layer.value === 'number' ? layer.value : null;
|
|
24
|
+
const request = (next) => {
|
|
25
|
+
if (openProp !== undefined || layer == null) {
|
|
26
|
+
change(next);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onOpenChange?.(next);
|
|
30
|
+
if (next == null)
|
|
31
|
+
layer.close();
|
|
32
|
+
else
|
|
33
|
+
layer.open(next);
|
|
34
|
+
};
|
|
20
35
|
const [leaving, setLeaving] = useState(null);
|
|
21
|
-
const [seen, setSeen] = useState(() => new Set(
|
|
22
|
-
const [was, setWas] = useState(
|
|
23
|
-
if (was !==
|
|
24
|
-
if (was != null &&
|
|
36
|
+
const [seen, setSeen] = useState(() => new Set(shown != null ? [shown] : []));
|
|
37
|
+
const [was, setWas] = useState(shown);
|
|
38
|
+
if (was !== shown) { // derived during render: which body must stay while its sheet slides home
|
|
39
|
+
if (was != null && shown == null)
|
|
25
40
|
setLeaving(was);
|
|
26
|
-
if (
|
|
27
|
-
setSeen(new Set(seen).add(
|
|
28
|
-
setWas(
|
|
41
|
+
if (shown != null && !seen.has(shown))
|
|
42
|
+
setSeen(new Set(seen).add(shown));
|
|
43
|
+
setWas(shown);
|
|
29
44
|
}
|
|
30
45
|
useEffect(() => {
|
|
31
46
|
if (leaving == null)
|
|
@@ -34,6 +49,6 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
|
|
|
34
49
|
return () => clearTimeout(t);
|
|
35
50
|
}, [leaving]);
|
|
36
51
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
37
|
-
const shows = (i) => i ===
|
|
38
|
-
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open:
|
|
52
|
+
const shows = (i) => i === shown || i === leaving || (keep && seen.has(i));
|
|
53
|
+
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: shown, onOpenChange: request, onRefresh: onRefresh, refreshing: refreshing, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, backAt: backAt, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
|
|
39
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.55",
|
|
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.",
|