@usableapp/cardds 0.7.25 → 0.7.27
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/css/sheet.css +3 -0
- package/dist/cardds.css +3 -0
- package/dist/sheets/StackSheet.d.ts +3 -2
- package/dist/sheets/StackSheet.js +1 -1
- package/dist/templates/Home.d.ts +4 -2
- package/dist/templates/Home.js +2 -2
- package/lint/rules.mjs +27 -4
- 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.27 — 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- `StackSheet` (so `Home`): the open sheet's back button (chevron down) is HIDDEN for now — Lh 2026-09-29: not used in real work yet. A tap on the open sheet's title still folds it. `backAt="end"` (or `"start"`) brings it back; `Home backAt` passes it through. New value `backAt="none"` = the default.
|
|
9
|
+
|
|
10
|
+
## 0.7.26 — 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Fixed (`cardds check` / ESLint — two holes mailpool found, 2026-09-29)
|
|
13
|
+
- `sheet-unmounted` now also catches a `Sheet` whose ANCESTOR is on a condition — `{cond && <SheetStage …><Sheet …/></SheetStage>}` vanishes the same way. Before: only a condition right on the `<Sheet>`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- `SheetStage over` on a SCROLLING screen (a `Screen` without `fill`) now holds to the phone's window (`position: fixed`). Before: it covered the whole page, so scrolled down the sheet and the scrim were off screen (mailpool, 2026-09-29). Nothing to change in a screen.
|
|
17
|
+
|
|
5
18
|
## 0.7.25 — 2026-09-29
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/css/sheet.css
CHANGED
|
@@ -257,6 +257,9 @@
|
|
|
257
257
|
background: transparent;
|
|
258
258
|
--_top: 0rem;
|
|
259
259
|
}
|
|
260
|
+
/* …on a SCROLLING screen (no fill) the screen is taller than the phone: absolute would cover the whole page, and scrolled
|
|
261
|
+
down the sheet and the scrim are off screen (mailpool, 2026-09-29). There the stage holds to the phone's window. */
|
|
262
|
+
.screen:not(.screen--fill) > .sheet-stage--over { position: fixed; }
|
|
260
263
|
.appbar + .sheet-stage { margin-top: calc(-1 * (var(--header-h) + var(--header-gap, 0rem))); }
|
|
261
264
|
.sheet-stage__bg { position: absolute; inset: 0; } /* map / previous cards behind */
|
|
262
265
|
/* the hero: ONE thing shown in the band a half sheet leaves open (a postcard being made,
|
package/dist/cardds.css
CHANGED
|
@@ -1545,6 +1545,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1545
1545
|
background: transparent;
|
|
1546
1546
|
--_top: 0rem;
|
|
1547
1547
|
}
|
|
1548
|
+
/* …on a SCROLLING screen (no fill) the screen is taller than the phone: absolute would cover the whole page, and scrolled
|
|
1549
|
+
down the sheet and the scrim are off screen (mailpool, 2026-09-29). There the stage holds to the phone's window. */
|
|
1550
|
+
.screen:not(.screen--fill) > .sheet-stage--over { position: fixed; }
|
|
1548
1551
|
.appbar + .sheet-stage { margin-top: calc(-1 * (var(--header-h) + var(--header-gap, 0rem))); }
|
|
1549
1552
|
.sheet-stage__bg { position: absolute; inset: 0; } /* map / previous cards behind */
|
|
1550
1553
|
/* the hero: ONE thing shown in the band a half sheet leaves open (a postcard being made,
|
|
@@ -26,8 +26,9 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
|
|
|
26
26
|
foot?: ReactNode;
|
|
27
27
|
/** the back button's label (shown once the sheet is open) */
|
|
28
28
|
backLabel?: string;
|
|
29
|
-
/** where the back button stands: `
|
|
30
|
-
|
|
29
|
+
/** where the back button stands: `none` (default — hidden for now, Lh 2026-09-29: not used in real work yet; a tap on the open sheet's title folds it) ·
|
|
30
|
+
* `end` (after the tabs, chevron DOWN: the sheet folds down) · `start` (before the title, chevron left) */
|
|
31
|
+
backAt?: 'none' | 'start' | 'end';
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* StackSheet — ONE sheet of a `SheetStack`, in the one sheet shape: SHEET HEAD (back · title · tabs or an icon) pinned at the top ·
|
|
@@ -17,7 +17,7 @@ import { Badge } from '../numbers/Badge.js';
|
|
|
17
17
|
* a count shows as a small circle over its icon. The body is the app's: render the page of the chosen tab (and nothing while the sheet
|
|
18
18
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
19
19
|
*/
|
|
20
|
-
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', backAt = '
|
|
20
|
+
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', backAt = 'none', className, children, ...rest }) {
|
|
21
21
|
const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
|
|
22
22
|
// 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
|
|
23
23
|
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
2
|
import type { Tone } from '../cards/Card.js';
|
|
3
3
|
import { Screen } from '../scaffold/Screen.js';
|
|
4
|
-
import { type StackSheetTab } from '../sheets/StackSheet.js';
|
|
4
|
+
import { type StackSheetTab, type StackSheetProps } from '../sheets/StackSheet.js';
|
|
5
5
|
export interface HomeSheet {
|
|
6
6
|
/** the sheet head's title — an h2 (the page's one h1 is in `base`) */
|
|
7
7
|
title: ReactNode;
|
|
@@ -36,6 +36,8 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
36
36
|
keep?: boolean;
|
|
37
37
|
/** the open sheet's back button label */
|
|
38
38
|
backLabel?: string;
|
|
39
|
+
/** the open sheet's back button — see `StackSheet backAt` (default `none`: hidden for now) */
|
|
40
|
+
backAt?: StackSheetProps['backAt'];
|
|
39
41
|
/** layers over the screen (a SheetStage over + Modal) */
|
|
40
42
|
children?: ReactNode;
|
|
41
43
|
}
|
|
@@ -46,4 +48,4 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
46
48
|
* 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
|
|
47
49
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
48
50
|
*/
|
|
49
|
-
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, base, keep, backLabel, children, ...rest }: HomeProps): import("react").JSX.Element;
|
|
51
|
+
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
package/dist/templates/Home.js
CHANGED
|
@@ -14,7 +14,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
14
14
|
* 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
|
|
15
15
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
16
16
|
*/
|
|
17
|
-
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = 'Back', children, ...rest }) {
|
|
17
|
+
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = 'Back', backAt, children, ...rest }) {
|
|
18
18
|
const [open, change] = useControlled(openProp, defaultOpen, onOpenChange);
|
|
19
19
|
const [leaving, setLeaving] = useState(null);
|
|
20
20
|
const [seen, setSeen] = useState(() => new Set(open != null ? [open] : []));
|
|
@@ -34,5 +34,5 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
|
|
|
34
34
|
}, [leaving]);
|
|
35
35
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
36
36
|
const shows = (i) => i === open || i === leaving || (keep && seen.has(i));
|
|
37
|
-
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: open, onOpenChange: change, 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, 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] }));
|
|
37
|
+
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: open, onOpenChange: change, 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] }));
|
|
38
38
|
}
|
package/lint/rules.mjs
CHANGED
|
@@ -42,6 +42,21 @@ function tagAt(text, start) {
|
|
|
42
42
|
return text.slice(start);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** a whole JSX element from its `<Name`: to its matching `</Name>` (same-name nesting counted), or the opening tag if self-closing */
|
|
46
|
+
function elementAt(text, start) {
|
|
47
|
+
const open = tagAt(text, start);
|
|
48
|
+
if (open.endsWith('/>')) return open;
|
|
49
|
+
const name = text.slice(start + 1).match(/^[\w.]+/)[0];
|
|
50
|
+
const re = new RegExp(`<${name}\\b|</${name}>`, 'g');
|
|
51
|
+
re.lastIndex = start + open.length;
|
|
52
|
+
let depth = 1, m;
|
|
53
|
+
while ((m = re.exec(text))) {
|
|
54
|
+
if (m[0].startsWith('</')) { if (--depth === 0) return text.slice(start, m.index + m[0].length); }
|
|
55
|
+
else if (!tagAt(text, m.index).endsWith('/>')) depth++;
|
|
56
|
+
}
|
|
57
|
+
return text.slice(start);
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
/** the expression of a `{…}` attribute value from its opening brace */
|
|
46
61
|
function braceAt(text, start) {
|
|
47
62
|
let depth = 0, quote = null;
|
|
@@ -88,14 +103,22 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
|
88
103
|
if (hit.length) add('raw-class', 'error', m.index, `cardds class written by hand (${hit.map((w) => '.' + w).join(' ')}): the look without the behaviour — use the cardds component (its props pick the classes), or a knob / theme.css for the look.`);
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
// 2 · Sheet: controlled with no way to move · put in the tree on a condition with no slide out
|
|
106
|
+
// 2 · Sheet: controlled with no way to move · put in the tree on a condition (its own, or an ancestor's — a SheetStage, a div) with no slide out
|
|
92
107
|
for (const m of text.matchAll(/<Sheet\b/g)) {
|
|
93
108
|
const tag = tagAt(text, m.index);
|
|
94
109
|
if (/\sstate\s*=/.test(tag) && !/\sonStateChange\s*=/.test(tag))
|
|
95
110
|
add('sheet-frozen', 'warn', m.index, '<Sheet state> without onStateChange: the handle, the drag and the keyboard climb only ASK — nothing answers, the sheet cannot move. Add onStateChange, or use defaultState.');
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
}
|
|
112
|
+
const flagged = new Set();
|
|
113
|
+
for (const c of text.matchAll(/(?:&&|\?|:)\s*\(?\s*(?=<[A-Za-z])/g)) {
|
|
114
|
+
const start = c.index + c[0].length;
|
|
115
|
+
const el = elementAt(text, start);
|
|
116
|
+
for (const s of el.matchAll(/<Sheet\b/g)) {
|
|
117
|
+
const at = start + s.index;
|
|
118
|
+
if (flagged.has(at) || /\sonSettled\s*=/.test(tagAt(text, at))) continue;
|
|
119
|
+
flagged.add(at);
|
|
120
|
+
add('sheet-unmounted', 'warn', at, `<Sheet> rendered on a condition${s.index ? ' (around its ' + el.match(/^<([\w.]+)/)[1] + ')' : ''} without onSettled: taken out of the tree it vanishes instead of sliding out. Set state="away" and unmount it in onSettled (state === "away") — or keep it mounted and switch its state.`);
|
|
121
|
+
}
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
// 3 · the screen: the bar is the Screen's bottom, never after it, never pinned; a sheet finishes on its own foot
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.27",
|
|
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.",
|