@usableapp/cardds 0.1.0 → 0.1.1
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/README.md +10 -7
- package/cardds.js +15 -109
- package/css/actions.css +2 -1
- package/css/base.css +1 -2
- package/css/card.css +36 -25
- package/css/choice.css +3 -5
- package/css/journey.css +10 -16
- package/css/layover.css +14 -3
- package/css/lists.css +17 -7
- package/css/sheet.css +1 -1
- package/css/stack.css +1 -5
- package/css/tokens.css +1 -0
- package/dist/actions/Chip.d.ts +1 -1
- package/dist/actions/Dropdown.d.ts +6 -2
- package/dist/actions/Dropdown.js +3 -3
- package/dist/actions/IconBtn.d.ts +10 -295
- package/dist/actions/IconBtn.js +9 -4
- package/dist/cardds.css +87 -66
- package/dist/cards/CardHead.d.ts +2 -2
- package/dist/cards/CardHead.js +2 -2
- package/dist/lists/Row.d.ts +1 -3
- package/dist/lists/Row.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -299,7 +299,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
299
299
|
| `css/tokens.css` | spacing, radius, type levels, motion (`--motion-sheet`), the human centre (`--screen-centre`), grayscale role defaults; registers the two sheet tokens (`--sheet-peek`, `--sheet-half`) so script can read them resolved | rarely |
|
|
300
300
|
| `css/palette.css` | **color settings — palettes as role-token overrides** | yes, this one |
|
|
301
301
|
| `css/base.css` | reset, surface, typography classes, `.section` | rarely |
|
|
302
|
-
| `css/card.css` | `.card` + its skeleton (`.card__head` / content / `.card__foot
|
|
302
|
+
| `css/card.css` | `.card` + its skeleton (`.card__head` / content / `.card__foot`), `.card-list`, `.card--row`, `.icon-row`, stats, `.card--centre` (one card at the human centre of a fill screen) and `.screen__centre` (a small group there) | when adding variants |
|
|
303
303
|
| `css/stack.css` | **sheet stack** — `.sheet-stack`: the menu, sheets stacked (heads peek, up to 5; `--tap` tap-to-open, the back button dissolves in; `--closed` nothing open, heads at the bottom, the base above (`.sheet-stack__base`); `--display` the title is the peek, `--fanned` wider peeks); always edge to edge; a stacked sheet is a `.card` | rarely |
|
|
304
304
|
| `css/sheet.css` | **sheet single** — `.sheet`: a card as bottom sheet, full height always, a state is how far it slid: `--peek` / `--half` / `--full` (translate only); handle · head · `.sheet__body` (the head pinned, the body scrolls); `.sheet-stage` (overlay stage: map sheet, a `__hero` — one thing centred in the band a half sheet leaves open, `--dim`, `--raised`, ask drawer) | when adding states |
|
|
305
305
|
| `css/step.css` | `.step` — one screen, one task: `.step__card` (reads) + optional `.sheet` (acts), the bar's room, the keyboard's room | rarely |
|
|
@@ -319,7 +319,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
319
319
|
| `docs/handoff/` | the Claude Design handoff spec the app was ported from | reference |
|
|
320
320
|
| `scripts/` | `build-icons.mjs` (icons.svg → `src/type/icons.ts`), `flatten-css.mjs` (`cardds.css` + imports → `dist/cardds.css`) | rarely |
|
|
321
321
|
| `docs/guides/` | `building-with-cardds.md` — the anatomy, the step, the two sheet systems and the vocabulary in React terms; shipped to Claude Design as its guidelines | when the contract changes |
|
|
322
|
-
| `cardds.js` | the behaviours (delegated, class/property toggles only — everything renders without it): dropdown value sync, `.sheet-stack--tap` open/close (tolerates one wrapper per sheet; close reads the tapped sheet), `.pin` auto-advance / Backspace / paste + `cardds:pin`,
|
|
322
|
+
| `cardds.js` | the behaviours (delegated, class/property toggles only — everything renders without it): dropdown value sync, `.sheet-stack--tap` open/close (tolerates one wrapper per sheet; close reads the tapped sheet), `.pin` auto-advance / Backspace / paste + `cardds:pin`, the sheet handle (tap / drag-snap / Enter / Space), the focus and keyboard raises (`--kb`), `cardds:sheet` | rarely |
|
|
323
323
|
|
|
324
324
|
## Anatomy — base, menus, content, and the card skeleton
|
|
325
325
|
|
|
@@ -333,11 +333,14 @@ Every screen is the same four things, and every card the same three:
|
|
|
333
333
|
note, help steps) may sit on the base directly.
|
|
334
334
|
- **A card is head · content · foot.**
|
|
335
335
|
- **Head** (`.card__head`): the heading on the left; hints, actions or a
|
|
336
|
-
more-menu — the contextual info — at the far right.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
more-menu — the contextual info — at the far right. With nothing at
|
|
337
|
+
the far right it runs all the way across. The heading wraps freely —
|
|
338
|
+
no line budget, no squeeze, no clamp (Lh 2026-09-16): a height rule on
|
|
339
|
+
words only distorts them. The head is FLAT markup — heading, then an
|
|
340
|
+
optional overline / caption as spans, then the far-right thing; no
|
|
341
|
+
wrapper element, no `<p>`: the head wraps, the overline takes the row
|
|
342
|
+
above and the caption the row below (`order`), the gap tokens do the
|
|
343
|
+
rest (Lh 2026-09-16).
|
|
341
344
|
- **Content**: whatever the card frames — rows, key-values, a gauge, a
|
|
342
345
|
paragraph. Frameless, per rule 9.
|
|
343
346
|
- **Foot** (`.card__foot`): the card's action buttons on one line. One
|
package/cardds.js
CHANGED
|
@@ -7,8 +7,10 @@ document.addEventListener('click', e => {
|
|
|
7
7
|
if (!item) return;
|
|
8
8
|
const menu = item.closest('.menu');
|
|
9
9
|
const dd = menu && menu.closest('.dropdown');
|
|
10
|
-
menu.
|
|
11
|
-
|
|
10
|
+
if (!menu.classList.contains('menu--actions')) { /* an actions menu holds no value: nothing stays current */
|
|
11
|
+
menu.querySelectorAll('.menu__item').forEach(i => i.removeAttribute('aria-current'));
|
|
12
|
+
item.setAttribute('aria-current', 'true');
|
|
13
|
+
}
|
|
12
14
|
if (dd) {
|
|
13
15
|
const label = dd.querySelector('.dropdown__label');
|
|
14
16
|
const value = item.dataset.value ?? item.textContent.trim();
|
|
@@ -168,113 +170,6 @@ function carddsCloseStack(stack, card) {
|
|
|
168
170
|
setTimeout(() => before.forEach(c => c.classList.remove('is-before')), CARDDS_STACK_PHASE);
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
/* ---- card heading squeeze ----
|
|
172
|
-
A skeleton head's heading has a budget of two lines (card.css, "Skeleton
|
|
173
|
-
head"). When the words need more, step --squeeze down — the CSS multiplies
|
|
174
|
-
the level's own size by it — until it fits two lines, or stop at the floor
|
|
175
|
-
and let the CSS clamp hold it to three.
|
|
176
|
-
|
|
177
|
-
Stack heads are skipped: a peek is one line at its own size. stack.css opts
|
|
178
|
-
those same cards out of the clamp, so the two files agree on what a skeleton
|
|
179
|
-
head is — change one, change the other.
|
|
180
|
-
|
|
181
|
-
What causes a measure:
|
|
182
|
-
- a ResizeObserver on each heading. A heading only needs a different squeeze
|
|
183
|
-
when its WIDTH changes, so that is what is watched: window resize, a
|
|
184
|
-
container that moved, display:none -> shown, all arrive here. Our own
|
|
185
|
-
font-size change is not a width change, so it does not come back round.
|
|
186
|
-
- a MutationObserver that only REGISTERS headings added later. It reads
|
|
187
|
-
added nodes, never the layout, so a host app that re-renders constantly
|
|
188
|
-
pays a node scan instead of a measuring pass over the page.
|
|
189
|
-
- document.fonts.ready, once: a font swap changes glyph metrics at an
|
|
190
|
-
unchanged width, which is the one case the ResizeObserver cannot see.
|
|
191
|
-
- a "cardds:layout" event, listened for in the CAPTURE phase so it arrives
|
|
192
|
-
whether or not the app sets bubbles: true. */
|
|
193
|
-
const CARDDS_SQUEEZE_STEPS = [0.94, 0.88, 0.82];
|
|
194
|
-
const CARDDS_HEAD_SEL = '.card__head :is(h1, h2, h3)';
|
|
195
|
-
|
|
196
|
-
/* a heading in a card's own head — not one in a sheet-stack peek (stack.css).
|
|
197
|
-
A sheet is read the way the stack reads it (carddsStackCards): a direct
|
|
198
|
-
child, or one wrapper down — a host that wraps each sheet must not turn its
|
|
199
|
-
one-line peek into a squeezed skeleton head. */
|
|
200
|
-
function carddsIsSkeletonHead(h) {
|
|
201
|
-
const card = h.closest('.card__head')?.parentElement;
|
|
202
|
-
if (!card) return false;
|
|
203
|
-
const stack = card.parentElement?.closest('.sheet-stack');
|
|
204
|
-
return !stack || !carddsStackCards(stack).includes(card);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function carddsSqueezeOne(h) {
|
|
208
|
-
h.style.removeProperty('--squeeze');
|
|
209
|
-
if (h.getClientRects().length === 0) return; // not rendered — nothing to measure
|
|
210
|
-
/* line-height is read again at every step: --squeeze scales it too, so a
|
|
211
|
-
count taken against the unsqueezed value is out by exactly the step */
|
|
212
|
-
const lines = () => {
|
|
213
|
-
const lh = parseFloat(getComputedStyle(h).lineHeight);
|
|
214
|
-
return lh ? Math.round(h.scrollHeight / lh) : 0;
|
|
215
|
-
};
|
|
216
|
-
if (lines() <= 2) return;
|
|
217
|
-
for (const step of CARDDS_SQUEEZE_STEPS) {
|
|
218
|
-
h.style.setProperty('--squeeze', step);
|
|
219
|
-
if (lines() <= 2) break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function carddsSqueeze(root = document) {
|
|
224
|
-
root.querySelectorAll(CARDDS_HEAD_SEL).forEach(h => {
|
|
225
|
-
if (carddsIsSkeletonHead(h)) carddsSqueezeOne(h);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/* measuring is batched into one frame, and the width we settle at is recorded
|
|
230
|
-
so the resize our own font-size change causes is recognised, not re-run */
|
|
231
|
-
const carddsHeadWidth = new WeakMap();
|
|
232
|
-
const carddsPending = new Set();
|
|
233
|
-
let carddsRaf = 0;
|
|
234
|
-
|
|
235
|
-
function carddsFlush() {
|
|
236
|
-
carddsRaf = 0;
|
|
237
|
-
const batch = [...carddsPending];
|
|
238
|
-
carddsPending.clear();
|
|
239
|
-
batch.forEach(carddsSqueezeOne);
|
|
240
|
-
batch.forEach(h => carddsHeadWidth.set(h, h.getBoundingClientRect().width));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const carddsHeadResize = new ResizeObserver(entries => {
|
|
244
|
-
for (const e of entries) {
|
|
245
|
-
const w = e.target.getBoundingClientRect().width;
|
|
246
|
-
if (Math.abs((carddsHeadWidth.get(e.target) ?? -1) - w) < 0.5) continue;
|
|
247
|
-
carddsHeadWidth.set(e.target, w);
|
|
248
|
-
carddsPending.add(e.target);
|
|
249
|
-
}
|
|
250
|
-
if (carddsPending.size && !carddsRaf) carddsRaf = requestAnimationFrame(carddsFlush);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
function carddsWatch(root = document) {
|
|
254
|
-
root.querySelectorAll(CARDDS_HEAD_SEL).forEach(h => {
|
|
255
|
-
if (carddsIsSkeletonHead(h)) carddsHeadResize.observe(h);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
carddsWatch();
|
|
260
|
-
(document.fonts ? document.fonts.ready : Promise.resolve()).then(() => carddsSqueeze());
|
|
261
|
-
|
|
262
|
-
new MutationObserver(records => {
|
|
263
|
-
for (const r of records) {
|
|
264
|
-
for (const n of r.addedNodes) {
|
|
265
|
-
if (n.nodeType !== 1) continue;
|
|
266
|
-
if (n.matches(CARDDS_HEAD_SEL) && carddsIsSkeletonHead(n)) carddsHeadResize.observe(n);
|
|
267
|
-
else carddsWatch(n);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
271
|
-
|
|
272
|
-
document.addEventListener('cardds:layout', e => {
|
|
273
|
-
const root = e.target instanceof Element ? e.target : document;
|
|
274
|
-
carddsWatch(root);
|
|
275
|
-
carddsSqueeze(root);
|
|
276
|
-
}, true);
|
|
277
|
-
|
|
278
173
|
/* ---- sheet (.sheet, sheet.css): peek / half / full ----
|
|
279
174
|
A sheet is rigid material, like a sheet in the stack (stack.css) — it never changes size, a
|
|
280
175
|
state is only how far it slid — so this, like the stack code, only ever
|
|
@@ -471,3 +366,14 @@ function carddsSheetReveal(el, atLeast) {
|
|
|
471
366
|
carddsSheetSet(sheet, to);
|
|
472
367
|
el.scrollIntoView({ block: 'nearest' });
|
|
473
368
|
}
|
|
369
|
+
|
|
370
|
+
/* .bleed track: .is-scrolling while it scrolls (and a beat after), so its
|
|
371
|
+
hairline scrollbar shows only while hovered or moving (card.css). */
|
|
372
|
+
const carddsScrollTimers = new WeakMap();
|
|
373
|
+
document.addEventListener('scroll', e => {
|
|
374
|
+
const t = e.target;
|
|
375
|
+
if (!(t instanceof Element) || !t.classList.contains('bleed')) return;
|
|
376
|
+
t.classList.add('is-scrolling');
|
|
377
|
+
clearTimeout(carddsScrollTimers.get(t));
|
|
378
|
+
carddsScrollTimers.set(t, setTimeout(() => t.classList.remove('is-scrolling'), 700));
|
|
379
|
+
}, true);
|
package/css/actions.css
CHANGED
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
|
140
140
|
|
|
141
141
|
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
|
|
142
|
+
.chip-row.bleed { flex-wrap: nowrap; } /* actions.css loads after card.css — the strip stays one line */
|
|
142
143
|
|
|
143
144
|
/* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
|
|
144
145
|
<div class="dropdown"><button class="dropdown__btn" popovertarget="f1"><span class="dropdown__label">this week</span><svg class="icon icon--xs"><use href="icons.svg#chevron-down"/></svg></button>
|
|
@@ -167,7 +168,7 @@
|
|
|
167
168
|
.dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
168
169
|
.dropdown__btn > .icon { transition: rotate 0.2s ease; }
|
|
169
170
|
.dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
|
|
170
|
-
.dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards */
|
|
171
|
+
.dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
|
|
171
172
|
.filter-row { display: flex; gap: var(--sp-2); }
|
|
172
173
|
.filter-row > .dropdown { flex: 1; min-width: 0; }
|
|
173
174
|
|
package/css/base.css
CHANGED
|
@@ -76,8 +76,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
76
76
|
.screen--fill > .sheet-stack { flex: 1; min-height: 0; }
|
|
77
77
|
|
|
78
78
|
/* ---- typography levels ---- */
|
|
79
|
-
/* --fs-self: each level publishes its own size,
|
|
80
|
-
heading (card.css --squeeze) can multiply it without knowing the level */
|
|
79
|
+
/* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
|
|
81
80
|
.t-display { font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
|
|
82
81
|
.t-h1 { font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
|
|
83
82
|
.t-h2 { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; --fs-self: var(--fs-h2); }
|
package/css/card.css
CHANGED
|
@@ -39,10 +39,19 @@
|
|
|
39
39
|
multi-line title can opt back out with .card__head--top. */
|
|
40
40
|
.card__head {
|
|
41
41
|
display: flex;
|
|
42
|
+
flex-wrap: wrap; /* the caption / overline take a line of their own — no wrapper div */
|
|
42
43
|
align-items: center;
|
|
43
44
|
justify-content: space-between;
|
|
44
|
-
gap: var(--sp-3);
|
|
45
|
+
column-gap: var(--sp-3);
|
|
46
|
+
row-gap: var(--sp-0);
|
|
45
47
|
}
|
|
48
|
+
/* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
|
|
49
|
+
<div class="card__head"><span class="t-overline t-muted">…</span><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
|
|
50
|
+
The heading and the far-right thing share the first row; an overline sits
|
|
51
|
+
on a row above (order −1), a caption on a row below (order 1), whatever the
|
|
52
|
+
DOM order — so an author writes the words in reading order. */
|
|
53
|
+
.card__head > :is(.t-overline, .t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
|
|
54
|
+
.card__head > .t-overline { order: -1; }
|
|
46
55
|
.card__head--top { align-items: flex-start; }
|
|
47
56
|
|
|
48
57
|
.card__media { border-radius: var(--r-media); overflow: hidden; position: relative; } /* relative: corner badges */
|
|
@@ -62,29 +71,12 @@
|
|
|
62
71
|
.card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
|
|
63
72
|
|
|
64
73
|
/* Skeleton head — the heading in a card's OWN head, and only there.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
and "stack headers" below opts it out of the budget, the squeeze and the
|
|
72
|
-
clamp. cardds.js skips those same cards, so CSS and JS agree on what counts
|
|
73
|
-
as a skeleton head. Change one, change the other. */
|
|
74
|
-
.card__head :is(h1, h2, h3) {
|
|
75
|
-
font-size: calc(var(--fs-self, var(--fs-h2)) * var(--squeeze, 1));
|
|
76
|
-
overflow-wrap: anywhere;
|
|
77
|
-
display: -webkit-box;
|
|
78
|
-
-webkit-box-orient: vertical;
|
|
79
|
-
-webkit-line-clamp: 3;
|
|
80
|
-
overflow: hidden;
|
|
81
|
-
/* the clip would take the hair the tight leading leaves outside the line
|
|
82
|
-
box — a stacked tone mark on top, สระอู below (fonts.css). Padding is
|
|
83
|
-
inside the clip, the negative margin gives the room back, so the head
|
|
84
|
-
measures exactly as before. */
|
|
85
|
-
padding-block: 0.15em;
|
|
86
|
-
margin-block: -0.15em;
|
|
87
|
-
}
|
|
74
|
+
The heading wraps freely: no line budget, no squeeze, no clamp (Lh
|
|
75
|
+
2026-09-16 — a height rule on words only distorts them; content that needs
|
|
76
|
+
more rows takes more rows). The group that holds overline · heading ·
|
|
77
|
+
caption is the head's own wrapping row (above) — no wrapper element, no
|
|
78
|
+
height, only the row gap. */
|
|
79
|
+
.card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
|
|
88
80
|
|
|
89
81
|
/* ---- bleed: a scrolling track that runs to the container's edges ----
|
|
90
82
|
For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
|
|
@@ -95,11 +87,30 @@
|
|
|
95
87
|
<div class="avatar-pick bleed">…</div>
|
|
96
88
|
Padding, not item margins: padding is inside the scrollable area, so the
|
|
97
89
|
last item scrolls fully clear of the edge instead of stopping against it.
|
|
98
|
-
scroll-padding keeps snap points on the content line, not the border edge.
|
|
90
|
+
scroll-padding keeps snap points on the content line, not the border edge.
|
|
91
|
+
The track IS the scroller (nowrap, overflow-x), so no one writes that inline.
|
|
92
|
+
Its scrollbar is designed, not hidden (Lh 2026-09-16): a --scroll-w line of
|
|
93
|
+
the track's own ink, and --sp-2 of room under the items so the line sits
|
|
94
|
+
clear of them and reads as a slider. It shows only while the track is
|
|
95
|
+
hovered or scrolling (cardds.js sets .is-scrolling for a beat after each
|
|
96
|
+
scroll event), otherwise the line is transparent — the room stays, so
|
|
97
|
+
nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
|
|
98
|
+
parts; Firefox gets its thin bar in the same ink. */
|
|
99
99
|
.bleed {
|
|
100
100
|
margin-inline: calc(-1 * var(--card-pad, 0px));
|
|
101
101
|
padding-inline: var(--card-pad, 0px);
|
|
102
|
+
padding-block-end: var(--sp-2);
|
|
102
103
|
scroll-padding-inline: var(--card-pad, 0px);
|
|
104
|
+
flex-wrap: nowrap;
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
}
|
|
107
|
+
.bleed::-webkit-scrollbar { height: var(--scroll-w); }
|
|
108
|
+
.bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0px); }
|
|
109
|
+
.bleed::-webkit-scrollbar-thumb { background: transparent; }
|
|
110
|
+
.bleed:is(:hover, .is-scrolling)::-webkit-scrollbar-thumb { background: currentColor; }
|
|
111
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
112
|
+
.bleed { scrollbar-width: thin; scrollbar-color: transparent transparent; }
|
|
113
|
+
.bleed:is(:hover, .is-scrolling) { scrollbar-color: currentColor transparent; }
|
|
103
114
|
}
|
|
104
115
|
/* a bleeding child sits over the container's rounded corner, so a strip that
|
|
105
116
|
is genuinely first or last needs the corner clipped for it */
|
package/css/choice.css
CHANGED
|
@@ -85,12 +85,10 @@
|
|
|
85
85
|
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
86
86
|
}
|
|
87
87
|
.chip--toggle:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
88
|
+
/* checked: the same plus turns 45° into an x — one mask, one rotate, no swap (Lh 2026-09-16) */
|
|
89
|
+
.chip--toggle:has(:checked)::after { rotate: 45deg; }
|
|
90
|
+
@media (prefers-reduced-motion: no-preference) { .chip--toggle::after { transition: rotate var(--motion-slide); } }
|
|
92
91
|
.chip--toggle:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
93
|
-
.chip--toggle:has(:checked) { font-style: italic; } /* the interest-picker flourish; drop if unwanted */
|
|
94
92
|
|
|
95
93
|
/* ---- chip grid: pick one slot / many options in a grid ----
|
|
96
94
|
<div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div> */
|
package/css/journey.css
CHANGED
|
@@ -5,30 +5,24 @@
|
|
|
5
5
|
============================================================ */
|
|
6
6
|
|
|
7
7
|
/* ---- route: endpoint · dotted connector (icon in middle) · endpoint ---- */
|
|
8
|
-
.route { display:
|
|
8
|
+
.route { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: var(--sp-1); } /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
|
|
9
|
+
/* the two endpoints get EQUAL columns whatever their words measure, so the via
|
|
10
|
+
(the icon alone) sits at the card's centre; each dotted run lives on ITS
|
|
11
|
+
endpoint's code row and fills that side's leftover — with short names the
|
|
12
|
+
runs are long and equal, a long place name wraps in its half instead of
|
|
13
|
+
pushing the plane (Lh 2026-09-16) */
|
|
9
14
|
|
|
10
15
|
.route__point { display: grid; gap: var(--sp-0); min-width: 0; }
|
|
11
16
|
.route__point--end { text-align: right; }
|
|
12
17
|
|
|
13
|
-
.route__code { font: var(--type-h2); letter-spacing: var(--track-h2); }
|
|
18
|
+
.route__code { font: var(--type-h2); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--sp-3); }
|
|
19
|
+
.route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
|
|
20
|
+
.route__point--end .route__code::after { content: none; }
|
|
14
21
|
.route__time { font: var(--type-body); font-variant-numeric: tabular-nums; }
|
|
15
22
|
.route__place { font: var(--type-caption); color: var(--card-muted); }
|
|
16
23
|
.card--3 .route__place { color: inherit; opacity: 0.7; }
|
|
17
24
|
|
|
18
|
-
.route__via {
|
|
19
|
-
flex: 1;
|
|
20
|
-
display: flex;
|
|
21
|
-
align-items: center;
|
|
22
|
-
gap: var(--sp-1);
|
|
23
|
-
opacity: 0.65;
|
|
24
|
-
font-size: var(--fs-body);
|
|
25
|
-
}
|
|
26
|
-
.route__via::before,
|
|
27
|
-
.route__via::after {
|
|
28
|
-
content: "";
|
|
29
|
-
flex: 1;
|
|
30
|
-
border-top: var(--rule-w) dotted currentColor;
|
|
31
|
-
}
|
|
25
|
+
.route__via { display: flex; align-items: center; opacity: 0.65; font-size: var(--fs-body); }
|
|
32
26
|
|
|
33
27
|
/* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
|
|
34
28
|
.tile-badge {
|
package/css/layover.css
CHANGED
|
@@ -55,14 +55,22 @@
|
|
|
55
55
|
<button class="chip" popovertarget="when">Now ▾</button>
|
|
56
56
|
<div class="menu" id="when" popover><button class="menu__item"><svg class="icon"/>Now</button>…</div>
|
|
57
57
|
Native popover: light dismiss, Esc, top layer. Anchored to the invoker where
|
|
58
|
-
anchor positioning exists; the fallback is the browser's centred placement.
|
|
58
|
+
anchor positioning exists; the fallback is the browser's centred placement.
|
|
59
|
+
Two kinds (Lh 2026-09-16): a VALUE menu (the dropdown's — the picked item
|
|
60
|
+
stays current) and an ACTIONS menu, .menu--actions — things to do to the
|
|
61
|
+
element the button belongs to (a card, a sheet), opened by an .icon-btn:
|
|
62
|
+
<button class="icon-btn icon-btn--sm" popovertarget="m1" aria-label="More">…</button>
|
|
63
|
+
<div class="menu menu--actions menu--end" id="m1" popover><button class="menu__item"><svg class="icon"/>Edit</button>…</div>
|
|
64
|
+
No item stays current there (cardds.js skips the mark). .menu--end = the
|
|
65
|
+
trigger sits at the far right, so the panel grows leftwards. */
|
|
59
66
|
.menu {
|
|
60
67
|
display: none; /* explicit closed state — never display on the base rule */
|
|
61
68
|
margin: 0;
|
|
62
69
|
inset: auto;
|
|
63
70
|
border: var(--border-w) solid var(--action-border);
|
|
64
71
|
border-radius: var(--r-card);
|
|
65
|
-
|
|
72
|
+
--menu-pad: var(--sp-2); /* published: a row's radius is concentric with the panel's */
|
|
73
|
+
padding: var(--menu-pad);
|
|
66
74
|
min-width: 12rem;
|
|
67
75
|
background: var(--action-bg);
|
|
68
76
|
color: var(--action-ink);
|
|
@@ -78,7 +86,9 @@
|
|
|
78
86
|
width: 100%;
|
|
79
87
|
padding: 0 var(--sp-3);
|
|
80
88
|
border: 0;
|
|
81
|
-
|
|
89
|
+
/* concentric with the panel: the card radius minus the panel's padding, so the
|
|
90
|
+
highlight's curve follows the outer corner (Lh 2026-09-16) */
|
|
91
|
+
border-radius: calc(var(--r-card) - var(--menu-pad, var(--sp-2)));
|
|
82
92
|
background: transparent;
|
|
83
93
|
color: inherit;
|
|
84
94
|
font: var(--type-title);
|
|
@@ -94,6 +104,7 @@
|
|
|
94
104
|
min-width: max(12rem, anchor-size(width)); /* at least as wide as its trigger */
|
|
95
105
|
position-try-fallbacks: flip-block, flip-inline;
|
|
96
106
|
}
|
|
107
|
+
.menu--end[popover] { position-area: block-end span-inline-start; }
|
|
97
108
|
}
|
|
98
109
|
@media (prefers-reduced-motion: no-preference) {
|
|
99
110
|
.menu[popover] {
|
package/css/lists.css
CHANGED
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
display: flex;
|
|
13
13
|
align-items: center;
|
|
14
14
|
gap: var(--sp-3);
|
|
15
|
-
min-height: var(--tap);
|
|
16
|
-
padding-block: var(--sp-2);
|
|
15
|
+
min-height: var(--tap); /* the tap target's room — the row's only vertical size */
|
|
17
16
|
min-width: 0;
|
|
18
17
|
color: inherit;
|
|
19
18
|
text-decoration: none; /* .row works as <a> too — it is a tap target */
|
|
20
19
|
}
|
|
20
|
+
/* no padding on a row (Lh 2026-09-16): padding is for room, not for spreading.
|
|
21
|
+
Space BETWEEN rows is .rows' gap; a lone row as card content takes the card's
|
|
22
|
+
gap. (.row--flush, which zeroed the old padding, is gone — every row is flush.) */
|
|
21
23
|
.row__lead {
|
|
22
24
|
width: var(--tap);
|
|
23
25
|
height: var(--tap);
|
|
@@ -28,13 +30,12 @@
|
|
|
28
30
|
place-items: center;
|
|
29
31
|
}
|
|
30
32
|
.row__body { flex: 1; min-width: 0; display: grid; gap: var(--sp-0); }
|
|
31
|
-
.row__title { font: var(--type-title); overflow:
|
|
33
|
+
.row__title { font: var(--type-title); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
|
|
32
34
|
.row__meta { font: var(--type-caption); color: var(--card-muted); display: flex; align-items: center; gap: var(--sp-1); }
|
|
33
35
|
.card--3 .row__meta { color: inherit; opacity: 0.7; }
|
|
34
36
|
.row__end { margin-left: auto; text-align: right; display: grid; gap: var(--sp-0); justify-items: end; flex: none; }
|
|
35
|
-
.row--lg { min-height: var(--fab);
|
|
37
|
+
.row--lg { min-height: var(--fab); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
|
|
36
38
|
.row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
|
|
37
|
-
.row--flush { padding-block: 0; } /* a single row as card content: the card's gap is the spacing */
|
|
38
39
|
/* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
|
|
39
40
|
the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
|
|
40
41
|
pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
|
|
@@ -55,8 +56,17 @@
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/* rows: a list of rows separated by hairlines */
|
|
58
|
-
.rows { display: grid; }
|
|
59
|
-
.rows
|
|
59
|
+
.rows { display: grid; --rows-gap: var(--sp-4); row-gap: var(--rows-gap); }
|
|
60
|
+
.rows:has(> .row--lg) { --rows-gap: var(--sp-6); }
|
|
61
|
+
/* the hairline between rows sits in the middle of the gap — drawn, not spaced:
|
|
62
|
+
no padding, no spacer element */
|
|
63
|
+
.rows > .row + .row { position: relative; }
|
|
64
|
+
.rows > .row + .row::before {
|
|
65
|
+
content: "";
|
|
66
|
+
position: absolute;
|
|
67
|
+
inset: calc(-1 * var(--rows-gap) / 2) 0 auto;
|
|
68
|
+
border-top: var(--border-w) solid var(--hairline);
|
|
69
|
+
}
|
|
60
70
|
|
|
61
71
|
/* ---- kv: label left, value right ----
|
|
62
72
|
<dl class="kv"><div class="kv__row"><dt class="kv__key">Pay with</dt><dd class="kv__val">GHO</dd></div>…</dl> */
|
package/css/sheet.css
CHANGED
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
/* the head — a title row at least --tap tall, either kind, so the peek strip
|
|
141
141
|
(--sheet-peek) is the same for every sheet:
|
|
142
142
|
- in a step it is a plain card head (card.css): the heading left, the
|
|
143
|
-
contextual thing at the far right
|
|
143
|
+
contextual thing at the far right
|
|
144
144
|
- .sheet__head is the stage sheet's three-column head: a round button,
|
|
145
145
|
the centred title and sub, a round button
|
|
146
146
|
- a label .row with its .check / .toggle may be the head when the sheet's
|
package/css/stack.css
CHANGED
|
@@ -94,17 +94,13 @@
|
|
|
94
94
|
min-height: var(--tap);
|
|
95
95
|
}
|
|
96
96
|
/* NOT a skeleton head: a peek is one line at its own size, and the display
|
|
97
|
-
stack deliberately runs a second line under the next card.
|
|
98
|
-
the whole skeleton-head treatment — budget, squeeze and clamp. cardds.js
|
|
99
|
-
skips the same cards. */
|
|
97
|
+
stack deliberately runs a second line under the next card. */
|
|
100
98
|
.sheet-stack > .card > .card__head :is(h1, h2, h3) {
|
|
101
99
|
font-size: var(--fs-h1);
|
|
102
100
|
line-height: 1;
|
|
103
101
|
letter-spacing: -0.01em;
|
|
104
102
|
margin-right: auto; /* pins the icon cluster right */
|
|
105
103
|
display: block;
|
|
106
|
-
-webkit-line-clamp: none;
|
|
107
|
-
overflow: visible;
|
|
108
104
|
overflow-wrap: normal;
|
|
109
105
|
}
|
|
110
106
|
|
package/css/tokens.css
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
/* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
|
|
70
70
|
--border-w: 1.5px;
|
|
71
71
|
--rule-w: 2px; /* dotted connectors, picker rules */
|
|
72
|
+
--scroll-w: 1px; /* the scrollbar of a .bleed track: a hairline of ink */
|
|
72
73
|
--focus: 2px solid currentColor; /* :focus-visible ring */
|
|
73
74
|
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.06), 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
|
|
74
75
|
--shadow-float: 0 0.25rem 0.75rem rgb(0 0 0 / 0.14);
|
package/dist/actions/Chip.d.ts
CHANGED
|
@@ -317,7 +317,7 @@ export interface ChipPickProps extends Omit<ComponentPropsWithoutRef<'label'>, '
|
|
|
317
317
|
/** ChipPick — a full-width pick target in a ChipGrid (9:00 · 9:30 · …); the picked one fills with accent. */
|
|
318
318
|
export declare function ChipPick({ type, name, value, checked, defaultChecked, onChange, className, children, ...rest }: ChipPickProps): import("react").JSX.Element;
|
|
319
319
|
export interface ChipRowProps extends ComponentPropsWithoutRef<'div'> {
|
|
320
|
-
/** run the row to the card's edges as a scrolling strip */
|
|
320
|
+
/** run the row to the card's edges as a one-line scrolling strip (`.bleed`: nowrap, overflow, the hairline ink scrollbar) */
|
|
321
321
|
bleed?: boolean;
|
|
322
322
|
}
|
|
323
323
|
/** ChipRow — chips on a wrapping line (or a bleeding strip). */
|
|
@@ -19,9 +19,13 @@ export declare function Dropdown({ id, label, end, className, children, ...rest
|
|
|
19
19
|
export interface MenuProps extends Omit<ComponentPropsWithoutRef<'div'>, 'popover'> {
|
|
20
20
|
/** render as a popover panel (needs an id and an invoker with popoverTarget); omit for an inline menu card */
|
|
21
21
|
popover?: boolean;
|
|
22
|
+
/** an ACTIONS menu — things to do, not a value to hold: no item stays current */
|
|
23
|
+
actions?: boolean;
|
|
24
|
+
/** the trigger sits at the far right: the panel grows leftwards */
|
|
25
|
+
end?: boolean;
|
|
22
26
|
}
|
|
23
|
-
/** Menu — a card of MenuItems: a popover anchored to its button, or inline rows of icon + label. */
|
|
24
|
-
export declare function Menu({ popover, className, ...rest }: MenuProps): import("react").JSX.Element;
|
|
27
|
+
/** Menu — a card of MenuItems: a popover anchored to its button, or inline rows of icon + label. `actions` for a to-do list (IconBtn's), plain for a value (Dropdown's). */
|
|
28
|
+
export declare function Menu({ popover, actions, end, className, ...rest }: MenuProps): import("react").JSX.Element;
|
|
25
29
|
export interface MenuItemProps extends ComponentPropsWithoutRef<'button'> {
|
|
26
30
|
/** the value cardds.js reports on change */
|
|
27
31
|
value?: string;
|
package/dist/actions/Dropdown.js
CHANGED
|
@@ -9,9 +9,9 @@ import { Icon } from '../type/Icon';
|
|
|
9
9
|
export function Dropdown({ id, label, end, className, children, ...rest }) {
|
|
10
10
|
return (_jsxs("div", { className: cx('dropdown', end && 'dropdown--end', className), ...rest, children: [_jsxs("button", { type: "button", className: "dropdown__btn", popoverTarget: id, children: [_jsx("span", { className: "dropdown__label", children: label }), _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx("div", { className: "menu", id: id, popover: "auto", children: children })] }));
|
|
11
11
|
}
|
|
12
|
-
/** Menu — a card of MenuItems: a popover anchored to its button, or inline rows of icon + label. */
|
|
13
|
-
export function Menu({ popover, className, ...rest }) {
|
|
14
|
-
return _jsx("div", { className: cx('menu', className), popover: popover ? 'auto' : undefined, ...rest });
|
|
12
|
+
/** Menu — a card of MenuItems: a popover anchored to its button, or inline rows of icon + label. `actions` for a to-do list (IconBtn's), plain for a value (Dropdown's). */
|
|
13
|
+
export function Menu({ popover, actions, end, className, ...rest }) {
|
|
14
|
+
return _jsx("div", { className: cx('menu', actions && 'menu--actions', end && 'menu--end', className), popover: popover ? 'auto' : undefined, ...rest });
|
|
15
15
|
}
|
|
16
16
|
/** MenuItem — one row in a Menu (48px tall): optional icon, label; the current one is filled. */
|
|
17
17
|
export function MenuItem({ value, current, icon, className, children, ...rest }) {
|