@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 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-23): ใช้ได้กับเบราว์เซอร์ตั้งแต่**ต้นปี 2025** ขึ้นไป
77
+ **เบราว์เซอร์** (เช็ก 2026-09-24): iPhone ตั้งแต่ **iOS 16.4** (มี.ค. 2023) ขึ้นไป
78
78
 
79
79
  | เบราว์เซอร์ | ขั้นต่ำ | ออกเมื่อ | เครื่องที่ใช้ได้ |
80
80
  |---|---|---|---|
81
- | Safari / iOS | 18.3 | ม.ค. 2025 | iPhone XS / XR (2018) ขึ้นไป ที่อัปเดตแล้ว |
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
- เก่ากว่านี้: เมนูและ dropdown เปิดไม่ได้ ส่วนเบราว์เซอร์ที่ผ่านขั้นต่ำแต่ยังไม่ใช่รุ่นล่าสุด
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 17–18.2 เช็กผ่าน แต่ยังรองรับไม่เต็ม เมนูอาจแสดงผิดเล็กน้อย
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 step and the bar share one flex column and touch; the step takes the rest. (A bar outside a
250
- fill screen — an app shell — is still fixed to the viewport and the step reads `--bar-reserve`: the old way,
251
- kept until 0.7.0.)
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(--base-content-gap, var(--sp-3));
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(--base-content-pad-y, var(--sp-4));
120
+ padding-block: var(--page-pad-y, var(--sp-4));
121
121
  }
122
- .page > .t-body { color: var(--base-content-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) */
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(--base-content-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(--base-content-media-h, 14rem); }
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), beside it small round
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="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
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
- /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
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) auto;
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 { 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 */
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 BaseContent is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
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 (--base-content-media-h), square — so the words start where they always did */
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(--base-content-media-h, 14rem));
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(--base-content-gap, var(--sp-3));
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(--base-content-pad-y, var(--sp-4));
500
+ padding-block: var(--page-pad-y, var(--sp-4));
501
501
  }
502
- .page > .t-body { color: var(--base-content-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) */
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(--base-content-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(--base-content-media-h, 14rem); }
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 BaseContent is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
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 (--base-content-media-h), square — so the words start where they always did */
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(--base-content-media-h, 14rem));
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), beside it small round
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="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
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
- /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
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) auto;
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 { 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 */
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
@@ -1,4 +1,5 @@
1
1
  import '../cardds.js';
2
+ import '@oddbird/popover-polyfill';
2
3
  export { cx } from './cx.js';
3
4
  export * from './type/Icon.js';
4
5
  export { ICON_NAMES } from './type/icons.js';
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';
@@ -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, `actions` beside it. Stack them in a Thread.
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, actions, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
16
+ export declare function Bubble({ side, meta, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
@@ -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, `actions` beside it. Stack them in a Thread.
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, actions, className, children, ...rest }) {
12
- return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), actions != null && _jsx("span", { className: "bubble__actions", children: actions }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
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
  }
@@ -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. */
@@ -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.6.15",
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
  }