@usableapp/cardds 0.7.35 → 0.7.36

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,11 @@
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.36 — 2026-10-01
6
+
7
+ ### Added
8
+ - **`Screen ready` — one outcome, drawn once** (Lh 2026-10-01). A screen that can come out more than one way (signed in or not, a list or its empty state, a redirect) showed one outcome and then turned into another. Now: `<Screen ready={known}>` — `false` while the app is still finding out. Nothing of the screen is drawn (no top, content, bottom; `data-pending`, `aria-busy`), and when it is reached through `slideScreen` or the back button (`installScreenSlide`) the screen BEFORE stays until it is ready — at most 3 s — then the right one slides in. Default `true`: nothing changes until a screen sets it. Every template passes it through (`Home`, `CallSheet`, `Walkthrough`). To do in an app: every screen that decides after its first paint (a check in an effect, a redirect) gets `ready`; on the first load hold `Splash done` until the first screen is known.
9
+
5
10
  ## 0.7.35 — 2026-10-01
6
11
 
7
12
  ### Fixed
package/README.md CHANGED
@@ -355,6 +355,11 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
355
355
  one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
356
356
  ONE SCREEN TO THE NEXT (a route change) slides the WHOLE screen the same way: `slideScreen(() => navigate(to), 'next' | 'back')`,
357
357
  and `installScreenSlide()` once at app start for the back button (story `Step · WalksOn`). `data-slide="off"` on a Screen = no slide.
358
+ ONE OUTCOME, DRAWN ONCE (Lh 2026-10-01): a screen that can come out more than one way (signed in or not, a list or
359
+ its empty state, a redirect) is NOT drawn until the app knows which — `<Screen ready={known}>`. Not ready = nothing of it
360
+ is drawn; arrived at through `slideScreen` / the back button, the screen before stays until it is ready (at most 3 s), then
361
+ the right one slides in. Never one outcome first and another after (story `Step · ArrivesReady`). On a first load there is
362
+ no screen before: hold the `Splash` (`done`) until the first screen is known.
358
363
  - *Sheets the app calls:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`); each
359
364
  way has a sheet that holds its WHOLE form and its own submit on its foot, starting `.sheet--away` (not on the
360
365
  screen: slid out, hidden once gone, `inert`). **The wiring is the app's, not cardds's**: a `useState` of which
@@ -11,15 +11,21 @@ export interface ScreenProps extends ComponentProps<'div'> {
11
11
  /** BOTTOM: an `ActionBar` (or a `FloatBar`) — a LAYER over the content's foot, as tall as its rows need.
12
12
  * Never with a `Sheet` in the content: a sheet finishes on its own foot (warns once). */
13
13
  bottom?: ReactNode;
14
+ /** `false` while the app does not yet know WHICH screen this is (signed in or not, a member with or without a request…):
15
+ * nothing of it is drawn — no top, content or bottom — so a screen never shows one outcome and then turns into another.
16
+ * Arrived at through `slideScreen`, the screen before stays until this turns true (at most 3 s). Default true. */
17
+ ready?: boolean;
14
18
  }
15
19
  /**
16
20
  * Screen — the mobile screen in THREE parts: **top** and **content** (the children) in one flex column, the content down to the
17
21
  * screen's foot; **bottom** a LAYER over it there (Lh 2026-09-23). Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start
18
22
  * 15% down). Content = a box of components one after another — a `Step` (`Pages` of `Page`s / a `Page` [+ `Sheet`]), a `Centre`, a
19
- * `SheetStack`… Bottom = an `ActionBar` (the walk's `PagerAt` its first row) or a `FloatBar`, covering the content's foot on purpose. Max 25rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
23
+ * `SheetStack`… Bottom = an `ActionBar` (the walk's `PagerAt` its first row) or a `FloatBar`, covering the content's foot on purpose. Max 30rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
24
+ * ONE OUTCOME, DRAWN ONCE (Lh 2026-10-01): a screen that can come out more than one way is not drawn until the app knows which —
25
+ * `ready={false}` until then.
20
26
  * The slots set the order; children laid by hand in the same order still work.
21
27
  */
22
- export declare function Screen({ fill, palette, as, top, bottom, className, children, ref, ...rest }: ScreenProps): import("react").DetailedReactHTMLElement<{
28
+ export declare function Screen({ fill, palette, as, top, bottom, ready, className, children, ref, ...rest }: ScreenProps): import("react").DetailedReactHTMLElement<{
23
29
  key?: import("react").Key | null | undefined;
24
30
  defaultChecked?: boolean | undefined;
25
31
  defaultValue?: string | number | readonly string[] | undefined;
@@ -80,7 +86,7 @@ export declare function Screen({ fill, palette, as, top, bottom, className, chil
80
86
  "aria-autocomplete"?: "none" | "inline" | "list" | "both" | undefined;
81
87
  "aria-braillelabel"?: string | undefined;
82
88
  "aria-brailleroledescription"?: string | undefined;
83
- "aria-busy"?: (boolean | "true" | "false") | undefined;
89
+ "aria-busy": (boolean | "true" | "false") | undefined;
84
90
  "aria-checked"?: boolean | "false" | "mixed" | "true" | undefined;
85
91
  "aria-colcount"?: number | undefined;
86
92
  "aria-colindex"?: number | undefined;
@@ -301,5 +307,6 @@ export declare function Screen({ fill, palette, as, top, bottom, className, chil
301
307
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
302
308
  className: string;
303
309
  'data-palette': Palette | undefined;
310
+ 'data-pending': string | undefined;
304
311
  ref: Ref<HTMLDivElement>;
305
312
  }, HTMLDivElement>;
@@ -5,10 +5,12 @@ import { cx } from '../cx.js';
5
5
  * Screen — the mobile screen in THREE parts: **top** and **content** (the children) in one flex column, the content down to the
6
6
  * screen's foot; **bottom** a LAYER over it there (Lh 2026-09-23). Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start
7
7
  * 15% down). Content = a box of components one after another — a `Step` (`Pages` of `Page`s / a `Page` [+ `Sheet`]), a `Centre`, a
8
- * `SheetStack`… Bottom = an `ActionBar` (the walk's `PagerAt` its first row) or a `FloatBar`, covering the content's foot on purpose. Max 25rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
8
+ * `SheetStack`… Bottom = an `ActionBar` (the walk's `PagerAt` its first row) or a `FloatBar`, covering the content's foot on purpose. Max 30rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
9
+ * ONE OUTCOME, DRAWN ONCE (Lh 2026-10-01): a screen that can come out more than one way is not drawn until the app knows which —
10
+ * `ready={false}` until then.
9
11
  * The slots set the order; children laid by hand in the same order still work.
10
12
  */
11
- export function Screen({ fill, palette, as = 'div', top, bottom, className, children, ref, ...rest }) {
13
+ export function Screen({ fill, palette, as = 'div', top, bottom, ready = true, className, children, ref, ...rest }) {
12
14
  const own = useRef(null);
13
15
  useEffect(() => {
14
16
  const el = own.current;
@@ -25,7 +27,7 @@ export function Screen({ fill, palette, as = 'div', top, bottom, className, chil
25
27
  return;
26
28
  }
27
29
  return watchSize([bar], () => el.style.setProperty('--bottom-h', `${bar.offsetHeight}px`));
28
- }, [bottom]);
30
+ }, [bottom, ready]);
29
31
  const setRef = (node) => {
30
32
  own.current = node;
31
33
  if (typeof ref === 'function')
@@ -33,5 +35,5 @@ export function Screen({ fill, palette, as = 'div', top, bottom, className, chil
33
35
  else if (ref)
34
36
  ref.current = node;
35
37
  };
36
- return createElement(as, { className: cx('screen', fill && 'screen--fill', top === 'lead' && 'screen--lead', className), 'data-palette': palette, ref: setRef, ...rest }, top !== 'lead' ? top : null, children, bottom);
38
+ return createElement(as, { className: cx('screen', fill && 'screen--fill', top === 'lead' && 'screen--lead', className), 'data-palette': palette, 'data-pending': ready ? undefined : '', 'aria-busy': ready ? undefined : true, ref: setRef, ...rest }, ready && top !== 'lead' ? top : null, ready ? children : null, ready ? bottom : null);
37
39
  }
@@ -3,6 +3,7 @@ import type { SlideDirection } from './Centre.js';
3
3
  * slideScreen — go to the NEXT SCREEN (a route change) with the whole screen sliding, as a walk's pages do. Wrap the app's
4
4
  * navigate in it: `slideScreen(() => navigate('/download'))` — `back` for a way back: `slideScreen(() => navigate(-1), 'back')`.
5
5
  * The update may render at once or later (a route's chunk loading): the slide waits for the new Screen (at most 0.8 s).
6
+ * A new Screen that is not `ready` is waited for (at most 3 s) — the old screen stays until then.
6
7
  * A Screen with `data-slide="off"` (or none at all) is not slid. `scope`: where the one Screen is (default the document — an
7
8
  * app has one). Without view transitions (the browser floor) it just runs the update.
8
9
  */
@@ -3,16 +3,31 @@
3
3
  transition named for the move only: the leaving `.screen` is `screen-out`, the arriving one `screen-in`. */
4
4
  const screenIn = (scope) => scope.querySelector('.screen:not(.screen .screen)');
5
5
  /** wait until the screen in `scope` CHANGED — another element, or new content in the same one (React may keep the element when
6
- both routes render a Screen in the same place) — a route rendered, sync or not; at most `ms` */
7
- function newScreen(scope, old, ms = 800) {
6
+ both routes render a Screen in the same place) — a route rendered, sync or not; at most `ms`. A new Screen that is not
7
+ `ready` yet (`data-pending`) is waited for too, at most `hold`: the old screen stays until the new one knows what it is. */
8
+ function newScreen(scope, old, ms = 800, hold = 3000) {
8
9
  return new Promise((done) => {
10
+ const pending = (el) => !!el?.hasAttribute('data-pending');
9
11
  const now = screenIn(scope);
10
- if (now !== old)
12
+ if (now !== old && !pending(now))
11
13
  return done(now);
12
14
  const root = scope instanceof Document ? scope.documentElement : scope;
13
- const mo = new MutationObserver(() => { mo.disconnect(); clearTimeout(t); done(screenIn(scope)); });
14
- mo.observe(root, { childList: true, subtree: true });
15
- const t = setTimeout(() => { mo.disconnect(); done(screenIn(scope)); }, ms);
15
+ const end = () => { mo.disconnect(); clearTimeout(t); done(screenIn(scope)); };
16
+ let held = false;
17
+ const look = () => {
18
+ if (!pending(screenIn(scope)))
19
+ return end();
20
+ if (!held) {
21
+ held = true;
22
+ clearTimeout(t);
23
+ t = setTimeout(end, hold);
24
+ }
25
+ };
26
+ const mo = new MutationObserver(look);
27
+ mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-pending'] });
28
+ let t = setTimeout(end, ms);
29
+ if (pending(now))
30
+ look();
16
31
  });
17
32
  }
18
33
  let running = false;
@@ -20,6 +35,7 @@ let running = false;
20
35
  * slideScreen — go to the NEXT SCREEN (a route change) with the whole screen sliding, as a walk's pages do. Wrap the app's
21
36
  * navigate in it: `slideScreen(() => navigate('/download'))` — `back` for a way back: `slideScreen(() => navigate(-1), 'back')`.
22
37
  * The update may render at once or later (a route's chunk loading): the slide waits for the new Screen (at most 0.8 s).
38
+ * A new Screen that is not `ready` is waited for (at most 3 s) — the old screen stays until then.
23
39
  * A Screen with `data-slide="off"` (or none at all) is not slid. `scope`: where the one Screen is (default the document — an
24
40
  * app has one). Without view transitions (the browser floor) it just runs the update.
25
41
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.35",
3
+ "version": "0.7.36",
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.",