@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 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: `end` (default — after the tabs, chevron DOWN: the sheet folds down, Lh 2026-09-29) or `start` (before the title, chevron left) */
30
- backAt?: 'start' | 'end';
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 = 'end', className, children, ...rest }) {
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
@@ -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;
@@ -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
- const before = text.slice(Math.max(0, m.index - 40), m.index);
97
- if (/(?:&&|\?|:)\s*\(?\s*$/.test(before) && !/\sonSettled\s*=/.test(tag))
98
- add('sheet-unmounted', 'warn', m.index, '<Sheet> rendered on a condition 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.');
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.25",
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.",