@usableapp/cardds 0.6.9 → 0.6.11
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 +33 -0
- package/css/card.css +4 -0
- package/css/journey.css +10 -4
- package/css/lists.css +1 -1
- package/css/media.css +17 -6
- package/css/sheet.css +3 -0
- package/css/step.css +5 -0
- package/dist/cardds.css +40 -11
- package/dist/cards/CardFoot.d.ts +7 -3
- package/dist/cards/CardFoot.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/media/Mosaic.d.ts +4 -2
- package/dist/media/Mosaic.js +6 -3
- package/dist/scaffold/EmptyState.d.ts +1 -1
- package/dist/scaffold/EmptyState.js +1 -1
- package/dist/templates/CallSheet.d.ts +38 -0
- package/dist/templates/CallSheet.js +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
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.11 — 2026-09-23
|
|
6
|
+
|
|
7
|
+
### A small link under a foot's buttons: `CardFoot link` (new, like an ActionBar's `ctaLink`)
|
|
8
|
+
- `CardFoot` has a new `link` slot: a `Link sm` on its own short row under the buttons, centred. It never shares the buttons'
|
|
9
|
+
line, and a sheet's foot strip grows by that row.
|
|
10
|
+
- `CallSheet` passes it as `sheet.footLink`.
|
|
11
|
+
- A `Link` put straight in a `CardFoot` is misuse: it takes a button's share and squeezes the button.
|
|
12
|
+
|
|
13
|
+
### EmptyState, redone for the new anatomy
|
|
14
|
+
- `Screen top={TopBar} › Step › EmptyState`. It is a component in the content and no longer sits in a `Centre` over the
|
|
15
|
+
screen.
|
|
16
|
+
- Alone in a Step, it stands at the content's human centre (about 40% down).
|
|
17
|
+
- The TopBar names the page; the EmptyState heading says what is (not) there.
|
|
18
|
+
|
|
19
|
+
## 0.6.10 — 2026-09-23
|
|
20
|
+
|
|
21
|
+
### New template: `CallSheet` (layout-3)
|
|
22
|
+
- No top · content = a `Step` holding ONE `Page` (media · `heading`, the page's h1 · `body` · one button, `action`) plus
|
|
23
|
+
the `Sheet` that button calls · no bottom.
|
|
24
|
+
- The page starts at the lead (15%).
|
|
25
|
+
- The sheet waits `away`; the button brings it up full; its handle takes it down to peek (`states`, default "peek full").
|
|
26
|
+
Its body is rendered only while it is up.
|
|
27
|
+
- State: `open` + `onOpenChange`, or `defaultOpen`. `sheet={{ title, body, foot }}`.
|
|
28
|
+
- Gallery: layout-3, and screen → Install.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **`Route`:** both ends and the plane share one grid, so the codes, the dotted runs and the plane sit on one line even
|
|
32
|
+
when a place name wraps on one side.
|
|
33
|
+
- **`Bubble`:** runs to 75% of the thread (knob `--bubble-max-w`, was 88% incl. the actions). The gallery stories no
|
|
34
|
+
longer show actions.
|
|
35
|
+
- **`Wave`:** rounded bars, one width, heights from `levels` (0–1; default a mock-up shape). Only whole bars show,
|
|
36
|
+
spread edge to edge. Knobs `--wave-bar-width` (2px), `--wave-gap` (2px), `--wave-h`.
|
|
37
|
+
|
|
5
38
|
## 0.6.9 — 2026-09-23
|
|
6
39
|
|
|
7
40
|
### Walkthrough (Lh 2026-09-23)
|
package/css/card.css
CHANGED
|
@@ -121,6 +121,10 @@
|
|
|
121
121
|
.card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
|
|
122
122
|
.card__foot > .btn { flex: 1 1 0; min-width: 0; }
|
|
123
123
|
.card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
|
|
124
|
+
/* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
|
|
125
|
+
whole short row of its own, centred — it never shares the buttons' line */
|
|
126
|
+
.card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
|
|
127
|
+
.card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
|
|
124
128
|
|
|
125
129
|
/* media placeholder for wireframes — currentColor so it survives any palette */
|
|
126
130
|
.ph {
|
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:
|
|
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:
|
|
16
|
-
.route__point
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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/css/sheet.css
CHANGED
|
@@ -147,9 +147,12 @@
|
|
|
147
147
|
stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
|
|
148
148
|
One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
|
|
149
149
|
.sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
|
|
150
|
+
/* …and the link row under the button, when there is one (its line + the gap over it) */
|
|
151
|
+
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + calc(var(--tap-base) * 7 / 12) + var(--card-pad)); }
|
|
150
152
|
.sheet > .card__foot {
|
|
151
153
|
position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
|
|
152
154
|
align-items: end;
|
|
155
|
+
align-content: end;
|
|
153
156
|
padding: 0 var(--card-pad) var(--card-pad);
|
|
154
157
|
background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
|
|
155
158
|
pointer-events: none;
|
package/css/step.css
CHANGED
|
@@ -158,3 +158,8 @@
|
|
|
158
158
|
border-top: 0;
|
|
159
159
|
object-fit: cover;
|
|
160
160
|
}
|
|
161
|
+
/* an EMPTY STATE is a component in the content box too (Lh 2026-09-23 — it no longer floats in a Centre over the screen): alone in a
|
|
162
|
+
Step it stands at the human centre of the content, 40% down (two parts of room above it, three below) */
|
|
163
|
+
.step:has(> .empty-state)::before { content: ""; flex: 2 1 0; }
|
|
164
|
+
.step:has(> .empty-state)::after { content: ""; flex: 3 1 0; }
|
|
165
|
+
.step > .empty-state { flex: none; }
|
package/dist/cardds.css
CHANGED
|
@@ -653,6 +653,10 @@ button { font: inherit; cursor: pointer; }
|
|
|
653
653
|
.card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
|
|
654
654
|
.card__foot > .btn { flex: 1 1 0; min-width: 0; }
|
|
655
655
|
.card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
|
|
656
|
+
/* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
|
|
657
|
+
whole short row of its own, centred — it never shares the buttons' line */
|
|
658
|
+
.card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
|
|
659
|
+
.card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
|
|
656
660
|
|
|
657
661
|
/* media placeholder for wireframes — currentColor so it survives any palette */
|
|
658
662
|
.ph {
|
|
@@ -1301,9 +1305,12 @@ button { font: inherit; cursor: pointer; }
|
|
|
1301
1305
|
stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
|
|
1302
1306
|
One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
|
|
1303
1307
|
.sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
|
|
1308
|
+
/* …and the link row under the button, when there is one (its line + the gap over it) */
|
|
1309
|
+
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + calc(var(--tap-base) * 7 / 12) + var(--card-pad)); }
|
|
1304
1310
|
.sheet > .card__foot {
|
|
1305
1311
|
position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
|
|
1306
1312
|
align-items: end;
|
|
1313
|
+
align-content: end;
|
|
1307
1314
|
padding: 0 var(--card-pad) var(--card-pad);
|
|
1308
1315
|
background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
|
|
1309
1316
|
pointer-events: none;
|
|
@@ -1684,6 +1691,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
1684
1691
|
border-top: 0;
|
|
1685
1692
|
object-fit: cover;
|
|
1686
1693
|
}
|
|
1694
|
+
/* an EMPTY STATE is a component in the content box too (Lh 2026-09-23 — it no longer floats in a Centre over the screen): alone in a
|
|
1695
|
+
Step it stands at the human centre of the content, 40% down (two parts of room above it, three below) */
|
|
1696
|
+
.step:has(> .empty-state)::before { content: ""; flex: 2 1 0; }
|
|
1697
|
+
.step:has(> .empty-state)::after { content: ""; flex: 3 1 0; }
|
|
1698
|
+
.step > .empty-state { flex: none; }
|
|
1687
1699
|
|
|
1688
1700
|
/* ---- css/forms.css — outlined fields, add-row ---- */
|
|
1689
1701
|
/* ============================================================
|
|
@@ -1762,15 +1774,21 @@ button { font: inherit; cursor: pointer; }
|
|
|
1762
1774
|
============================================================ */
|
|
1763
1775
|
|
|
1764
1776
|
/* ---- route: endpoint · dotted connector (icon in middle) · endpoint ---- */
|
|
1765
|
-
.route { display: grid; grid-template-columns: 1fr auto 1fr; align-items:
|
|
1777
|
+
.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)); }
|
|
1778
|
+
/* 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
|
|
1779
|
+
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
1780
|
/* the two endpoints get EQUAL columns whatever their words measure, so the via
|
|
1767
1781
|
(the icon alone) sits at the card's centre; each dotted run lives on ITS
|
|
1768
1782
|
endpoint's code row and fills that side's leftover — with short names the
|
|
1769
1783
|
runs are long and equal, a long place name wraps in its half instead of
|
|
1770
1784
|
pushing the plane (Lh 2026-09-16) */
|
|
1771
1785
|
|
|
1772
|
-
.route__point { display:
|
|
1773
|
-
.route__point
|
|
1786
|
+
.route__point { display: contents; }
|
|
1787
|
+
.route__point > * { grid-column: 1; min-width: 0; }
|
|
1788
|
+
.route__point--end > * { grid-column: 3; text-align: right; }
|
|
1789
|
+
.route__place { grid-row: 1; }
|
|
1790
|
+
.route__code { grid-row: 2; align-self: center; }
|
|
1791
|
+
.route__time { grid-row: 3; align-self: start; }
|
|
1774
1792
|
|
|
1775
1793
|
.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
1794
|
.route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
|
|
@@ -1779,7 +1797,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1779
1797
|
.route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
|
|
1780
1798
|
.card--3 .route__place { color: inherit; opacity: 0.7; }
|
|
1781
1799
|
|
|
1782
|
-
.route__via { display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
|
|
1800
|
+
.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
1801
|
|
|
1784
1802
|
/* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
|
|
1785
1803
|
.tile-badge {
|
|
@@ -2916,7 +2934,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2916
2934
|
justify-content: start;
|
|
2917
2935
|
align-items: center;
|
|
2918
2936
|
gap: var(--sp-1) var(--sp-3);
|
|
2919
|
-
max-width:
|
|
2937
|
+
max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
|
|
2920
2938
|
margin-right: auto; /* theirs: hugs the left */
|
|
2921
2939
|
}
|
|
2922
2940
|
.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 +3168,25 @@ button { font: inherit; cursor: pointer; }
|
|
|
3150
3168
|
/* ---- wave: audio waveform stand-in for voice rows ----
|
|
3151
3169
|
<span class="wave"></span> — sits in a .row__body, fills the width */
|
|
3152
3170
|
.wave {
|
|
3153
|
-
|
|
3171
|
+
/* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
|
|
3172
|
+
(auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
|
|
3173
|
+
display: grid;
|
|
3174
|
+
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 */
|
|
3175
|
+
grid-template-rows: 100%;
|
|
3176
|
+
grid-auto-rows: 0;
|
|
3177
|
+
justify-content: space-between;
|
|
3178
|
+
align-items: center;
|
|
3179
|
+
column-gap: var(--wave-gap, 0.125rem);
|
|
3154
3180
|
height: var(--wave-h, var(--icon));
|
|
3155
3181
|
width: 100%;
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3182
|
+
overflow: hidden;
|
|
3183
|
+
}
|
|
3184
|
+
/* 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) */
|
|
3185
|
+
.wave > i {
|
|
3186
|
+
width: 100%; /* its column: every bar one width, only the height tells the level (Lh 2026-09-23) */
|
|
3187
|
+
height: calc(var(--wave-at, 0.5) * 100%);
|
|
3188
|
+
border-radius: var(--r-chip);
|
|
3189
|
+
background: currentColor;
|
|
3161
3190
|
}
|
|
3162
3191
|
|
|
3163
3192
|
/* ---- mosaic: one big photo + two small, inside a card ----
|
package/dist/cards/CardFoot.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { ComponentProps } from 'react';
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
2
|
export interface CardFootProps extends ComponentProps<'div'> {
|
|
3
|
+
/** a SMALL LINK under the buttons, centred on its own short row (a `Link sm` — "details for developers") — the same as an
|
|
4
|
+
* ActionBar's ctaLink (Lh 2026-09-23). It never takes a share of the buttons' line */
|
|
5
|
+
link?: ReactNode;
|
|
3
6
|
}
|
|
4
|
-
/** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only
|
|
5
|
-
|
|
7
|
+
/** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
|
|
8
|
+
* link goes in `link`, under it. */
|
|
9
|
+
export declare function CardFoot({ link, className, children, ...rest }: CardFootProps): import("react").JSX.Element;
|
package/dist/cards/CardFoot.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
|
|
4
|
+
* link goes in `link`, under it. */
|
|
5
|
+
export function CardFoot({ link, className, children, ...rest }) {
|
|
6
|
+
return (_jsxs("div", { className: cx('card__foot', link != null && 'card__foot--link', className), ...rest, children: [children, link != null && _jsx("div", { className: "card__foot-link", children: link })] }));
|
|
6
7
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/media/Mosaic.d.ts
CHANGED
|
@@ -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 —
|
|
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;
|
package/dist/media/Mosaic.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
}
|
|
@@ -9,6 +9,6 @@ export interface EmptyStateProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
9
9
|
/** the heading's level: h2 on a surface (default) · h3 inside a card's section, under its h3 heading */
|
|
10
10
|
level?: 'h2' | 'h3';
|
|
11
11
|
}
|
|
12
|
-
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA.
|
|
12
|
+
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
|
|
13
13
|
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16). */
|
|
14
14
|
export declare function EmptyState({ icon, title, action, level, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA.
|
|
3
|
+
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
|
|
4
4
|
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16). */
|
|
5
5
|
export function EmptyState({ icon, title, action, level = 'h2', className, children, ...rest }) {
|
|
6
6
|
const H = level;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
/** a small link UNDER that button (a `Link sm`, e.g. details for developers) */
|
|
11
|
+
footLink?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'children' | 'fill' | 'top' | 'bottom' | 'title'> {
|
|
14
|
+
/** a picture above the words (optional) */
|
|
15
|
+
media?: ReactNode;
|
|
16
|
+
/** the page's one heading — its h1 (there is no top) */
|
|
17
|
+
heading: ReactNode;
|
|
18
|
+
/** the words: a string is one paragraph */
|
|
19
|
+
body?: ReactNode;
|
|
20
|
+
/** the button that calls the sheet */
|
|
21
|
+
action: ReactNode;
|
|
22
|
+
/** what comes up */
|
|
23
|
+
sheet: CallSheetSheet;
|
|
24
|
+
/** CONTROLLED: the sheet is up — with `onOpenChange`; or `defaultOpen` */
|
|
25
|
+
open?: boolean;
|
|
26
|
+
defaultOpen?: boolean;
|
|
27
|
+
onOpenChange?: (open: boolean) => void;
|
|
28
|
+
/** the sheet's resting states once up (default "peek full") */
|
|
29
|
+
states?: string;
|
|
30
|
+
/** layers over the screen */
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
|
|
35
|
+
* words · the button) and the `Sheet` that button calls · no bottom (the sheet finishes on its own foot). The page starts at the
|
|
36
|
+
* screen's lead (15%). The sheet waits `away`; the button brings it up full; its handle takes it down to peek.
|
|
37
|
+
*/
|
|
38
|
+
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 || sheet.footLink != null) && _jsx(CardFoot, { link: sheet.footLink, children: sheet.foot })] })] }), children] }));
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
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.",
|