@usableapp/cardds 0.7.41 → 0.7.43
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 +10 -0
- package/README.md +5 -0
- package/css/actions.css +5 -5
- package/css/layover.css +35 -0
- package/dist/cardds.css +40 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/layover/Snackbar.d.ts +30 -0
- package/dist/layover/Snackbar.js +35 -0
- package/dist/templates/CallSheet.d.ts +3 -2
- package/dist/templates/CallSheet.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.43 — 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`Snackbar` — a quick good-news line over the screen** (Lh 2026-10-02): something went well, said in passing; a warning or a question stays a `Modal`. A child of the `Screen`: `<Snackbar icon="check" onClosed={…}>ส่งคำขอแล้ว</Snackbar>` slides in from the top, stays `duration` (4000 ms; 0 = until the app closes it), slides out and calls `onClosed` — unmount it there. `open` + `onOpenChange(open, 'timeout' | 'tap')` or `defaultOpen`; `caption` = a second small line; `bottom` = at the foot, above the bottom layer; `onTap` = it can be tapped (open the notification it tells about). One at a time — the queue and the notification list are the app's. Knobs `--snackbar-*` (docs/knobs.md). Stories `Snackbar · Success` · `Controlled` · `Bottom` · `LongWords`.
|
|
9
|
+
|
|
10
|
+
## 0.7.42 — 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`CallSheet ways`: a way with no `action`** — a sheet with no button on the page, called by the sheet before it (the app sets `open` to its index from that sheet's foot): a form in several sheets, one after another. Before, every way drew a button. The first way WITH an action is the primary button. Story `CallSheet · Follows`.
|
|
14
|
+
|
|
5
15
|
## 0.7.41 — 2026-10-02
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -306,6 +306,11 @@ whose markup has rules, because the sheet's box is the step itself:
|
|
|
306
306
|
opened, a `SheetStack` sheet opened, a `Modal` arrived. Once it has settled: the field marked `data-autofocus`, else its
|
|
307
307
|
first field, else its heading (a step's title, a modal's question) or, for a sheet come onto the screen, its handle. A field
|
|
308
308
|
that already has the cursor keeps it. One mechanic for all of them (`focusWhenActive`, src/state.ts); an app writes no focus code.
|
|
309
|
+
- **Good news is a `Snackbar`, a warning or a question is a `Modal`** (Lh 2026-10-02): a `Snackbar` is a quick line over the
|
|
310
|
+
screen — something went well, said in passing. A child of the `Screen`; it slides in from the top (or `bottom`, above the
|
|
311
|
+
bottom layer), stays `duration` (4 s), slides out, never takes the cursor, never stops the member. `open` +
|
|
312
|
+
`onOpenChange(open, reason)` or `defaultOpen`; unmount it in `onClosed`. One at a time: the queue and the notification
|
|
313
|
+
list are the app's — the app shows its newest notification in it, `onTap` opens that notification.
|
|
309
314
|
- **cardds names its own controls, in the page's language** (Lh 2026-09-29): a handle, a back button, a Pin's cells… read
|
|
310
315
|
their name from `src/words.ts` by `<html lang>` (en, th ship; `setWords(lang, {…})` adds one). An app passes no label that
|
|
311
316
|
only repeats these words; a prop still wins.
|
package/css/actions.css
CHANGED
|
@@ -359,7 +359,7 @@
|
|
|
359
359
|
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
|
|
360
360
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
361
361
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
362
|
-
a.avatar, button.avatar, .menu__item) {
|
|
362
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body) {
|
|
363
363
|
cursor: pointer;
|
|
364
364
|
touch-action: manipulation;
|
|
365
365
|
transition: scale var(--motion-press) ease-out;
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
}
|
|
379
379
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
380
380
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
381
|
-
a.avatar, button.avatar, .menu__item):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
381
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
382
382
|
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
383
383
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
384
384
|
/* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back) must not turn see-through:
|
|
@@ -388,17 +388,17 @@
|
|
|
388
388
|
/* the focus ring: on the control, or on the label that wraps a hidden input (chip, tile, file button, a row that is a label) */
|
|
389
389
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
390
390
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
391
|
-
a.avatar, button.avatar, .menu__item):focus-visible,
|
|
391
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body):focus-visible,
|
|
392
392
|
:where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
393
393
|
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
394
394
|
/* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
|
|
395
395
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
396
396
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
397
|
-
a.avatar, button.avatar, .menu__item, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):is(:disabled, [aria-disabled="true"]),
|
|
397
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):is(:disabled, [aria-disabled="true"]),
|
|
398
398
|
:where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
|
|
399
399
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
400
400
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
401
|
-
a.avatar, button.avatar, .menu__item):not(.link):is(:disabled, [aria-disabled="true"]),
|
|
401
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body):not(.link):is(:disabled, [aria-disabled="true"]),
|
|
402
402
|
:where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
|
|
403
403
|
.sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
|
|
404
404
|
@media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
|
package/css/layover.css
CHANGED
|
@@ -308,3 +308,38 @@
|
|
|
308
308
|
.splash__mark > * { display: block; width: 100%; height: auto; }
|
|
309
309
|
.splash__text { margin: 0; font: var(--type-body); color: var(--splash-text-ink, var(--on-surface-muted)); }
|
|
310
310
|
@media (prefers-reduced-motion: reduce) { .splash { transition: none; } }
|
|
311
|
+
/* ---- snackbar: a quick good-news line over the screen — slides in, stays a moment, slides out (Lh 2026-10-02) ----
|
|
312
|
+
<div class="snackbar snackbar--open" role="status"><div class="snackbar__body"><svg class="icon"/>
|
|
313
|
+
<span class="snackbar__words"><span class="snackbar__text">…</span><span class="snackbar__caption">…</span></span></div></div>
|
|
314
|
+
A LAYER of the screen (its child), over the top bar; --bottom = at the foot, above the screen's bottom layer (--bottom-h).
|
|
315
|
+
Rigid like a sheet: `translate` only. `button.snackbar__body` = it can be tapped. Never a warning — that is a Modal. */
|
|
316
|
+
.snackbar {
|
|
317
|
+
--_out: -1;
|
|
318
|
+
position: fixed; inset-inline: 0; top: calc(var(--sp-3) + var(--safe-top));
|
|
319
|
+
max-width: var(--screen-w); margin-inline: auto; padding-inline: var(--screen-pad, var(--sp-4));
|
|
320
|
+
display: grid; justify-items: center;
|
|
321
|
+
z-index: calc(var(--z-top) + 1);
|
|
322
|
+
pointer-events: none; visibility: hidden;
|
|
323
|
+
translate: 0 calc(var(--_out) * (100% + var(--sp-8) + var(--safe-top)));
|
|
324
|
+
transition: translate var(--motion-sheet) ease, visibility 0s var(--motion-sheet);
|
|
325
|
+
}
|
|
326
|
+
.screen--fill > .snackbar, .sheet-stage > .snackbar { position: absolute; }
|
|
327
|
+
.snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0px) + max(var(--safe-bottom), var(--kb, 0px))); }
|
|
328
|
+
.snackbar--open { translate: 0; visibility: visible; transition-delay: 0s; }
|
|
329
|
+
.snackbar__body {
|
|
330
|
+
display: flex; align-items: center; gap: var(--snackbar-gap, var(--sp-3));
|
|
331
|
+
max-width: 100%; min-height: var(--snackbar-min-h, var(--tap));
|
|
332
|
+
padding: var(--snackbar-pad-y, var(--sp-2)) var(--snackbar-pad-x, var(--sp-5));
|
|
333
|
+
border: var(--border-w) solid var(--snackbar-border, var(--action-border));
|
|
334
|
+
border-radius: var(--snackbar-radius, var(--r-card));
|
|
335
|
+
background: var(--snackbar-bg, var(--action-bg));
|
|
336
|
+
color: var(--snackbar-ink, var(--action-ink));
|
|
337
|
+
box-shadow: var(--snackbar-shadow, var(--shadow-float));
|
|
338
|
+
font: inherit; text-align: start;
|
|
339
|
+
pointer-events: auto;
|
|
340
|
+
}
|
|
341
|
+
.snackbar__body > .icon { flex: none; }
|
|
342
|
+
.snackbar__words { display: grid; min-width: 0; overflow-wrap: anywhere; }
|
|
343
|
+
.snackbar__text { font: var(--snackbar-text-font, var(--type-body)); }
|
|
344
|
+
.snackbar__caption { font: var(--snackbar-caption-font, var(--type-caption)); opacity: var(--dim); }
|
|
345
|
+
@media (prefers-reduced-motion: reduce) { .snackbar { transition: none; } }
|
package/dist/cardds.css
CHANGED
|
@@ -2414,7 +2414,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2414
2414
|
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
|
|
2415
2415
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2416
2416
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2417
|
-
a.avatar, button.avatar, .menu__item) {
|
|
2417
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body) {
|
|
2418
2418
|
cursor: pointer;
|
|
2419
2419
|
touch-action: manipulation;
|
|
2420
2420
|
transition: scale var(--motion-press) ease-out;
|
|
@@ -2433,7 +2433,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2433
2433
|
}
|
|
2434
2434
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2435
2435
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2436
|
-
a.avatar, button.avatar, .menu__item):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
2436
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
2437
2437
|
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
2438
2438
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
2439
2439
|
/* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back) must not turn see-through:
|
|
@@ -2443,17 +2443,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2443
2443
|
/* the focus ring: on the control, or on the label that wraps a hidden input (chip, tile, file button, a row that is a label) */
|
|
2444
2444
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2445
2445
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2446
|
-
a.avatar, button.avatar, .menu__item):focus-visible,
|
|
2446
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body):focus-visible,
|
|
2447
2447
|
:where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2448
2448
|
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2449
2449
|
/* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
|
|
2450
2450
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2451
2451
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2452
|
-
a.avatar, button.avatar, .menu__item, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):is(:disabled, [aria-disabled="true"]),
|
|
2452
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):is(:disabled, [aria-disabled="true"]),
|
|
2453
2453
|
:where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
|
|
2454
2454
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2455
2455
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2456
|
-
a.avatar, button.avatar, .menu__item):not(.link):is(:disabled, [aria-disabled="true"]),
|
|
2456
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body):not(.link):is(:disabled, [aria-disabled="true"]),
|
|
2457
2457
|
:where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
|
|
2458
2458
|
.sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
|
|
2459
2459
|
@media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
|
|
@@ -3935,4 +3935,39 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3935
3935
|
.splash__mark > * { display: block; width: 100%; height: auto; }
|
|
3936
3936
|
.splash__text { margin: 0; font: var(--type-body); color: var(--splash-text-ink, var(--on-surface-muted)); }
|
|
3937
3937
|
@media (prefers-reduced-motion: reduce) { .splash { transition: none; } }
|
|
3938
|
+
/* ---- snackbar: a quick good-news line over the screen — slides in, stays a moment, slides out (Lh 2026-10-02) ----
|
|
3939
|
+
<div class="snackbar snackbar--open" role="status"><div class="snackbar__body"><svg class="icon"/>
|
|
3940
|
+
<span class="snackbar__words"><span class="snackbar__text">…</span><span class="snackbar__caption">…</span></span></div></div>
|
|
3941
|
+
A LAYER of the screen (its child), over the top bar; --bottom = at the foot, above the screen's bottom layer (--bottom-h).
|
|
3942
|
+
Rigid like a sheet: `translate` only. `button.snackbar__body` = it can be tapped. Never a warning — that is a Modal. */
|
|
3943
|
+
.snackbar {
|
|
3944
|
+
--_out: -1;
|
|
3945
|
+
position: fixed; inset-inline: 0; top: calc(var(--sp-3) + var(--safe-top));
|
|
3946
|
+
max-width: var(--screen-w); margin-inline: auto; padding-inline: var(--screen-pad, var(--sp-4));
|
|
3947
|
+
display: grid; justify-items: center;
|
|
3948
|
+
z-index: calc(var(--z-top) + 1);
|
|
3949
|
+
pointer-events: none; visibility: hidden;
|
|
3950
|
+
translate: 0 calc(var(--_out) * (100% + var(--sp-8) + var(--safe-top)));
|
|
3951
|
+
transition: translate var(--motion-sheet) ease, visibility 0s var(--motion-sheet);
|
|
3952
|
+
}
|
|
3953
|
+
.screen--fill > .snackbar, .sheet-stage > .snackbar { position: absolute; }
|
|
3954
|
+
.snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0px) + max(var(--safe-bottom), var(--kb, 0px))); }
|
|
3955
|
+
.snackbar--open { translate: 0; visibility: visible; transition-delay: 0s; }
|
|
3956
|
+
.snackbar__body {
|
|
3957
|
+
display: flex; align-items: center; gap: var(--snackbar-gap, var(--sp-3));
|
|
3958
|
+
max-width: 100%; min-height: var(--snackbar-min-h, var(--tap));
|
|
3959
|
+
padding: var(--snackbar-pad-y, var(--sp-2)) var(--snackbar-pad-x, var(--sp-5));
|
|
3960
|
+
border: var(--border-w) solid var(--snackbar-border, var(--action-border));
|
|
3961
|
+
border-radius: var(--snackbar-radius, var(--r-card));
|
|
3962
|
+
background: var(--snackbar-bg, var(--action-bg));
|
|
3963
|
+
color: var(--snackbar-ink, var(--action-ink));
|
|
3964
|
+
box-shadow: var(--snackbar-shadow, var(--shadow-float));
|
|
3965
|
+
font: inherit; text-align: start;
|
|
3966
|
+
pointer-events: auto;
|
|
3967
|
+
}
|
|
3968
|
+
.snackbar__body > .icon { flex: none; }
|
|
3969
|
+
.snackbar__words { display: grid; min-width: 0; overflow-wrap: anywhere; }
|
|
3970
|
+
.snackbar__text { font: var(--snackbar-text-font, var(--type-body)); }
|
|
3971
|
+
.snackbar__caption { font: var(--snackbar-caption-font, var(--type-caption)); opacity: var(--dim); }
|
|
3972
|
+
@media (prefers-reduced-motion: reduce) { .snackbar { transition: none; } }
|
|
3938
3973
|
|
package/dist/index.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export * from './media/Tile.js';
|
|
|
79
79
|
export * from './media/Mosaic.js';
|
|
80
80
|
export * from './layover/Banner.js';
|
|
81
81
|
export * from './layover/Callout.js';
|
|
82
|
+
export * from './layover/Snackbar.js';
|
|
82
83
|
export * from './journey/Route.js';
|
|
83
84
|
export * from './journey/TileBadge.js';
|
|
84
85
|
export * from './journey/NoteRow.js';
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,7 @@ export * from './media/Tile.js';
|
|
|
83
83
|
export * from './media/Mosaic.js';
|
|
84
84
|
export * from './layover/Banner.js';
|
|
85
85
|
export * from './layover/Callout.js';
|
|
86
|
+
export * from './layover/Snackbar.js';
|
|
86
87
|
export * from './journey/Route.js';
|
|
87
88
|
export * from './journey/TileBadge.js';
|
|
88
89
|
export * from './journey/NoteRow.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import type { IconName } from '../type/icons.js';
|
|
3
|
+
export type SnackbarReason = 'timeout' | 'tap';
|
|
4
|
+
export interface SnackbarProps extends Omit<ComponentProps<'div'>, 'onClick'> {
|
|
5
|
+
/** CONTROLLED: in sight — with `onOpenChange`; or `defaultOpen` */
|
|
6
|
+
open?: boolean;
|
|
7
|
+
/** UNCONTROLLED: starts in sight (default true — mounting it is showing it) */
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
/** it asks to leave: its time is up (`timeout`) or it was tapped (`tap`) */
|
|
10
|
+
onOpenChange?: (open: boolean, reason: SnackbarReason) => void;
|
|
11
|
+
/** how long it stays, in ms (default 4000); 0 = until the app closes it */
|
|
12
|
+
duration?: number;
|
|
13
|
+
/** a glyph before the words */
|
|
14
|
+
icon?: IconName;
|
|
15
|
+
/** a second, smaller line under the words */
|
|
16
|
+
caption?: ReactNode;
|
|
17
|
+
/** at the screen's foot, above its bottom layer, instead of the top */
|
|
18
|
+
bottom?: boolean;
|
|
19
|
+
/** the member tapped it (the app opens what it tells about — its notification); it then asks to leave */
|
|
20
|
+
onTap?: () => void;
|
|
21
|
+
/** it has slid out: the app unmounts it now, or shows the next one */
|
|
22
|
+
onClosed?: () => void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Snackbar — a quick GOOD-NEWS line laid over the screen: something went well, said in passing. It slides in from the screen's top
|
|
26
|
+
* (or `bottom`), stays `duration`, slides out, never takes the cursor and never stops the member. Not for a warning or a question —
|
|
27
|
+
* that is a `Modal`. A child of the `Screen`. Controlled like an input: `open` + `onOpenChange(open, reason)`, or `defaultOpen`;
|
|
28
|
+
* ONE at a time — the queue is the app's (what it shows is the app's own notification, kept in the app's list; a tap = `onTap`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function Snackbar({ open, defaultOpen, onOpenChange, duration, icon, caption, bottom, onTap, onClosed, className, children, ...rest }: SnackbarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { cx } from '../cx.js';
|
|
4
|
+
import { Icon } from '../type/Icon.js';
|
|
5
|
+
import { reducedMotion, useControlled, useLatest } from '../state.js';
|
|
6
|
+
/**
|
|
7
|
+
* Snackbar — a quick GOOD-NEWS line laid over the screen: something went well, said in passing. It slides in from the screen's top
|
|
8
|
+
* (or `bottom`), stays `duration`, slides out, never takes the cursor and never stops the member. Not for a warning or a question —
|
|
9
|
+
* that is a `Modal`. A child of the `Screen`. Controlled like an input: `open` + `onOpenChange(open, reason)`, or `defaultOpen`;
|
|
10
|
+
* ONE at a time — the queue is the app's (what it shows is the app's own notification, kept in the app's list; a tap = `onTap`).
|
|
11
|
+
*/
|
|
12
|
+
export function Snackbar({ open, defaultOpen = true, onOpenChange, duration = 4000, icon, caption, bottom, onTap, onClosed, className, children, ...rest }) {
|
|
13
|
+
const [shown, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
14
|
+
const [entered, setEntered] = useState(false); // its first frame is out of sight, so one mounted open slides in
|
|
15
|
+
const closed = useLatest(onClosed);
|
|
16
|
+
const up = shown && entered;
|
|
17
|
+
useEffect(() => { const f = requestAnimationFrame(() => setEntered(true)); return () => cancelAnimationFrame(f); }, []);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!up || !duration)
|
|
20
|
+
return;
|
|
21
|
+
const t = setTimeout(() => request(false, 'timeout'), duration);
|
|
22
|
+
return () => clearTimeout(t);
|
|
23
|
+
}, [up, duration, request]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (shown || !entered)
|
|
26
|
+
return;
|
|
27
|
+
const ms = reducedMotion() ? 0 : parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--motion-sheet')) * 1000 || 350;
|
|
28
|
+
const t = setTimeout(() => closed.current?.(), ms);
|
|
29
|
+
return () => clearTimeout(t);
|
|
30
|
+
}, [shown, entered]);
|
|
31
|
+
const words = (_jsxs(_Fragment, { children: [icon && _jsx(Icon, { name: icon }), _jsxs("span", { className: "snackbar__words", children: [_jsx("span", { className: "snackbar__text", children: children }), caption != null && _jsx("span", { className: "snackbar__caption", children: caption })] })] }));
|
|
32
|
+
return (_jsx("div", { className: cx('snackbar', bottom && 'snackbar--bottom', up && 'snackbar--open', className), role: "status", "aria-live": "polite", inert: !up, ...rest, children: onTap
|
|
33
|
+
? _jsx("button", { type: "button", className: "snackbar__body", onClick: () => { onTap(); request(false, 'tap'); }, children: words })
|
|
34
|
+
: _jsx("div", { className: "snackbar__body", children: words }) }));
|
|
35
|
+
}
|
|
@@ -15,8 +15,9 @@ export interface CallSheetSheet {
|
|
|
15
15
|
}
|
|
16
16
|
/** one way in: its button and the sheet it calls */
|
|
17
17
|
export interface CallSheetWay {
|
|
18
|
-
/** the button's words
|
|
19
|
-
|
|
18
|
+
/** the button's words. NONE = a sheet with no button on the page: the sheet BEFORE it calls it (the app sets `open` to its
|
|
19
|
+
* index from that sheet's foot) — a form in several sheets, one after another */
|
|
20
|
+
action?: ReactNode;
|
|
20
21
|
/** what that button brings up */
|
|
21
22
|
sheet: CallSheetSheet;
|
|
22
23
|
}
|
|
@@ -21,6 +21,7 @@ export function CallSheet({ media, heading, body, action, sheet, ways, open: ope
|
|
|
21
21
|
const list = ways ?? (sheet ? [{ action, sheet }] : []);
|
|
22
22
|
const [on, set] = useControlled(openProp === undefined ? undefined : index(openProp), index(defaultOpen), onOpenChange && ((next) => onOpenChange(next !== null, next)));
|
|
23
23
|
const [at, setAt] = useState('full');
|
|
24
|
+
const first = list.findIndex((w) => w.action != null); // the first way WITH a button is the primary one
|
|
24
25
|
// a sheet on its way out keeps its body until it has slid away — an emptied sheet sliding down read as a broken one
|
|
25
26
|
const [leaving, setLeaving] = useState(null), [was, setWas] = useState(on);
|
|
26
27
|
if (was !== on) {
|
|
@@ -28,7 +29,7 @@ export function CallSheet({ media, heading, body, action, sheet, ways, open: ope
|
|
|
28
29
|
if (was !== null)
|
|
29
30
|
setLeaving(was);
|
|
30
31
|
}
|
|
31
|
-
return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(CardHead, { level: "h1", heading: heading }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: list.map((w, i) => (_jsx(Btn, { primary: i ===
|
|
32
|
+
return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(CardHead, { level: "h1", heading: heading }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: list.map((w, i) => w.action == null ? null : (_jsx(Btn, { primary: i === first, quiet: i !== first, "aria-expanded": on === i, onClick: () => { setAt('full'); set(i); }, children: w.action }, i))) })] }), list.map(({ sheet: sh }, i) => (_jsxs(Sheet, { state: on === i ? at : 'away', onStateChange: (s) => { if (s === 'away')
|
|
32
33
|
set(null);
|
|
33
34
|
else
|
|
34
35
|
setAt(s); }, onSettled: (s) => { if (s === 'away' && leaving === i)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.43",
|
|
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.",
|