@usableapp/cardds 0.2.2 → 0.4.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/README.md +35 -107
- package/cardds.js +6 -449
- package/css/actions.css +41 -35
- package/css/base.css +9 -9
- package/css/card.css +26 -25
- package/css/choice.css +50 -50
- package/css/forms.css +18 -18
- package/css/journey.css +16 -16
- package/css/layover.css +45 -45
- package/css/lists.css +43 -43
- package/css/media.css +33 -33
- package/css/numbers.css +25 -25
- package/css/people.css +11 -11
- package/css/sheet.css +29 -29
- package/css/step.css +6 -6
- package/css/theme.template.css +6 -3
- package/css/tokens.css +2 -0
- package/dist/actions/Btn.d.ts +109 -108
- package/dist/actions/BtnRow.d.ts +2 -2
- package/dist/actions/Chip.d.ts +102 -101
- package/dist/actions/Dot.d.ts +2 -2
- package/dist/actions/Dropdown.d.ts +36 -19
- package/dist/actions/Dropdown.js +41 -11
- package/dist/actions/Fab.d.ts +2 -2
- package/dist/actions/IconBtn.d.ts +298 -10
- package/dist/actions/IconBtn.js +6 -8
- package/dist/actions/Link.d.ts +105 -104
- package/dist/actions/Segment.d.ts +3 -3
- package/dist/cardds.css +354 -345
- package/dist/cards/Card.d.ts +99 -98
- package/dist/cards/CardFoot.d.ts +2 -2
- package/dist/cards/CardHead.d.ts +2 -2
- package/dist/cards/CardParts.d.ts +7 -7
- package/dist/cards/Placeholder.d.ts +2 -2
- package/dist/cards/Stat.d.ts +7 -5
- package/dist/cards/Stat.js +2 -2
- package/dist/choice/Calendar.d.ts +17 -6
- package/dist/choice/Calendar.js +9 -3
- package/dist/choice/Check.d.ts +3 -3
- package/dist/choice/DayStrip.d.ts +3 -3
- package/dist/choice/Mood.d.ts +2 -2
- package/dist/choice/Slider.d.ts +3 -3
- package/dist/choice/Slider.js +12 -8
- package/dist/forms/AddRow.d.ts +2 -2
- package/dist/forms/Composer.d.ts +2 -2
- package/dist/forms/Field.d.ts +2 -2
- package/dist/forms/FileBtn.d.ts +2 -2
- package/dist/forms/Pin.d.ts +10 -7
- package/dist/forms/Pin.js +47 -18
- package/dist/journey/NoteRow.d.ts +2 -2
- package/dist/journey/Route.d.ts +2 -2
- package/dist/journey/TileBadge.d.ts +2 -2
- package/dist/layover/Banner.d.ts +2 -2
- package/dist/layover/Callout.d.ts +2 -2
- package/dist/layover/Deck.d.ts +3 -3
- package/dist/lists/Bubble.d.ts +3 -3
- package/dist/lists/Kv.d.ts +5 -5
- package/dist/lists/Legend.d.ts +3 -3
- package/dist/lists/Row.d.ts +5 -4
- package/dist/lists/Timeline.d.ts +3 -3
- package/dist/media/MapArea.d.ts +2 -2
- package/dist/media/Mosaic.d.ts +3 -3
- package/dist/media/Postcard.d.ts +9 -5
- package/dist/media/Postcard.js +8 -3
- package/dist/media/Quote.d.ts +2 -2
- package/dist/media/Tile.d.ts +5 -4
- package/dist/numbers/Badge.d.ts +2 -2
- package/dist/numbers/Band.d.ts +3 -3
- package/dist/numbers/Bars.d.ts +3 -3
- package/dist/numbers/DotGrid.d.ts +2 -2
- package/dist/numbers/Picker.d.ts +20 -8
- package/dist/numbers/Picker.js +51 -13
- package/dist/numbers/Ring.d.ts +2 -2
- package/dist/numbers/Track.d.ts +3 -3
- package/dist/people/Avatar.d.ts +100 -99
- package/dist/people/AvatarPick.d.ts +3 -3
- package/dist/scaffold/ActionBar.d.ts +3 -3
- package/dist/scaffold/AppBar.d.ts +2 -2
- package/dist/scaffold/BaseContent.d.ts +7 -7
- package/dist/scaffold/BaseContent.js +4 -4
- package/dist/scaffold/Centre.d.ts +3 -3
- package/dist/scaffold/EmptyState.d.ts +2 -2
- package/dist/scaffold/FilterRow.d.ts +2 -2
- package/dist/scaffold/FloatBar.d.ts +105 -104
- package/dist/scaffold/PagerAt.d.ts +2 -2
- package/dist/scaffold/Screen.d.ts +100 -99
- package/dist/scaffold/TopBar.d.ts +2 -2
- package/dist/sheets/CardBack.d.ts +2 -2
- package/dist/sheets/Drawer.d.ts +19 -8
- package/dist/sheets/Drawer.js +29 -5
- package/dist/sheets/Sheet.d.ts +29 -16
- package/dist/sheets/Sheet.js +157 -11
- package/dist/sheets/SheetBody.d.ts +2 -2
- package/dist/sheets/SheetHead.d.ts +2 -2
- package/dist/sheets/SheetStack.d.ts +15 -3
- package/dist/sheets/SheetStack.js +74 -2
- package/dist/sheets/SheetStage.d.ts +2 -2
- package/dist/sheets/Step.d.ts +2 -2
- package/dist/type/Icon.d.ts +22 -7
- package/dist/type/Icon.js +27 -4
- package/dist/type/Text.d.ts +7 -1
- package/dist/type/Text.js +2 -2
- package/package.json +3 -2
package/cardds.js
CHANGED
|
@@ -1,337 +1,10 @@
|
|
|
1
|
-
/* cardds.js —
|
|
2
|
-
Dropdown: clicking a .menu__item marks it current, copies its label into the
|
|
3
|
-
.dropdown__label of the enclosing .dropdown, closes the popover and fires
|
|
4
|
-
"change" (detail.value) on the .dropdown. */
|
|
5
|
-
document.addEventListener('click', e => {
|
|
6
|
-
const item = e.target.closest('.menu__item');
|
|
7
|
-
if (!item) return;
|
|
8
|
-
const menu = item.closest('.menu');
|
|
9
|
-
const dd = menu && menu.closest('.dropdown');
|
|
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
|
-
}
|
|
14
|
-
if (dd) {
|
|
15
|
-
const label = dd.querySelector('.dropdown__label');
|
|
16
|
-
const value = item.dataset.value ?? item.textContent.trim();
|
|
17
|
-
if (label) label.textContent = item.dataset.label ?? item.textContent.trim();
|
|
18
|
-
dd.dataset.value = value;
|
|
19
|
-
dd.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value } }));
|
|
20
|
-
}
|
|
21
|
-
if (menu.matches(':popover-open')) menu.hidePopover();
|
|
22
|
-
});
|
|
1
|
+
/* cardds.js — the little that is not (yet) a component's own code. Nothing here ties one component to another. */
|
|
23
2
|
|
|
24
|
-
/*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/* Postcard (.postcard, media.css): a tap on either face turns the card over.
|
|
30
|
-
Delegated; the faces are buttons, so Enter and Space arrive as clicks. */
|
|
31
|
-
document.addEventListener('click', e => {
|
|
32
|
-
const face = e.target instanceof Element && e.target.closest('.postcard__face');
|
|
33
|
-
if (!face) return;
|
|
34
|
-
const card = face.closest('.postcard');
|
|
35
|
-
if (!card) return;
|
|
36
|
-
const flipped = card.classList.toggle('is-flipped');
|
|
37
|
-
card.dispatchEvent(new CustomEvent('cardds:flip', { bubbles: true, detail: { flipped } }));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
/* Slider (.slider, choice.css): the knob's place along the rail, published on
|
|
41
|
-
the .slider as --slider-at (0..1) so the .slider__glyph can ride the knob.
|
|
42
|
-
Delegated, and run once for every slider already on the page. */
|
|
43
|
-
function carddsSliderAt(range) {
|
|
44
|
-
const min = Number(range.min) || 0, max = Number(range.max) || 100;
|
|
45
|
-
const at = max > min ? (Number(range.value) - min) / (max - min) : 0;
|
|
46
|
-
range.closest('.slider')?.style.setProperty('--slider-at', String(Math.min(1, Math.max(0, at))));
|
|
47
|
-
}
|
|
48
|
-
document.addEventListener('input', e => {
|
|
49
|
-
if (e.target instanceof HTMLInputElement && e.target.classList.contains('slider__range')) carddsSliderAt(e.target);
|
|
50
|
-
});
|
|
51
|
-
document.addEventListener('input', e => {
|
|
52
|
-
const cell = e.target;
|
|
53
|
-
if (!(cell instanceof HTMLInputElement) || !cell.classList.contains('pin__cell')) return;
|
|
54
|
-
const cells = [...cell.closest('.pin').querySelectorAll('.pin__cell')];
|
|
55
|
-
const i = cells.indexOf(cell);
|
|
56
|
-
const digits = cell.value.replace(/\D/g, '');
|
|
57
|
-
// a paste (or a typed run) spills over the cells that follow
|
|
58
|
-
digits.split('').forEach((d, k) => { if (cells[i + k]) cells[i + k].value = d; });
|
|
59
|
-
if (!digits) cell.value = '';
|
|
60
|
-
else {
|
|
61
|
-
const next = cells[Math.min(i + digits.length, cells.length - 1)];
|
|
62
|
-
if (next !== cell) next.focus();
|
|
63
|
-
else if (cell.value !== digits.slice(-1)) cell.value = digits.slice(-1);
|
|
64
|
-
}
|
|
65
|
-
carddsPinPublish(cells);
|
|
66
|
-
});
|
|
67
|
-
/* "cardds:pin" (bubbles, detail.code = the digits so far, detail.complete)
|
|
68
|
-
after every change — the one thing a host listens to; the React adapter's
|
|
69
|
-
Pin turns it into onChange */
|
|
70
|
-
function carddsPinPublish(cells) {
|
|
71
|
-
const code = cells.map(c => c.value).join('');
|
|
72
|
-
cells[0].closest('.pin').dispatchEvent(new CustomEvent('cardds:pin', { bubbles: true, detail: { code, complete: code.length === cells.length } }));
|
|
73
|
-
}
|
|
74
|
-
/* a cell holds one digit; typing into a full one replaces it — select the
|
|
75
|
-
digit before the insert lands (a paste of a whole code still spills over
|
|
76
|
-
the cells that follow, above). No maxlength on the cells: the browser
|
|
77
|
-
would truncate a pasted code before this ever saw it. */
|
|
78
|
-
document.addEventListener('beforeinput', e => {
|
|
79
|
-
const cell = e.target;
|
|
80
|
-
if (!(cell instanceof HTMLInputElement) || !cell.classList.contains('pin__cell')) return;
|
|
81
|
-
if (e.inputType === 'insertText' && cell.value && cell.selectionStart === cell.selectionEnd) cell.select();
|
|
82
|
-
});
|
|
83
|
-
document.addEventListener('focusin', e => {
|
|
84
|
-
const cell = e.target;
|
|
85
|
-
if (cell instanceof HTMLInputElement && cell.classList.contains('pin__cell') && cell.value) cell.select();
|
|
86
|
-
});
|
|
87
|
-
document.addEventListener('keydown', e => {
|
|
88
|
-
const cell = e.target;
|
|
89
|
-
if (!(cell instanceof HTMLInputElement) || !cell.classList.contains('pin__cell')) return;
|
|
90
|
-
const cells = [...cell.closest('.pin').querySelectorAll('.pin__cell')];
|
|
91
|
-
const i = cells.indexOf(cell);
|
|
92
|
-
if (e.key === 'Backspace' && !cell.value && i > 0) {
|
|
93
|
-
e.preventDefault();
|
|
94
|
-
cells[i - 1].value = '';
|
|
95
|
-
cells[i - 1].focus();
|
|
96
|
-
carddsPinPublish(cells);
|
|
97
|
-
} else if (e.key === 'ArrowLeft' && i > 0) { e.preventDefault(); cells[i - 1].focus(); }
|
|
98
|
-
else if (e.key === 'ArrowRight' && i < cells.length - 1) { e.preventDefault(); cells[i + 1].focus(); }
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/* Sheet stack (.sheet-stack--tap, stack.css): tap a peeked sheet to open it, its back
|
|
102
|
-
button returns it. Sheets are rigid material — they move in, move out, or
|
|
103
|
-
get covered, never morph — so the only animated property is translate;
|
|
104
|
-
see the --tap block in stack.css for the whole model.
|
|
105
|
-
|
|
106
|
-
This only ever toggles classes. Every card's resting slot, offset and
|
|
107
|
-
paint order is declared in CSS, so a stack still renders correctly with
|
|
108
|
-
this file absent — it just won't open. Works for any number of stacks on
|
|
109
|
-
a page, with no ids or per-page wiring. */
|
|
110
|
-
const CARDDS_STACK_PHASE = 380; /* ms before the before-pack un-tucks on
|
|
111
|
-
close, so the after-pack is clear first */
|
|
112
|
-
|
|
113
|
-
/* the stack's sheets, in slot order. Normally the stack's direct children;
|
|
114
|
-
a host that wraps each child in one box of its own (a design tool's
|
|
115
|
-
per-component frame, a router's element) is tolerated: the first .card
|
|
116
|
-
inside each child then counts. Never deeper — a card inside a sheet's
|
|
117
|
-
content is content, not a sheet. */
|
|
118
|
-
function carddsStackCards(stack) {
|
|
119
|
-
return [...stack.children]
|
|
120
|
-
.filter(c => !c.classList.contains('sheet-stack__base')) // the base is what shows behind the sheets, never a sheet (a Card in it is content)
|
|
121
|
-
.map(c => c.classList.contains('card') ? c : c.querySelector(':scope > .card')).filter(Boolean);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
document.addEventListener('click', e => {
|
|
125
|
-
const stack = e.target.closest('.sheet-stack--tap');
|
|
126
|
-
if (!stack) return;
|
|
127
|
-
const cards = carddsStackCards(stack);
|
|
128
|
-
// the sheet this tap landed in: the member the target sits in, not the
|
|
129
|
-
// nearest .card (a content card inside an open sheet must not count)
|
|
130
|
-
const card = cards.find(c => c.contains(e.target));
|
|
131
|
-
if (e.target.closest('.card__back')) { carddsCloseStack(stack, card); return; }
|
|
132
|
-
if (card && !cards.some(c => c.classList.contains('is-open'))) {
|
|
133
|
-
carddsOpenStack(stack, card);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
function carddsOpenStack(stack, card) {
|
|
138
|
-
const cards = carddsStackCards(stack);
|
|
139
|
-
const idx = cards.indexOf(card);
|
|
140
|
-
cards.forEach((c, i) => {
|
|
141
|
-
c.classList.remove('is-before', 'is-after');
|
|
142
|
-
c.style.setProperty('--stagger', 0); // only is-before overrides this
|
|
143
|
-
if (i < idx) {
|
|
144
|
-
// already behind the tapped card — tuck under the next card's strip,
|
|
145
|
-
// nearest one first, and let the opening card cover them
|
|
146
|
-
c.style.setProperty('--stagger', idx - 1 - i);
|
|
147
|
-
c.classList.add('is-before');
|
|
148
|
-
} else if (i > idx) {
|
|
149
|
-
c.classList.add('is-after'); // in front of it, so these must leave
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
card.classList.add('is-open');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/* close: the sheet whose back button was tapped, read from the tap itself
|
|
156
|
-
(a page with several stacks, or a re-render mid-animation, can leave a
|
|
157
|
-
stale .is-open elsewhere); without one, the stack's open sheet */
|
|
158
|
-
function carddsCloseStack(stack, card) {
|
|
159
|
-
const cards = carddsStackCards(stack);
|
|
160
|
-
card = card && cards.includes(card) ? card : cards.find(c => c.classList.contains('is-open'));
|
|
161
|
-
if (!card) return;
|
|
162
|
-
const idx = cards.indexOf(card);
|
|
163
|
-
// phase 1: the after-pack moves back in from below while the opened card
|
|
164
|
-
// drops into its own slot, passing down over the still-covered before-pack
|
|
165
|
-
cards.filter((c, i) => i > idx).forEach(c => c.classList.remove('is-after'));
|
|
166
|
-
card.classList.remove('is-open'); // back button starts its own crop-out now
|
|
167
|
-
// phase 2: then the before-pack slides back up out from behind it, same
|
|
168
|
-
// order it tucked away — nearest to the card first
|
|
169
|
-
const before = cards.filter((c, i) => i < idx);
|
|
170
|
-
setTimeout(() => before.forEach(c => c.classList.remove('is-before')), CARDDS_STACK_PHASE);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/* ---- sheet (.sheet, sheet.css): peek / half / full ----
|
|
174
|
-
A sheet is rigid material, like a sheet in the stack (stack.css) — it never changes size, a
|
|
175
|
-
state is only how far it slid — so this, like the stack code, only ever
|
|
176
|
-
toggles classes and two custom properties. The CSS renders every state
|
|
177
|
-
without it; this adds the handle's click and drag, the focus raise, the
|
|
178
|
-
keyboard room, and one event. Delegated: any sheet on the page, no ids.
|
|
179
|
-
|
|
180
|
-
- click / Enter / Space on .sheet__handle TOGGLES the sheet between its
|
|
181
|
-
own state and peek (Lh 2026-09-16 — never a step-by-step climb). "Its
|
|
182
|
-
own state" is the last non-peek state it rested in (authored, dragged
|
|
183
|
-
to, or set), kept in data-sheet-home
|
|
184
|
-
- data-sheet-states (default "peek half 3q full") lists the states a
|
|
185
|
-
drag snaps to
|
|
186
|
-
- drag on the handle writes the offset to --_drag (the CSS adds it to the
|
|
187
|
-
state's translate) and on release snaps to the nearest state; a move
|
|
188
|
-
shorter than --sp-2 is a tap. The snap is the one place the sheet's
|
|
189
|
-
geometry is read, once per release: its height, and the two state
|
|
190
|
-
tokens — registered in tokens.css so they read back resolved
|
|
191
|
-
- focus inside a peeked sheet raises it to half (or full, if the sheet has
|
|
192
|
-
no half) and reveals the control — the sheet's own scroll-padding
|
|
193
|
-
already knows how much of it slid under the box's edge
|
|
194
|
-
- the visual viewport's height is published on :root as --kb, the height
|
|
195
|
-
the on-screen keyboard covers, so the bar (actions.css) and the step
|
|
196
|
-
(step.css) can stay above it on iOS
|
|
197
|
-
- "cardds:sheet" (bubbles, detail.state) after every state change
|
|
198
|
-
|
|
199
|
-
carddsSheetSet(sheet, state) is the one entry point; an app that sets
|
|
200
|
-
the state itself calls it rather than swapping classes, so listeners hear. */
|
|
201
|
-
const CARDDS_SHEET_STATES = ['peek', 'half', '3q', 'full'];
|
|
202
|
-
const CARDDS_SHEET_TAP_REM = 0.5; /* = --sp-2: a move shorter than this is a tap, not a drag */
|
|
203
|
-
|
|
204
|
-
function carddsSheetStates(sheet) {
|
|
205
|
-
const list = (sheet.dataset.sheetStates || '').split(/\s+/).filter(s => CARDDS_SHEET_STATES.includes(s));
|
|
206
|
-
return list.length ? list : CARDDS_SHEET_STATES;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function carddsSheetState(sheet) {
|
|
210
|
-
if (sheet.classList.contains('sheet--away')) return 'away'; // a called sheet not on the screen — a state to set, never one to snap or tap to
|
|
211
|
-
return CARDDS_SHEET_STATES.find(s => sheet.classList.contains('sheet--' + s)) ?? 'half'; // the CSS default
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function carddsSheetSet(sheet, state) {
|
|
215
|
-
if (state !== 'away' && !CARDDS_SHEET_STATES.includes(state)) return;
|
|
216
|
-
const was = carddsSheetState(sheet);
|
|
217
|
-
[...CARDDS_SHEET_STATES, 'away'].forEach(s => sheet.classList.toggle('sheet--' + s, s === state));
|
|
218
|
-
sheet.inert = state === 'away';
|
|
219
|
-
/* where a tap from peek returns to: the non-peek state it is going to, or the
|
|
220
|
-
one it is leaving (an authored state never passed through here before) */
|
|
221
|
-
const rests = s => s !== 'peek' && s !== 'away';
|
|
222
|
-
const home = rests(state) ? state : rests(was) ? was : null;
|
|
223
|
-
if (home) sheet.dataset.sheetHome = home;
|
|
224
|
-
if (was !== state) sheet.dispatchEvent(new CustomEvent('cardds:sheet', { bubbles: true, detail: { state } }));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/* the click that follows a drag on the same handle is not a tap — the drag
|
|
228
|
-
already chose the state. Cleared on the next frame, so a drag that ends
|
|
229
|
-
without a click (pointercancel) cannot eat a later, real one. */
|
|
230
|
-
let carddsSheetSkipClick = null;
|
|
231
|
-
|
|
232
|
-
document.addEventListener('click', e => {
|
|
233
|
-
const handle = e.target.closest('.sheet__handle');
|
|
234
|
-
if (!handle) return;
|
|
235
|
-
if (handle === carddsSheetSkipClick) { carddsSheetSkipClick = null; return; }
|
|
236
|
-
const sheet = handle.closest('.sheet');
|
|
237
|
-
if (!sheet) return;
|
|
238
|
-
carddsSheetSet(sheet, carddsSheetState(sheet) === 'peek' ? carddsSheetHome(sheet) : 'peek');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
/* the state a tap from peek returns to: the last non-peek state it rested in,
|
|
242
|
-
else the authored one, else the first non-peek state it snaps to */
|
|
243
|
-
function carddsSheetHome(sheet) {
|
|
244
|
-
const states = carddsSheetStates(sheet);
|
|
245
|
-
const home = sheet.dataset.sheetHome;
|
|
246
|
-
if (home && states.includes(home)) return home;
|
|
247
|
-
const now = carddsSheetState(sheet);
|
|
248
|
-
return now !== 'peek' ? now : states.find(s => s !== 'peek') ?? 'half';
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/* how far the sheet has slid, in px: its rendered top against its resting top
|
|
252
|
-
at full (the box's padding edge + its own offset). Read from geometry, not
|
|
253
|
-
from the computed translate — that keeps percentages and calcs as text. */
|
|
254
|
-
function carddsSheetY(sheet) {
|
|
255
|
-
const box = sheet.offsetParent ?? sheet.parentElement;
|
|
256
|
-
return sheet.getBoundingClientRect().top - (box.getBoundingClientRect().top + box.clientTop + sheet.offsetTop);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/* drag: where each state rests, in px from the sheet's top at full */
|
|
260
|
-
function carddsSheetStops(sheet) {
|
|
261
|
-
const cs = getComputedStyle(sheet);
|
|
262
|
-
const height = sheet.getBoundingClientRect().height;
|
|
263
|
-
return {
|
|
264
|
-
full: 0,
|
|
265
|
-
half: height * (parseFloat(cs.getPropertyValue('--sheet-half')) || 0), // a share of the height
|
|
266
|
-
'3q': height * (parseFloat(cs.getPropertyValue('--sheet-3q')) || 0),
|
|
267
|
-
peek: height - (parseFloat(cs.getPropertyValue('--sheet-peek')) || 0), // a length, resolved
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
let carddsSheetDrag = null;
|
|
272
|
-
|
|
273
|
-
document.addEventListener('pointerdown', e => {
|
|
274
|
-
const handle = e.target.closest('.sheet__handle');
|
|
275
|
-
if (!handle || !e.isPrimary || e.button !== 0) return;
|
|
276
|
-
const sheet = handle.closest('.sheet');
|
|
277
|
-
if (!sheet) return;
|
|
278
|
-
const stops = carddsSheetStops(sheet);
|
|
279
|
-
const states = carddsSheetStates(sheet);
|
|
280
|
-
const y = carddsSheetY(sheet); // where it is now — mid-slide too, so a grab never jumps
|
|
281
|
-
carddsSheetDrag = {
|
|
282
|
-
sheet, handle, states, stops,
|
|
283
|
-
startY: e.clientY,
|
|
284
|
-
y0: y,
|
|
285
|
-
y,
|
|
286
|
-
min: Math.min(...states.map(s => stops[s])),
|
|
287
|
-
max: Math.max(...states.map(s => stops[s])),
|
|
288
|
-
tap: CARDDS_SHEET_TAP_REM * parseFloat(getComputedStyle(document.documentElement).fontSize),
|
|
289
|
-
moved: false,
|
|
290
|
-
};
|
|
291
|
-
handle.setPointerCapture(e.pointerId);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
document.addEventListener('pointermove', e => {
|
|
295
|
-
const d = carddsSheetDrag;
|
|
296
|
-
if (!d || !e.isPrimary) return;
|
|
297
|
-
const dy = e.clientY - d.startY;
|
|
298
|
-
if (!d.moved) {
|
|
299
|
-
if (Math.abs(dy) < d.tap) return;
|
|
300
|
-
d.moved = true;
|
|
301
|
-
d.sheet.classList.add('is-dragging');
|
|
302
|
-
}
|
|
303
|
-
d.y = Math.min(Math.max(d.y0 + dy, d.min), d.max); // never above full, never below the lowest state
|
|
304
|
-
d.sheet.style.setProperty('--_drag', (d.y - d.y0) + 'px');
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
function carddsSheetRelease() {
|
|
308
|
-
const d = carddsSheetDrag;
|
|
309
|
-
if (!d) return;
|
|
310
|
-
carddsSheetDrag = null;
|
|
311
|
-
d.sheet.classList.remove('is-dragging');
|
|
312
|
-
if (!d.moved) return; // a tap: the click that follows cycles the state
|
|
313
|
-
d.sheet.style.removeProperty('--_drag');
|
|
314
|
-
const nearest = d.states.reduce((a, b) => Math.abs(d.stops[b] - d.y) < Math.abs(d.stops[a] - d.y) ? b : a);
|
|
315
|
-
carddsSheetSet(d.sheet, nearest); // same state: --_drag is gone, so it eases back on its own
|
|
316
|
-
carddsSheetSkipClick = d.handle;
|
|
317
|
-
requestAnimationFrame(() => { carddsSheetSkipClick = null; });
|
|
318
|
-
}
|
|
319
|
-
document.addEventListener('pointerup', carddsSheetRelease);
|
|
320
|
-
document.addEventListener('pointercancel', carddsSheetRelease);
|
|
321
|
-
|
|
322
|
-
/* focus in a peeked sheet: a control under the cut cannot be used, so raise
|
|
323
|
-
the sheet first — to its own state (the one a tap returns to) at least,
|
|
324
|
-
further if that one has no room for the control under the pinned head (a
|
|
325
|
-
small phone at a large text size). The browser's own reveal scrolled the body against the
|
|
326
|
-
peek strip before this fired; start over from the top and let the reveal,
|
|
327
|
-
with the raised state's scroll-padding, do the least. */
|
|
328
|
-
document.addEventListener('focusin', e => {
|
|
329
|
-
const sheet = e.target.closest?.('.sheet');
|
|
330
|
-
if (!sheet || !sheet.classList.contains('sheet--peek') || e.target.closest('.sheet__handle')) return;
|
|
331
|
-
const body = sheet.querySelector(':scope > .sheet__body');
|
|
332
|
-
if (body) body.scrollTop = 0;
|
|
333
|
-
carddsSheetReveal(e.target, carddsSheetHome(sheet));
|
|
334
|
-
});
|
|
3
|
+
/* ---- the sheet is a React component now (src/sheets/Sheet.tsx, 2026-09-21) ----
|
|
4
|
+
cardds gives components; the app owns their state and wires them to each other. The handle's tap and
|
|
5
|
+
drag, the focus raise and the keyboard reveal are the Sheet's own behaviour and live in it; what one
|
|
6
|
+
component does to another — a button calling a sheet, a bar standing down beside one — is the app's.
|
|
7
|
+
What stays here is the environment: --kb. */
|
|
335
8
|
|
|
336
9
|
/* --kb: the height of the viewport an on-screen keyboard covers. iOS keeps
|
|
337
10
|
the layout viewport as it was and shrinks only the visual one, so a fixed
|
|
@@ -344,51 +17,12 @@ if (window.visualViewport) {
|
|
|
344
17
|
const publish = () => {
|
|
345
18
|
const covered = vv.scale > 1.01 ? 0 : Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
|
|
346
19
|
document.documentElement.style.setProperty('--kb', covered + 'px');
|
|
347
|
-
/* the keyboard just moved the cut: a sheet's box shrank with the step (or
|
|
348
|
-
the viewport did), and the control being typed into may now sit under
|
|
349
|
-
it. Half was enough before the keyboard; it may not be now — raise the
|
|
350
|
-
sheet as far as its states go and reveal the control. This is the one
|
|
351
|
-
listener that reads layout, and it reads it after the change it made. */
|
|
352
|
-
carddsSheetReveal(document.activeElement);
|
|
353
20
|
};
|
|
354
21
|
vv.addEventListener('resize', publish);
|
|
355
22
|
vv.addEventListener('scroll', publish);
|
|
356
23
|
publish();
|
|
357
24
|
}
|
|
358
25
|
|
|
359
|
-
/* bring a control in a sheet into the visible part — the part of its body
|
|
360
|
-
between the pinned head and the cut (the box's bottom edge, or the keyboard
|
|
361
|
-
above it). The sheet goes to the lowest of its states, from the current one
|
|
362
|
-
(or `atLeast`) upward, whose visible body has room for the control; if none
|
|
363
|
-
has, to its highest; then the body scrolls the control into view. An app
|
|
364
|
-
that moves the cut itself (its own keyboard handling, a bar it shows late)
|
|
365
|
-
calls this with the control; the keyboard listener and the focus raise call
|
|
366
|
-
it. It reads layout — after the change that moved the cut — and reasons
|
|
367
|
-
from the resting offsets, so it is right mid-slide too. */
|
|
368
|
-
function carddsSheetReveal(el, atLeast) {
|
|
369
|
-
const sheet = el?.closest?.('.sheet');
|
|
370
|
-
if (!sheet || el.closest('.sheet__handle')) return;
|
|
371
|
-
el = el.closest('.field') ?? el; // the control's box, not the bare input inside a .field
|
|
372
|
-
const states = carddsSheetStates(sheet);
|
|
373
|
-
const from = Math.max(states.indexOf(carddsSheetState(sheet)), states.indexOf(atLeast), 0);
|
|
374
|
-
const stops = carddsSheetStops(sheet);
|
|
375
|
-
const sheetRect = sheet.getBoundingClientRect();
|
|
376
|
-
const restTop = sheetRect.top - carddsSheetY(sheet); // where the sheet's top sits at full
|
|
377
|
-
const bodyTop = (sheet.querySelector(':scope > .sheet__body') ?? sheet).getBoundingClientRect().top - sheetRect.top; // the scrollport, from the sheet's top
|
|
378
|
-
const box = sheet.parentElement.getBoundingClientRect(); // the box that clips it: .step or .sheet-stage
|
|
379
|
-
const vv = window.visualViewport;
|
|
380
|
-
const cut = vv ? Math.min(box.bottom, vv.offsetTop + vv.height) : box.bottom;
|
|
381
|
-
const foot = sheet.querySelector(':scope > .card__foot'); // the sheet's own button stands on the cut (sheet.css): the control has to clear it too
|
|
382
|
-
const button = foot?.firstElementChild; // the foot is a layer as tall as the sheet: what the control must clear is its strip
|
|
383
|
-
const need = (el.closest('.field') ?? el).getBoundingClientRect().height + (button ? box.bottom - button.getBoundingClientRect().top : 0); // from the button's top to the box's bottom edge, where it stands
|
|
384
|
-
let to = states[states.length - 1];
|
|
385
|
-
for (let i = from; i < states.length; i++) {
|
|
386
|
-
if (cut - (restTop + stops[states[i]] + bodyTop) >= need) { to = states[i]; break; }
|
|
387
|
-
}
|
|
388
|
-
carddsSheetSet(sheet, to);
|
|
389
|
-
el.scrollIntoView({ block: 'nearest' });
|
|
390
|
-
}
|
|
391
|
-
|
|
392
26
|
/* .bleed track: .is-scrolling while it scrolls (and a beat after), so its
|
|
393
27
|
hairline scrollbar shows only while hovered or moving (card.css). */
|
|
394
28
|
const carddsScrollTimers = new WeakMap();
|
|
@@ -399,80 +33,3 @@ document.addEventListener('scroll', e => {
|
|
|
399
33
|
clearTimeout(carddsScrollTimers.get(t));
|
|
400
34
|
carddsScrollTimers.set(t, setTimeout(() => t.classList.remove('is-scrolling'), 700));
|
|
401
35
|
}, true);
|
|
402
|
-
|
|
403
|
-
/* ---- Modal (layover.css, .sheet-stage--ask): a click on any button in the
|
|
404
|
-
drawer closes the modal by playing its open animations backwards —
|
|
405
|
-
Animation.reverse() on every animation in the card, the same keyframes, no
|
|
406
|
-
closing keyframes to keep in step — then fires "cardds:modal" on the stage
|
|
407
|
-
(bubbles, detail.answer = the button's data-answer) so the app removes the
|
|
408
|
-
stage. Reduced motion (no animations): the event fires at once. */
|
|
409
|
-
document.addEventListener('click', e => {
|
|
410
|
-
const btn = e.target.closest('.modal__drawer button');
|
|
411
|
-
if (!btn) return;
|
|
412
|
-
const stage = btn.closest('.sheet-stage--ask');
|
|
413
|
-
const modal = btn.closest('.modal');
|
|
414
|
-
if (!stage || !modal || stage.classList.contains('is-closing')) return;
|
|
415
|
-
stage.classList.add('is-closing');
|
|
416
|
-
const anims = modal.getAnimations({ subtree: true });
|
|
417
|
-
anims.forEach(a => a.reverse());
|
|
418
|
-
Promise.all(anims.map(a => a.finished)).then(() => {
|
|
419
|
-
stage.dispatchEvent(new CustomEvent('cardds:modal', { bubbles: true, detail: { answer: btn.dataset.answer ?? null } }));
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
/* the two calls an app makes are on window: a bundler loads this file as a module, where a top-level
|
|
424
|
-
function is not a global — and README has always said "call carddsSheetSet(sheet, state)". */
|
|
425
|
-
if (typeof window !== 'undefined') Object.assign(window, { carddsSheetSet, carddsSheetReveal });
|
|
426
|
-
|
|
427
|
-
/* ---- called sheets (sheet.css): a button in a step's words names a sheet with aria-controls ----
|
|
428
|
-
(The base offers ways in — sign in · new phone · join — each sheet holds that way's whole form and
|
|
429
|
-
its own submit on its foot.) A tap on it sends every other sheet of that step away and raises its own to the top state it snaps
|
|
430
|
-
to (full) — from away, and from peek alike. The step then publishes --sheet-room, the strip the
|
|
431
|
-
sheet keeps at peek, so the words can scroll their buttons clear of it (step.css); the buttons say
|
|
432
|
-
aria-expanded. */
|
|
433
|
-
function carddsSheetCall(sheet) {
|
|
434
|
-
const step = sheet.parentElement;
|
|
435
|
-
step.querySelectorAll(':scope > .sheet').forEach(s => { if (s !== sheet && carddsSheetState(s) !== 'away') carddsSheetSet(s, 'away'); });
|
|
436
|
-
const states = carddsSheetStates(sheet);
|
|
437
|
-
const top = states.includes('full') ? 'full' : states[states.length - 1];
|
|
438
|
-
const was = carddsSheetState(sheet);
|
|
439
|
-
if (was !== top) carddsSheetSet(sheet, top);
|
|
440
|
-
if (was === 'away') sheet.querySelector(':scope > .sheet__handle')?.focus({ preventScroll: true });
|
|
441
|
-
}
|
|
442
|
-
function carddsStepSync(step) {
|
|
443
|
-
const callers = [...step.querySelectorAll('[aria-controls]')].filter(b => document.getElementById(b.getAttribute('aria-controls'))?.matches('.step > .sheet'));
|
|
444
|
-
if (!callers.length) return; // a plain step: one sheet, nothing calls it, nothing to publish
|
|
445
|
-
const on = [...step.querySelectorAll(':scope > .sheet')].find(s => carddsSheetState(s) !== 'away');
|
|
446
|
-
if (on) step.style.setProperty('--sheet-room', getComputedStyle(on).getPropertyValue('--sheet-peek')); // registered: reads back in px
|
|
447
|
-
else step.style.removeProperty('--sheet-room');
|
|
448
|
-
callers.forEach(b => b.setAttribute('aria-expanded', String(carddsSheetState(document.getElementById(b.getAttribute('aria-controls'))) !== 'away')));
|
|
449
|
-
}
|
|
450
|
-
document.addEventListener('click', e => {
|
|
451
|
-
const caller = e.target.closest('[aria-controls]');
|
|
452
|
-
const sheet = caller && document.getElementById(caller.getAttribute('aria-controls'));
|
|
453
|
-
if (sheet && sheet.matches('.step > .sheet')) carddsSheetCall(sheet);
|
|
454
|
-
});
|
|
455
|
-
document.addEventListener('cardds:sheet', e => { const step = e.target.closest?.('.step'); if (step) carddsStepSync(step); });
|
|
456
|
-
|
|
457
|
-
/* ---- bar guard: a bar is the base's, never in front of a sheet (actions.css, Lh 2026-09-20) ----
|
|
458
|
-
A sheet on the screen covers the bar and CSS takes the pointer away; `inert` takes the keyboard and
|
|
459
|
-
the accessibility tree too. Re-checked when a sheet changes state and when the page changes. */
|
|
460
|
-
function carddsPagerGuard() {
|
|
461
|
-
document.querySelectorAll('.action-bar').forEach((bar) => {
|
|
462
|
-
const host = bar.parentElement;
|
|
463
|
-
bar.inert = !!(host && host.querySelector('.step > .sheet:not(.sheet--away)'));
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
if (typeof document !== 'undefined') {
|
|
467
|
-
document.addEventListener('cardds:sheet', carddsPagerGuard);
|
|
468
|
-
const start = () => {
|
|
469
|
-
carddsPagerGuard();
|
|
470
|
-
let queued = false;
|
|
471
|
-
new MutationObserver(() => {
|
|
472
|
-
if (queued) return;
|
|
473
|
-
queued = true;
|
|
474
|
-
requestAnimationFrame(() => { queued = false; carddsPagerGuard(); });
|
|
475
|
-
}).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
|
476
|
-
};
|
|
477
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
|
|
478
|
-
}
|
package/css/actions.css
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
topbar, chips, buttons, selects, bottom CTA pack
|
|
4
4
|
============================================================ */
|
|
5
5
|
|
|
6
|
+
/* KNOBS (Lh 2026-09-21) — cardds is a framework: a project adjusts ONE component from outside, without touching this file.
|
|
7
|
+
A knob is a custom property a rule reads WITH ITS DEFAULT AS THE FALLBACK — `padding-inline: var(--btn-pad-x, var(--sp-6))` —
|
|
8
|
+
so it is declared nowhere: unset, nothing moves (tests/style-snapshot proves it); set in the project's theme.css it retunes
|
|
9
|
+
that component everywhere; set on any element it retunes the components inside that element only. Names: --<component>-<what>
|
|
10
|
+
(--btn-pad-x, --chip-bg, --icon-btn-size, --appbar-pad-x); a variant says so (--btn-primary-bg, --chip-on-ink, --icon-btn-sm-size).
|
|
11
|
+
The sizes that already were root tokens (--btn-h, --chip-h, --bar-h, --header-h, --fab…) are knobs too — set them the same way.
|
|
12
|
+
Prefer a BASE when the whole system should move (tokens.css); reach for a knob when ONE component should. `npm run knobs` lists them all. */
|
|
13
|
+
|
|
6
14
|
/* ---- the bars across the top of a screen: one height, whatever they carry ----
|
|
7
15
|
.topbar (screen title) and .appbar (app context) are the same bar with
|
|
8
16
|
different contents, so the height is declared here once rather than left to
|
|
@@ -13,8 +21,8 @@
|
|
|
13
21
|
.topbar, .appbar {
|
|
14
22
|
display: flex;
|
|
15
23
|
align-items: center;
|
|
16
|
-
gap: var(--sp-3);
|
|
17
|
-
padding-block: var(--sp-2);
|
|
24
|
+
gap: var(--bar-gap, var(--sp-3));
|
|
25
|
+
padding-block: var(--bar-pad-y, var(--sp-2));
|
|
18
26
|
min-height: var(--header-h);
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -43,9 +51,9 @@
|
|
|
43
51
|
top: 0;
|
|
44
52
|
max-width: var(--screen-w);
|
|
45
53
|
margin-inline: auto;
|
|
46
|
-
padding-inline: var(--screen-pad); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
|
|
54
|
+
padding-inline: var(--appbar-pad-x, var(--screen-pad)); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
|
|
47
55
|
z-index: 40; /* topmost — card animations never cover it */
|
|
48
|
-
background: var(--surface); /* solid: sliding cards never show through */
|
|
56
|
+
background: var(--appbar-bg, var(--surface)); /* solid: sliding cards never show through */
|
|
49
57
|
}
|
|
50
58
|
/* fixed, so it covers the top of the screen it sits in — reserve its height
|
|
51
59
|
there, from the same token, the way the bottom bars reserve theirs. The
|
|
@@ -62,19 +70,19 @@
|
|
|
62
70
|
|
|
63
71
|
/* ---- icon button (round) ---- */
|
|
64
72
|
.icon-btn {
|
|
65
|
-
width: var(--tap); height: var(--tap);
|
|
73
|
+
width: var(--icon-btn-size, var(--tap)); height: var(--icon-btn-size, var(--tap));
|
|
66
74
|
flex: none;
|
|
67
|
-
border-radius: var(--r-chip);
|
|
68
|
-
border: var(--border-w) solid var(--outline); /* see .btn — same reason */
|
|
69
|
-
background: var(--action-bg);
|
|
70
|
-
color: var(--action-ink);
|
|
75
|
+
border-radius: var(--icon-btn-radius, var(--r-chip));
|
|
76
|
+
border: var(--border-w) solid var(--icon-btn-border, var(--outline)); /* see .btn — same reason */
|
|
77
|
+
background: var(--icon-btn-bg, var(--action-bg));
|
|
78
|
+
color: var(--icon-btn-ink, var(--action-ink));
|
|
71
79
|
display: grid;
|
|
72
80
|
place-items: center;
|
|
73
81
|
font-size: var(--fs-lg);
|
|
74
82
|
line-height: 1;
|
|
75
83
|
}
|
|
76
|
-
.icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
77
|
-
.icon-btn--sm { width: var(--tap-sm); height: var(--tap-sm); font-size: var(--fs-title); }
|
|
84
|
+
.icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
|
|
85
|
+
.icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
|
|
78
86
|
/* fab: the square floating action, bottom-right of a stage or screen */
|
|
79
87
|
.fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: 31; }
|
|
80
88
|
|
|
@@ -83,21 +91,21 @@
|
|
|
83
91
|
/* --outline, not --action-border: same reason as .chip — on a palette where
|
|
84
92
|
--card-1-bg equals --action-bg a transparent border makes a plain button
|
|
85
93
|
invisible on a white card. --primary and --quiet set their own edge. */
|
|
86
|
-
border: var(--border-w) solid var(--outline);
|
|
87
|
-
background: var(--action-bg);
|
|
88
|
-
color: var(--action-ink);
|
|
89
|
-
border-radius: var(--r-chip);
|
|
90
|
-
padding: var(--sp-4) var(--sp-6);
|
|
94
|
+
border: var(--border-w) solid var(--btn-border, var(--outline));
|
|
95
|
+
background: var(--btn-bg, var(--action-bg));
|
|
96
|
+
color: var(--btn-ink, var(--action-ink));
|
|
97
|
+
border-radius: var(--btn-radius, var(--r-chip));
|
|
98
|
+
padding: var(--btn-pad-y, var(--sp-4)) var(--btn-pad-x, var(--sp-6));
|
|
91
99
|
font: var(--type-label);
|
|
92
|
-
font-size: var(--fs-md);
|
|
100
|
+
font-size: var(--btn-font-size, var(--fs-md));
|
|
93
101
|
display: inline-flex;
|
|
94
102
|
align-items: center;
|
|
95
103
|
justify-content: center;
|
|
96
|
-
gap: var(--sp-2);
|
|
104
|
+
gap: var(--btn-gap, var(--sp-2));
|
|
97
105
|
min-height: var(--btn-h);
|
|
98
106
|
text-decoration: none; /* .btn works as <a> too */
|
|
99
107
|
}
|
|
100
|
-
.btn--primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
108
|
+
.btn--primary { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
|
|
101
109
|
/* quiet keeps a faint outline so it still reads as a button on colored palettes;
|
|
102
110
|
it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
|
|
103
111
|
would take the light-card action ink and vanish */
|
|
@@ -117,25 +125,25 @@
|
|
|
117
125
|
.chip {
|
|
118
126
|
display: inline-flex;
|
|
119
127
|
align-items: center;
|
|
120
|
-
gap: var(--sp-2);
|
|
121
|
-
border-radius: var(--r-chip);
|
|
128
|
+
gap: var(--chip-gap, var(--sp-2));
|
|
129
|
+
border-radius: var(--chip-radius, var(--r-chip));
|
|
122
130
|
/* --outline, not --action-border: a chip is a control, and on a palette
|
|
123
131
|
where --card-1-bg == --action-bg (field, roast, hydro) a transparent
|
|
124
132
|
border makes it invisible on a white card. Same edge fields, checks,
|
|
125
133
|
toggles, tiles and the dropdown already use. */
|
|
126
|
-
border: var(--border-w) solid var(--outline);
|
|
127
|
-
background: var(--action-bg);
|
|
128
|
-
color: var(--action-ink);
|
|
129
|
-
padding: var(--sp-2) var(--sp-3);
|
|
134
|
+
border: var(--border-w) solid var(--chip-border, var(--outline));
|
|
135
|
+
background: var(--chip-bg, var(--action-bg));
|
|
136
|
+
color: var(--chip-ink, var(--action-ink));
|
|
137
|
+
padding: var(--chip-pad-y, var(--sp-2)) var(--chip-pad-x, var(--sp-3));
|
|
130
138
|
font: var(--type-label);
|
|
131
|
-
font-size: var(--fs-sm);
|
|
139
|
+
font-size: var(--chip-font-size, var(--fs-sm));
|
|
132
140
|
min-height: var(--chip-h);
|
|
133
141
|
text-decoration: none;
|
|
134
142
|
justify-self: start; /* as a grid child it hugs its text, never stretches */
|
|
135
143
|
}
|
|
136
144
|
.dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
|
|
137
145
|
.dot--b { background: var(--dot-b); }
|
|
138
|
-
.chip--on { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
146
|
+
.chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
|
|
139
147
|
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
|
140
148
|
|
|
141
149
|
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
|
|
@@ -239,14 +247,9 @@
|
|
|
239
247
|
<button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
|
|
240
248
|
</div> — or the same two in an .action-bar__tier--pager under a control */
|
|
241
249
|
.action-bar--pager { align-items: center; }
|
|
242
|
-
/* The bar belongs to the BASE
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
|
|
246
|
-
past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
|
|
247
|
-
is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
|
|
248
|
-
the eye cannot see. */
|
|
249
|
-
:has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
|
|
250
|
+
/* The bar belongs to the BASE: the move of a page of words — a pager turning them, or one wide button. A screen
|
|
251
|
+
with a sheet finishes on the sheet's own foot instead (sheet.css). Guidance for composing, not a rule the
|
|
252
|
+
CSS enforces: a component never changes because another one is there. */
|
|
250
253
|
/* the page dots: under the card in a .screen__centre group; tight gaps */
|
|
251
254
|
.pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
|
|
252
255
|
.pager__at > * {
|
|
@@ -276,6 +279,9 @@
|
|
|
276
279
|
|
|
277
280
|
/* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
|
|
278
281
|
:is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
|
|
282
|
+
/* …and whatever goes BETWEEN the two (a "skip" link) stands in the middle: the bar places it, never the link itself */
|
|
283
|
+
:is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
|
|
284
|
+
:is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
|
|
279
285
|
|
|
280
286
|
/* .screen's default bottom padding is a constant; derive the reserve from the
|
|
281
287
|
bar instead, so a taller button (a longer label, a script with more leading)
|