@usableapp/cardds 0.6.15 → 0.7.0
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 +32 -0
- package/README.md +10 -9
- package/css/actions.css +10 -0
- package/css/base.css +5 -5
- package/css/layover.css +4 -0
- package/css/lists.css +5 -12
- package/css/stack.css +1 -1
- package/css/step.css +2 -2
- package/dist/cardds.css +27 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/lists/Bubble.d.ts +2 -4
- package/dist/lists/Bubble.js +3 -3
- package/dist/scaffold/Page.d.ts +0 -3
- package/dist/scaffold/Page.js +0 -9
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
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.0 — 2026-09-24
|
|
6
|
+
|
|
7
|
+
Breaking — the clean-up promised since 0.6.5. Four renames/removals, one new way.
|
|
8
|
+
|
|
9
|
+
### A bar on a scrolling screen goes INSIDE the Screen
|
|
10
|
+
- Before: on a screen that is not `fill` (it scrolls), the `ActionBar` was written after `</Screen>`, fixed to the
|
|
11
|
+
viewport, and the screen reserved `--bar-reserve` at its bottom. After: write it as `<Screen bottom={<ActionBar>…}>`
|
|
12
|
+
like on a fill screen. The screen becomes a column at least one phone tall; the bar is its last row, `position: sticky`
|
|
13
|
+
at the viewport's foot (above the keyboard) — in reach while the words scroll, under them when they end.
|
|
14
|
+
`validateScreen` already reports a bar after `</Screen>`. Story `ActionBar · OnAScrollingScreen`.
|
|
15
|
+
|
|
16
|
+
### `BaseContent` removed
|
|
17
|
+
- Before: `BaseContent` (an alias that warned). After: `Page` — same props, same markup.
|
|
18
|
+
|
|
19
|
+
### `--base-content-*` knobs renamed `--page-*` (no fallback)
|
|
20
|
+
- `--base-content-media-h` → `--page-media-h`, and likewise `-gap`, `-pad-y`, `-body-ink`. The old names do nothing now:
|
|
21
|
+
rename them in your theme.css.
|
|
22
|
+
|
|
23
|
+
### `Bubble actions` removed
|
|
24
|
+
- Before: small buttons beside a chat message (`actions`, `.bubble__actions`, knob `--bubble-actions-gap`). After: gone —
|
|
25
|
+
a Bubble is the message and its `meta`. The bubble is one column now.
|
|
26
|
+
|
|
27
|
+
## 0.6.16 — 2026-09-24
|
|
28
|
+
|
|
29
|
+
### iOS 16.4 supported (was 18.3)
|
|
30
|
+
- Before: on iOS 16 no Menu or Dropdown opened (the popover API arrived in Safari 17). After: cardds ships
|
|
31
|
+
`@oddbird/popover-polyfill` (a dependency, loaded by the package, active only where popover is missing) and CSS for the
|
|
32
|
+
polyfill's open state. The floor is **iOS 16.4**: under it `@property` / `color-mix()` are missing and the sheet's room
|
|
33
|
+
and the derived colours go wrong.
|
|
34
|
+
- A project that checks the browser before starting: DROP the `'popover' in HTMLElement.prototype` check — it fails on
|
|
35
|
+
iOS 16.4–16.x, which now works. Keep `@property`, `color-mix()`, container queries, `:has()` (README "เบราว์เซอร์").
|
|
36
|
+
|
|
5
37
|
## 0.6.15 — 2026-09-24
|
|
6
38
|
|
|
7
39
|
- **`ActionNote` icon colour:** new knob `--action-note-icon-ink` (default: the words' muted ink). A project can colour the
|
package/README.md
CHANGED
|
@@ -74,15 +74,16 @@ import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the
|
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
**เบราว์เซอร์** (เช็ก 2026-09-
|
|
77
|
+
**เบราว์เซอร์** (เช็ก 2026-09-24): iPhone ตั้งแต่ **iOS 16.4** (มี.ค. 2023) ขึ้นไป
|
|
78
78
|
|
|
79
79
|
| เบราว์เซอร์ | ขั้นต่ำ | ออกเมื่อ | เครื่องที่ใช้ได้ |
|
|
80
80
|
|---|---|---|---|
|
|
81
|
-
| Safari / iOS |
|
|
81
|
+
| Safari / iOS | 16.4 | มี.ค. 2023 | iPhone 8 / X (2017) ขึ้นไป ที่อัปเดตแล้ว |
|
|
82
82
|
| Chrome | 114 | พ.ค. 2023 | Android ส่วนใหญ่ที่ Chrome อัปเดตอยู่ |
|
|
83
83
|
| Firefox | 128 | ก.ค. 2024 | — |
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
iOS 16.4–16.x ไม่มี popover ในตัว — cardds ใส่ `@oddbird/popover-polyfill` มาให้ (ทำงานเฉพาะเครื่องที่ไม่มี)
|
|
86
|
+
เก่ากว่า 16.4: ระยะของ sheet และสีเพี้ยน (`@property`, `color-mix()`) ส่วนเบราว์เซอร์ที่ผ่านขั้นต่ำแต่ยังไม่ใช่รุ่นล่าสุด
|
|
86
87
|
เมนูจะขึ้นกลางจอแทนที่จะอยู่ใต้ปุ่ม
|
|
87
88
|
|
|
88
89
|
สำหรับโปรเจกต์ที่อยากเช็กเบราว์เซอร์ก่อนเริ่ม (เช่นหน้าติดตั้ง): เช็กเฉพาะฟีเจอร์ที่**จำเป็น**
|
|
@@ -90,14 +91,13 @@ import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the
|
|
|
90
91
|
|
|
91
92
|
| จำเป็น: ไม่มีแล้วหน้าจอพัง | วิธีเช็ก |
|
|
92
93
|
|---|---|
|
|
93
|
-
| popover | `'popover' in HTMLElement.prototype` |
|
|
94
94
|
| `@property` | `'registerProperty' in CSS` |
|
|
95
95
|
| `color-mix()` | `CSS.supports('color', 'color-mix(in srgb, red, blue)')` |
|
|
96
96
|
| container queries | `CSS.supports('container-type: size') && CSS.supports('height: 1cqh')` |
|
|
97
97
|
| `:has()` | `CSS.supports('selector(:has(a))')` |
|
|
98
98
|
|
|
99
|
-
มีตัวสำรองแล้ว ไม่ต้องเช็ก: anchor positioning, view transitions, `@starting-style`, `text-wrap`
|
|
100
|
-
iOS
|
|
99
|
+
มีตัวสำรองแล้ว ไม่ต้องเช็ก: popover (polyfill), anchor positioning, view transitions, `@starting-style`, `text-wrap`
|
|
100
|
+
iOS 16.4–18.2 เช็กผ่าน แต่ยังรองรับไม่เต็ม เมนูขึ้นกลางจอแทนใต้ปุ่ม
|
|
101
101
|
|
|
102
102
|
Working with Claude in another project? The `/cardds` skill
|
|
103
103
|
(`~/.claude/skills/cardds/`) carries the install and the contract, and points
|
|
@@ -246,9 +246,10 @@ whose markup has rules, because the sheet's box is the step itself:
|
|
|
246
246
|
step that follows anything else keeps the screen's gap, and the sheet
|
|
247
247
|
stops under that thing.
|
|
248
248
|
- **The bar is after the step, inside the screen** (the screen's bottom — *Screen: top · content · bottom*). In a
|
|
249
|
-
`.screen--fill` the
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
`.screen--fill` the bar is a layer over the content's foot. On a SCROLLING screen (not fill, 0.7.0) the screen is a
|
|
250
|
+
column at least one phone tall and the bar its last row, `position: sticky` at the viewport's foot (above `--kb`):
|
|
251
|
+
it stays in reach while the words scroll and sits under them when they end. Either way the bar is written INSIDE
|
|
252
|
+
the Screen (`bottom`) — a bar after `</Screen>` is an error (`validateScreen`).
|
|
252
253
|
- **Every sheet has one shape** (Lh 2026-09-21): the handle and the head stay at the top and never scroll · the
|
|
253
254
|
foot's button stands at the bottom — above the keyboard when there is one (`--kb`: a step gives the keyboard
|
|
254
255
|
its room, a stage sheet gives it up itself as `--_lift`) · everything between them scrolls, as far as it
|
package/css/actions.css
CHANGED
|
@@ -190,6 +190,7 @@
|
|
|
190
190
|
.dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
191
191
|
.dropdown__btn > .icon { transition: rotate 0.2s ease; }
|
|
192
192
|
.dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
|
|
193
|
+
.dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { rotate: 180deg; } /* the polyfill's open (layover.css) */
|
|
193
194
|
.dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
|
|
194
195
|
.filter-row { display: flex; gap: var(--sp-2); }
|
|
195
196
|
.filter-row > .dropdown { flex: 1; min-width: 0; }
|
|
@@ -237,6 +238,15 @@
|
|
|
237
238
|
display: flex;
|
|
238
239
|
gap: var(--sp-2);
|
|
239
240
|
}
|
|
241
|
+
/* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
|
|
242
|
+
last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
|
|
243
|
+
it takes the room it needs in the column, so nothing is added to the content for it */
|
|
244
|
+
.screen:not(.screen--fill):has(> .action-bar) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
245
|
+
.screen:not(.screen--fill) > .action-bar {
|
|
246
|
+
position: sticky; inset-inline: auto; bottom: var(--kb, 0rem);
|
|
247
|
+
max-width: none;
|
|
248
|
+
margin: auto calc(-1 * var(--screen-pad)) 0;
|
|
249
|
+
}
|
|
240
250
|
.action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
|
|
241
251
|
.action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
|
|
242
252
|
|
package/css/base.css
CHANGED
|
@@ -115,18 +115,18 @@ button { font: inherit; cursor: pointer; }
|
|
|
115
115
|
In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
|
|
116
116
|
.page {
|
|
117
117
|
display: grid;
|
|
118
|
-
gap: var(--
|
|
118
|
+
gap: var(--page-gap, var(--sp-3));
|
|
119
119
|
align-content: start; /* a tall column never stretches the rows — the words hug the top */
|
|
120
|
-
padding-block: var(--
|
|
120
|
+
padding-block: var(--page-pad-y, var(--sp-4));
|
|
121
121
|
}
|
|
122
|
-
.page > .t-body { color: var(--
|
|
122
|
+
.page > .t-body { color: var(--page-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
|
|
123
123
|
/* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
|
|
124
124
|
(the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
|
|
125
125
|
/* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
|
|
126
126
|
h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
|
|
127
127
|
.page > .card__head > .t-h1 { font-size: var(--fs-h2); }
|
|
128
|
-
.page > :is(img, .ph) { max-height: var(--
|
|
129
|
-
.page > .ph { width: 100%; height: var(--
|
|
128
|
+
.page > :is(img, .ph) { max-height: var(--page-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
|
|
129
|
+
.page > .ph { width: 100%; height: var(--page-media-h, 14rem); }
|
|
130
130
|
.page .cta-pack {
|
|
131
131
|
display: flex;
|
|
132
132
|
align-items: start; /* buttons keep their own height, never the row's */
|
package/css/layover.css
CHANGED
|
@@ -123,6 +123,9 @@
|
|
|
123
123
|
box-shadow: var(--menu-shadow, var(--shadow-float));
|
|
124
124
|
}
|
|
125
125
|
.menu:popover-open { display: grid; }
|
|
126
|
+
/* iOS 16.4–16.x has no popover: @oddbird/popover-polyfill (src/index.ts) marks an open one with the
|
|
127
|
+
class `:popover-open`. Its own rule — an unknown pseudo-class would drop the whole selector list. */
|
|
128
|
+
.menu.\:popover-open { display: grid; }
|
|
126
129
|
.menu:not([popover]) { display: grid; position: relative; } /* inline usage (no attribute) */
|
|
127
130
|
.menu__item {
|
|
128
131
|
display: flex;
|
|
@@ -159,6 +162,7 @@
|
|
|
159
162
|
transition: display 0.2s allow-discrete, overlay 0.2s allow-discrete, opacity 0.2s, translate 0.2s;
|
|
160
163
|
}
|
|
161
164
|
.menu:popover-open { opacity: 1; translate: 0 0; }
|
|
165
|
+
.menu.\:popover-open { opacity: 1; translate: 0 0; }
|
|
162
166
|
@starting-style { .menu:popover-open { opacity: 0; translate: 0 calc(-1 * var(--sp-2)); } }
|
|
163
167
|
}
|
|
164
168
|
|
package/css/lists.css
CHANGED
|
@@ -98,29 +98,23 @@
|
|
|
98
98
|
/* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
|
|
99
99
|
No tail: a rounded block whose corner nearest the sender is square-ish
|
|
100
100
|
(bottom-left for theirs, bottom-right for mine), the way messaging apps
|
|
101
|
-
draw it. Under it a caption line (time · a tick)
|
|
102
|
-
actions (a heart, more). A .thread stacks them.
|
|
101
|
+
draw it. Under it a caption line (time · a tick). A .thread stacks them.
|
|
103
102
|
<div class="thread">
|
|
104
|
-
<div class="bubble"><p class="bubble__body">…</p><span class="
|
|
103
|
+
<div class="bubble"><p class="bubble__body">…</p><span class="bubble__meta">Dec 4 · 8:15</span></div>
|
|
105
104
|
<div class="bubble bubble--right">…</div>
|
|
106
105
|
</div> */
|
|
107
106
|
.thread { display: grid; gap: var(--thread-gap, var(--sp-4)); }
|
|
108
|
-
/*
|
|
109
|
-
and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
|
|
107
|
+
/* one column: the message (max-content, but it gives way — minmax(0) — so a long one shrinks and wraps); the meta under it */
|
|
110
108
|
.bubble {
|
|
111
109
|
display: grid;
|
|
112
|
-
grid-template-columns: minmax(0, max-content)
|
|
110
|
+
grid-template-columns: minmax(0, max-content);
|
|
113
111
|
justify-content: start;
|
|
114
112
|
align-items: center;
|
|
115
113
|
gap: var(--sp-1) var(--sp-3);
|
|
116
114
|
max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
|
|
117
115
|
margin-right: auto; /* theirs: hugs the left */
|
|
118
116
|
}
|
|
119
|
-
.bubble--right {
|
|
120
|
-
.bubble__body, .bubble__meta { grid-column: 1; }
|
|
121
|
-
.bubble__actions { grid-column: 2; grid-row: 1; }
|
|
122
|
-
.bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
|
|
123
|
-
.bubble--right > .bubble__actions { grid-column: 1; }
|
|
117
|
+
.bubble--right { justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right */
|
|
124
118
|
.bubble__body {
|
|
125
119
|
margin: 0;
|
|
126
120
|
min-width: 0;
|
|
@@ -139,7 +133,6 @@
|
|
|
139
133
|
color: var(--on-accent);
|
|
140
134
|
text-align: right;
|
|
141
135
|
}
|
|
142
|
-
.bubble__actions { display: flex; gap: var(--bubble-actions-gap, var(--sp-1)); flex: none; opacity: 0.7; }
|
|
143
136
|
.bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
|
|
144
137
|
.bubble--right > .bubble__meta { justify-self: end; }
|
|
145
138
|
.bubble__meta > .icon { color: var(--accent); }
|
package/css/stack.css
CHANGED
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
gap: var(--card-gap, var(--sp-5));
|
|
231
231
|
overflow: clip;
|
|
232
232
|
}
|
|
233
|
-
/* a base that is ONE
|
|
233
|
+
/* a base that is ONE Page is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
|
|
234
234
|
loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
|
|
235
235
|
.sheet-stack__base:has(> .page:only-child) { align-content: start; }
|
|
236
236
|
|
package/css/step.css
CHANGED
|
@@ -145,13 +145,13 @@
|
|
|
145
145
|
.screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
|
|
146
146
|
.screen--fill > .appbar + .sheet-stage { margin-top: 0; }
|
|
147
147
|
/* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
|
|
148
|
-
the lead + the picture's own height (--
|
|
148
|
+
the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
|
|
149
149
|
.step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
|
|
150
150
|
.step > .pages > .page > :is(img, .ph):first-child {
|
|
151
151
|
width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
|
|
152
152
|
max-width: none;
|
|
153
153
|
margin-inline: calc(-1 * var(--screen-pad));
|
|
154
|
-
height: calc(var(--_lead, 0rem) + var(--
|
|
154
|
+
height: calc(var(--_lead, 0rem) + var(--page-media-h, 14rem));
|
|
155
155
|
max-height: none;
|
|
156
156
|
border-radius: 0;
|
|
157
157
|
border-inline: 0;
|
package/dist/cardds.css
CHANGED
|
@@ -495,18 +495,18 @@ button { font: inherit; cursor: pointer; }
|
|
|
495
495
|
In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
|
|
496
496
|
.page {
|
|
497
497
|
display: grid;
|
|
498
|
-
gap: var(--
|
|
498
|
+
gap: var(--page-gap, var(--sp-3));
|
|
499
499
|
align-content: start; /* a tall column never stretches the rows — the words hug the top */
|
|
500
|
-
padding-block: var(--
|
|
500
|
+
padding-block: var(--page-pad-y, var(--sp-4));
|
|
501
501
|
}
|
|
502
|
-
.page > .t-body { color: var(--
|
|
502
|
+
.page > .t-body { color: var(--page-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
|
|
503
503
|
/* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
|
|
504
504
|
(the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
|
|
505
505
|
/* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
|
|
506
506
|
h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
|
|
507
507
|
.page > .card__head > .t-h1 { font-size: var(--fs-h2); }
|
|
508
|
-
.page > :is(img, .ph) { max-height: var(--
|
|
509
|
-
.page > .ph { width: 100%; height: var(--
|
|
508
|
+
.page > :is(img, .ph) { max-height: var(--page-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
|
|
509
|
+
.page > .ph { width: 100%; height: var(--page-media-h, 14rem); }
|
|
510
510
|
.page .cta-pack {
|
|
511
511
|
display: flex;
|
|
512
512
|
align-items: start; /* buttons keep their own height, never the row's */
|
|
@@ -1073,7 +1073,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1073
1073
|
gap: var(--card-gap, var(--sp-5));
|
|
1074
1074
|
overflow: clip;
|
|
1075
1075
|
}
|
|
1076
|
-
/* a base that is ONE
|
|
1076
|
+
/* a base that is ONE Page is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
|
|
1077
1077
|
loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
|
|
1078
1078
|
.sheet-stack__base:has(> .page:only-child) { align-content: start; }
|
|
1079
1079
|
|
|
@@ -1678,13 +1678,13 @@ button { font: inherit; cursor: pointer; }
|
|
|
1678
1678
|
.screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
|
|
1679
1679
|
.screen--fill > .appbar + .sheet-stage { margin-top: 0; }
|
|
1680
1680
|
/* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
|
|
1681
|
-
the lead + the picture's own height (--
|
|
1681
|
+
the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
|
|
1682
1682
|
.step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
|
|
1683
1683
|
.step > .pages > .page > :is(img, .ph):first-child {
|
|
1684
1684
|
width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
|
|
1685
1685
|
max-width: none;
|
|
1686
1686
|
margin-inline: calc(-1 * var(--screen-pad));
|
|
1687
|
-
height: calc(var(--_lead, 0rem) + var(--
|
|
1687
|
+
height: calc(var(--_lead, 0rem) + var(--page-media-h, 14rem));
|
|
1688
1688
|
max-height: none;
|
|
1689
1689
|
border-radius: 0;
|
|
1690
1690
|
border-inline: 0;
|
|
@@ -2022,6 +2022,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2022
2022
|
.dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2023
2023
|
.dropdown__btn > .icon { transition: rotate 0.2s ease; }
|
|
2024
2024
|
.dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
|
|
2025
|
+
.dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { rotate: 180deg; } /* the polyfill's open (layover.css) */
|
|
2025
2026
|
.dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
|
|
2026
2027
|
.filter-row { display: flex; gap: var(--sp-2); }
|
|
2027
2028
|
.filter-row > .dropdown { flex: 1; min-width: 0; }
|
|
@@ -2069,6 +2070,15 @@ button { font: inherit; cursor: pointer; }
|
|
|
2069
2070
|
display: flex;
|
|
2070
2071
|
gap: var(--sp-2);
|
|
2071
2072
|
}
|
|
2073
|
+
/* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
|
|
2074
|
+
last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
|
|
2075
|
+
it takes the room it needs in the column, so nothing is added to the content for it */
|
|
2076
|
+
.screen:not(.screen--fill):has(> .action-bar) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
2077
|
+
.screen:not(.screen--fill) > .action-bar {
|
|
2078
|
+
position: sticky; inset-inline: auto; bottom: var(--kb, 0rem);
|
|
2079
|
+
max-width: none;
|
|
2080
|
+
margin: auto calc(-1 * var(--screen-pad)) 0;
|
|
2081
|
+
}
|
|
2072
2082
|
.action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
|
|
2073
2083
|
.action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
|
|
2074
2084
|
|
|
@@ -2940,29 +2950,23 @@ button { font: inherit; cursor: pointer; }
|
|
|
2940
2950
|
/* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
|
|
2941
2951
|
No tail: a rounded block whose corner nearest the sender is square-ish
|
|
2942
2952
|
(bottom-left for theirs, bottom-right for mine), the way messaging apps
|
|
2943
|
-
draw it. Under it a caption line (time · a tick)
|
|
2944
|
-
actions (a heart, more). A .thread stacks them.
|
|
2953
|
+
draw it. Under it a caption line (time · a tick). A .thread stacks them.
|
|
2945
2954
|
<div class="thread">
|
|
2946
|
-
<div class="bubble"><p class="bubble__body">…</p><span class="
|
|
2955
|
+
<div class="bubble"><p class="bubble__body">…</p><span class="bubble__meta">Dec 4 · 8:15</span></div>
|
|
2947
2956
|
<div class="bubble bubble--right">…</div>
|
|
2948
2957
|
</div> */
|
|
2949
2958
|
.thread { display: grid; gap: var(--thread-gap, var(--sp-4)); }
|
|
2950
|
-
/*
|
|
2951
|
-
and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
|
|
2959
|
+
/* one column: the message (max-content, but it gives way — minmax(0) — so a long one shrinks and wraps); the meta under it */
|
|
2952
2960
|
.bubble {
|
|
2953
2961
|
display: grid;
|
|
2954
|
-
grid-template-columns: minmax(0, max-content)
|
|
2962
|
+
grid-template-columns: minmax(0, max-content);
|
|
2955
2963
|
justify-content: start;
|
|
2956
2964
|
align-items: center;
|
|
2957
2965
|
gap: var(--sp-1) var(--sp-3);
|
|
2958
2966
|
max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
|
|
2959
2967
|
margin-right: auto; /* theirs: hugs the left */
|
|
2960
2968
|
}
|
|
2961
|
-
.bubble--right {
|
|
2962
|
-
.bubble__body, .bubble__meta { grid-column: 1; }
|
|
2963
|
-
.bubble__actions { grid-column: 2; grid-row: 1; }
|
|
2964
|
-
.bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
|
|
2965
|
-
.bubble--right > .bubble__actions { grid-column: 1; }
|
|
2969
|
+
.bubble--right { justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right */
|
|
2966
2970
|
.bubble__body {
|
|
2967
2971
|
margin: 0;
|
|
2968
2972
|
min-width: 0;
|
|
@@ -2981,7 +2985,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
2981
2985
|
color: var(--on-accent);
|
|
2982
2986
|
text-align: right;
|
|
2983
2987
|
}
|
|
2984
|
-
.bubble__actions { display: flex; gap: var(--bubble-actions-gap, var(--sp-1)); flex: none; opacity: 0.7; }
|
|
2985
2988
|
.bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
|
|
2986
2989
|
.bubble--right > .bubble__meta { justify-self: end; }
|
|
2987
2990
|
.bubble__meta > .icon { color: var(--accent); }
|
|
@@ -3467,6 +3470,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
3467
3470
|
box-shadow: var(--menu-shadow, var(--shadow-float));
|
|
3468
3471
|
}
|
|
3469
3472
|
.menu:popover-open { display: grid; }
|
|
3473
|
+
/* iOS 16.4–16.x has no popover: @oddbird/popover-polyfill (src/index.ts) marks an open one with the
|
|
3474
|
+
class `:popover-open`. Its own rule — an unknown pseudo-class would drop the whole selector list. */
|
|
3475
|
+
.menu.\:popover-open { display: grid; }
|
|
3470
3476
|
.menu:not([popover]) { display: grid; position: relative; } /* inline usage (no attribute) */
|
|
3471
3477
|
.menu__item {
|
|
3472
3478
|
display: flex;
|
|
@@ -3503,6 +3509,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
3503
3509
|
transition: display 0.2s allow-discrete, overlay 0.2s allow-discrete, opacity 0.2s, translate 0.2s;
|
|
3504
3510
|
}
|
|
3505
3511
|
.menu:popover-open { opacity: 1; translate: 0 0; }
|
|
3512
|
+
.menu.\:popover-open { opacity: 1; translate: 0 0; }
|
|
3506
3513
|
@starting-style { .menu:popover-open { opacity: 0; translate: 0 calc(-1 * var(--sp-2)); } }
|
|
3507
3514
|
}
|
|
3508
3515
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// markup contract the CSS documents, nothing more. cardds.js rides along for
|
|
3
3
|
// the behaviours (sheet handle, stack open/close, dropdown value sync).
|
|
4
4
|
import '../cardds.js';
|
|
5
|
+
// Popover for Safari/iOS 16.4–16.x (native from 17): applies itself only where the platform lacks it.
|
|
6
|
+
import '@oddbird/popover-polyfill';
|
|
5
7
|
export { cx } from './cx.js';
|
|
6
8
|
export * from './type/Icon.js';
|
|
7
9
|
export { ICON_NAMES } from './type/icons.js';
|
package/dist/lists/Bubble.d.ts
CHANGED
|
@@ -8,11 +8,9 @@ export interface BubbleProps extends ComponentProps<'div'> {
|
|
|
8
8
|
side?: 'left' | 'right';
|
|
9
9
|
/** the caption under the message — a time, a tick (an Icon) */
|
|
10
10
|
meta?: ReactNode;
|
|
11
|
-
/** small round buttons beside the message (a heart, more) */
|
|
12
|
-
actions?: ReactNode;
|
|
13
11
|
}
|
|
14
12
|
/**
|
|
15
13
|
* Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
|
|
16
|
-
* for mine), no tail. `meta` sits under it on the sender's side
|
|
14
|
+
* for mine), no tail. `meta` sits under it on the sender's side. Stack them in a Thread.
|
|
17
15
|
*/
|
|
18
|
-
export declare function Bubble({ side, meta,
|
|
16
|
+
export declare function Bubble({ side, meta, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
|
package/dist/lists/Bubble.js
CHANGED
|
@@ -6,8 +6,8 @@ export function Thread({ className, ...rest }) {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
|
|
9
|
-
* for mine), no tail. `meta` sits under it on the sender's side
|
|
9
|
+
* for mine), no tail. `meta` sits under it on the sender's side. Stack them in a Thread.
|
|
10
10
|
*/
|
|
11
|
-
export function Bubble({ side = 'left', meta,
|
|
12
|
-
return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }),
|
|
11
|
+
export function Bubble({ side = 'left', meta, className, children, ...rest }) {
|
|
12
|
+
return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
|
|
13
13
|
}
|
package/dist/scaffold/Page.d.ts
CHANGED
|
@@ -8,9 +8,6 @@ export interface PageProps extends ComponentProps<'section'> {
|
|
|
8
8
|
* a still, one-scaffold screen, which is gone.
|
|
9
9
|
*/
|
|
10
10
|
export declare function Page({ className, ...rest }: PageProps): import("react").JSX.Element;
|
|
11
|
-
/** @deprecated renamed `Page` (2026-09-23) — same props, same markup; kept one version for the move */
|
|
12
|
-
export declare function BaseContent(props: PageProps): import("react").JSX.Element;
|
|
13
|
-
export type BaseContentProps = PageProps;
|
|
14
11
|
export interface CtaPackProps extends ComponentProps<'div'> {
|
|
15
12
|
}
|
|
16
13
|
/** CtaPack — the buttons under a Page's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
|
package/dist/scaffold/Page.js
CHANGED
|
@@ -9,15 +9,6 @@ import { cx } from '../cx.js';
|
|
|
9
9
|
export function Page({ className, ...rest }) {
|
|
10
10
|
return _jsx("section", { className: cx('page', className), ...rest });
|
|
11
11
|
}
|
|
12
|
-
let warned = false;
|
|
13
|
-
/** @deprecated renamed `Page` (2026-09-23) — same props, same markup; kept one version for the move */
|
|
14
|
-
export function BaseContent(props) {
|
|
15
|
-
if (!warned) {
|
|
16
|
-
warned = true;
|
|
17
|
-
console.warn('cardds: BaseContent is now Page — rename it (same props).');
|
|
18
|
-
}
|
|
19
|
-
return _jsx(Page, { ...props });
|
|
20
|
-
}
|
|
21
12
|
/** CtaPack — the buttons under a Page's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
|
|
22
13
|
export function CtaPack({ className, ...rest }) {
|
|
23
14
|
return _jsx("div", { className: cx('cta-pack', className), ...rest });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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.",
|
|
@@ -57,5 +57,8 @@
|
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@oddbird/popover-polyfill": "^0.7.2"
|
|
60
63
|
}
|
|
61
64
|
}
|