@usableapp/cardds 0.7.37 → 0.7.38
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 +10 -0
- package/css/layover.css +4 -2
- package/css/numbers.css +8 -0
- package/css/sheet.css +2 -2
- package/dist/cardds.css +14 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/numbers/Spinner.d.ts +13 -0
- package/dist/numbers/Spinner.js +9 -0
- package/dist/sheets/Sheet.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.38 — 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`Banner`: its words ran past the band when a line held a word with no break in it** (a link; found in mailpool, Lh 2026-10-01) — the body's column grew as wide as that word. The column is now held to the band and such a word breaks where it must. Nothing to change in a screen.
|
|
9
|
+
|
|
10
|
+
- **`Sheet`: the body's words ran under the foot's button at a large text size** (found in timebank, Lh 2026-10-01). The room a sheet kept for its foot was a fixed sum (the button's token height), but a button grows with the member's text size, and a line may stand over it. The Sheet now measures its foot's rows and keeps that much room (`--sheet-foot-h`, its own data — not a knob); the strip's ground covers all of it. Nothing to change in a screen; a `Text` put in a `CardFoot` over the button is counted too.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`Spinner` — "working on it"** (Lh 2026-10-01, asked by mailpool): a ring that turns, for a wait of unknown length (a known share is a `Progress`). `<Spinner label="…" />` alone (the label is read out), or `<Spinner>words</Spinner>` with a caption-sized line beside it; `size` = an Icon's (`xs` · `md` · `lg`); `role="status"`. In a row's `end`, a card, a sheet's body, an `EmptyState`'s `icon`. Still when motion is reduced. Knob `--spinner-ink`. Before, the only turning ring was inside `FormNote busy`, not exported. A whole screen that is not known yet still shows nothing (`Screen ready`), not a spinner.
|
|
14
|
+
|
|
5
15
|
## 0.7.37 — 2026-10-01
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/css/layover.css
CHANGED
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
<div class="banner__body">…</div></details>
|
|
177
177
|
A light tint (not a card tone) with a slightly darker rule above and below, edge to edge of its card or screen (Lh 2026-09-24). */
|
|
178
178
|
.banner {
|
|
179
|
-
display: grid;
|
|
179
|
+
display: grid; grid-template-columns: minmax(0, 1fr);
|
|
180
180
|
background: var(--banner-bg, var(--ink-tint));
|
|
181
181
|
color: var(--banner-ink, inherit);
|
|
182
182
|
border-block: var(--border-w) solid var(--banner-border, var(--hairline));
|
|
@@ -192,7 +192,9 @@
|
|
|
192
192
|
.banner__summary::-webkit-details-marker { display: none; }
|
|
193
193
|
.banner__summary .icon { transition: rotate var(--motion-quick) ease; }
|
|
194
194
|
.banner[open] .banner__summary .icon { rotate: 180deg; }
|
|
195
|
-
|
|
195
|
+
/* the band's words never run past it: one column that cannot be wider than the band (a grid track is as wide as its longest
|
|
196
|
+
word otherwise), and a word with no break in it — a link — breaks where it must (mailpool, Lh 2026-10-01) */
|
|
197
|
+
.banner__body { display: grid; grid-template-columns: minmax(0, 1fr); overflow-wrap: anywhere; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
|
|
196
198
|
.banner::details-content { width: 100%; }
|
|
197
199
|
@supports (interpolate-size: allow-keywords) {
|
|
198
200
|
@media (prefers-reduced-motion: no-preference) {
|
package/css/numbers.css
CHANGED
|
@@ -242,6 +242,14 @@
|
|
|
242
242
|
.bar--on::after { background: var(--accent); }
|
|
243
243
|
.bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
|
|
244
244
|
|
|
245
|
+
/* ---- spinner: "working on it", a wait of unknown length (Lh 2026-10-01) ----
|
|
246
|
+
<span class="spinner" role="status"><svg class="icon">…loader-circle…</svg>Loading…</span>
|
|
247
|
+
The ring turns; words beside it are optional and take the caption's look. Still when motion is reduced. */
|
|
248
|
+
.spinner { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); vertical-align: middle; font: var(--type-caption); color: var(--spinner-ink, currentColor); }
|
|
249
|
+
.spinner > .icon { flex: none; }
|
|
250
|
+
@media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation: spinner-turn 0.9s linear infinite; } }
|
|
251
|
+
@keyframes spinner-turn { to { rotate: 1turn; } }
|
|
252
|
+
|
|
245
253
|
/* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
|
|
246
254
|
timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
|
|
247
255
|
--progress-at is DATA the component writes (0–1), not a knob (scripts/knobs.mjs lists it). The done part is a
|
package/css/sheet.css
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
border-radius: var(--r-card) var(--r-card) 0 0;
|
|
82
82
|
border-bottom: 0;
|
|
83
83
|
box-shadow: var(--sheet-shadow, var(--shadow-sheet));
|
|
84
|
-
--_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
84
|
+
--_foot: calc(var(--card-gap) + var(--sheet-foot-h, var(--btn-h)) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
85
85
|
}
|
|
86
86
|
.sheet--full { --_y: 0%; --_cover: 0rem; }
|
|
87
87
|
.sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
|
|
149
149
|
One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
|
|
150
150
|
/* …and the link row under the button, when there is one (its line + the gap over it) */
|
|
151
|
-
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad) + var(--_safe-b-open)); }
|
|
151
|
+
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
|
|
152
152
|
.sheet > .card__foot {
|
|
153
153
|
position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
|
|
154
154
|
align-items: end;
|
package/dist/cardds.css
CHANGED
|
@@ -1374,7 +1374,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1374
1374
|
border-radius: var(--r-card) var(--r-card) 0 0;
|
|
1375
1375
|
border-bottom: 0;
|
|
1376
1376
|
box-shadow: var(--sheet-shadow, var(--shadow-sheet));
|
|
1377
|
-
--_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
1377
|
+
--_foot: calc(var(--card-gap) + var(--sheet-foot-h, var(--btn-h)) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
1378
1378
|
}
|
|
1379
1379
|
.sheet--full { --_y: 0%; --_cover: 0rem; }
|
|
1380
1380
|
.sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
|
|
@@ -1441,7 +1441,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1441
1441
|
stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
|
|
1442
1442
|
One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
|
|
1443
1443
|
/* …and the link row under the button, when there is one (its line + the gap over it) */
|
|
1444
|
-
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad) + var(--_safe-b-open)); }
|
|
1444
|
+
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
|
|
1445
1445
|
.sheet > .card__foot {
|
|
1446
1446
|
position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
|
|
1447
1447
|
align-items: end;
|
|
@@ -2678,6 +2678,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2678
2678
|
.bar--on::after { background: var(--accent); }
|
|
2679
2679
|
.bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
|
|
2680
2680
|
|
|
2681
|
+
/* ---- spinner: "working on it", a wait of unknown length (Lh 2026-10-01) ----
|
|
2682
|
+
<span class="spinner" role="status"><svg class="icon">…loader-circle…</svg>Loading…</span>
|
|
2683
|
+
The ring turns; words beside it are optional and take the caption's look. Still when motion is reduced. */
|
|
2684
|
+
.spinner { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); vertical-align: middle; font: var(--type-caption); color: var(--spinner-ink, currentColor); }
|
|
2685
|
+
.spinner > .icon { flex: none; }
|
|
2686
|
+
@media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation: spinner-turn 0.9s linear infinite; } }
|
|
2687
|
+
@keyframes spinner-turn { to { rotate: 1turn; } }
|
|
2688
|
+
|
|
2681
2689
|
/* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
|
|
2682
2690
|
timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
|
|
2683
2691
|
--progress-at is DATA the component writes (0–1), not a knob (scripts/knobs.mjs lists it). The done part is a
|
|
@@ -3770,7 +3778,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3770
3778
|
<div class="banner__body">…</div></details>
|
|
3771
3779
|
A light tint (not a card tone) with a slightly darker rule above and below, edge to edge of its card or screen (Lh 2026-09-24). */
|
|
3772
3780
|
.banner {
|
|
3773
|
-
display: grid;
|
|
3781
|
+
display: grid; grid-template-columns: minmax(0, 1fr);
|
|
3774
3782
|
background: var(--banner-bg, var(--ink-tint));
|
|
3775
3783
|
color: var(--banner-ink, inherit);
|
|
3776
3784
|
border-block: var(--border-w) solid var(--banner-border, var(--hairline));
|
|
@@ -3786,7 +3794,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3786
3794
|
.banner__summary::-webkit-details-marker { display: none; }
|
|
3787
3795
|
.banner__summary .icon { transition: rotate var(--motion-quick) ease; }
|
|
3788
3796
|
.banner[open] .banner__summary .icon { rotate: 180deg; }
|
|
3789
|
-
|
|
3797
|
+
/* the band's words never run past it: one column that cannot be wider than the band (a grid track is as wide as its longest
|
|
3798
|
+
word otherwise), and a word with no break in it — a link — breaks where it must (mailpool, Lh 2026-10-01) */
|
|
3799
|
+
.banner__body { display: grid; grid-template-columns: minmax(0, 1fr); overflow-wrap: anywhere; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
|
|
3790
3800
|
.banner::details-content { width: 100%; }
|
|
3791
3801
|
@supports (interpolate-size: allow-keywords) {
|
|
3792
3802
|
@media (prefers-reduced-motion: no-preference) {
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export * from './choice/Mood.js';
|
|
|
56
56
|
export * from './choice/Slider.js';
|
|
57
57
|
export * from './numbers/Ring.js';
|
|
58
58
|
export * from './numbers/Progress.js';
|
|
59
|
+
export * from './numbers/Spinner.js';
|
|
59
60
|
export * from './numbers/Track.js';
|
|
60
61
|
export * from './numbers/Band.js';
|
|
61
62
|
export * from './numbers/DotGrid.js';
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,7 @@ export * from './choice/Mood.js';
|
|
|
60
60
|
export * from './choice/Slider.js';
|
|
61
61
|
export * from './numbers/Ring.js';
|
|
62
62
|
export * from './numbers/Progress.js';
|
|
63
|
+
export * from './numbers/Spinner.js';
|
|
63
64
|
export * from './numbers/Track.js';
|
|
64
65
|
export * from './numbers/Band.js';
|
|
65
66
|
export * from './numbers/DotGrid.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
export interface SpinnerProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
3
|
+
/** the ring's size, an Icon's: xs = in a caption or a row's meta · md (default) · lg = an EmptyState's `icon` */
|
|
4
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
5
|
+
/** what is being waited for, READ OUT (not shown) — give it when there are no words beside the ring */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** words shown beside the ring ("กำลังโหลด…"): the line says it, the ring only turns */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/** Spinner — "working on it": a ring that turns, alone or with a line of words beside it. For a wait of unknown length — a known
|
|
11
|
+
* share is a `Progress`. `role="status"`: the words (or `label`) are read out when it appears. In a row's `end`, a card, a
|
|
12
|
+
* sheet's body, an `EmptyState`'s `icon`; a whole SCREEN that is not known yet shows nothing instead (`Screen ready`). */
|
|
13
|
+
export declare function Spinner({ size, label, className, children, ...rest }: SpinnerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../cx.js';
|
|
3
|
+
import { Icon } from '../type/Icon.js';
|
|
4
|
+
/** Spinner — "working on it": a ring that turns, alone or with a line of words beside it. For a wait of unknown length — a known
|
|
5
|
+
* share is a `Progress`. `role="status"`: the words (or `label`) are read out when it appears. In a row's `end`, a card, a
|
|
6
|
+
* sheet's body, an `EmptyState`'s `icon`; a whole SCREEN that is not known yet shows nothing instead (`Screen ready`). */
|
|
7
|
+
export function Spinner({ size = 'md', label, className, children, ...rest }) {
|
|
8
|
+
return (_jsxs("span", { className: cx('spinner', className), role: "status", "aria-label": children == null ? label : undefined, ...rest, children: [_jsx(Icon, { name: "loader-circle", size: size }), children] }));
|
|
9
|
+
}
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -173,6 +173,26 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
173
173
|
const stop = watchSize([head], publish);
|
|
174
174
|
return () => { stop(); box.style.removeProperty('--sheet-peek'); };
|
|
175
175
|
}, []);
|
|
176
|
+
/* 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
|
|
177
|
+
line may stand over it (an error) — the CSS strip was a fixed sum (--btn-h), so the body's words ran under the button. The
|
|
178
|
+
sheet measures its foot's rows and publishes --sheet-foot-h; the strip, its ground and the body's stop all read it. */
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
const el = ref.current, foot = el?.querySelector(':scope > .card__foot');
|
|
181
|
+
if (!el || !foot) {
|
|
182
|
+
el?.style.removeProperty('--sheet-foot-h');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const publish = () => {
|
|
186
|
+
const rows = [...foot.children].map((c) => c.getBoundingClientRect()).filter((r) => r.height > 0);
|
|
187
|
+
if (!rows.length)
|
|
188
|
+
return el.style.removeProperty('--sheet-foot-h');
|
|
189
|
+
el.style.setProperty('--sheet-foot-h', `${Math.max(...rows.map((r) => r.bottom)) - Math.min(...rows.map((r) => r.top))}px`);
|
|
190
|
+
};
|
|
191
|
+
const stop = watchSize([...foot.children], publish);
|
|
192
|
+
const mo = new MutationObserver(publish);
|
|
193
|
+
mo.observe(foot, { childList: true });
|
|
194
|
+
return () => { stop(); mo.disconnect(); };
|
|
195
|
+
}); // every render: the foot's rows are the app's, they come and go
|
|
176
196
|
const focus = (e) => {
|
|
177
197
|
onFocus?.(e);
|
|
178
198
|
if (current !== 'peek' || e.target.closest('.sheet__handle'))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.38",
|
|
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.",
|