@usableapp/cardds 0.7.100 → 0.7.102

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/css/base.css +20 -0
  3. package/css/card.css +93 -0
  4. package/css/choice.css +22 -0
  5. package/css/forms.css +15 -3
  6. package/css/journey.css +23 -1
  7. package/css/media.css +1 -10
  8. package/css/sheet.css +21 -1
  9. package/css/tokens.css +2 -2
  10. package/dist/cardds.css +197 -17
  11. package/dist/cards/Card.d.ts +31 -3
  12. package/dist/cards/Card.js +33 -8
  13. package/dist/cards/CardParts.d.ts +9 -3
  14. package/dist/cards/CardParts.js +41 -3
  15. package/dist/cards/cardGo.d.ts +2 -0
  16. package/dist/cards/cardGo.js +6 -1
  17. package/dist/catalog.json +1 -1
  18. package/dist/choice/Calendar.js +5 -1
  19. package/dist/choice/CalendarWeeks.d.ts +41 -0
  20. package/dist/choice/CalendarWeeks.js +64 -0
  21. package/dist/choice/calendarDay.d.ts +13 -0
  22. package/dist/choice/calendarDay.js +6 -0
  23. package/dist/forms/Composer.d.ts +1 -1
  24. package/dist/forms/Composer.js +1 -1
  25. package/dist/forms/Field.d.ts +9 -1
  26. package/dist/forms/Field.js +35 -24
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/journey/JourneyBadge.d.ts +5 -2
  30. package/dist/journey/JourneyBadge.js +3 -3
  31. package/dist/media/MapArea.d.ts +10 -4
  32. package/dist/media/MapArea.js +8 -3
  33. package/dist/media/Postcard.d.ts +1 -1
  34. package/dist/media/Postcard.js +2 -2
  35. package/dist/post/PostDesk.d.ts +2 -1
  36. package/dist/post/PostDesk.js +11 -13
  37. package/dist/room.d.ts +12 -0
  38. package/dist/room.js +38 -0
  39. package/dist/sticky/Sticky.js +5 -38
  40. package/dist/type/Icon.d.ts +1 -1
  41. package/dist/type/Icon.js +1 -1
  42. package/dist/type/icons.d.ts +1 -1
  43. package/dist/type/icons.js +2 -2
  44. package/dist/words.d.ts +1 -1
  45. package/dist/words.js +2 -2
  46. package/icons.svg +1 -0
  47. package/lint/rules.mjs +2 -2
  48. package/package.json +1 -1
package/dist/cardds.css CHANGED
@@ -322,8 +322,8 @@
322
322
  surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (a stat
323
323
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
324
324
  :where(body, [data-palette], [data-mode], .card, .tile, .modal__drawer, .postcard__front, .postcard__back, .sticky, .post-paper,
325
- .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .sheet--paper > .sheet__body,
326
- .postcard__band, .track__step, .toggle, .chip) {
325
+ .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > .sheet__body,
326
+ .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.card__head, .card__foot), .card--bands > .card__face > :is(.card__head, .card__foot)) {
327
327
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
328
328
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
329
329
  --on-accent: color-mix(in srgb, var(--ink-ondark) calc(var(--accent-dark) * 100%), var(--ink-onlight));
@@ -571,6 +571,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
571
571
  .icon--sm { width: var(--icon-sm-size, var(--icon-sm)); height: var(--icon-sm-size, var(--icon-sm)); }
572
572
  .icon--xs { width: var(--icon-xs-size, var(--icon-xs)); height: var(--icon-xs-size, var(--icon-xs)); } /* inside captions, chips, links */
573
573
  .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
574
+ /* an icon among words sits ON the line: `img, svg { display: block }` above would put it on a line of its own. Only a direct child of a
575
+ type level is running text; an icon in a button, chip, link or row is a flex/grid item and never matches. */
576
+ :is(.t-display, .t-h1, .t-h2, .t-title, .t-body, .t-label, .t-caption) > .icon { display: inline-block; vertical-align: middle; }
574
577
 
575
578
  /* mobile screen column — the standard horizontal inset, and nothing vertical.
576
579
  Top is zero on purpose: room there is only ever a RESERVE for something
@@ -703,6 +706,23 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
703
706
  /* inside a card (a card's own "nothing yet"): the card's ink */
704
707
  .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
705
708
 
709
+ /* ---- the turn: one flip mechanic for everything with two faces (a Postcard, a Card's `back`).
710
+ The faces share one grid cell and turn on Y at --motion-flip; where motion is off they swap at once.
711
+ The owner sets the box's size and the faces' look; .is-flipped on .flip shows the back. */
712
+ .flip { perspective: 60rem; display: grid; }
713
+ .flip__face {
714
+ grid-area: 1 / 1;
715
+ backface-visibility: hidden;
716
+ -webkit-backface-visibility: hidden;
717
+ transition: transform var(--motion-flip) ease-in-out;
718
+ }
719
+ .flip__face--back { transform: rotateY(180deg); }
720
+ .flip.is-flipped > .flip__face--front { transform: rotateY(-180deg); }
721
+ .flip.is-flipped > .flip__face--back { transform: rotateY(0); }
722
+ @media (prefers-reduced-motion: reduce) {
723
+ .flip__face { transition: none; }
724
+ }
725
+
706
726
  /* ---- css/card.css — card boundary + list variants ---- */
707
727
  /* ============================================================
708
728
  cardds/card.css — the card boundary + list variants
@@ -802,6 +822,56 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
802
822
  .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
803
823
  .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
804
824
 
825
+ /* a JourneyBadge at the edge of the card's own first head (the date box in the top-left or top-right corner) keeps its corner concentric
826
+ with the card's: the card's corner (--r-card) less the room the padding leaves round it. journey.css reads it behind the theme's --tile-badge-radius,
827
+ so a theme that sets that knob still wins. */
828
+ .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
829
+
830
+ /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are white paper bands edge to
831
+ edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
832
+ clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
833
+ :is(.card--bands, .card--bands > .card__face) {
834
+ background: var(--card-middle-bg, color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg)));
835
+ --dark: var(--card-1-dark); /* the middle is mostly paper: its words take the paper's side */
836
+ overflow: hidden;
837
+ }
838
+ :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
839
+ --card-bg: var(--card-1-bg);
840
+ --dark: var(--card-1-dark);
841
+ background: var(--card-bg);
842
+ color: var(--card-ink);
843
+ margin-inline: calc(-1 * var(--card-pad));
844
+ padding-inline: var(--card-pad);
845
+ }
846
+ :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
847
+ :is(.card--bands, .card--bands > .card__face) > .card__foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); } /* the foot band is slim */
848
+
849
+ /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
850
+ each face is the card's paper — head · middle · foot — and both lie in one grid cell. The head and the foot are the same on both
851
+ faces; the middle is one fixed height (--card-middle-h, the theme's call), so the card never changes height as it turns. The hidden
852
+ face is inert. Each face holds an unseen turn button: the keyboard's way to turn it; its ring is drawn on the face. */
853
+ .card--turns { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
854
+ .card__face {
855
+ display: grid;
856
+ align-content: start;
857
+ gap: var(--card-gap);
858
+ min-width: 0;
859
+ padding: var(--card-pad);
860
+ background: var(--card-bg);
861
+ border: var(--border-w) solid var(--card-border);
862
+ border-radius: inherit;
863
+ overflow: hidden;
864
+ position: relative;
865
+ }
866
+ .card__face > * { min-width: 0; }
867
+ .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, 18.75rem); overflow: hidden; }
868
+ .card__turn { position: absolute; inset: 0; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
869
+ .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
870
+ /* a head's ChipRow takes a line of its own under the title: at most two lines of chips, a chip's words cut with an ellipsis */
871
+ .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * 1.5 + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
872
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.5; }
873
+ .card__head > .chip-row > .chip > .icon { display: inline-block; vertical-align: -0.15em; margin-inline-end: var(--chip-sm-gap, var(--sp-1)); }
874
+
805
875
  /* ---- bleed: a scrolling track that runs to the container's edges ----
806
876
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
807
877
  a tile grid: the TRACK reaches the edges so it reads as scrollable, while
@@ -1008,6 +1078,49 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1008
1078
  }
1009
1079
  .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
1010
1080
 
1081
+ /* ---- a short window of past entries (`CardList show`, Lh 2026-10-19): the list scrolls IN ITSELF, a few rows in sight and the next one cut
1082
+ in half to say there is more. The height is counted in rows by the component (`--_card-list-h`, px: rows wrap to any height); the room
1083
+ round the rows (`--card-list-window-pad`) keeps a picked row's focus ring and a card's shadow inside the scroll box, and the margin gives
1084
+ that room back so the rows stand where an ordinary list's would. ---- */
1085
+ .card-list--window {
1086
+ --_pad: var(--card-list-window-pad, var(--sp-2));
1087
+ max-height: var(--_card-list-h, none);
1088
+ overflow-y: auto;
1089
+ overscroll-behavior: contain;
1090
+ scrollbar-width: none;
1091
+ padding: var(--_pad);
1092
+ margin: calc(-1 * var(--_pad));
1093
+ }
1094
+
1095
+ /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
1096
+ <label class="card card--row"><input type="radio">…<div class="card__body">…</div><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
1097
+ Smaller than any button (a ring half a thing's size, `--tap-base` / 2 = 24) and it grows with the size of things; the touch target is an
1098
+ unseen ring round it, never under --tap-floor (44). It stands in the row's flow, so the title wraps before it, tucked into the corner
1099
+ by the row's own padding; it sits over the row's radio (which covers the whole card), so a touch on it removes and never picks. */
1100
+ .card__remove {
1101
+ --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
1102
+ position: relative;
1103
+ z-index: 1;
1104
+ flex: none;
1105
+ align-self: flex-start;
1106
+ display: grid;
1107
+ place-items: center;
1108
+ width: var(--_size);
1109
+ height: var(--_size);
1110
+ margin-block-start: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
1111
+ margin-inline-end: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
1112
+ padding: 0;
1113
+ border: var(--border-w) solid currentColor;
1114
+ border-radius: 50%;
1115
+ background: var(--card-remove-bg, transparent);
1116
+ color: var(--card-remove-ink, currentColor);
1117
+ font: inherit;
1118
+ cursor: pointer;
1119
+ }
1120
+ .card__remove::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; width: max(var(--tap-floor), var(--_size)); aspect-ratio: 1; border-radius: 50%; }
1121
+ .card__remove > .icon { width: 62%; height: 62%; }
1122
+ .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
1123
+
1011
1124
  /* leading thumb/icon in a row */
1012
1125
  .card__thumb {
1013
1126
  width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
@@ -1975,7 +2088,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1975
2088
  wireframe's street pattern, so a map is never a hole. The ring marks the
1976
2089
  rough area the member is choosing: fixed at the box's centre, so dragging
1977
2090
  the map underneath is what moves it (the centre IS the choice), never a
1978
- pin to place. The hint rides at the top, under the bar's line. */
2091
+ pin to place. The hint rides at the top, under the bar's line.
2092
+ For a MEETING place (Lh 2026-10-19) the centre mark is a pin instead of the ring: `.map__pin`, below the ring's rule,
2093
+ its tip on the point the ring's centre sits. */
1979
2094
  .map {
1980
2095
  position: absolute;
1981
2096
  inset: 0;
@@ -2003,6 +2118,24 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2003
2118
  pointer-events: none;
2004
2119
  z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
2005
2120
  }
2121
+ .map__pin { /* mark="pin": an exact point — the pin's TIP is where the ring's centre sits, so the engine reads the element's bottom-centre */
2122
+ position: absolute;
2123
+ left: 50%;
2124
+ top: var(--_band-centre);
2125
+ width: var(--map-pin-size, var(--tap));
2126
+ height: var(--map-pin-size, var(--tap));
2127
+ translate: -50% -100%;
2128
+ /* ink follows its ground: the pin stands on the map's own tone-2 fill, not on the sheet round it — in a dark sheet the inherited
2129
+ --accent is the card's ondark ink, cream on clay's orange map (2.8:1); so the pin takes the tone's side and its ink (tokens.css) */
2130
+ --dark: var(--card-2-dark);
2131
+ color: var(--map-pin-ink, var(--card-ink));
2132
+ filter: drop-shadow(var(--map-pin-drop, var(--shadow-float)));
2133
+ pointer-events: none;
2134
+ z-index: 1;
2135
+ }
2136
+ /* the lucide tip stops 2.2 of 24 short of the box's bottom: lowered so the tip IS the bottom-centre; filled with the surface so the
2137
+ street pattern never shows through the pin's body */
2138
+ .map__pin > .icon { width: 100%; height: 100%; translate: 0 9%; fill: var(--surface); }
2006
2139
  .map__credit { /* the tile provider's credit — required by OSM; a caption, not the engine's own control */
2007
2140
  position: absolute;
2008
2141
  right: var(--sp-2);
@@ -2388,6 +2521,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2388
2521
  cardds/forms.css — inputs + inline form actions
2389
2522
  .field: flat outlined input, the label IS the placeholder.
2390
2523
  Markup: <label class="field"><svg?/><input placeholder="Passport Number"></label>
2524
+ Several lines (Field rows): <label class="field field--rows"><svg?/><textarea rows="4" placeholder="…"></textarea></label>
2391
2525
  ============================================================ */
2392
2526
 
2393
2527
  .field {
@@ -2405,17 +2539,28 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2405
2539
  }
2406
2540
 
2407
2541
  .field input,
2408
- .field select {
2542
+ .field select,
2543
+ .field textarea,
2544
+ .field--rows {
2545
+ font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does. .field--rows too, so 1lh is the textarea's line for its icon */
2546
+ }
2547
+ .field input,
2548
+ .field select,
2549
+ .field textarea {
2409
2550
  border: 0;
2410
2551
  background: transparent;
2411
2552
  color: inherit;
2412
- font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does */
2413
2553
  width: 100%;
2414
2554
  min-width: 0;
2415
2555
  outline: none; /* the browser's ring drew a second box inside the field (Lh 2026-09-22) — the field itself tells focus */
2416
2556
  caret-color: currentColor;
2417
2557
  }
2418
- .field input::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
2558
+ .field input::placeholder,
2559
+ .field textarea::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
2560
+ /* several lines in the same frame (Field rows): the textarea is as tall as its `rows` × line, the frame grows round it, the icon and the action stay at the top (Lh 2026-10-19) */
2561
+ .field--rows { align-items: flex-start; padding-block: var(--field-rows-pad-y, var(--sp-3)); }
2562
+ .field textarea { flex: 1; resize: none; display: block; }
2563
+ .field--rows > .icon { margin-top: calc((1lh - var(--icon-sm)) / 2); } /* centred on the first line */
2419
2564
  /* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
2420
2565
  .field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
2421
2566
 
@@ -2504,7 +2649,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2504
2649
  justify-items: center;
2505
2650
  gap: var(--tile-badge-gap, var(--sp-0));
2506
2651
  padding: var(--tile-badge-pad-y, var(--sp-1)) var(--tile-badge-pad-x, var(--sp-2));
2507
- border-radius: var(--tile-badge-radius, var(--r-badge));
2652
+ border-radius: var(--tile-badge-radius, var(--_badge-radius, var(--r-badge))); /* --_badge-radius: card.css, a badge in a card's corner */
2508
2653
  background: var(--tile-badge-bg, var(--card-3-bg));
2509
2654
  --dark: var(--card-3-dark);
2510
2655
  color: var(--tile-badge-ink, var(--card-ink));
@@ -2518,6 +2663,28 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2518
2663
  .tile-badge--plain { background: transparent; --dark: inherit; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
2519
2664
  .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
2520
2665
 
2666
+ /* stretch: a date in a head (Lh 2026-10-19) — as tall as the title block beside it and no taller (the head stretches it; size
2667
+ containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
2668
+ over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. */
2669
+ .tile-badge--stretch {
2670
+ align-self: stretch;
2671
+ container-type: size;
2672
+ inline-size: calc(var(--fs-lg) * 3.25);
2673
+ grid-template-columns: auto auto;
2674
+ align-content: center;
2675
+ justify-content: center;
2676
+ align-items: baseline;
2677
+ column-gap: var(--sp-1);
2678
+ row-gap: 0;
2679
+ padding-block: var(--sp-3);
2680
+ line-height: var(--tile-badge-line, 0.95);
2681
+ }
2682
+ .tile-badge--stretch > * { line-height: inherit; }
2683
+ .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
2684
+ @container (min-height: 5.5rem) {
2685
+ .tile-badge--stretch > * { grid-column: 1 / -1; }
2686
+ }
2687
+
2521
2688
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
2522
2689
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
2523
2690
 
@@ -3493,6 +3660,28 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3493
3660
  .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, color-mix(in srgb, var(--accent) 12%, transparent)); color: var(--calendar-day-mark2-ink, inherit); box-shadow: var(--calendar-day-mark2-shadow, inset 0 0 0 var(--border-w) var(--accent)); }
3494
3661
  .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: 700; }
3495
3662
  .calendar__day:disabled { opacity: 0.3; }
3663
+ .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
3664
+
3665
+ /* ---- calendar weeks: a run of week rows that scrolls week by week, `show` rows in sight; the head sticks at the top of the box
3666
+ <div class="calendar calendar--weeks"><div class="calendar__weeks-head"><span class="calendar__label">June 2026</span><div class="calendar__grid">…wd×7</div></div>
3667
+ <div class="calendar__grid calendar__week"><button class="calendar__day">…</button>×7</div>…</div>
3668
+ --_weeks-h (head + `show` rows) and --_weeks-lead (the head and the gap under it, where a row snaps) are measured by src/choice/CalendarWeeks.tsx; the cells and every --calendar-* knob are Calendar's */
3669
+ .calendar--weeks {
3670
+ position: relative;
3671
+ max-height: var(--_weeks-h, none);
3672
+ overflow-y: auto;
3673
+ overscroll-behavior: contain;
3674
+ scroll-snap-type: y mandatory;
3675
+ scroll-padding-block-start: var(--_weeks-lead, 0px);
3676
+ overflow-anchor: none; /* a pick rerenders the rows; the browser must not shift the scroll to keep an anchor */
3677
+ scrollbar-width: none;
3678
+ }
3679
+ .calendar--weeks::-webkit-scrollbar { display: none; }
3680
+ .calendar__weeks-head { position: sticky; inset-block-start: 0; z-index: 1; display: grid; gap: var(--calendar-head-gap, var(--sp-2)); background: var(--calendar-weeks-head-bg, var(--card-bg)); }
3681
+ .calendar__label { font: var(--calendar-label-font, var(--type-label)); color: var(--calendar-label-ink, inherit); justify-self: start; }
3682
+ button.calendar__label { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; border-radius: var(--sp-1); }
3683
+ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
3684
+ .calendar__week { scroll-snap-align: start; }
3496
3685
 
3497
3686
  /* ---- mood: five dots in a pill, the picked one grows ----
3498
3687
  <fieldset class="mood"><label class="mood__opt"><input type="radio" name="mood"><span class="mood__dot"></span></label>×5</fieldset> */
@@ -5118,8 +5307,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5118
5307
  liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
5119
5308
  .postcard {
5120
5309
  position: relative;
5310
+ display: block; /* its faces lie absolute over the box (.flip's grid cell is not used) */
5121
5311
  min-height: var(--postcard-min-h, var(--_cover-min-h));
5122
- perspective: 60rem;
5123
5312
  border-radius: var(--postcard-radius, var(--r-card));
5124
5313
  isolation: isolate;
5125
5314
  }
@@ -5132,9 +5321,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5132
5321
  border-radius: inherit;
5133
5322
  overflow: hidden;
5134
5323
  isolation: isolate;
5135
- backface-visibility: hidden;
5136
- -webkit-backface-visibility: hidden;
5137
- transition: transform var(--motion-flip) ease-in-out;
5138
5324
  box-shadow: var(--postcard-face-shadow, var(--shadow-float));
5139
5325
  }
5140
5326
  /* the turn: the button that fills the face, under the band */
@@ -5174,7 +5360,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5174
5360
  background: var(--postcard-back-bg, var(--card-2-bg));
5175
5361
  --dark: var(--card-2-dark);
5176
5362
  color: var(--postcard-back-ink, var(--card-ink));
5177
- transform: rotateY(180deg);
5178
5363
  }
5179
5364
  .postcard__back .postcard__turn { align-content: start; padding-top: var(--postcard-back-top, 20%); gap: var(--postcard-back-gap, var(--sp-5)); } /* the name a fifth down the paper, the words after it */
5180
5365
  .postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
@@ -5193,16 +5378,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5193
5378
  .postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
5194
5379
  .postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-ink)); --accent-dark: calc(1 - var(--dark)); }
5195
5380
  .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
5196
- .postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
5197
- .postcard.is-flipped > .postcard__back { transform: rotateY(0); }
5198
5381
  /* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
5199
5382
  under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
5200
5383
  .postcard__voice { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
5201
5384
  .postcard__voice > * { pointer-events: auto; }
5202
5385
  .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
5203
- @media (prefers-reduced-motion: reduce) {
5204
- .postcard__face { transition: none; }
5205
- }
5206
5386
 
5207
5387
  /* ---- css/layover.css — ask drawer, popover menu, banner, float bar, callout ---- */
5208
5388
  /* ============================================================
@@ -2,13 +2,17 @@ import { type ComponentProps, type MouseEvent, type ReactNode } from 'react';
2
2
  import { type CardHeadProps } from './CardHead.js';
3
3
  import { type StackPlace } from '../sheets/stackCard.js';
4
4
  export type { StackPlace };
5
+ /** which side of a two-faced card shows */
6
+ export type CardFace = 'front' | 'back';
5
7
  /** a card's paper: 1 (default, light) · 2 (tinted) · 3 (dark / accent — controls invert) · 4 · 5 (more colours, from the palette) */
6
8
  export type Tone = 1 | 2 | 3 | 4 | 5;
7
9
  export interface CardProps extends ComponentProps<'article'> {
8
10
  /** the tone slot — see `Tone` */
9
11
  tone?: Tone;
10
- /** the content is white paper and the head keeps the card's colour. The same look as `Sheet paper`. Omit it and the content matches the head */
11
- paper?: boolean;
12
+ /** `true`: the content is white paper and the head keeps the card's colour (the look of `Sheet paper`). `'bands'`: the opposite, two-tone —
13
+ * the head and the foot are white paper bands edge to edge, the middle between them keeps the card's tone, lightened (knob `--card-middle-bg`).
14
+ * Omit it and the content matches the head */
15
+ paper?: boolean | 'bands';
12
16
  /** compact: a small card inside a card or a sheet body (a note, a promo) */
13
17
  sm?: boolean;
14
18
  /** a list row that is its own card: thumb · body · end, in a CardList. Its corner is the big card's PROPORTION, not its size (`--r-card-row`, 13; knob `--card-row-radius`).
@@ -49,6 +53,28 @@ export interface CardProps extends ComponentProps<'article'> {
49
53
  leave?: boolean;
50
54
  /** a card that was asked to `leave` HAS left (its motion ended; at once where motion is off, or when it has no motion) — the app does what follows: changes the page, closes. Once per leave */
51
55
  onLeft?: () => void;
56
+ /** a `row` card the member can take off its list: a small cross at the row's top right corner (smaller than a button, growing with the size of
57
+ * things; its touch target stays 44). A touch on it calls this and never picks the row. The app takes the row out of its own list. Knobs
58
+ * `--card-remove-size` · `--card-remove-inset` · `--card-remove-ink` · `--card-remove-bg` */
59
+ onRemove?: () => void;
60
+ /** THE BACK — the card has two faces and the member turns the WHOLE card over: a tap anywhere on it that is not on a control asks for the other
61
+ * face. The head and the foot (`heading`/`caption`/`end` or a CardHead child, `foot`) are given once and drawn on BOTH faces, so they read the same
62
+ * either way; the children are the front's middle, `back` the back's. The middle is one fixed height on both faces (knob `--card-middle-h`), so the
63
+ * card never changes height when it turns. The hidden face is `inert`; an `id` in the head or the foot would be drawn twice, so give it none.
64
+ * Not on a `row`, `sm` or `stack` card */
65
+ back?: ReactNode;
66
+ /** CONTROLLED: the face that shows — pair it with `onFaceChange`. The app owns it */
67
+ face?: CardFace;
68
+ /** UNCONTROLLED: the face it starts on (default `'front'`) */
69
+ defaultFace?: CardFace;
70
+ /** a tap asked for the other face */
71
+ onFaceChange?: (face: CardFace) => void;
72
+ /** the spoken name of the front's hidden turn-over button (default: the words' `turnToBack`) */
73
+ toBackLabel?: string;
74
+ /** the spoken name of the back's hidden turn-over button (default: the words' `turnToFront`) */
75
+ toFrontLabel?: string;
76
+ /** the cross's spoken name (default: the words' `remove`) — name what goes: "Remove 'buy medicine'" */
77
+ removeLabel?: string;
52
78
  }
53
79
  /**
54
80
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -58,8 +84,10 @@ export interface CardProps extends ComponentProps<'article'> {
58
84
  * A page with one feature and one CTA may be a single card. Keep everything big.
59
85
  * PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
60
86
  * button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
87
+ * TWO FACES (`back`): the whole card turns over on a tap off a control (the flip mechanic it shares with Postcard); the head and the foot
88
+ * read the same on both faces, the middle is one fixed height. `paper="bands"`: white head and foot bands edge to edge, the tint between.
61
89
  */
62
- export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, leave, onLeft, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
90
+ export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, leave, onLeft, onRemove, removeLabel, back, face, defaultFace, onFaceChange, toBackLabel, toFrontLabel, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
63
91
  onClick: (e: MouseEvent<HTMLElement>) => void;
64
92
  inert?: boolean | undefined;
65
93
  "aria-busy"?: (boolean | "true" | "false") | undefined;
@@ -1,10 +1,12 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Children, createElement, isValidElement, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
- import { useLatest, warnOnce } from '../state.js';
4
+ import { useControlled, useLatest, warnOnce } from '../state.js';
5
5
  import { CardHead } from './CardHead.js';
6
6
  import { CardFoot } from './CardFoot.js';
7
- import { goControl } from './cardGo.js';
7
+ import { goControl, onControl } from './cardGo.js';
8
+ import { Icon } from '../type/Icon.js';
9
+ import { word } from '../words.js';
8
10
  import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '../sheets/stackCard.js';
9
11
  /**
10
12
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -14,8 +16,17 @@ import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '.
14
16
  * A page with one feature and one CTA may be a single card. Keep everything big.
15
17
  * PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
16
18
  * button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
19
+ * TWO FACES (`back`): the whole card turns over on a tap off a control (the flip mechanic it shares with Postcard); the head and the foot
20
+ * read the same on both faces, the middle is one fixed height. `paper="bands"`: white head and foot bands edge to edge, the tint between.
17
21
  */
18
- export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, leave, onLeft, className, style, children, onClick, ref, ...rest }) {
22
+ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, leave, onLeft, onRemove, removeLabel, back, face, defaultFace = 'front', onFaceChange, toBackLabel, toFrontLabel, className, style, children, onClick, ref, ...rest }) {
23
+ if (onRemove && !row)
24
+ warnOnce('card-remove-row', 'cardds Card: `onRemove` draws its cross on a `row` card only');
25
+ if (back != null && (row || sm || stack))
26
+ warnOnce('card-back-kind', 'cardds Card: `back` turns a whole card — not a `row`, `sm` or `stack` card; the back is ignored');
27
+ const turns = back != null && !row && !sm && !stack;
28
+ const [shown, ask] = useControlled(face, defaultFace, onFaceChange);
29
+ const flipped = turns && shown === 'back';
19
30
  const head = heading != null || caption != null || end != null;
20
31
  const tail = foot != null || footLink != null;
21
32
  if (head || tail) {
@@ -51,7 +62,7 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
51
62
  return () => { live = false; };
52
63
  }, [away]);
53
64
  const [go, setGo] = useState(false);
54
- useLayoutEffect(() => { setGo(own.current != null && goControl(own.current) != null); }); // every render: what the card holds is its children's
65
+ useLayoutEffect(() => { setGo(!turns && own.current != null && goControl(own.current) != null); }); // every render: what the card holds is its children's
55
66
  const pressing = useRef(false);
56
67
  const click = (e) => {
57
68
  if (pressing.current)
@@ -59,6 +70,11 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
59
70
  onClick?.(e);
60
71
  const target = e.target;
61
72
  const card = own.current;
73
+ if (turns) {
74
+ if (!e.defaultPrevented && card && !onControl(card, target))
75
+ ask(flipped ? 'front' : 'back');
76
+ return;
77
+ }
62
78
  if (!e.defaultPrevented && card?.contains(target)) {
63
79
  const control = goControl(card);
64
80
  if (control && !control.contains(target)) {
@@ -90,13 +106,22 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
90
106
  const props = {
91
107
  ...rest,
92
108
  ref: setRef,
93
- className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', go && 'card--go', leaver && 'card--leaver', away && 'card--away', painted.className, className),
109
+ className: cx('card', tone && tone !== 1 && `card--${tone}`, paper === true && 'sheet--paper', paper === 'bands' && 'card--bands', turns && 'card--turns flip', flipped && 'is-flipped', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', go && 'card--go', leaver && 'card--leaver', away && 'card--away', painted.className, className),
94
110
  style: painted.style || style ? { ...painted.style, ...style } : undefined,
95
111
  ...(stack ? { 'aria-busy': painted.busy } : {}),
96
112
  ...(away ? { inert: true } : {}),
97
113
  onClick: click,
98
114
  };
115
+ const cross = row && onRemove ? (_jsx("button", { type: "button", className: "card__remove", "aria-label": removeLabel ?? word('remove'), onClick: (e) => { e.preventDefault(); e.stopPropagation(); onRemove(); }, children: _jsx(Icon, { name: "x" }) }, "remove")) : null;
116
+ if (turns) {
117
+ const kids = Children.toArray(children);
118
+ const ownHeadKid = kids.find((c) => isValidElement(c) && c.type === CardHead);
119
+ const ownFootKid = kids.find((c) => isValidElement(c) && c.type === CardFoot);
120
+ const front = kids.filter((c) => c !== ownHeadKid && c !== ownFootKid);
121
+ const side = (which, middle) => (_jsxs("div", { className: cx('card__face flip__face', `flip__face--${which}`), ...((which === 'back') !== flipped ? { inert: true } : {}), children: [_jsx("button", { type: "button", className: "card__turn", "aria-label": which === 'front' ? toBackLabel ?? word('turnToBack') : toFrontLabel ?? word('turnToFront'), onClick: () => ask(which === 'front' ? 'back' : 'front') }), head ? _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }) : ownHeadKid, _jsx("div", { className: "card__middle", children: which === 'front' ? front : middle }), tail ? _jsx(CardFoot, { link: footLink, children: foot }) : ownFootKid] }, which));
122
+ return createElement(as, props, inner(_jsxs(_Fragment, { children: [side('front', null), side('back', back)] })));
123
+ }
99
124
  if (!head && !tail)
100
- return createElement(as, props, inner(children));
101
- return createElement(as, props, inner(_jsxs(_Fragment, { children: [head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), Children.toArray(children), tail && _jsx(CardFoot, { link: footLink, children: foot }, "foot")] })));
125
+ return createElement(as, props, inner(_jsxs(_Fragment, { children: [children, cross] })));
126
+ return createElement(as, props, inner(_jsxs(_Fragment, { children: [head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), Children.toArray(children), tail && _jsx(CardFoot, { link: footLink, children: foot }, "foot"), cross] })));
102
127
  }
@@ -1,4 +1,4 @@
1
- import type { ComponentProps } from 'react';
1
+ import { type ComponentProps } from 'react';
2
2
  export interface CardMediaProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /** CardMedia — rounded media inside a card (a Placeholder or img); a corner JourneyBadge pins to it. */
@@ -16,10 +16,16 @@ export interface CardEndProps extends ComponentProps<'div'> {
16
16
  /** CardEnd — the trailing value + caption, right-aligned (in a head or a row card). */
17
17
  export declare function CardEnd({ className, ...rest }: CardEndProps): import("react").JSX.Element;
18
18
  export interface CardListProps extends ComponentProps<'div'> {
19
+ /** how many rows show before the list scrolls IN ITSELF, counted in rows, not a height: `2.5` = two whole rows and half of the third, the cut row
20
+ * saying there is more. The list measures its own rows (they wrap to any height) and keeps the window right when they change. Leave it out and the
21
+ * list is as long as its rows and scrolls with what holds it */
22
+ show?: number;
19
23
  }
20
24
  /** CardList — a stack of `Card row`s, each its own boundary: a list of things to open or to pick (past requests to reuse). It stands on the base or straight in a sheet's
21
- * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`. */
22
- export declare function CardList({ className, ...rest }: CardListProps): import("react").JSX.Element;
25
+ * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`.
26
+ * A SHORT WINDOW of past entries under a form (`show={2.5}`): the list scrolls in itself, a few rows in sight; each row a `Card row as="label"` round a radio
27
+ * (pick one) with `onRemove` for its corner cross (take it off the list) — the app owns the list, the picked one and what a removal does. */
28
+ export declare function CardList({ show, className, style, ref, ...rest }: CardListProps): import("react").JSX.Element;
23
29
  export interface IconRowProps extends ComponentProps<'span'> {
24
30
  }
25
31
  /** IconRow — a trailing cluster of Icons in a card or stack head. */
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from 'react';
2
3
  import { cx } from '../cx.js';
3
4
  /** CardMedia — rounded media inside a card (a Placeholder or img); a corner JourneyBadge pins to it. */
4
5
  export function CardMedia({ className, ...rest }) {
@@ -17,9 +18,46 @@ export function CardEnd({ className, ...rest }) {
17
18
  return _jsx("div", { className: cx('card__end', className), ...rest });
18
19
  }
19
20
  /** CardList — a stack of `Card row`s, each its own boundary: a list of things to open or to pick (past requests to reuse). It stands on the base or straight in a sheet's
20
- * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`. */
21
- export function CardList({ className, ...rest }) {
22
- return _jsx("div", { className: cx('card-list', className), ...rest });
21
+ * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`.
22
+ * A SHORT WINDOW of past entries under a form (`show={2.5}`): the list scrolls in itself, a few rows in sight; each row a `Card row as="label"` round a radio
23
+ * (pick one) with `onRemove` for its corner cross (take it off the list) — the app owns the list, the picked one and what a removal does. */
24
+ export function CardList({ show, className, style, ref, ...rest }) {
25
+ const own = useRef(null);
26
+ useLayoutEffect(() => {
27
+ const list = own.current;
28
+ if (!list || show == null)
29
+ return;
30
+ const fit = () => {
31
+ const rows = [...list.children];
32
+ const whole = Math.floor(show);
33
+ const cut = rows[whole];
34
+ if (!cut) {
35
+ list.style.removeProperty('--_card-list-h');
36
+ return;
37
+ }
38
+ const box = cut.getBoundingClientRect();
39
+ const at = box.top - list.getBoundingClientRect().top + list.scrollTop; // from the list's top edge, wherever it is scrolled
40
+ list.style.setProperty('--_card-list-h', `${at + (show - whole) * box.height}px`);
41
+ };
42
+ fit();
43
+ const watch = new ResizeObserver(fit);
44
+ watch.observe(list);
45
+ for (const row of list.children)
46
+ watch.observe(row);
47
+ const rows = new MutationObserver(() => { for (const row of list.children)
48
+ watch.observe(row); fit(); });
49
+ rows.observe(list, { childList: true });
50
+ return () => { watch.disconnect(); rows.disconnect(); };
51
+ }, [show]);
52
+ const setRef = (node) => {
53
+ own.current = node;
54
+ const passed = ref;
55
+ if (typeof passed === 'function')
56
+ passed(node);
57
+ else if (passed)
58
+ passed.current = node;
59
+ };
60
+ return _jsx("div", { ref: setRef, className: cx('card-list', show != null && 'card-list--window', className), style: style, ...rest });
23
61
  }
24
62
  /** IconRow — a trailing cluster of Icons in a card or stack head. */
25
63
  export function IconRow({ className, ...rest }) {
@@ -1,2 +1,4 @@
1
1
  /** the control this card presses when it is touched off that control, or null: the card is only read */
2
2
  export declare function goControl(card: HTMLElement): HTMLElement | null;
3
+ /** the press landed on something the member presses inside this card (a Card with a `back` turns only when it did not) */
4
+ export declare function onControl(card: HTMLElement, target: Element): boolean;
@@ -24,6 +24,11 @@ export function goControl(card) {
24
24
  return null;
25
25
  const foot = control.parentElement;
26
26
  const inFoot = foot?.matches('.card__foot') && foot.parentElement?.closest('.card') === card; // the card's OWN foot, also when a part of the card holds it (a FeedCard's region)
27
- const atRowEnd = card.matches('.card--row') && card.lastElementChild === control;
27
+ const atRowEnd = card.matches('.card--row') && card.lastElementChild === control && !control.matches('.card__remove'); // a row's cross takes it off its list: never pressed by a touch elsewhere
28
28
  return inFoot || atRowEnd ? control : null;
29
29
  }
30
+ /** the press landed on something the member presses inside this card (a Card with a `back` turns only when it did not) */
31
+ export function onControl(card, target) {
32
+ const control = target.closest(CONTROLS);
33
+ return control != null && control !== card && card.contains(control);
34
+ }