@usableapp/cardds 0.7.93 → 0.7.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
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.94 — 2026-10-18
6
+
7
+ From timebank's help-ask sheet (Lh 2026-10-17). One look changes on existing screens: a row card's corner (41.6 → 13). Everything else is added, and a screen that does not use it is as it was.
8
+
9
+ Nothing here breaks an existing screen: one prop (`Day today`), one class (`.day--today`) and two knobs are added and none removed; a week without `today` is as it was.
10
+
11
+ One look CHANGES: a row card's corner. Before → after: `Card row` took the one card radius, `--r-card` (41.6), so a two-line row (88 tall at 375) read as a pill → it takes `--r-card-row` (13 = `--r-base` × 0.625), the big card's proportion of corner to height (Lh 2026-10-17: a small card's corner shrinks to keep the same proportion as a big card's). A project that wants the old corner sets `--card-row-radius: var(--r-card)`. One new check may fail a project's `cardds check`: a card inside a card.
12
+
13
+ Nothing here breaks an existing screen: one prop (`MapArea inline`), one component (`MapPick`), four classes (`.map--inline`, `.map-pick`, `.map-pick__mark`, `.map-pick__name`) and thirteen knobs are added, none removed. One look changes: a map's hint is no longer squeezed onto two lines.
14
+
15
+ Nothing here breaks a screen that passes `lead`, `hint` or `send`: one part (`ComposerArea`), one class (`.composer__area`) and four knobs are added and none removed. One thing a screen can see: a `Composer` with no `lead`, `hint` or `send` no longer draws an empty bar.
16
+
17
+ ### Added
18
+ - **`Day today`: the week view says which day is today** (Lh 2026-10-17: "today" for the week view; `Calendar` is not touched). Before → after: a `DayStrip` drew every unpicked day alike, so the member could not tell today from the rest → the day with `today` gets `aria-current="date"` and a light tone behind it, the knob `--day-today-bg` (default `--ink-tint`, the ink's own 12%), its words `--day-today-ink` (default the day's own ink). It is no louder than a resting day: not the accent of the selected pill, not the dot's shape. When today is also the selected day the filled pill wins and `aria-current="date"` stays. The app says which day is today, as it says which is selected. Stories `DayStrip · Today` and `DayStrip · TodayPicked`.
19
+ - **`--r-card-row`, the row card's corner.** A root token on `--r-base`, so a square theme (`--r-base: 0`) squares rows too. `--card-row-radius` still sets it from outside.
20
+ - **A pickable row card.** Before → after: `Card row as="label"` round a radio showed the native radio → the radio lies unseen over the whole card (as a chip's or a tile's does), the focus ring is the card's while its radio has the keyboard, and the app gives the picked card its `tone`. Story `Card · CardListPick` (a `CardList` straight in a sheet's body); the proportion side by side in `Card · RowProportion`.
21
+ - **`cardds check` rule `card-in-card` (error).** A `CardList`, `CardFeed`, `FeedCard` or `Card` written inside a `Card` is flagged: a card never wraps a card (Lh 2026-10-17). A `Card sm` inside a card is not flagged. The ESLint plugin reports it under `cardds/check`.
22
+ - **`MapArea inline`: the map in the sheet** (Lh 2026-10-17: "build the map in the sheet", from timebank's help-ask, choosing where to meet). Before → after: a map could only lie behind a stage sheet (`SheetStage bg`), where a half sheet covers half of it and an `over` stage covers the sheet's head; inside a `CardMedia` it was small and its ring was centred on a stage band that was not there → `inline` puts the map in flow. In a card it is a rounded media box (`--map-inline-h`, default 1.5 × `--map-ring`; `--map-radius`, default `--r-media`). As a `SheetBody`'s only child it fills the body, inside the sheet's padding, from under the head to above the foot, so the open sheet becomes the map and its head stays. The ring is at the centre of the map's own box and the hint at its own top. Stories `MapArea · InSheet` and `MapArea · InCard`.
23
+ - **`MapPick`: a place on a map the member taps to choose it.** Before → after: the only thing to pin on a map was a `Callout`, a card that shows and takes no tap, and five of them crowd a phone → a small round mark (an optional icon inside) with the place's short name under it, standing on its point (`top` / `left`, as a Callout). The tap is a `--tap` square round the point, and the name takes a tap too. It is a radio: the marks of one choice share `name`, the app owns `checked` + `onChange` (or `defaultChecked`), and the picked one fills with accent. It goes in a `MapArea` after the engine's mount. Knobs `--map-pick-size`, `-bg`, `-border`, `-mark-ink`, `-shadow`, `-ink`, `-name-bg`, `-name-radius`, `-font`, `-on-bg`, `-on-ink`. Stories `MapPick · Five` and `MapPick · Plain`.
24
+ - **`ComposerArea`: a second writing area in the same Composer, behind a dashed rule** (Lh 2026-10-17, timebank's help ask: the request over several lines and, if the member wants, the reason under it, in ONE card and one look; a `Composer` beside a `Field` was two looks, rejected). Before → after: a reason under the request was a second `Composer` (11rem tall, nothing between the two) or a `Field` (one line, another type) → `<Composer …><ComposerArea aria-label="เหตุผล" value={why} onChange={…} /></Composer>` draws a textarea in the Composer's own type, two lines tall (`2lh`), above the bar, with a dashed rule over it that the area draws itself (`border-block-start`, no selector looks at what sits before it). It is controlled like any textarea and takes the step cursor (`useStepCursor`) as the Composer's own textarea does: the open step's first field keeps the mark. Knobs: `--composer-area-min-h` (`2lh`, its words alone: the rule and the room come on top), `--composer-area-pad` (`--sp-3`, the room under the rule), `--composer-area-border` (the rule's colour, `--card-muted`, the hint's ink); the rule is `--rule-w` wide, the system's dotted-connector width. Story `Composer · WithReason`.
25
+ - **`--composer-input-min-h`: the first writing area's minimum height** (default `11rem`, the value it had, hard-coded). Before → after: a shorter request area needed cardds → a project sets it.
26
+
27
+ ### Changed
28
+ - **The pager may say where it goes** (Lh 2026-10-17, from timebank's help-ask sheet: back and next in a stepped task name the step they lead to). Before → after: a pager button was a round `IconBtn` and never carried words (Lh 2026-09-16, "for good") → it may also be a `Btn` with the step's name and an arrow: `<ActionBar variant="pager"><Btn><Icon name="arrow-left" /> คำขอ</Btn><Btn primary>วันเวลา <Icon name="arrow-right" /></Btn></ActionBar>`, and the same in an `ActionBarTier pager`. Worded, they read as left and right like the round pair (Lh 2026-10-17, second call): each hugs its edge at its own size, words plus an arrow as big as the round pager's (the md `Icon`, not `xs`), with open ground between; two widths are normal. A `Link` between them stays in the middle. CSS: in a pager bar or tier a `.btn` is `flex: 0 0 auto` (a Btn in the plain bar still stretches) and the edge rules that placed the first `.icon-btn` now place a first `.btn` the same way; a pager with a link between is now a three-column grid (`1fr auto 1fr`), so the link stays at the bar's middle when the two buttons are two widths (the round `PagerSkip` lands on the same pixels as before: link centre 187.5 of 375); no knob added. The round pager is unchanged and still valid. The names are steps: a word that commits ("Done") is still the plain bar's one wide `Btn`. Rule rewritten in css/actions.css, `ActionBar` doc, README, CLAUDE.md, docs/guides/building-with-cardds.md, .design-sync/conventions.md. Stories `ActionBar · PagerWords`, `PagerWordsSkip`, `PagerWordsOnAStep`; tests/gallery-components.spec.js.
29
+ - **A map's hint is centred by its two sides.** Before → after: `left: 50%` + `translate` left the words only half the map's width to wrap in, so "Drag the map until the ring covers your area" broke onto two lines on a 375 phone → the hint is as wide as its words up to the map less `--sp-4` at each side. On the stage stories the hint pill went from 186.5px to 237px (`AddressStage`) and 327px (`TrackOnTop`) wide, one line.
30
+ - **`Composer` draws its bar only when it holds a `lead`, a `hint` or a `send`.** Before → after: with none of them an empty `.composer__bar` stood under the textarea and took one `--composer-gap` → nothing is drawn (`SheetSteps · ComposerFirst` is that case).
31
+
5
32
  ## 0.7.93 — 2026-10-17
6
33
 
7
34
  Nothing here breaks an existing screen: one prop (`StampBook fill`), one class (`.stamp-book--fill`) and one knob are added and none removed; a book without `fill` is as it was.
package/README.md CHANGED
@@ -437,9 +437,12 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
437
437
  - **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
438
438
  control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
439
439
  control tier above the buttons (it publishes the taller `--bar-reserve` the centred card still reads for its cap).
440
- - **The round pager bar** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
441
- holding two `.icon-btn`s and nothing else: they stand at the two edges. The
442
- step that commits still goes back to one wide button (rule 3).
440
+ - **The pager bar** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
441
+ holding two peers at the two edges, a skip link between or nothing: two round
442
+ `.icon-btn`s, or two `.btn`s that name the step each leads to with the arrow at the
443
+ round pager's size ("← คำขอ" · "วันเวลา →", Lh 2026-10-17; the 2026-09-16 "never words" is lifted).
444
+ Worded, each hugs its edge at its own size with open ground between: left and right, never two halves. The names are steps; the step that
445
+ commits ("Done") still goes back to one wide button (rule 3).
443
446
  - **Page that moves** (Lh 2026-09-20) — two ways, never both on one screen:
444
447
  - *Pages of words:* `.step > .pages > .page` (one `aria-current="step"`, the rest `inert`) — every page
445
448
  one height, the round pager bar turns them (`slideTo()`), the words slide like a page. No sheet there.
@@ -909,7 +912,7 @@ That file is where a project's look lives. Bases and roles:
909
912
  |---|---|---|
910
913
  | room **between** things | `--sp-base` (0.25rem = 4) | every `--sp-N` = base × N → paddings, gaps, the screen's inset, the bar's reserve, the sheet's peek |
911
914
  | type | `--fs-base` (1.375rem = 22 — the body) | every `--fs-*`, `--badge`, `--icon-xs` |
912
- | corners | `--r-base` (1.3rem = 20.8) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
915
+ | corners | `--r-base` (1.3rem = 20.8) | every `--r-*`; `0` = square. `--r-chip` stays fully round. One card radius (`--r-card`) for every card, tile, menu, sheet, fab, but a small card keeps the big card's PROPORTION, not its size: a row card is `--r-card-row` (13) |
913
916
  | the size **of** things | `--tap-base` (3rem = 48) | `--tap*`, `--btn-h`, `--chip-h`, `--bar-h`, `--header-h`, `--fab`, `--icon*`, `--avatar*`, `--dot*`, `--ring*`, `--tile-*`. `--tap*` never under `--tap-floor` (44px) |
914
917
  | faces | `--font-ui`, `--font-display` | |
915
918
  | colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
package/css/actions.css CHANGED
@@ -284,18 +284,24 @@
284
284
  .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
285
285
  .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
286
286
 
287
- /* ---- pager: previous / next as two round icon buttons at the two edges ----
287
+ /* ---- pager: previous / next as two peers at the two edges ----
288
288
  For a sequence you can walk BOTH ways — a three-card feature intro, a step
289
289
  flow. Use it instead of the default pack whenever going back is as ordinary
290
290
  as going on: a wide CTA beside a small round icon reads as one action plus
291
291
  an afterthought, which is wrong when the two are peers.
292
292
 
293
- The two are the same round shape and size (.icon-btn), one at each edge in
294
- thumb reach from either side; the fill (--invert) alone leans forward. They
295
- NEVER carry words (Lh 2026-09-16, for good): an arrow is the whole meaning,
296
- and a word that isn't a direction ("Get started", "Done") is not a pager
297
- button — that step COMMITS and goes back to the plain .action-bar with one
298
- wide button. Where you are is said by the .pager__at dots — under the card
293
+ The two are the same shape and size, one at each edge in thumb reach from
294
+ either side; the fill alone leans forward. Two ways to draw them, both
295
+ valid: two round .icon-btn (an arrow is the whole meaning), or two .btn
296
+ that SAY the step each leads to, the arrow beside the name ("← คำขอ",
297
+ "วันเวลา →"; Lh 2026-10-17, lifting the 2026-09-16 "never words"). Worded,
298
+ they read as LEFT and RIGHT like the round pair (Lh 2026-10-17, second
299
+ call): each hugs its edge at its own size (its words + the arrow, no
300
+ stretch, so two widths are normal), open ground between them, and the
301
+ arrow stays the round pager's size (the md .icon, not xs). The words are
302
+ a STEP's name, a direction: a word that commits ("Get started", "Done")
303
+ is not a pager button — that step goes back to the plain .action-bar
304
+ with one wide button. Where you are is said by the .pager__at dots — under the card
299
305
  that changes (a .screen__centre group, card.css) for a card walk, never in
300
306
  that bar (Lh, 2026-09-12); a walk of words instead floats its dots as the
301
307
  bar's own first row (step.css, Lh 2026-09-23) — either way the dots move
@@ -304,7 +310,11 @@
304
310
  <div class="action-bar action-bar--pager">
305
311
  <button class="icon-btn" aria-label="ย้อนกลับ" disabled>…</button>
306
312
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
307
- </div> — or the same two in an .action-bar__tier--pager under a control */
313
+ </div> — or the same two in an .action-bar__tier--pager under a control
314
+ <div class="action-bar action-bar--pager">
315
+ <button class="btn"><svg class="icon">…</svg> คำขอ</button>
316
+ <button class="btn btn--primary">วันเวลา <svg class="icon">…</svg></button>
317
+ </div> — worded: each at its edge, its own size; a skip link between stays centred */
308
318
  .action-bar--pager { align-items: center; }
309
319
  /* The bar belongs to the BASE: the move of a page of words — a pager turning them, or one wide button. A screen
310
320
  with a sheet finishes on the sheet's own foot instead (sheet.css). Guidance for composing, not a rule the
@@ -342,11 +352,17 @@
342
352
  :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
343
353
  :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
344
354
 
345
- /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
346
- :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
355
+ /* the first button (round or worded) sits at the left edge, the last at the right; a skip link between them stays centred.
356
+ A worded one keeps its own size here, not the plain bar's stretch: left and right, open ground between (Lh 2026-10-17) */
357
+ :is(.action-bar--pager, .action-bar__tier--pager) > .btn { flex: 0 0 auto; }
358
+ :is(.action-bar--pager, .action-bar__tier--pager) > :is(.icon-btn, .btn):first-child { margin-right: auto; }
347
359
  /* …and whatever goes BETWEEN the two (a "skip" link) stands in the middle: the bar places it, never the link itself */
348
- :is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
349
- :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
360
+ :is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn, .btn):not(:first-child):not(:last-child) { margin-inline: auto; }
361
+ :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > :is(.icon-btn, .btn):first-child { margin-right: 0; }
362
+ /* with a link between, three columns: two worded buttons are two widths, and only even side columns keep the link at the bar's middle */
363
+ :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) { display: grid; grid-template-columns: 1fr auto 1fr; }
364
+ :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > :first-child { justify-self: start; }
365
+ :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > :last-child { justify-self: end; }
350
366
 
351
367
 
352
368
  /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
@@ -381,7 +397,7 @@
381
397
  button's, the card itself is never focused or disabled. */
382
398
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
383
399
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
384
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove) {
400
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove, .map-pick) {
385
401
  cursor: pointer;
386
402
  touch-action: manipulation;
387
403
  transition: scale var(--motion-press) ease-out;
@@ -393,14 +409,14 @@
393
409
  }
394
410
  /* a CHOICE's fill fades in as it is picked (a plain button's colour never changes on a tap, so it does not fade) */
395
411
  :where(.chip--pick, button.chip, .tile--pick, button.tile, .day, .segment__item, .calendar__day, .float-bar__item, .picker__item, .check,
396
- .icon-btn[role="tab"], .avatar-pick__item > .avatar) {
412
+ .icon-btn[role="tab"], .avatar-pick__item > .avatar, .map-pick > .map-pick__mark, .map-pick > .map-pick__name) {
397
413
  transition-property: scale, background-color, color, border-color, opacity;
398
414
  transition-duration: var(--motion-press);
399
415
  transition-timing-function: ease-out;
400
416
  }
401
417
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
402
418
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
403
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
419
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove, .map-pick):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
404
420
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
405
421
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
406
422
  /* a pile of notes pressed whole (Lh 2026-10-11): the button lies invisible UNDER the notes, so it is the notes that answer — DIM, as a card does (the group dims as one, the
@@ -413,11 +429,11 @@
413
429
  it darkens a touch instead (--press-shade) */
414
430
  :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) ease-out; }
415
431
  :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))):active { filter: brightness(var(--press-shade)); }
416
- /* 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) */
432
+ /* 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, a card that is a label round its radio) */
417
433
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
418
434
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
419
435
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):focus-visible,
420
- :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
436
+ :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible), :where(label.card:has(> input[type="radio"]:focus-visible)) { outline: var(--focus); outline-offset: var(--sp-1); }
421
437
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .feed-card--small, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
422
438
  /* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
423
439
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
package/css/base.css CHANGED
@@ -9,7 +9,7 @@
9
9
  the member wants to copy), and inputs are untouched. Native buttons and
10
10
  links are matched by element; the rest by the class that makes them one */
11
11
  button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--pick,
12
- .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .track__step, .pager__at {
12
+ .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .track__step, .pager__at, .map-pick {
13
13
  user-select: none;
14
14
  -webkit-user-select: none;
15
15
  -webkit-touch-callout: none;
package/css/card.css CHANGED
@@ -300,7 +300,7 @@
300
300
  .card-list { display: grid; gap: var(--card-list-gap, var(--sp-3)); }
301
301
 
302
302
  .card--row {
303
- border-radius: var(--card-row-radius, var(--r-card));
303
+ border-radius: var(--card-row-radius, var(--r-card-row)); /* small: the big card's corner in proportion, not its size */
304
304
  padding: var(--card-row-pad, var(--sp-4));
305
305
  display: flex;
306
306
  align-items: center;
package/css/choice.css CHANGED
@@ -84,8 +84,9 @@
84
84
  .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); }
85
85
  .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
86
86
  /* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
87
- a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css) — the native input lies over its whole box, unseen */
88
- :is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch) > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
87
+ a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css), a place on a map (sheet.css), a card that is a label round a radio
88
+ (a pickable row card: the app gives the picked one its `tone`) — the native input lies over its whole box, unseen */
89
+ :is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch, .map-pick) > input, label.card > input[type="radio"] { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
89
90
  :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
90
91
  /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
91
92
  .chip--sign { padding-right: var(--sp-2); }
@@ -101,7 +102,7 @@
101
102
  .chip--sign:has(:checked)::after { rotate: 45deg; }
102
103
  @media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
103
104
 
104
- /* ---- day strip: a week, one day selected ----
105
+ /* ---- day strip: a week, one day selected, today a light tone ----
105
106
  <div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
106
107
  .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--day-strip-gap, var(--sp-1)); }
107
108
  .day {
@@ -124,6 +125,8 @@
124
125
  }
125
126
  .day__name { font: var(--day-name-font, var(--type-caption)); }
126
127
  .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
128
+ /* today: a light tone, no louder than a rest; the selected pill below wins when it is both (it comes after and weighs more) */
129
+ .day--today { background: var(--day-today-bg, var(--ink-tint)); color: var(--day-today-ink, currentColor); }
127
130
  .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); opacity: 1; }
128
131
  .day__dot { grid-row: 3; align-self: center; width: var(--day-dot-size, var(--dot-xs)); height: var(--day-dot-size, var(--dot-xs)); border-radius: 50%; background: currentColor; }
129
132
 
@@ -237,11 +240,13 @@
237
240
 
238
241
  /* ---- composer: big text area with a bottom bar (icon · hint · send) ----
239
242
  <div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
240
- <div class="composer__bar"><button class="icon-btn">…</button><span class="composer__hint">Your idea is born</span><button class="icon-btn icon-btn--invert">↑</button></div></div> */
243
+ <div class="composer__bar"><button class="icon-btn">…</button><span class="composer__hint">Your idea is born</span><button class="icon-btn icon-btn--invert">↑</button></div></div>
244
+ A second thing to write (the reason under the request) is <textarea class="composer__input composer__area"> after the
245
+ first: the same type, shorter, and the area draws its own dashed rule above it (no selector looks at what sits before it). */
241
246
  .composer { display: grid; gap: var(--composer-gap, var(--sp-3)); }
242
247
  .composer__input {
243
248
  width: 100%;
244
- min-height: 11rem;
249
+ min-height: var(--composer-input-min-h, 11rem);
245
250
  resize: none;
246
251
  border: 0;
247
252
  padding: 0;
@@ -252,6 +257,12 @@
252
257
  outline: none;
253
258
  }
254
259
  .composer__input::placeholder { color: inherit; opacity: var(--off); }
260
+ .composer__area {
261
+ box-sizing: content-box; /* its min-height is its words alone: the rule and the room under it come on top */
262
+ min-height: var(--composer-area-min-h, 2lh);
263
+ padding-block-start: var(--composer-area-pad, var(--sp-3));
264
+ border-block-start: var(--rule-w) dashed var(--composer-area-border, var(--card-muted)); /* the rule: the hint's ink, the system's rule width */
265
+ }
255
266
  .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--composer-bar-gap, var(--sp-3)); }
256
267
  .composer__hint { font: var(--composer-hint-font, var(--type-caption)); color: var(--composer-hint-ink, var(--card-muted)); }
257
268
 
package/css/sheet.css CHANGED
@@ -368,8 +368,7 @@
368
368
  repeating-linear-gradient(-18deg, transparent 0 8.75rem, var(--ink-tint) 8.75rem 9.25rem),
369
369
  var(--card-2-bg);
370
370
  }
371
- .map > :first-child { position: absolute; inset: 0; z-index: 0; isolation: isolate; } /* its own stacking context: an engine's layered panes stay under the ring */
372
- .sheet-stage__bg .map > :first-child { background: transparent; } /* an engine's own grey ground would hide the pattern while tiles load */
371
+ .map > :first-child { position: absolute; inset: 0; z-index: 0; isolation: isolate; background: transparent; } /* its own stacking context: an engine's layered panes stay under the ring; an engine's own grey ground would hide the pattern while tiles load */
373
372
  .map__ring {
374
373
  position: absolute;
375
374
  left: 50%;
@@ -401,9 +400,10 @@
401
400
  .map__hint {
402
401
  position: absolute;
403
402
  top: calc(var(--_top, 0rem) + var(--sp-3));
404
- left: 50%;
405
- translate: -50% 0;
406
- max-width: calc(100% - 2 * var(--sp-4));
403
+ /* centred by its two sides, not left 50% + translate: that left the words only half the map to wrap in, so a short hint broke onto two lines */
404
+ inset-inline: var(--sp-4);
405
+ width: fit-content;
406
+ margin-inline: auto;
407
407
  padding: var(--sp-2) var(--sp-4);
408
408
  border-radius: var(--map-hint-radius, var(--r-chip));
409
409
  background: var(--map-hint-bg, var(--card-3-bg));
@@ -415,6 +415,69 @@
415
415
  z-index: 1;
416
416
  }
417
417
 
418
+ /* ---- the map IN FLOW (Lh 2026-10-17): `.map--inline` — a box in a card, or the whole of a sheet's body ----
419
+ <div class="sheet__body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
420
+ <span class="map__ring" …></span></div></div>
421
+ Choosing where to meet without leaving the sheet: the open sheet becomes the map and its head stays on top (a stage
422
+ behind a half sheet covers the head and changes the look). No stage, so no band and no bar: the ring is at the centre
423
+ of the map's OWN box and the hint at its own top. In a card it is a rounded media box; as a SheetBody's only child it
424
+ fills the body (the part above the foot), however far the sheet slid. */
425
+ .map--inline {
426
+ --_top: 0rem;
427
+ --_band-centre: 50%;
428
+ position: relative;
429
+ min-height: var(--map-inline-h, calc(var(--map-ring) * 3 / 2));
430
+ border-radius: var(--map-radius, var(--r-media));
431
+ overflow: hidden;
432
+ }
433
+ .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
434
+ .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
435
+
436
+ /* ---- map pick: a place on the map the member taps to choose it (Lh 2026-10-17) ----
437
+ <label class="map-pick" style="top: 30%; left: 40%"><input type="radio" name="meet" value="library">
438
+ <span class="map-pick__mark">{icon}</span><span class="map-pick__name">ห้องสมุด</span></label>
439
+ A small round mark on its point and the place's short name under it — small, so five fit a phone's map (a Callout
440
+ is a card: five crowd it). The label is a --tap square centred on the point: the tap is never smaller than the
441
+ control floor however small the mark is drawn, and the name takes a tap too. The radios of one choice share a
442
+ name; the picked one fills with accent. Above the ring: the ring takes no taps, the places do. */
443
+ .map-pick {
444
+ position: absolute;
445
+ z-index: 2;
446
+ width: var(--tap);
447
+ height: var(--tap);
448
+ translate: -50% -50%;
449
+ display: grid;
450
+ place-items: center;
451
+ color: var(--map-pick-ink, var(--on-surface));
452
+ }
453
+ .map-pick__mark {
454
+ display: grid;
455
+ place-items: center;
456
+ width: var(--map-pick-size, var(--sp-6));
457
+ height: var(--map-pick-size, var(--sp-6));
458
+ border-radius: 50%;
459
+ border: var(--border-w) solid var(--map-pick-border, var(--accent));
460
+ background: var(--map-pick-bg, var(--surface));
461
+ color: var(--map-pick-mark-ink, var(--accent));
462
+ box-shadow: var(--map-pick-shadow, var(--shadow-float));
463
+ pointer-events: none;
464
+ }
465
+ .map-pick__name {
466
+ position: absolute;
467
+ top: calc(50% + var(--map-pick-size, var(--sp-6)) / 2 + var(--sp-1));
468
+ left: 50%;
469
+ translate: -50% 0;
470
+ white-space: nowrap;
471
+ padding: 0 var(--sp-2);
472
+ border-radius: var(--map-pick-name-radius, var(--r-chip));
473
+ background: var(--map-pick-name-bg, color-mix(in srgb, var(--surface) 85%, transparent));
474
+ font: var(--map-pick-font, var(--type-caption));
475
+ }
476
+ .map-pick:has(:checked) { z-index: 3; } /* the picked place's name lies over a neighbour's */
477
+ .map-pick:has(:checked) > :is(.map-pick__mark, .map-pick__name) { background: var(--map-pick-on-bg, var(--accent)); color: var(--map-pick-on-ink, var(--on-accent)); }
478
+ .map-pick:has(:focus-visible) > .map-pick__mark { outline: var(--focus); outline-offset: var(--sp-0); }
479
+ .map-pick:has(:disabled) { opacity: var(--off); cursor: default; }
480
+
418
481
  /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
419
482
  foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
420
483
  scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
package/css/tokens.css CHANGED
@@ -79,7 +79,10 @@
79
79
 
80
80
  /* ---- radius ---- */
81
81
  --r-chip: 999px; /* pills, chips, buttons — always fully round */
82
- --r-card: calc(var(--r-base) * 2); /* 41.6 the one card radius: every card, row, tile, menu, sheet, fab */
82
+ --r-card: calc(var(--r-base) * 2); /* 41.6 the one card radius: every card, tile, menu, sheet, fab — but a row card (--r-card-row) */
83
+ --r-card-row: calc(var(--r-base) * 0.625); /* 13 a row card's corner (Lh 2026-10-17: a small card's corner shrinks to keep the big card's PROPORTION). Measured at 375:
84
+ whole cards stand 186–437 tall, corner/height 0.095–0.224 (Card · Skeleton 0.122); a two-line row stands 88, so its
85
+ corner is 11–20 — at 41.6 it was 0.475, a pill. 13 = 0.148 of 88. On --r-base, so a square theme squares it too */
83
86
  --r-media: var(--r-base); /* 20.8 images inside cards */
84
87
  --r-control: calc(var(--r-base) * 0.875); /* 18.2 fields, thumbs, menu rows */
85
88
  --r-badge: calc(var(--r-base) * 0.625); /* 13 tile badge, bars */