@usableapp/cardds 0.7.2 → 0.7.3

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,13 @@
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.3 — 2026-09-24
6
+
7
+ ### New
8
+ - `Modal onDismiss(reason)` — a tap on the scrim (anywhere outside the card) or Esc asks to leave unanswered
9
+ (`'scrim' | 'escape'`); the app sets `open={false}` and the modal leaves as usual. Without `onDismiss` the scrim does
10
+ nothing — a question that must be answered.
11
+
5
12
  ## 0.7.2 — 2026-09-24
6
13
 
7
14
  ### `Banner` is one light band (breaking)
@@ -5,15 +5,19 @@ export interface ModalProps extends ComponentProps<'article'> {
5
5
  open?: boolean;
6
6
  /** the leaving has finished (at once, when motion is reduced) — unmount here */
7
7
  onClosed?: () => void;
8
+ /** the member asked to leave WITHOUT answering: a tap on the scrim (anywhere outside the card) or Esc. The modal only asks — the app
9
+ * sets `open={false}`. Without it the scrim does nothing (a question that must be answered). */
10
+ onDismiss?: (reason: 'scrim' | 'escape') => void;
8
11
  }
9
12
  /**
10
13
  * Modal — the ask: ONE card at the centre of a `SheetStage dim`, two parts — a `Lift` (the thing being asked about, light, scrolls
11
14
  * when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the drawer comes
12
15
  * out below while the lift moves up.
13
16
  * A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
14
- * records the answer and sets `open={false}`; the modal's one behaviour is how it leaves.
17
+ * records the answer and sets `open={false}`; the modal's one behaviour is how it leaves. `onDismiss` asks to leave unanswered (a tap on
18
+ * the scrim, Esc).
15
19
  */
16
- export declare function Modal({ open, onClosed, className, ...rest }: ModalProps): import("react").JSX.Element;
20
+ export declare function Modal({ open, onClosed, onDismiss, className, ...rest }: ModalProps): import("react").JSX.Element;
17
21
  export interface LiftProps extends ComponentProps<'div'> {
18
22
  }
19
23
  /** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
@@ -6,9 +6,10 @@ import { cx } from '../cx.js';
6
6
  * when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the drawer comes
7
7
  * out below while the lift moves up.
8
8
  * A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
9
- * records the answer and sets `open={false}`; the modal's one behaviour is how it leaves.
9
+ * records the answer and sets `open={false}`; the modal's one behaviour is how it leaves. `onDismiss` asks to leave unanswered (a tap on
10
+ * the scrim, Esc).
10
11
  */
11
- export function Modal({ open = true, onClosed, className, ...rest }) {
12
+ export function Modal({ open = true, onClosed, onDismiss, className, ...rest }) {
12
13
  const ref = useRef(null);
13
14
  const closed = useRef(onClosed);
14
15
  closed.current = onClosed;
@@ -27,6 +28,21 @@ export function Modal({ open = true, onClosed, className, ...rest }) {
27
28
  closed.current?.(); });
28
29
  return () => { live = false; };
29
30
  }, [open]);
31
+ const dismiss = useRef(onDismiss);
32
+ dismiss.current = onDismiss;
33
+ const has = onDismiss !== undefined;
34
+ useEffect(() => {
35
+ const el = ref.current, stage = el?.parentElement;
36
+ if (!el || !stage || !has || !open)
37
+ return;
38
+ const tap = (e) => { if (!el.contains(e.target))
39
+ dismiss.current?.('scrim'); }; // the scrim is the stage's own ::after
40
+ const key = (e) => { if (e.key === 'Escape' && !e.defaultPrevented)
41
+ dismiss.current?.('escape'); };
42
+ stage.addEventListener('click', tap);
43
+ document.addEventListener('keydown', key);
44
+ return () => { stage.removeEventListener('click', tap); document.removeEventListener('keydown', key); };
45
+ }, [has, open]);
30
46
  return _jsx("article", { ref: ref, className: cx('card modal', className), ...rest });
31
47
  }
32
48
  /** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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.",