@usableapp/cardds 0.6.9 → 0.6.10

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,25 @@
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.6.10 — 2026-09-23
6
+
7
+ ### New template: `CallSheet` (layout-3)
8
+ - No top · content = a `Step` holding ONE `Page` (media · `heading`, the page's h1 · `body` · one button, `action`) plus
9
+ the `Sheet` that button calls · no bottom.
10
+ - The page starts at the lead (15%).
11
+ - The sheet waits `away`; the button brings it up full; its handle takes it down to peek (`states`, default "peek full").
12
+ Its body is rendered only while it is up.
13
+ - State: `open` + `onOpenChange`, or `defaultOpen`. `sheet={{ title, body, foot }}`.
14
+ - Gallery: layout-3, and screen → Install.
15
+
16
+ ### Changed
17
+ - **`Route`:** both ends and the plane share one grid, so the codes, the dotted runs and the plane sit on one line even
18
+ when a place name wraps on one side.
19
+ - **`Bubble`:** runs to 75% of the thread (knob `--bubble-max-w`, was 88% incl. the actions). The gallery stories no
20
+ longer show actions.
21
+ - **`Wave`:** rounded bars, one width, heights from `levels` (0–1; default a mock-up shape). Only whole bars show,
22
+ spread edge to edge. Knobs `--wave-bar-width` (2px), `--wave-gap` (2px), `--wave-h`.
23
+
5
24
  ## 0.6.9 — 2026-09-23
6
25
 
7
26
  ### Walkthrough (Lh 2026-09-23)
package/css/journey.css CHANGED
@@ -5,15 +5,21 @@
5
5
  ============================================================ */
6
6
 
7
7
  /* ---- route: endpoint · dotted connector (icon in middle) · endpoint ---- */
8
- .route { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: var(--route-col-gap, var(--sp-1)); } /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
8
+ .route { display: grid; grid-template-columns: 1fr auto 1fr; grid-template-rows: auto auto auto; align-items: end; column-gap: var(--route-col-gap, var(--sp-1)); row-gap: var(--route-point-gap, var(--sp-0)); }
9
+ /* ONE grid for both ends (Lh 2026-09-23): place · code · time are rows shared by the two ends, so the codes — and the dotted runs
10
+ on them — sit on one line even when a long place name wraps on one side; the plane sits on that code row */ /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
9
11
  /* the two endpoints get EQUAL columns whatever their words measure, so the via
10
12
  (the icon alone) sits at the card's centre; each dotted run lives on ITS
11
13
  endpoint's code row and fills that side's leftover — with short names the
12
14
  runs are long and equal, a long place name wraps in its half instead of
13
15
  pushing the plane (Lh 2026-09-16) */
14
16
 
15
- .route__point { display: grid; gap: var(--route-point-gap, var(--sp-0)); min-width: 0; }
16
- .route__point--end { text-align: right; }
17
+ .route__point { display: contents; }
18
+ .route__point > * { grid-column: 1; min-width: 0; }
19
+ .route__point--end > * { grid-column: 3; text-align: right; }
20
+ .route__place { grid-row: 1; }
21
+ .route__code { grid-row: 2; align-self: center; }
22
+ .route__time { grid-row: 3; align-self: start; }
17
23
 
18
24
  .route__code { font: var(--route-code-font, var(--type-h2)); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--route-code-gap, var(--sp-3)); }
19
25
  .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
@@ -22,7 +28,7 @@
22
28
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
23
29
  .card--3 .route__place { color: inherit; opacity: 0.7; }
24
30
 
25
- .route__via { display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
31
+ .route__via { grid-column: 2; grid-row: 2; align-self: center; display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
26
32
 
27
33
  /* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
28
34
  .tile-badge {
package/css/lists.css CHANGED
@@ -113,7 +113,7 @@
113
113
  justify-content: start;
114
114
  align-items: center;
115
115
  gap: var(--sp-1) var(--sp-3);
116
- max-width: 88%;
116
+ max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
117
117
  margin-right: auto; /* theirs: hugs the left */
118
118
  }
119
119
  .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
package/css/media.css CHANGED
@@ -132,14 +132,25 @@
132
132
  /* ---- wave: audio waveform stand-in for voice rows ----
133
133
  <span class="wave"></span> — sits in a .row__body, fills the width */
134
134
  .wave {
135
- display: block;
135
+ /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
136
+ (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
137
+ display: grid;
138
+ grid-template-columns: repeat(auto-fit, var(--wave-bar-width, 0.125rem)); /* auto-FIT: columns no bar fills collapse, so a wide row still spreads its bars edge to edge */
139
+ grid-template-rows: 100%;
140
+ grid-auto-rows: 0;
141
+ justify-content: space-between;
142
+ align-items: center;
143
+ column-gap: var(--wave-gap, 0.125rem);
136
144
  height: var(--wave-h, var(--icon));
137
145
  width: 100%;
138
- background: repeating-linear-gradient(90deg, currentColor 0 var(--sp-1), transparent var(--sp-1) var(--sp-2));
139
- /* masks only read alpha: currentColor = "opaque" without naming a color */
140
- mask: radial-gradient(ellipse 60% 100% at 50% 50%, currentColor 55%, transparent 58%) 0 0 / var(--tap-lg) 100% repeat-x;
141
- -webkit-mask: radial-gradient(ellipse 60% 100% at 50% 50%, currentColor 55%, transparent 58%) 0 0 / var(--tap-lg) 100% repeat-x;
142
- opacity: 0.8;
146
+ overflow: hidden;
147
+ }
148
+ /* a bar: as tall as its level (--wave-at, 0–1 — DATA, set by Wave), rounded ends, the ink of whatever it sits on (Lh 2026-09-23) */
149
+ .wave > i {
150
+ width: 100%; /* its column: every bar one width, only the height tells the level (Lh 2026-09-23) */
151
+ height: calc(var(--wave-at, 0.5) * 100%);
152
+ border-radius: var(--r-chip);
153
+ background: currentColor;
143
154
  }
144
155
 
145
156
  /* ---- mosaic: one big photo + two small, inside a card ----
package/dist/cardds.css CHANGED
@@ -1762,15 +1762,21 @@ button { font: inherit; cursor: pointer; }
1762
1762
  ============================================================ */
1763
1763
 
1764
1764
  /* ---- route: endpoint · dotted connector (icon in middle) · endpoint ---- */
1765
- .route { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: var(--route-col-gap, var(--sp-1)); } /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
1765
+ .route { display: grid; grid-template-columns: 1fr auto 1fr; grid-template-rows: auto auto auto; align-items: end; column-gap: var(--route-col-gap, var(--sp-1)); row-gap: var(--route-point-gap, var(--sp-0)); }
1766
+ /* ONE grid for both ends (Lh 2026-09-23): place · code · time are rows shared by the two ends, so the codes — and the dotted runs
1767
+ on them — sit on one line even when a long place name wraps on one side; the plane sits on that code row */ /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
1766
1768
  /* the two endpoints get EQUAL columns whatever their words measure, so the via
1767
1769
  (the icon alone) sits at the card's centre; each dotted run lives on ITS
1768
1770
  endpoint's code row and fills that side's leftover — with short names the
1769
1771
  runs are long and equal, a long place name wraps in its half instead of
1770
1772
  pushing the plane (Lh 2026-09-16) */
1771
1773
 
1772
- .route__point { display: grid; gap: var(--route-point-gap, var(--sp-0)); min-width: 0; }
1773
- .route__point--end { text-align: right; }
1774
+ .route__point { display: contents; }
1775
+ .route__point > * { grid-column: 1; min-width: 0; }
1776
+ .route__point--end > * { grid-column: 3; text-align: right; }
1777
+ .route__place { grid-row: 1; }
1778
+ .route__code { grid-row: 2; align-self: center; }
1779
+ .route__time { grid-row: 3; align-self: start; }
1774
1780
 
1775
1781
  .route__code { font: var(--route-code-font, var(--type-h2)); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--route-code-gap, var(--sp-3)); }
1776
1782
  .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
@@ -1779,7 +1785,7 @@ button { font: inherit; cursor: pointer; }
1779
1785
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
1780
1786
  .card--3 .route__place { color: inherit; opacity: 0.7; }
1781
1787
 
1782
- .route__via { display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
1788
+ .route__via { grid-column: 2; grid-row: 2; align-self: center; display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
1783
1789
 
1784
1790
  /* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
1785
1791
  .tile-badge {
@@ -2916,7 +2922,7 @@ button { font: inherit; cursor: pointer; }
2916
2922
  justify-content: start;
2917
2923
  align-items: center;
2918
2924
  gap: var(--sp-1) var(--sp-3);
2919
- max-width: 88%;
2925
+ max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
2920
2926
  margin-right: auto; /* theirs: hugs the left */
2921
2927
  }
2922
2928
  .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
@@ -3150,14 +3156,25 @@ button { font: inherit; cursor: pointer; }
3150
3156
  /* ---- wave: audio waveform stand-in for voice rows ----
3151
3157
  <span class="wave"></span> — sits in a .row__body, fills the width */
3152
3158
  .wave {
3153
- display: block;
3159
+ /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
3160
+ (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
3161
+ display: grid;
3162
+ grid-template-columns: repeat(auto-fit, var(--wave-bar-width, 0.125rem)); /* auto-FIT: columns no bar fills collapse, so a wide row still spreads its bars edge to edge */
3163
+ grid-template-rows: 100%;
3164
+ grid-auto-rows: 0;
3165
+ justify-content: space-between;
3166
+ align-items: center;
3167
+ column-gap: var(--wave-gap, 0.125rem);
3154
3168
  height: var(--wave-h, var(--icon));
3155
3169
  width: 100%;
3156
- background: repeating-linear-gradient(90deg, currentColor 0 var(--sp-1), transparent var(--sp-1) var(--sp-2));
3157
- /* masks only read alpha: currentColor = "opaque" without naming a color */
3158
- mask: radial-gradient(ellipse 60% 100% at 50% 50%, currentColor 55%, transparent 58%) 0 0 / var(--tap-lg) 100% repeat-x;
3159
- -webkit-mask: radial-gradient(ellipse 60% 100% at 50% 50%, currentColor 55%, transparent 58%) 0 0 / var(--tap-lg) 100% repeat-x;
3160
- opacity: 0.8;
3170
+ overflow: hidden;
3171
+ }
3172
+ /* a bar: as tall as its level (--wave-at, 0–1 — DATA, set by Wave), rounded ends, the ink of whatever it sits on (Lh 2026-09-23) */
3173
+ .wave > i {
3174
+ width: 100%; /* its column: every bar one width, only the height tells the level (Lh 2026-09-23) */
3175
+ height: calc(var(--wave-at, 0.5) * 100%);
3176
+ border-radius: var(--r-chip);
3177
+ background: currentColor;
3161
3178
  }
3162
3179
 
3163
3180
  /* ---- mosaic: one big photo + two small, inside a card ----
package/dist/index.d.ts CHANGED
@@ -75,3 +75,4 @@ export * from './journey/NoteRow.js';
75
75
  export * from './templates/Walkthrough.js';
76
76
  export * from './templates/Home.js';
77
77
  export * from './sheets/StackSheet.js';
78
+ export * from './templates/CallSheet.js';
package/dist/index.js CHANGED
@@ -78,3 +78,4 @@ export * from './journey/NoteRow.js';
78
78
  export * from './templates/Walkthrough.js';
79
79
  export * from './templates/Home.js';
80
80
  export * from './sheets/StackSheet.js';
81
+ export * from './templates/CallSheet.js';
@@ -6,6 +6,8 @@ export interface MosaicProps extends ComponentProps<'div'> {
6
6
  export declare function Mosaic({ label, className, ...rest }: MosaicProps): import("react").JSX.Element;
7
7
  export interface WaveProps extends ComponentProps<'span'> {
8
8
  label?: string;
9
+ /** the bars' heights, 0–1, left to right — a real recording's levels; default: a mock-up shape */
10
+ levels?: number[];
9
11
  }
10
- /** Wave — the waveform stand-in for a voice note, filling a Row's body. */
11
- export declare function Wave({ label, className, ...rest }: WaveProps): import("react").JSX.Element;
12
+ /** Wave — a voice note's waveform: rounded bars, short and tall, filling a Row's body. `levels` draws a real one; without it, a mock-up. */
13
+ export declare function Wave({ label, levels, className, ...rest }: WaveProps): import("react").JSX.Element;
@@ -4,7 +4,10 @@ import { cx } from '../cx.js';
4
4
  export function Mosaic({ label = 'Photos', className, ...rest }) {
5
5
  return _jsx("div", { className: cx('mosaic', className), role: "img", "aria-label": label, ...rest });
6
6
  }
7
- /** Wave — the waveform stand-in for a voice note, filling a Row's body. */
8
- export function Wave({ label = 'Voice note', className, ...rest }) {
9
- return _jsx("span", { className: cx('wave', className), role: "img", "aria-label": label, ...rest });
7
+ /* a MOCK voice shape (Lh 2026-09-23): short and tall rounded bars, loud in bursts, quiet between — not a real recording */
8
+ const MOCK = [0.25, 0.45, 0.3, 0.6, 0.4, 0.2, 0.5, 0.85, 0.35, 0.7, 1, 0.55, 0.3, 0.75, 0.9, 0.45, 0.25, 0.6, 0.8, 0.4,
9
+ 0.2, 0.5, 0.7, 0.35, 0.95, 0.6, 0.3, 0.45, 0.8, 0.5, 0.25, 0.65, 0.9, 0.4, 0.2, 0.55, 0.75, 0.35, 0.5, 0.3];
10
+ /** Wave — a voice note's waveform: rounded bars, short and tall, filling a Row's body. `levels` draws a real one; without it, a mock-up. */
11
+ export function Wave({ label = 'Voice note', levels = MOCK, className, ...rest }) {
12
+ return (_jsx("span", { className: cx('wave', className), role: "img", "aria-label": label, ...rest, children: levels.map((v, i) => _jsx("i", { style: { '--wave-at': Math.max(0.12, Math.min(1, v)) } }, i)) }));
10
13
  }
@@ -0,0 +1,36 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ import { Screen } from '../scaffold/Screen.js';
3
+ export interface CallSheetSheet {
4
+ /** the sheet head's title */
5
+ title: ReactNode;
6
+ /** the sheet body — rendered only while the sheet is up; a function is called then */
7
+ body: ReactNode | (() => ReactNode);
8
+ /** the sheet foot: the one button that finishes the sheet */
9
+ foot?: ReactNode;
10
+ }
11
+ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'children' | 'fill' | 'top' | 'bottom' | 'title'> {
12
+ /** a picture above the words (optional) */
13
+ media?: ReactNode;
14
+ /** the page's one heading — its h1 (there is no top) */
15
+ heading: ReactNode;
16
+ /** the words: a string is one paragraph */
17
+ body?: ReactNode;
18
+ /** the button that calls the sheet */
19
+ action: ReactNode;
20
+ /** what comes up */
21
+ sheet: CallSheetSheet;
22
+ /** CONTROLLED: the sheet is up — with `onOpenChange`; or `defaultOpen` */
23
+ open?: boolean;
24
+ defaultOpen?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
26
+ /** the sheet's resting states once up (default "peek full") */
27
+ states?: string;
28
+ /** layers over the screen */
29
+ children?: ReactNode;
30
+ }
31
+ /**
32
+ * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
33
+ * words · the button) and the `Sheet` that button calls · no bottom (the sheet finishes on its own foot). The page starts at the
34
+ * screen's lead (15%). The sheet waits `away`; the button brings it up full; its handle takes it down to peek.
35
+ */
36
+ export declare function CallSheet({ media, heading, body, action, sheet, open: openProp, defaultOpen, onOpenChange, states, children, ...rest }: CallSheetProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Screen } from '../scaffold/Screen.js';
4
+ import { Step } from '../sheets/Step.js';
5
+ import { Page, CtaPack } from '../scaffold/Page.js';
6
+ import { Sheet } from '../sheets/Sheet.js';
7
+ import { SheetBody } from '../sheets/SheetBody.js';
8
+ import { CardHead } from '../cards/CardHead.js';
9
+ import { CardFoot } from '../cards/CardFoot.js';
10
+ import { Text } from '../type/Text.js';
11
+ import { Btn } from '../actions/Btn.js';
12
+ /**
13
+ * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
14
+ * words · the button) and the `Sheet` that button calls · no bottom (the sheet finishes on its own foot). The page starts at the
15
+ * screen's lead (15%). The sheet waits `away`; the button brings it up full; its handle takes it down to peek.
16
+ */
17
+ export function CallSheet({ media, heading, body, action, sheet, open: openProp, defaultOpen = false, onOpenChange, states = 'peek full', children, ...rest }) {
18
+ const [own, setOwn] = useState(defaultOpen);
19
+ const open = openProp ?? own;
20
+ const [at, setAt] = useState('full');
21
+ const set = (next) => { if (openProp === undefined)
22
+ setOwn(next); onOpenChange?.(next); };
23
+ return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(CardHead, { level: "h1", heading: heading }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: _jsx(Btn, { primary: true, "aria-expanded": open, onClick: () => { setAt('full'); set(true); }, children: action }) })] }), _jsxs(Sheet, { state: open ? at : 'away', onStateChange: (s) => { if (s === 'away')
24
+ set(false);
25
+ else
26
+ setAt(s); }, states: states, children: [_jsx(CardHead, { heading: sheet.title }), _jsx(SheetBody, { children: open ? (typeof sheet.body === 'function' ? sheet.body() : sheet.body) : null }), sheet.foot != null && _jsx(CardFoot, { children: sheet.foot })] })] }), children] }));
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
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.",