@usableapp/cardds 0.7.61 → 0.7.63

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.63 — 2026-10-06
6
+
7
+ ### Added
8
+ - **The cursor standby has a switch: `data-standby="off"`** (Lh 2026-10-06) — the default does not move: a sheet come up, a step opened, a stack sheet opened or a `Modal` arrived still has its field waiting with the cursor, and an app writes no focus code. New: put `data-standby="off"` on any box around the part and no field is given the cursor — the part's heading or handle still is, so the keyboard does not come. Omit it and nothing changes. To choose another field than the first, as before: `<Field data-autofocus … />`.
9
+
10
+ ### Changed
11
+ - **The grab line is 2px again** (Lh 2026-10-06) — `--hairline-w` 1px → 2px: the line of a `Sheet` and of a stack with `mark="line"`. Before (0.7.61–0.7.62): 1px. You change nothing; a theme that wants the thin line sets `--hairline-w: 1px`.
12
+
13
+ ## 0.7.62 — 2026-10-06
14
+
15
+ ### Added
16
+ - **`Sheet` takes `refreshing`** (Lh 2026-10-06) — a boolean, off unless you set it. While `true`, the sheet's grab mark (the line, or the dots with `mark="dots"`) changes colour the way a stack sheet's does, and the sheet is `aria-busy`. Set it when you start reloading what the sheet shows, clear it when the data is in: `<Sheet refreshing={loading} …>`. The single sheet has no pull to refresh of its own and does not fetch — when to reload is yours. Omit it and nothing changes.
17
+
5
18
  ## 0.7.61 — 2026-10-06
6
19
 
7
20
  ### Changed
package/README.md CHANGED
@@ -278,7 +278,9 @@ whose markup has rules, because the sheet's box is the step itself:
278
278
  `.sheet__body` with what the member acts on. The handle and the head are
279
279
  pinned; the body is the one thing that scrolls, to the card's own edges. The
280
280
  handle is a line, or four dots (`mark="dots"`): both sit in the card's top
281
- padding, the same distance from the edge, so the title stays put. `paper` makes
281
+ padding, the same distance from the edge, so the title stays put. `refreshing`
282
+ (off unless the app sets it) changes the mark's colour while the app reloads
283
+ what the sheet shows, as a stack sheet's does; the sheet does not fetch. `paper` makes
282
284
  the content white while the head keeps the sheet's colour (`tone`, as a card).
283
285
  The same prop on a `StackSheet`, or on a `Card` in a stack: the head keeps the
284
286
  tone, the body and the foot are white, and the gap under the head stays the tone.
@@ -323,6 +325,8 @@ whose markup has rules, because the sheet's box is the step itself:
323
325
  opened, a `SheetStack` sheet opened, a `Modal` arrived. Once it has settled: the field marked `data-autofocus`, else its
324
326
  first field, else its heading (a step's title, a modal's question) or, for a sheet come onto the screen, its handle. A field
325
327
  that already has the cursor keeps it. One mechanic for all of them (`focusWhenActive`, src/state.ts); an app writes no focus code.
328
+ Another field than the first: `<Field data-autofocus … />`. Off: `data-standby="off"` on any box around the part — no field
329
+ is given the cursor (the heading or the handle still is). The gallery sets it on its stories; its Cursor button (C) turns it on.
326
330
  - **The signal set — four colours, four meanings, everywhere** (Lh 2026-10-02): `--signal-ok` green (it went right) ·
327
331
  `--signal-warn` yellow (look out) · `--signal-error` red (it went wrong) · `--signal-info` blue (for your information).
328
332
  The wireframe has them too. A theme may retune a shade, never a meaning. Each is dark enough for words and edges;
package/css/sheet.css CHANGED
@@ -231,6 +231,22 @@
231
231
  background-size: var(--_dot) var(--_dot);
232
232
  background-position: 0 0, var(--_step) 0, calc(2 * var(--_step)) 0, calc(3 * var(--_step)) 0;
233
233
  }
234
+ /* the app reloads what the sheet shows (`refreshing`, off unless set): the mark changes colour exactly as a stack sheet's grip
235
+ does — white → black and back, so a light sheet and a dark one each get a moment it stands out. --_grip and the keyframes are
236
+ the stack's (stack.css): one sign for both. Nothing travels. */
237
+ .sheet.is-loading > .sheet__handle {
238
+ opacity: 1;
239
+ background-color: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
240
+ animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
241
+ }
242
+ .sheet.is-loading > .sheet__handle--dots {
243
+ --_ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
244
+ background-color: transparent;
245
+ }
246
+ @media (prefers-reduced-motion: reduce) {
247
+ .sheet.is-loading > .sheet__handle { animation: none; background-color: currentColor; }
248
+ .sheet.is-loading > .sheet__handle--dots { --_ink: currentColor; background-color: transparent; }
249
+ }
234
250
  .sheet__handle::before {
235
251
  content: "";
236
252
  position: absolute;
package/css/tokens.css CHANGED
@@ -86,7 +86,7 @@
86
86
 
87
87
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
88
88
  --border-w: 1.5px;
89
- --hairline-w: 1px; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06) */
89
+ --hairline-w: 2px; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
90
90
  --rule-w: 2px; /* dotted connectors, picker rules */
91
91
  --scroll-w: 1px; /* the scrollbar of a .bleed track: a hairline of ink */
92
92
  --focus: 2px solid currentColor; /* :focus-visible ring */
package/dist/cardds.css CHANGED
@@ -127,7 +127,7 @@
127
127
 
128
128
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
129
129
  --border-w: 1.5px;
130
- --hairline-w: 1px; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06) */
130
+ --hairline-w: 2px; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
131
131
  --rule-w: 2px; /* dotted connectors, picker rules */
132
132
  --scroll-w: 1px; /* the scrollbar of a .bleed track: a hairline of ink */
133
133
  --focus: 2px solid currentColor; /* :focus-visible ring */
@@ -1686,6 +1686,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1686
1686
  background-size: var(--_dot) var(--_dot);
1687
1687
  background-position: 0 0, var(--_step) 0, calc(2 * var(--_step)) 0, calc(3 * var(--_step)) 0;
1688
1688
  }
1689
+ /* the app reloads what the sheet shows (`refreshing`, off unless set): the mark changes colour exactly as a stack sheet's grip
1690
+ does — white → black and back, so a light sheet and a dark one each get a moment it stands out. --_grip and the keyframes are
1691
+ the stack's (stack.css): one sign for both. Nothing travels. */
1692
+ .sheet.is-loading > .sheet__handle {
1693
+ opacity: 1;
1694
+ background-color: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
1695
+ animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
1696
+ }
1697
+ .sheet.is-loading > .sheet__handle--dots {
1698
+ --_ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
1699
+ background-color: transparent;
1700
+ }
1701
+ @media (prefers-reduced-motion: reduce) {
1702
+ .sheet.is-loading > .sheet__handle { animation: none; background-color: currentColor; }
1703
+ .sheet.is-loading > .sheet__handle--dots { --_ink: currentColor; background-color: transparent; }
1704
+ }
1689
1705
  .sheet__handle::before {
1690
1706
  content: "";
1691
1707
  position: absolute;
@@ -23,6 +23,8 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
23
23
  handle?: boolean | string;
24
24
  /** the grab mark. `line` (default) or `dots`: both sit in the card's top padding, the same distance from the edge, so the title does not move */
25
25
  mark?: 'line' | 'dots';
26
+ /** the app is reloading what this sheet shows: while `true` the grab mark (line or dots) changes colour, as a stack sheet's does, and the sheet is `aria-busy`. Off unless the app sets it — the sheet does not fetch and has no pull to refresh of its own */
27
+ refreshing?: boolean;
26
28
  /** the card tone slot, as `Card tone` — 2 and up add `card--N`. The head shows it; with `paper` the content does not */
27
29
  tone?: 1 | 2 | 3 | 4 | 5;
28
30
  /** the content is white paper and the head keeps the sheet's colour. The same prop on a `StackSheet` or a `Card` in a stack. Omit it and the content is the same colour as the head */
@@ -49,4 +51,4 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
49
51
  * crossing and never one continuous move, whatever the app's wiring (both states set in one render, or the next raised from the
50
52
  * first's `onSettled`) — the app adds no timer.
51
53
  */
52
- export declare function Sheet({ state, defaultState, onStateChange, onSettled, states, handle, mark, tone, paper, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
54
+ export declare function Sheet({ state, defaultState, onStateChange, onSettled, states, handle, mark, refreshing, tone, paper, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
@@ -38,7 +38,7 @@ function markLong(node, long) {
38
38
  * crossing and never one continuous move, whatever the app's wiring (both states set in one render, or the next raised from the
39
39
  * first's `onSettled`) — the app adds no timer.
40
40
  */
41
- export function Sheet({ state, defaultState = 'half', onStateChange, onSettled, states, handle = true, mark = 'line', tone, paper, className, children, onFocus, ...rest }) {
41
+ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled, states, handle = true, mark = 'line', refreshing, tone, paper, className, children, onFocus, ...rest }) {
42
42
  const [current, request] = useControlled(state, defaultState, onStateChange);
43
43
  const ref = useRef(null);
44
44
  const handleRef = useRef(null);
@@ -131,5 +131,5 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
131
131
  return; // the click that follows a drag is not a tap
132
132
  request(current === 'peek' ? (allowed().includes(home.current) ? home.current : allowed().find((s) => s !== 'peek') ?? 'half') : 'peek', 'tap');
133
133
  };
134
- return (_jsxs("article", { ref: ref, className: cx('card sheet', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', `sheet--${shown}`, className), inert: current === 'away' || held || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: cx('sheet__handle', mark === 'dots' && 'sheet__handle--dots'), type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), Children.map(children, (node) => markLong(node, titleLong))] }));
134
+ return (_jsxs("article", { ref: ref, className: cx('card sheet', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', `sheet--${shown}`, refreshing && 'is-loading', className), "aria-busy": refreshing || undefined, inert: current === 'away' || held || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: cx('sheet__handle', mark === 'dots' && 'sheet__handle--dots'), type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), Children.map(children, (node) => markLong(node, titleLong))] }));
135
135
  }
package/dist/state.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const warnOnce: (key: string, text: string) => void;
23
23
  export declare const reducedMotion: () => boolean;
24
24
  /** give `box` the cursor once it has settled: after its own `translate` transition (600ms at most), or after `wait` ms. It lands on the field
25
25
  * marked `data-autofocus`, else the first field inside `scope` (a selector under `box`; default: all of it), else `fallback`. A field in
26
- * `box` that already has the cursor keeps it. Nothing moves the page (`preventScroll`). Returns the cancel — `true` when it had already
26
+ * `box` that already has the cursor keeps it. Inside `[data-standby="off"]` no field is looked for: `fallback` only. Nothing moves the page (`preventScroll`). Returns the cancel — `true` when it had already
27
27
  * landed (so a component can tell a move cut short from a finished one). */
28
28
  export declare function focusWhenActive(box: HTMLElement | null | undefined, { scope, fallback, wait, then }?: {
29
29
  scope?: string;
package/dist/state.js CHANGED
@@ -56,11 +56,13 @@ export const warnOnce = (key, text) => { if (!warned.has(key)) {
56
56
  /** the member asked for less motion (false where there is no window, e.g. a server render) */
57
57
  export const reducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
58
58
  /* THE RULE (Lh 2026-09-29): a part of the screen that has just become ACTIVE — a sheet come up, a step opened, a stack sheet opened, a
59
- modal asking — has its field waiting with the cursor. ONE mechanic for every such part; the component only says WHEN it became active. */
59
+ modal asking — has its field waiting with the cursor. ONE mechanic for every such part; the component only says WHEN it became active.
60
+ THE SWITCH (Lh 2026-10-06): under `data-standby="off"` (on any box around the part) no field is given the cursor — the part's
61
+ `fallback` still is. The gallery sets it: a page of many stories, each with a sheet up, is not one screen a member is typing in. */
60
62
  const FIELD = 'input:not([type=checkbox], [type=radio], [type=hidden], [disabled], [tabindex="-1"]), textarea:not([disabled]), select:not([disabled]), [contenteditable="true"]';
61
63
  /** give `box` the cursor once it has settled: after its own `translate` transition (600ms at most), or after `wait` ms. It lands on the field
62
64
  * marked `data-autofocus`, else the first field inside `scope` (a selector under `box`; default: all of it), else `fallback`. A field in
63
- * `box` that already has the cursor keeps it. Nothing moves the page (`preventScroll`). Returns the cancel — `true` when it had already
65
+ * `box` that already has the cursor keeps it. Inside `[data-standby="off"]` no field is looked for: `fallback` only. Nothing moves the page (`preventScroll`). Returns the cancel — `true` when it had already
64
66
  * landed (so a component can tell a move cut short from a finished one). */
65
67
  export function focusWhenActive(box, { scope, fallback, wait = 'translate', then } = {}) {
66
68
  if (!box)
@@ -74,8 +76,8 @@ export function focusWhenActive(box, { scope, fallback, wait = 'translate', then
74
76
  const at = document.activeElement;
75
77
  if (!(at instanceof HTMLElement && box.contains(at) && at.matches(FIELD))) {
76
78
  const root = scope ? box.querySelector(scope) : box;
77
- const target = box.querySelector('[data-autofocus]:not([inert] *)')
78
- ?? root?.querySelector(`:is(${FIELD}):not([inert] *)`) ?? fallback?.();
79
+ const target = (box.closest('[data-standby="off"]') ? null : box.querySelector('[data-autofocus]:not([inert] *)')
80
+ ?? root?.querySelector(`:is(${FIELD}):not([inert] *)`)) ?? fallback?.();
79
81
  target?.focus({ preventScroll: true });
80
82
  }
81
83
  then?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.61",
3
+ "version": "0.7.63",
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.",