@usableapp/cardds 0.7.74 → 0.7.76

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 (50) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +24 -9
  3. package/css/actions.css +6 -3
  4. package/css/layover.css +1 -1
  5. package/css/lists.css +1 -1
  6. package/css/stack.css +8 -1
  7. package/dist/actions/{ActionNote.d.ts → BarNote.d.ts} +3 -3
  8. package/dist/actions/{ActionNote.js → BarNote.js} +2 -2
  9. package/dist/actions/BtnGroup.d.ts +11 -0
  10. package/dist/actions/{Segment.js → BtnGroup.js} +4 -4
  11. package/dist/actions/{WaitLink.d.ts → Countdown.d.ts} +3 -3
  12. package/dist/actions/{WaitLink.js → Countdown.js} +2 -2
  13. package/dist/actions/Link.d.ts +2 -2
  14. package/dist/cardds.css +16 -6
  15. package/dist/cards/Card.d.ts +5 -3
  16. package/dist/cards/Card.js +26 -4
  17. package/dist/cards/CardParts.d.ts +1 -1
  18. package/dist/cards/CardParts.js +1 -1
  19. package/dist/cards/cardGo.d.ts +2 -0
  20. package/dist/cards/cardGo.js +27 -0
  21. package/dist/catalog.json +1 -1
  22. package/dist/index.d.ts +7 -7
  23. package/dist/index.js +7 -7
  24. package/dist/journey/{TileBadge.d.ts → JourneyBadge.d.ts} +3 -3
  25. package/dist/journey/{TileBadge.js → JourneyBadge.js} +2 -2
  26. package/dist/numbers/NumberBand.d.ts +18 -0
  27. package/dist/numbers/{Band.js → NumberBand.js} +4 -4
  28. package/dist/scaffold/ActionBar.d.ts +2 -2
  29. package/dist/scaffold/ActionBar.js +1 -1
  30. package/dist/scaffold/Centre.d.ts +3 -3
  31. package/dist/scaffold/Centre.js +3 -3
  32. package/dist/scaffold/Page.d.ts +1 -1
  33. package/dist/scaffold/Page.js +1 -1
  34. package/dist/scaffold/Pager.d.ts +11 -0
  35. package/dist/scaffold/Pager.js +6 -0
  36. package/dist/scaffold/Panes.d.ts +2 -2
  37. package/dist/scaffold/Panes.js +3 -3
  38. package/dist/scaffold/Screen.d.ts +1 -1
  39. package/dist/scaffold/Screen.js +1 -1
  40. package/dist/sheets/{Drawer.d.ts → Modal.d.ts} +12 -12
  41. package/dist/sheets/{Drawer.js → Modal.js} +9 -9
  42. package/dist/templates/Walkthrough.d.ts +4 -4
  43. package/dist/templates/Walkthrough.js +2 -2
  44. package/lib/mcp.mjs +1 -1
  45. package/lint/rules.mjs +5 -4
  46. package/package.json +1 -1
  47. package/dist/actions/Segment.d.ts +0 -11
  48. package/dist/numbers/Band.d.ts +0 -18
  49. package/dist/scaffold/PagerAt.d.ts +0 -11
  50. package/dist/scaffold/PagerAt.js +0 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
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.76 — 2026-10-09
6
+
7
+ ### Changed
8
+ - **A card with one button is pressed whole** (Lh 2026-10-09) — a `Card` whose ONLY control is its foot's button (`foot` / `CardFoot`), or the end button of a `Card row`, now answers a touch anywhere on it: the button's own action runs once (its `onClick`, its `href`, its `popoverTarget`), and the whole card dims while the finger is down. Before → after: only the button could be pressed, the rest of the card did nothing → the heading, the words and the empty room press the button. Nothing to write in an app, and no prop turns it off. A card `onClick` of your own still runs once per press. Unchanged: a card with two or more controls (a second button, a field, a choice, a button in a card inside it), a card whose one button is in its head or in its content, a disabled button, a `Sheet`, a sheet of a `SheetStack`, a `Modal`, a card that is a link or a label. If a screen relied on a touch beside the lone foot button doing nothing, that touch now presses it. Story `Card · PressedWhole`.
9
+
10
+ - **Cards in a closed stack's base: room under the last one, and no shadow** (Lh 2026-10-09, from timebank's home) — loose things in `SheetStack base` / `Home base` rest against the heads. Before → after: the last one's bottom edge lay ON the first sheet's top edge → it keeps the same room under it as between two of them (20px at the defaults). A base that is one `Page` (reads from the top) or one `Centre` (stands in the middle) is unchanged. And a `Card` standing in the base is FLAT in a coloured theme (`data-palette`) and under `data-finish="lit"`: the base clips at its own edges, so a card's shadow ended in a straight line under the top bar and along the first sheet. Nothing to change in a project. New knob `--stack-base-gap` (the room between the base's things and under the last one; default the card gap, `--sp-5`). The room above the heads is shorter by that gap for loose content — on a short phone check what you put there still fits. Story `SheetStack · ClosedCards`.
11
+
12
+ ## 0.7.75 — 2026-10-09
13
+
14
+ ### Changed — ten names say what the component is for (BREAKING: the old names are gone)
15
+ A name that said a shape now says a function (Lh 2026-10-08). The CSS classes and every knob are UNCHANGED, so a project's `theme.css` and its `--segment-*` / `--band-*` / `--wait-link-*` knobs keep working; only the import changes. Nothing of the old names is exported any more — update the import or the build fails (Lh 2026-10-09: a clean cut, no alias).
16
+
17
+ | was | is | what it is for |
18
+ |---|---|---|
19
+ | `Segment`, `SegmentItem` | `BtnGroup`, `BtnGroupItem` | a panel of buttons, one on at a time (a grouped button; "segment" is kept for a divider of content) |
20
+ | `WaitLink` | `Countdown` | a link that counts down and takes no tap until the time has passed |
21
+ | `PagerAt` | `Pager` | the dots that say where one is in a walk (the bar is still `ActionBar variant="pager"`) |
22
+ | `TileBadge` | `JourneyBadge` | the small badge on a journey's tile |
23
+ | `Band`, `BandStack` | `NumberBand`, `NumberBandStack` | one number as a band; several stacked |
24
+ | `Lift` | `ModalBody` | the part of a Modal that lifts: what is shown above the question |
25
+ | `Drawer` | `ModalAsk` | the question and its answers at the Modal's foot (`ModalAsk light` as before) |
26
+ | `ActionNote` | `BarNote` | words standing where the bar's button would be |
27
+
28
+ Their Props types follow (`BtnGroupProps`, `ModalAskProps`…). `Centre`, `CtaPack`, `ActionBarTier`, `BtnTrio`, `IconBtnGroup` keep their names.
29
+
30
+ ### Changed — fewer stories, one per prop axis
31
+ Ten gallery stories that differed from a sibling only by a tone, a side or an on/off are folded into that sibling (values of one axis stand side by side in one story): `Mood/Default` (was Picked + Unpicked), `Sticky/Looks` (was Tones + Flat + Tilt), `AvatarPick/Inset` (+ OnDark), `Modal/Confirm` (+ ConfirmLight), `Sheet/Half` (+ Look), `Field/Plain` (+ WithIcon); `StickyWelcome/WelcomeRight`, `SheetStack/ClosedOpened` and `SheetStack/StackBackBackOfStack` are gone. 419 → 409 stories; the catalog's `scaffold` ids change with them. The rendered html of every other story is byte-identical.
32
+
5
33
  ## 0.7.74 — 2026-10-08
6
34
 
7
35
  ### Added
package/README.md CHANGED
@@ -205,6 +205,10 @@ the card is the material, the sheet is what it is in the stack.
205
205
  front one too, peeks at the stack's bottom; the headroom above is the base's.
206
206
  An optional `<div class="sheet-stack__base">` — written AFTER the sheets, so
207
207
  it takes no sheet slot — is what the base shows there, behind every sheet.
208
+ Loose things in it rest against the heads, with the same room under the
209
+ last one as between two of them (`--stack-base-gap`); one `Page` reads from
210
+ the top, one `Centre` stands in the middle. A card standing there is flat:
211
+ the base clips at its edges, so a shadow would end in a straight line.
208
212
  A tap, or a pull up, opens any sheet over it. The open title or its back
209
213
  button, tapped or pulled down, drops it home. A pull down in the open
210
214
  content, already at the top, asks `onRefresh` — omit it and that pull only
@@ -239,16 +243,16 @@ of `Screen`:
239
243
  |---|---|---|
240
244
  | **top** | `top` | `TopBar` · `AppBar` · `"lead"` (no bar: the content starts `--screen-lead`, 15% of the screen, down) |
241
245
  | **content** | children | a `Step` (`Pages` · `Page` · `Page` + `Sheet`) · `Centre` · `SheetStack` … |
242
- | **bottom** | `bottom` | `ActionBar` (rows: `controls` · the one `cta` row — the pager ← → or one wide button · a `ctaLink`) · `FloatBar` · nothing |
246
+ | **bottom** | `bottom` | `ActionBar` (rows: `controls` · the one `cta` row — the pager bar ← → or one wide button · a `ctaLink`) · `FloatBar` · nothing |
243
247
 
244
248
  ```tsx
245
249
  <Screen fill top="lead" bottom={<ActionBar variant="pager">…</ActionBar>}>
246
- <Step><Pages>…</Pages><PagerAt at={1} of={3} /></Step>
250
+ <Step><Pages>…</Pages><Pager at={1} of={3} /></Step>
247
251
  </Screen>
248
252
  ```
249
253
 
250
254
  - **Top and content are one flex column; the bottom is a LAYER** (Lh 2026-09-23). In `Screen fill` the content runs to the
251
- screen's bottom edge; the bottom (`ActionBar`, with the walk's `PagerAt` as its first row) lies over it at the foot and
255
+ screen's bottom edge; the bottom (`ActionBar`, with the walk's `Pager` as its first row) lies over it at the foot and
252
256
  covers what is under it — on purpose, nothing is added to the content to make room. The keyboard lifts the layer (`--kb`).
253
257
  - **The bar is INSIDE the screen** — `bottom`, never a sibling after `</Screen>`, never pinned with a hand-set `position`.
254
258
  A `FloatBar` in `bottom` floats the same way.
@@ -339,7 +343,7 @@ whose markup has rules, because the sheet's box is the step itself:
339
343
  needs to: the body's bottom padding is the part that slid under (`--_cover`), so the last row comes up to
340
344
  the button at half and 3q as it does at full.
341
345
  - **Layers: the base, the sheet over it, the top bar** (Lh 2026-09-20). The `.action-bar` belongs to the
342
- BASE — the move of a page of words (a pager, or one wide button). It is never in front of a sheet. A
346
+ BASE — the move of a page of words (a pager bar, or one wide button). It is never in front of a sheet. A
343
347
  screen with a sheet finishes ON the sheet: a `.card__foot`, the sheet's last child — ONE row, the page's
344
348
  conclusion. The foot stands on the screen's bottom edge at half, 3q and full alike (a layer as tall as
345
349
  the sheet, slid up by the share the sheet slid down — `sheet.css`), and is gone at peek. Nothing inside
@@ -404,19 +408,19 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
404
408
  above the centre (2 × (40% − the title bar)): a card taller than that is
405
409
  not a centred card — it is a plain scrolling `.screen` (the detail view).
406
410
  - **A small group at the same centre** — an empty state; a card with its quiet
407
- second button and the pager dots under it — is `.screen__centre` (React: `Centre`), a block in the fill
411
+ second button and the `Pager` dots under it — is `.screen__centre` (React: `Centre`), a block in the fill
408
412
  screen after the topbar. Same placement and cap; it runs edge to edge with
409
413
  the inset as padding, and stacks its children at the
410
414
  screen's gap. Never hand-lay `top: 40%` on a screen.
411
415
  - **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
412
416
  control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
413
417
  control tier above the buttons (it publishes the taller `--bar-reserve` the centred card still reads for its cap).
414
- - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
418
+ - **The round pager bar** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
415
419
  holding two `.icon-btn`s and nothing else: they stand at the two edges. The
416
420
  step that commits still goes back to one wide button (rule 3).
417
421
  - **Page that moves** (Lh 2026-09-20) — two ways, never both on one screen:
418
422
  - *Pages of words:* `.step > .pages > .page` (one `aria-current="step"`, the rest `inert`) — every page
419
- one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
423
+ one height, the round pager bar turns them (`slideTo()`), the words slide like a page. No sheet there.
420
424
  ONE SCREEN TO THE NEXT (a route change) slides the WHOLE screen the same way: `slideScreen(() => navigate(to), 'next' | 'back')`,
421
425
  and `installScreenSlide()` once at app start for the back button (story `Step · WalksOn`). `data-slide="off"` on a Screen = no slide.
422
426
  ONE OUTCOME, DRAWN ONCE (Lh 2026-10-01): a screen that can come out more than one way (signed in or not, a list or
@@ -449,7 +453,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
449
453
  it scroll clear.
450
454
  **Never a form taken apart** — one sheet per field, a "send" left on the base: a form lives whole in ONE sheet.
451
455
  Fixture `tests/fixtures/calls.html`, tests `tests/calls.spec.js`, stories `Step · Walks` / `Step · Calls`.
452
- - **A pager turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
456
+ - **A pager bar turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
453
457
  sheet means this screen has work that cannot be walked past: no ← →, no bar at all — the sheet's
454
458
  own foot finishes it. (Guidance — not enforced.)
455
459
  - A screen is **top · content · bottom** on the base (*Screen: top · content · bottom*). The base is the
@@ -476,6 +480,17 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
476
480
  `<Card heading caption end foot footLink>` with the content as children —
477
481
  the card writes the order (head first, foot last). A head of its own shape
478
482
  is a `<CardHead>` child instead; never both (Lh 2026-09-27).
483
+ - **A card that goes on is pressed whole** (Lh 2026-10-09). A card whose ONLY
484
+ control is its foot's button, or the end button of a row card, is pressed
485
+ anywhere: a touch off the button runs that button's own action once, and the
486
+ whole card dims under the finger (`.card--go`, the card-wide press answer of
487
+ a row or a card link). `Card` does it by itself — the app wires the button
488
+ and nothing else, and there is no prop to turn it off. The keyboard and a
489
+ screen reader meet the button only. Every other card is only read: two or
490
+ more controls anywhere in it (a field, a choice, a nested card's button),
491
+ the one button in the head or in the content, a disabled button, a sheet, a
492
+ stack's sheet, a modal. A card that is itself a link or a label was pressed
493
+ whole already. Story `Card · PressedWhole`.
479
494
  - A page with **one feature and one CTA** (an install page, a single
480
495
  decision) may be contained in one card: head, the pitch, foot.
481
496
  - **A bottom sheet holds what the member acts on** — controls, or small
@@ -700,7 +715,7 @@ question and hunts for its answer. Rules 11–13 serve that:
700
715
  not filled with more content; a new edge is made instead (line the buttons
701
716
  up with the avatar).
702
717
  12. **Show, don't explain.** Where a known picture carries it — an avatar for a
703
- person, an icon, a `Ring`/`Track`/`Band`, a `Chip` for a category — use it
718
+ person, an icon, a `Ring`/`Track`/`NumberBand`, a `Chip` for a category — use it
704
719
  instead of words. Never add a label, a caption line, a comparison or a
705
720
  tooltip to explain what the picture already says (the retired overline was
706
721
  that mistake). Long lists are differentiated, not spaced out: group them
package/css/actions.css CHANGED
@@ -361,7 +361,10 @@
361
361
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
362
362
  something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
363
363
  instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
364
- pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
364
+ pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
365
+ .card--go (Lh 2026-10-09) = a card pressed whole: `Card` renders it while its ONE control is its foot's button or a row card's end
366
+ button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
367
+ button's, the card itself is never focused or disabled. */
365
368
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
366
369
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
367
370
  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) {
@@ -369,7 +372,7 @@
369
372
  touch-action: manipulation;
370
373
  transition: scale var(--motion-press) ease-out;
371
374
  }
372
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge) {
375
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
373
376
  cursor: pointer;
374
377
  touch-action: manipulation;
375
378
  transition: opacity var(--motion-press) ease-out;
@@ -384,7 +387,7 @@
384
387
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
385
388
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
386
389
  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):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
387
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
390
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
388
391
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
389
392
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
390
393
  it darkens a touch instead (--press-shade) */
package/css/layover.css CHANGED
@@ -79,7 +79,7 @@
79
79
  --accent: var(--card-ink);
80
80
  --on-accent: var(--card-bg);
81
81
  }
82
- /* the CONFIRM: a Modal with no Lift — the drawer IS the card, as tall as its words (no fixed band, no second beat) */
82
+ /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
83
83
  .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
84
84
  .modal__drawer .t-caption { opacity: var(--dim); }
85
85
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
package/css/lists.css CHANGED
@@ -201,7 +201,7 @@
201
201
  .link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
202
202
  .link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
203
203
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
204
- .link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a WaitLink counting down) */
204
+ .link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a Countdown) */
205
205
  .link--wait { font-variant-numeric: tabular-nums; } /* the countdown's digits keep their width: the words do not jiggle each second */
206
206
  .card--3 .link--muted { color: var(--card-3-muted); }
207
207
 
package/css/stack.css CHANGED
@@ -280,9 +280,10 @@
280
280
  z-index: 0;
281
281
  height: var(--_headroom, 0px);
282
282
  padding-inline: var(--screen-pad);
283
+ --_gap: var(--card-gap, var(--sp-5));
283
284
  display: grid;
284
285
  align-content: end;
285
- gap: var(--card-gap, var(--sp-5));
286
+ gap: var(--stack-base-gap, var(--_gap));
286
287
  overflow: hidden;
287
288
  overscroll-behavior: none;
288
289
  touch-action: none;
@@ -295,6 +296,12 @@
295
296
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
296
297
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
297
298
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
299
+ /* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
300
+ bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
301
+ .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) { padding-bottom: var(--stack-base-gap, var(--_gap)); }
302
+ /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
303
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
304
+ :is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
298
305
 
299
306
  /* ============================================================
300
307
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
@@ -1,11 +1,11 @@
1
1
  import type { ComponentProps } from 'react';
2
- export interface ActionNoteProps extends ComponentProps<'div'> {
2
+ export interface BarNoteProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /**
5
- * ActionNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
5
+ * BarNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
6
6
  * size (full width, `--btn-h` tall), lying on the ground — no fill, no border, no rounding — holding one line of muted words, an
7
7
  * inline `Icon` allowed ("กดที่ <Icon name="share" /> ด้านล่างนี้" — the member does the thing elsewhere). It does nothing on a tap.
8
8
  * A member of the BOTTOM: a `CardFoot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
9
9
  * it (CardFoot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
10
10
  */
11
- export declare function ActionNote({ className, children, ...rest }: ActionNoteProps): import("react").JSX.Element;
11
+ export declare function BarNote({ className, children, ...rest }: BarNoteProps): import("react").JSX.Element;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
- * ActionNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
4
+ * BarNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
5
5
  * size (full width, `--btn-h` tall), lying on the ground — no fill, no border, no rounding — holding one line of muted words, an
6
6
  * inline `Icon` allowed ("กดที่ <Icon name="share" /> ด้านล่างนี้" — the member does the thing elsewhere). It does nothing on a tap.
7
7
  * A member of the BOTTOM: a `CardFoot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
8
8
  * it (CardFoot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
9
9
  */
10
- export function ActionNote({ className, children, ...rest }) {
10
+ export function BarNote({ className, children, ...rest }) {
11
11
  return _jsx("div", { role: "note", className: cx('action-note', className), ...rest, children: _jsx("span", { className: "action-note__words", children: children }) });
12
12
  }
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps } from 'react';
2
+ export interface BtnGroupProps extends ComponentProps<'div'> {
3
+ }
4
+ /** BtnGroup — a panel of buttons, one on at a time (a tab strip, a day picker): its items share the width; children are BtnGroupItems. */
5
+ export declare function BtnGroup({ className, ...rest }: BtnGroupProps): import("react").JSX.Element;
6
+ export interface BtnGroupItemProps extends ComponentProps<'button'> {
7
+ /** the selected tab (filled) */
8
+ selected?: boolean;
9
+ }
10
+ /** BtnGroupItem — one tab in a BtnGroup. */
11
+ export declare function BtnGroupItem({ selected, className, ...rest }: BtnGroupItemProps): import("react").JSX.Element;
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** Segment — a tab strip / day picker whose items share the width; children are SegmentItems. */
4
- export function Segment({ className, ...rest }) {
3
+ /** BtnGroup — a panel of buttons, one on at a time (a tab strip, a day picker): its items share the width; children are BtnGroupItems. */
4
+ export function BtnGroup({ className, ...rest }) {
5
5
  return _jsx("div", { className: cx('segment', className), role: "tablist", ...rest });
6
6
  }
7
- /** SegmentItem — one tab in a Segment. */
8
- export function SegmentItem({ selected, className, ...rest }) {
7
+ /** BtnGroupItem — one tab in a BtnGroup. */
8
+ export function BtnGroupItem({ selected, className, ...rest }) {
9
9
  return _jsx("button", { type: "button", role: "tab", "aria-selected": selected ? 'true' : undefined, className: cx('segment__item', className), ...rest });
10
10
  }
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type LinkProps } from './Link.js';
3
- export interface WaitLinkProps extends Omit<LinkProps, 'href'> {
3
+ export interface CountdownProps extends Omit<LinkProps, 'href'> {
4
4
  /** CONTROLLED: when it may be pressed again (a time in ms, `Date.now()` based). Before that it counts down and takes no taps;
5
5
  * null / past = ready. The APP owns the rule — 30 s, +30 s each time — and sets a new `until` in its onClick. */
6
6
  until?: number | null;
@@ -8,7 +8,7 @@ export interface WaitLinkProps extends Omit<LinkProps, 'href'> {
8
8
  waiting?: (left: string) => ReactNode;
9
9
  }
10
10
  /**
11
- * WaitLink — a Link that waits: "ขอรหัสใหม่ได้ใน 0:27", counting down each second, then "ขอรหัสใหม่" to press (a new OTP, a retry).
11
+ * Countdown — a Link that waits: "ขอรหัสใหม่ได้ใน 0:27", counting down each second, then "ขอรหัสใหม่" to press (a new OTP, a retry).
12
12
  * A link, not a button: it is the step's second way, under its control; in a SheetStep it stands centred on its own row.
13
13
  */
14
- export declare function WaitLink({ until, waiting, children, className, disabled, ...rest }: WaitLinkProps): import("react").JSX.Element;
14
+ export declare function Countdown({ until, waiting, children, className, disabled, ...rest }: CountdownProps): import("react").JSX.Element;
@@ -3,10 +3,10 @@ import { useEffect, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Link } from './Link.js';
5
5
  /**
6
- * WaitLink — a Link that waits: "ขอรหัสใหม่ได้ใน 0:27", counting down each second, then "ขอรหัสใหม่" to press (a new OTP, a retry).
6
+ * Countdown — a Link that waits: "ขอรหัสใหม่ได้ใน 0:27", counting down each second, then "ขอรหัสใหม่" to press (a new OTP, a retry).
7
7
  * A link, not a button: it is the step's second way, under its control; in a SheetStep it stands centred on its own row.
8
8
  */
9
- export function WaitLink({ until, waiting, children, className, disabled, ...rest }) {
9
+ export function Countdown({ until, waiting, children, className, disabled, ...rest }) {
10
10
  const [now, setNow] = useState(() => Date.now());
11
11
  const left = until != null ? Math.ceil((until - now) / 1000) : 0;
12
12
  useEffect(() => {
@@ -9,12 +9,12 @@ export interface LinkProps extends ComponentProps<'a'> {
9
9
  sm?: boolean;
10
10
  /** without an href it renders as a button */
11
11
  href?: string;
12
- /** a button link that takes no taps now (reads inactive: muted) — `WaitLink` while it counts down */
12
+ /** a button link that takes no taps now (reads inactive: muted) — `Countdown` while it counts down */
13
13
  disabled?: boolean;
14
14
  }
15
15
  /** Link — an inline text action big enough to tap (40px): "See all ↗" in a card head, "Skip" between pager buttons. */
16
16
  export declare function Link({ muted, sm, icon, href, className, children, ...rest }: LinkProps): import("react").DetailedReactHTMLElement<{
17
- /** a button link that takes no taps now (reads inactive: muted) — `WaitLink` while it counts down */
17
+ /** a button link that takes no taps now (reads inactive: muted) — `Countdown` while it counts down */
18
18
  disabled?: boolean;
19
19
  ref?: import("react").Ref<HTMLAnchorElement> | undefined;
20
20
  key?: import("react").Key | null | undefined;
package/dist/cardds.css CHANGED
@@ -1304,9 +1304,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1304
1304
  z-index: 0;
1305
1305
  height: var(--_headroom, 0px);
1306
1306
  padding-inline: var(--screen-pad);
1307
+ --_gap: var(--card-gap, var(--sp-5));
1307
1308
  display: grid;
1308
1309
  align-content: end;
1309
- gap: var(--card-gap, var(--sp-5));
1310
+ gap: var(--stack-base-gap, var(--_gap));
1310
1311
  overflow: hidden;
1311
1312
  overscroll-behavior: none;
1312
1313
  touch-action: none;
@@ -1319,6 +1320,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1319
1320
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
1320
1321
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
1321
1322
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
1323
+ /* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
1324
+ bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
1325
+ .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) { padding-bottom: var(--stack-base-gap, var(--_gap)); }
1326
+ /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
1327
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
1328
+ :is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
1322
1329
 
1323
1330
  /* ============================================================
1324
1331
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
@@ -2720,7 +2727,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2720
2727
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
2721
2728
  something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
2722
2729
  instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
2723
- pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
2730
+ pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
2731
+ .card--go (Lh 2026-10-09) = a card pressed whole: `Card` renders it while its ONE control is its foot's button or a row card's end
2732
+ button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
2733
+ button's, the card itself is never focused or disabled. */
2724
2734
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2725
2735
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2726
2736
  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) {
@@ -2728,7 +2738,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2728
2738
  touch-action: manipulation;
2729
2739
  transition: scale var(--motion-press) ease-out;
2730
2740
  }
2731
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge) {
2741
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
2732
2742
  cursor: pointer;
2733
2743
  touch-action: manipulation;
2734
2744
  transition: opacity var(--motion-press) ease-out;
@@ -2743,7 +2753,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2743
2753
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2744
2754
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2745
2755
  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):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
2746
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
2756
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
2747
2757
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2748
2758
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
2749
2759
  it darkens a touch instead (--press-shade) */
@@ -3662,7 +3672,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3662
3672
  .link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
3663
3673
  .link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
3664
3674
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
3665
- .link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a WaitLink counting down) */
3675
+ .link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a Countdown) */
3666
3676
  .link--wait { font-variant-numeric: tabular-nums; } /* the countdown's digits keep their width: the words do not jiggle each second */
3667
3677
  .card--3 .link--muted { color: var(--card-3-muted); }
3668
3678
 
@@ -4785,7 +4795,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4785
4795
  --accent: var(--card-ink);
4786
4796
  --on-accent: var(--card-bg);
4787
4797
  }
4788
- /* the CONFIRM: a Modal with no Lift — the drawer IS the card, as tall as its words (no fixed band, no second beat) */
4798
+ /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
4789
4799
  .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
4790
4800
  .modal__drawer .t-caption { opacity: var(--dim); }
4791
4801
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
@@ -1,4 +1,4 @@
1
- import { type ComponentProps, type ReactNode } from 'react';
1
+ 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 };
@@ -19,7 +19,7 @@ export interface CardProps extends ComponentProps<'article'> {
19
19
  fold?: boolean;
20
20
  /** one card at the human centre of a `Screen fill` (the introduction screens) */
21
21
  centre?: boolean;
22
- /** a data band: label · giant number · footnote — see Band */
22
+ /** a data band: label · giant number · footnote — see NumberBand */
23
23
  band?: boolean;
24
24
  /** the element: article (default), a when it links, label when it wraps an input, div/section */
25
25
  as?: 'article' | 'div' | 'a' | 'label' | 'section' | 'li';
@@ -47,9 +47,11 @@ export interface CardProps extends ComponentProps<'article'> {
47
47
  * (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
48
48
  * boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
49
49
  * A page with one feature and one CTA may be a single card. Keep everything big.
50
+ * 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
51
+ * 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.
50
52
  */
51
53
  export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
52
- onClick: import("react").MouseEventHandler<HTMLElement> | undefined;
54
+ onClick: (e: MouseEvent<HTMLElement>) => void;
53
55
  "aria-busy"?: (boolean | "true" | "false") | undefined;
54
56
  ref: (node: HTMLElement | null) => void;
55
57
  className: string;
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createElement, isValidElement, useRef } from 'react';
2
+ import { Children, createElement, isValidElement, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { 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
8
  import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/stackCard.js';
8
9
  /**
9
10
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -11,6 +12,8 @@ import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/
11
12
  * (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
12
13
  * boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
13
14
  * A page with one feature and one CTA may be a single card. Keep everything big.
15
+ * 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
+ * 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.
14
17
  */
15
18
  export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }) {
16
19
  const head = heading != null || caption != null || end != null;
@@ -34,11 +37,30 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
34
37
  else if (passed)
35
38
  passed.current = node;
36
39
  };
40
+ const [go, setGo] = useState(false);
41
+ useLayoutEffect(() => { setGo(own.current != null && goControl(own.current) != null); }); // every render: what the card holds is its children's
42
+ const pressing = useRef(false);
37
43
  const click = (e) => {
44
+ if (pressing.current)
45
+ return; // the control's click, pressed from here, coming back up: the app's onClick has run for this press
38
46
  onClick?.(e);
47
+ const target = e.target;
48
+ const card = own.current;
49
+ if (!e.defaultPrevented && card?.contains(target)) {
50
+ const control = goControl(card);
51
+ if (control && !control.contains(target)) {
52
+ pressing.current = true;
53
+ try {
54
+ control.click();
55
+ }
56
+ finally {
57
+ pressing.current = false;
58
+ }
59
+ return;
60
+ }
61
+ }
39
62
  if (!stack || e.defaultPrevented || swallow.current)
40
63
  return;
41
- const target = e.target;
42
64
  if (target.closest('.sheet-stack__back')) {
43
65
  stack.onClose();
44
66
  return;
@@ -54,10 +76,10 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
54
76
  const props = {
55
77
  ...rest,
56
78
  ref: setRef,
57
- 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', painted.className, className),
79
+ 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', painted.className, className),
58
80
  style: painted.style || style ? { ...painted.style, ...style } : undefined,
59
81
  ...(stack ? { 'aria-busy': painted.busy } : {}),
60
- onClick: stack ? click : onClick,
82
+ onClick: click,
61
83
  };
62
84
  if (!head && !tail)
63
85
  return createElement(as, props, inner(children));
@@ -1,7 +1,7 @@
1
1
  import type { ComponentProps } from 'react';
2
2
  export interface CardMediaProps extends ComponentProps<'div'> {
3
3
  }
4
- /** CardMedia — rounded media inside a card (a Placeholder or img); a corner TileBadge pins to it. */
4
+ /** CardMedia — rounded media inside a card (a Placeholder or img); a corner JourneyBadge pins to it. */
5
5
  export declare function CardMedia({ className, ...rest }: CardMediaProps): import("react").JSX.Element;
6
6
  export interface CardBodyProps extends ComponentProps<'div'> {
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** CardMedia — rounded media inside a card (a Placeholder or img); a corner TileBadge pins to it. */
3
+ /** CardMedia — rounded media inside a card (a Placeholder or img); a corner JourneyBadge pins to it. */
4
4
  export function CardMedia({ className, ...rest }) {
5
5
  return _jsx("div", { className: cx('card__media', className), ...rest });
6
6
  }
@@ -0,0 +1,2 @@
1
+ /** the control this card presses when it is touched off that control, or null: the card is only read */
2
+ export declare function goControl(card: HTMLElement): HTMLElement | null;
@@ -0,0 +1,27 @@
1
+ /* THE RULE (Lh 2026-10-09): a card that holds exactly ONE control, standing where the card's own move sits (its foot, or the end of a
2
+ row card), is pressed whole — a touch anywhere on it is a press on that control. Asked by `Card` only, once for the class it
3
+ renders and once, fresh, for each click. */
4
+ /** everything a member presses, types in or drags. One more of these anywhere in the card (a nested card's too) and the card is only read */
5
+ const CONTROLS = [
6
+ 'button', 'a[href]', 'input:not([type=hidden])', 'textarea', 'select', 'summary',
7
+ '[role=button]', '[role=link]', '[role=tab]', '[role=radio]', '[role=checkbox]', '[role=switch]', '[role=slider]', '[role=menuitem]',
8
+ '[tabindex]:not([tabindex="-1"])', '[contenteditable]',
9
+ '.stamp-on', // dragged
10
+ ].join(', ');
11
+ /** a card that is pressed whole already (a link, a label, a button), or where a touch means something else: a sheet is where the member
12
+ * acts, a modal asks, a touch on a stack's sheet opens it (`stack-card` is what `Card` renders from its `stack` prop) */
13
+ const NOT_A_GO_CARD = 'a, label, button, .sheet, .modal, .stack-card';
14
+ /** the control this card presses when it is touched off that control, or null: the card is only read */
15
+ export function goControl(card) {
16
+ if (card.matches(NOT_A_GO_CARD))
17
+ return null;
18
+ const controls = card.querySelectorAll(CONTROLS);
19
+ if (controls.length !== 1)
20
+ return null;
21
+ const control = controls[0];
22
+ if (control.matches(':disabled, [aria-disabled="true"]'))
23
+ return null;
24
+ const inFoot = control.parentElement?.matches('.card__foot') && control.parentElement.parentElement === card;
25
+ const atRowEnd = card.matches('.card--row') && card.lastElementChild === control;
26
+ return inFoot || atRowEnd ? control : null;
27
+ }