@usableapp/cardds 0.7.45 → 0.7.47
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 +17 -0
- package/README.md +3 -3
- package/css/numbers.css +7 -1
- package/css/sheet.css +4 -2
- package/css/tokens.css +1 -0
- package/dist/cardds.css +12 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/sheets/Sheet.d.ts +5 -0
- package/dist/sheets/Sheet.js +60 -8
- package/dist/type/textScale.d.ts +11 -3
- package/dist/type/textScale.js +18 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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.47 — 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **One sheet follows another: the first goes all the way down, the base is seen, then the next comes up** (Lh 2026-10-03). It is the `Sheet`'s own habit now: a sheet asked up while another sheet of the same box (`Step`, `SheetStage`) is leaving waits at `away` until that one has landed, the base shows for `--motion-sheet-rest` (0.2s), then it rises — 0.35s down · 0.2s base · 0.35s up, never two sheets in the box. Before, two sheets set in one render crossed (`CallSheet ways` going from one way to another, `Step · Calls`), and a sheet raised from the first one's `onSettled('away')` started in the very frame the first landed — one continuous move. Nothing to change in a screen: both wirings now play the same, and an app adds no timer of its own (remove one if you had it). `onSettled` of the sheet coming up fires later by the wait. With reduced motion there is no wait. New root token `--motion-sheet-rest`.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`TrackStep num`: the number sits in the middle of its round at every text size** — the round is cut from its own number (`1.877em`, 32px at the default text as before) and the digit is lifted onto the middle, as the SheetSteps' number. Knobs `--track-dot-size` · `--track-dot-lift`. At a large text a NUMBERED track's pill grows with its rounds and is taller than the top bar (160% text: 88px beside a 78px bar); an icon track keeps the bar's height.
|
|
12
|
+
|
|
13
|
+
## 0.7.46 — 2026-10-03
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **A larger text pulls the controls with it, half as fast** (Lh 2026-10-03): `setTextScale(percent)` now writes `--tap-base` as well as `--fs-base` — 115% text → 107.5% controls · 130% → 115% · 145% → 122.5% · 160% → 130%; at 100% and under, the controls keep the theme's size. Before, only the words grew and outgrew what holds them (a number on its round's bottom edge, a title tight in the top bar). Nothing to change in an app that calls `setTextScale`: buttons, bars, rounds and icons grow by themselves — look at a dense screen at 160%, it has less room for content than before. An app that sets `--fs-base` on `<html>` by hand gets none of this: call `setTextScale`.
|
|
17
|
+
- New export **`controlsFor(percent)`** — the controls' share for a text size, for an app that wants to show or store the pair.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **`SheetSteps numbered`: the number sits in the middle of its disc at every text size** — the disc is cut from its own number (`1.408em`, 24px at the default text as before) instead of a fixed 24px, and the digit is lifted onto the middle. Knobs `--sheet-step-num-size` (now listed) · `--sheet-step-num-lift`. If your theme set `--sheet-step-num-size` in rem, it still wins — but then it no longer follows the text.
|
|
21
|
+
|
|
5
22
|
## 0.7.45 — 2026-10-03
|
|
6
23
|
|
|
7
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -653,7 +653,7 @@ Rules:
|
|
|
653
653
|
any role can be set outright.
|
|
654
654
|
- The member's text-size setting is the same `--fs-base`, written on `<html>`
|
|
655
655
|
at run time by `setTextScale(percent)` (a share of the theme's base; 100 = the
|
|
656
|
-
theme's own): the words grow, the things
|
|
656
|
+
theme's own): the words grow, and the things grow HALF as much (`--tap-base`; `controlsFor(percent)` — 130% text → 115% controls, 160% → 130%), so a larger text still fits what holds it. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
|
|
657
657
|
|
|
658
658
|
Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
|
|
659
659
|
palette `own` shows a colour well per colour base.
|
|
@@ -684,8 +684,8 @@ default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
|
684
684
|
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
685
685
|
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
|
686
686
|
An app with its own text-size setting calls `setTextScale(percent)`, which writes
|
|
687
|
-
`--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default):
|
|
688
|
-
badge, the inline icon) grow
|
|
687
|
+
`--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default): the type and its holders (the count
|
|
688
|
+
badge, the inline icon) grow, the controls grow half as much (`--tap-base`), spacing and radii keep their size.
|
|
689
689
|
Components never write a raw length: spacing from `--sp-*`, control sizes from
|
|
690
690
|
`--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
|
|
691
691
|
padding from `--screen-pad` / `--card-pad`, radii from
|
package/css/numbers.css
CHANGED
|
@@ -89,7 +89,13 @@
|
|
|
89
89
|
background: var(--ink-rail);
|
|
90
90
|
color: var(--_ink);
|
|
91
91
|
}
|
|
92
|
-
.track__step > .track__dot {
|
|
92
|
+
.track__step > .track__dot { /* a number in the round (TrackStep num) */
|
|
93
|
+
display: grid; place-items: center; box-sizing: border-box;
|
|
94
|
+
font: var(--track-dot-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
|
|
95
|
+
width: var(--track-dot-size, 1.877em); height: var(--track-dot-size, 1.877em); /* em, not rem (Lh 2026-10-03, as the SheetSteps' number and the StackSheet's count): the round is cut from ITS OWN
|
|
96
|
+
number's size — 1.877em of a 17px caption = the 32px of an icon's round; at a larger text it grows with its number, and the pill with it */
|
|
97
|
+
padding: 0 0 var(--track-dot-lift, 0.1em); /* the room UNDER the number lifts it onto the round's middle: a digit has no descender */
|
|
98
|
+
}
|
|
93
99
|
.track__step.is-done > .icon,
|
|
94
100
|
.track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
|
|
95
101
|
|
package/css/sheet.css
CHANGED
|
@@ -413,8 +413,10 @@
|
|
|
413
413
|
}
|
|
414
414
|
.sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
415
415
|
.sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a white disc on the bar, the number in the ink */
|
|
416
|
-
flex: none; display: grid; place-items: center; inline-size: var(--sheet-step-num-size,
|
|
417
|
-
|
|
416
|
+
flex: none; display: grid; place-items: center; box-sizing: border-box; inline-size: var(--sheet-step-num-size, 1.408em); block-size: var(--sheet-step-num-size, 1.408em); /* em, not rem (Lh 2026-10-03, as the StackSheet's count): the disc is cut from ITS OWN number's size —
|
|
417
|
+
at a large text size the number grew and a 24px disc did not, so the digit sat on its bottom edge; 1.408em of a 17px caption = the 24px it always was */
|
|
418
|
+
padding-bottom: var(--sheet-step-num-lift, 0.1em); /* the room UNDER the number lifts it onto the disc's middle: a digit has no descender, so a centred line box sets it low */
|
|
419
|
+
border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
|
|
418
420
|
background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
|
|
419
421
|
}
|
|
420
422
|
.sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
package/css/tokens.css
CHANGED
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
|
|
96
96
|
/* ---- motion ---- */
|
|
97
97
|
--motion-sheet: 0.35s;
|
|
98
|
+
--motion-sheet-rest: 0.2s; /* one sheet follows another: how long the base is seen between the one that left and the one that comes (Sheet.tsx reads it) */
|
|
98
99
|
--motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
|
|
99
100
|
--motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
|
|
100
101
|
--motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
|
package/dist/cardds.css
CHANGED
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
|
|
137
137
|
/* ---- motion ---- */
|
|
138
138
|
--motion-sheet: 0.35s;
|
|
139
|
+
--motion-sheet-rest: 0.2s; /* one sheet follows another: how long the base is seen between the one that left and the one that comes (Sheet.tsx reads it) */
|
|
139
140
|
--motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
|
|
140
141
|
--motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
|
|
141
142
|
--motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
|
|
@@ -1737,8 +1738,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1737
1738
|
}
|
|
1738
1739
|
.sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1739
1740
|
.sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a white disc on the bar, the number in the ink */
|
|
1740
|
-
flex: none; display: grid; place-items: center; inline-size: var(--sheet-step-num-size,
|
|
1741
|
-
|
|
1741
|
+
flex: none; display: grid; place-items: center; box-sizing: border-box; inline-size: var(--sheet-step-num-size, 1.408em); block-size: var(--sheet-step-num-size, 1.408em); /* em, not rem (Lh 2026-10-03, as the StackSheet's count): the disc is cut from ITS OWN number's size —
|
|
1742
|
+
at a large text size the number grew and a 24px disc did not, so the digit sat on its bottom edge; 1.408em of a 17px caption = the 24px it always was */
|
|
1743
|
+
padding-bottom: var(--sheet-step-num-lift, 0.1em); /* the room UNDER the number lifts it onto the disc's middle: a digit has no descender, so a centred line box sets it low */
|
|
1744
|
+
border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
|
|
1742
1745
|
background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
|
|
1743
1746
|
}
|
|
1744
1747
|
.sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -2575,7 +2578,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2575
2578
|
background: var(--ink-rail);
|
|
2576
2579
|
color: var(--_ink);
|
|
2577
2580
|
}
|
|
2578
|
-
.track__step > .track__dot {
|
|
2581
|
+
.track__step > .track__dot { /* a number in the round (TrackStep num) */
|
|
2582
|
+
display: grid; place-items: center; box-sizing: border-box;
|
|
2583
|
+
font: var(--track-dot-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
|
|
2584
|
+
width: var(--track-dot-size, 1.877em); height: var(--track-dot-size, 1.877em); /* em, not rem (Lh 2026-10-03, as the SheetSteps' number and the StackSheet's count): the round is cut from ITS OWN
|
|
2585
|
+
number's size — 1.877em of a 17px caption = the 32px of an icon's round; at a larger text it grows with its number, and the pill with it */
|
|
2586
|
+
padding: 0 0 var(--track-dot-lift, 0.1em); /* the room UNDER the number lifts it onto the round's middle: a digit has no descender */
|
|
2587
|
+
}
|
|
2579
2588
|
.track__step.is-done > .icon,
|
|
2580
2589
|
.track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
|
|
2581
2590
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { cx } from './cx.js';
|
|
|
4
4
|
export * from './type/Icon.js';
|
|
5
5
|
export { ICON_NAMES } from './type/icons.js';
|
|
6
6
|
export * from './type/Text.js';
|
|
7
|
-
export { setTextScale } from './type/textScale.js';
|
|
7
|
+
export { setTextScale, controlsFor } from './type/textScale.js';
|
|
8
8
|
export { setWords, word, type WordKey } from './words.js';
|
|
9
9
|
export * from './scaffold/Screen.js';
|
|
10
10
|
export * from './scaffold/AppBar.js';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { cx } from './cx.js';
|
|
|
8
8
|
export * from './type/Icon.js';
|
|
9
9
|
export { ICON_NAMES } from './type/icons.js';
|
|
10
10
|
export * from './type/Text.js';
|
|
11
|
-
export { setTextScale } from './type/textScale.js';
|
|
11
|
+
export { setTextScale, controlsFor } from './type/textScale.js';
|
|
12
12
|
export { setWords, word } from './words.js';
|
|
13
13
|
export * from './scaffold/Screen.js';
|
|
14
14
|
export * from './scaffold/AppBar.js';
|
package/dist/sheets/Sheet.d.ts
CHANGED
|
@@ -37,6 +37,11 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
|
|
|
37
37
|
* It ARRIVES and LEAVES by sliding, always: a sheet that mounts already up starts `away` for one frame and slides to its state, so
|
|
38
38
|
* `{open && <Sheet state="full">}` slides in. Leaving is the app's beat: set `away`, and unmount on `onSettled('away')` — unmount it
|
|
39
39
|
* while it is still up and it can only vanish (a dev warning says so).
|
|
40
|
+
*
|
|
41
|
+
* ONE FOLLOWS ANOTHER: a sheet that comes up while another sheet of the same box is on its way out waits — the leaving one goes
|
|
42
|
+
* ALL the way down, the base is seen for a beat (`--motion-sheet-rest`), and only then it rises: "one left, another came", never two
|
|
43
|
+
* sheets crossing and never one continuous move. It is the Sheet's own habit, whatever the app's wiring (both states set in one
|
|
44
|
+
* render, or the next raised from the first's `onSettled`) — the app adds no timer.
|
|
40
45
|
*/
|
|
41
46
|
export declare function Sheet({ state, defaultState, onStateChange, onSettled, states, handle, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
|
|
42
47
|
export {};
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
-
import { focusWhenActive, useControlled, useLatest, warnOnce, watchSize } from '../state.js';
|
|
3
|
+
import { focusWhenActive, reducedMotion, useControlled, useLatest, warnOnce, watchSize } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { word } from '../words.js';
|
|
6
6
|
const ORDER = ['peek', 'half', '3q', 'full']; // low → high; `away` is below them all and outside them
|
|
7
7
|
const RANK = { away: 0, peek: 1, half: 2, '3q': 3, full: 4 };
|
|
8
8
|
const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
9
|
+
/* ONE SHEET FOLLOWS ANOTHER (Lh 2026-10-03): when a box's sheet landed away, per box — a sheet coming up reads it to give the base its beat */
|
|
10
|
+
const landed = new WeakMap();
|
|
9
11
|
const ms = (t) => Math.max(0, ...t.split(',').map((v) => parseFloat(v) * (v.trim().endsWith('ms') ? 1 : 1000) || 0)); // a computed `0.35s, 0s` list → its longest, in ms
|
|
10
12
|
/**
|
|
11
13
|
* Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
|
|
@@ -22,6 +24,11 @@ const ms = (t) => Math.max(0, ...t.split(',').map((v) => parseFloat(v) * (v.trim
|
|
|
22
24
|
* It ARRIVES and LEAVES by sliding, always: a sheet that mounts already up starts `away` for one frame and slides to its state, so
|
|
23
25
|
* `{open && <Sheet state="full">}` slides in. Leaving is the app's beat: set `away`, and unmount on `onSettled('away')` — unmount it
|
|
24
26
|
* while it is still up and it can only vanish (a dev warning says so).
|
|
27
|
+
*
|
|
28
|
+
* ONE FOLLOWS ANOTHER: a sheet that comes up while another sheet of the same box is on its way out waits — the leaving one goes
|
|
29
|
+
* ALL the way down, the base is seen for a beat (`--motion-sheet-rest`), and only then it rises: "one left, another came", never two
|
|
30
|
+
* sheets crossing and never one continuous move. It is the Sheet's own habit, whatever the app's wiring (both states set in one
|
|
31
|
+
* render, or the next raised from the first's `onSettled`) — the app adds no timer.
|
|
25
32
|
*/
|
|
26
33
|
export function Sheet({ state, defaultState = 'half', onStateChange, onSettled, states, handle = true, className, children, onFocus, ...rest }) {
|
|
27
34
|
const [current, request] = useControlled(state, defaultState, onStateChange);
|
|
@@ -31,7 +38,9 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
31
38
|
const was = useRef('away'); // mounted up = it came onto the screen: it rises from away like any arrival
|
|
32
39
|
const skipClick = useRef(false);
|
|
33
40
|
const [entered, setEntered] = useState(false); // false for the first frame: the class says `away`, so a sheet mounted up has somewhere to slide from
|
|
34
|
-
const
|
|
41
|
+
const [held, setHeld] = useState(false); // true while it waits for the sheet before it to land and the base to be seen
|
|
42
|
+
const shown = entered && !held ? current : 'away';
|
|
43
|
+
const live = held ? 'away' : current; // where it is as far as its own habits go: a held sheet has not come up yet
|
|
35
44
|
const latest = useLatest({ current, onSettled });
|
|
36
45
|
const drag = useRef(null);
|
|
37
46
|
const allowed = useCallback(() => {
|
|
@@ -42,6 +51,47 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
42
51
|
void ref.current?.offsetHeight; // the browser styles the `away` frame first, or there is no transition to run
|
|
43
52
|
setEntered(true);
|
|
44
53
|
}, []);
|
|
54
|
+
/* ONE FOLLOWS ANOTHER: it is asked up from `away` (or mounted up) while a sibling sheet is still leaving — or landed less than the
|
|
55
|
+
rest ago: it holds at `away` until that one's slide has ended and the base has been seen for --motion-sheet-rest. Read from the
|
|
56
|
+
DOM in the commit itself (a leaving sibling already says `away` but still stands in the box), so the order of the two sheets
|
|
57
|
+
in the tree, and whether the app set both in one render or one after the other, make no difference. */
|
|
58
|
+
const before = useRef('away');
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
const el = ref.current, box = el?.parentElement, from = before.current;
|
|
61
|
+
before.current = current;
|
|
62
|
+
if (!el || !box || from !== 'away' || current === 'away' || reducedMotion()) {
|
|
63
|
+
setHeld(false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const rest = ms(getComputedStyle(el).getPropertyValue('--motion-sheet-rest') || '0s');
|
|
67
|
+
const floor = box.getBoundingClientRect().bottom - 1;
|
|
68
|
+
const leaving = [...box.querySelectorAll(':scope > .sheet--away')].filter((s) => s !== el && s.getBoundingClientRect().top < floor);
|
|
69
|
+
const since = performance.now() - (landed.get(box) ?? -Infinity);
|
|
70
|
+
if (!leaving.length && since >= rest) {
|
|
71
|
+
setHeld(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setHeld(true);
|
|
75
|
+
let timer = 0, left = leaving.length;
|
|
76
|
+
const go = (after) => { timer = window.setTimeout(() => setHeld(false), after); };
|
|
77
|
+
if (!left) {
|
|
78
|
+
go(rest - since);
|
|
79
|
+
return () => clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
const stops = leaving.map((s) => {
|
|
82
|
+
let done = false;
|
|
83
|
+
const end = () => { if (done)
|
|
84
|
+
return; done = true; clearTimeout(t); s.removeEventListener('transitionend', onEnd); if (--left === 0)
|
|
85
|
+
go(rest); };
|
|
86
|
+
const onEnd = (e) => { if (e.target === s && e.propertyName === 'translate')
|
|
87
|
+
end(); };
|
|
88
|
+
const cs = getComputedStyle(s);
|
|
89
|
+
s.addEventListener('transitionend', onEnd);
|
|
90
|
+
const t = setTimeout(end, ms(cs.transitionDuration) + ms(cs.transitionDelay) + 100);
|
|
91
|
+
return () => { done = true; clearTimeout(t); s.removeEventListener('transitionend', onEnd); };
|
|
92
|
+
});
|
|
93
|
+
return () => { stops.forEach((stop) => stop()); clearTimeout(timer); };
|
|
94
|
+
}, [current]);
|
|
45
95
|
/* it has finished moving: the transition ended (or at once where there is none) — say so once per move; a move cut short by another says nothing */
|
|
46
96
|
const last = useRef(shown);
|
|
47
97
|
useEffect(() => {
|
|
@@ -57,6 +107,8 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
57
107
|
done = true;
|
|
58
108
|
clearTimeout(t);
|
|
59
109
|
el.removeEventListener('transitionend', onEnd);
|
|
110
|
+
if (shown === 'away' && el.parentElement)
|
|
111
|
+
landed.set(el.parentElement, performance.now()); // the sheet after it counts its rest from here
|
|
60
112
|
latest.current.onSettled?.(latest.current.current);
|
|
61
113
|
};
|
|
62
114
|
const onEnd = (e) => { if (e.target === el && e.propertyName === 'translate')
|
|
@@ -85,17 +137,17 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
85
137
|
field), else the body's first field; with no field, a sheet that came onto the screen gives it to its handle. A field that already
|
|
86
138
|
has the cursor keeps it. */
|
|
87
139
|
useEffect(() => {
|
|
88
|
-
if (
|
|
89
|
-
home.current =
|
|
90
|
-
const rose =
|
|
140
|
+
if (live !== 'peek' && live !== 'away')
|
|
141
|
+
home.current = live;
|
|
142
|
+
const rose = live !== 'peek' && live !== 'away' && RANK[live] > RANK[was.current];
|
|
91
143
|
const from = was.current;
|
|
92
|
-
was.current =
|
|
144
|
+
was.current = live;
|
|
93
145
|
if (!rose)
|
|
94
146
|
return;
|
|
95
147
|
const cancel = focusWhenActive(ref.current, { scope: ':scope > .sheet__body', fallback: () => (from === 'away' ? handleRef.current : null) });
|
|
96
148
|
return () => { if (!cancel())
|
|
97
149
|
was.current = from; }; // cut short (StrictMode's trial run, or a new state mid-slide): the rise still counts from where it began
|
|
98
|
-
}, [
|
|
150
|
+
}, [live]);
|
|
99
151
|
/* geometry, read once per gesture — never stored: how far it has slid, and where each state rests, in px from its top at full */
|
|
100
152
|
const slid = (el) => {
|
|
101
153
|
const box = (el.offsetParent ?? el.parentElement);
|
|
@@ -246,5 +298,5 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
246
298
|
skipClick.current = true;
|
|
247
299
|
requestAnimationFrame(() => { skipClick.current = false; });
|
|
248
300
|
};
|
|
249
|
-
return (_jsxs("article", { ref: ref, className: cx('card sheet', `sheet--${shown}`, className), inert: current === 'away' || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), children] }));
|
|
301
|
+
return (_jsxs("article", { ref: ref, className: cx('card sheet', `sheet--${shown}`, className), inert: current === 'away' || held || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), children] }));
|
|
250
302
|
}
|
package/dist/type/textScale.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* How much the CONTROLS grow for a text size (both a %, 100 = as the theme set it): half as fast as the words. A text-holder of
|
|
3
|
+
* a fixed size (a round with a number, a button, the top bar) is drawn for the theme's text — at a larger text the words outgrew
|
|
4
|
+
* it; growing the things at the same rate would zoom the whole UI and leave no room for content (Lh 2026-10-03, pairs set by
|
|
5
|
+
* measuring the gallery: 115 → 107.5 · 130 → 115 · 145 → 122.5 · 160 → 130). A smaller text never shrinks a control.
|
|
6
|
+
*/
|
|
7
|
+
export declare const controlsFor: (percent: number) => number;
|
|
8
|
+
/**
|
|
9
|
+
* The member's text size: `--fs-base` on <html> as a share of the theme's own base (100 = as the theme set it), and with it
|
|
10
|
+
* `--tap-base` — the things grow half as much as the words (`controlsFor`), so a larger text still fits what holds it.
|
|
11
|
+
* The bases are read from the CSS (cardds's default or the project's theme.css) — never copied into app code. Keeping the choice
|
|
12
|
+
* (localStorage, an account) is the app's.
|
|
5
13
|
*/
|
|
6
14
|
export declare function setTextScale(percent: number, root?: HTMLElement): void;
|
package/dist/type/textScale.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* How much the CONTROLS grow for a text size (both a %, 100 = as the theme set it): half as fast as the words. A text-holder of
|
|
3
|
+
* a fixed size (a round with a number, a button, the top bar) is drawn for the theme's text — at a larger text the words outgrew
|
|
4
|
+
* it; growing the things at the same rate would zoom the whole UI and leave no room for content (Lh 2026-10-03, pairs set by
|
|
5
|
+
* measuring the gallery: 115 → 107.5 · 130 → 115 · 145 → 122.5 · 160 → 130). A smaller text never shrinks a control.
|
|
6
|
+
*/
|
|
7
|
+
export const controlsFor = (percent) => 100 + Math.max(0, percent - 100) / 2;
|
|
8
|
+
/**
|
|
9
|
+
* The member's text size: `--fs-base` on <html> as a share of the theme's own base (100 = as the theme set it), and with it
|
|
10
|
+
* `--tap-base` — the things grow half as much as the words (`controlsFor`), so a larger text still fits what holds it.
|
|
11
|
+
* The bases are read from the CSS (cardds's default or the project's theme.css) — never copied into app code. Keeping the choice
|
|
12
|
+
* (localStorage, an account) is the app's.
|
|
5
13
|
*/
|
|
6
14
|
export function setTextScale(percent, root = document.documentElement) {
|
|
7
15
|
root.style.removeProperty('--fs-base');
|
|
16
|
+
root.style.removeProperty('--tap-base');
|
|
8
17
|
if (percent === 100)
|
|
9
18
|
return;
|
|
10
|
-
const
|
|
11
|
-
|
|
19
|
+
const cs = getComputedStyle(root);
|
|
20
|
+
const fs = parseFloat(cs.getPropertyValue('--fs-base')) || 1.375, tap = parseFloat(cs.getPropertyValue('--tap-base')) || 3; // rem
|
|
21
|
+
root.style.setProperty('--fs-base', `${(fs * percent) / 100}rem`);
|
|
22
|
+
const controls = controlsFor(percent);
|
|
23
|
+
if (controls !== 100)
|
|
24
|
+
root.style.setProperty('--tap-base', `${(tap * controls) / 100}rem`);
|
|
12
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.47",
|
|
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.",
|