@usableapp/cardds 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -104
- package/cardds.js +6 -445
- package/css/actions.css +3 -8
- package/css/card.css +2 -2
- package/css/sheet.css +26 -9
- package/css/step.css +6 -6
- package/css/tokens.css +2 -0
- package/dist/actions/Dropdown.d.ts +34 -17
- package/dist/actions/Dropdown.js +41 -11
- package/dist/actions/IconBtn.d.ts +296 -9
- package/dist/actions/IconBtn.js +6 -8
- package/dist/cardds.css +39 -25
- package/dist/choice/Calendar.d.ts +17 -6
- package/dist/choice/Calendar.js +9 -3
- package/dist/choice/Slider.d.ts +1 -1
- package/dist/choice/Slider.js +12 -8
- package/dist/forms/Pin.d.ts +9 -6
- package/dist/forms/Pin.js +47 -18
- package/dist/media/Postcard.d.ts +8 -4
- package/dist/media/Postcard.js +8 -3
- package/dist/numbers/Picker.d.ts +19 -7
- package/dist/numbers/Picker.js +51 -13
- package/dist/scaffold/BaseContent.d.ts +4 -4
- package/dist/scaffold/BaseContent.js +4 -4
- package/dist/sheets/Drawer.d.ts +14 -3
- package/dist/sheets/Drawer.js +29 -5
- package/dist/sheets/Sheet.d.ts +28 -15
- package/dist/sheets/Sheet.js +157 -11
- package/dist/sheets/SheetStack.d.ts +13 -1
- package/dist/sheets/SheetStack.js +74 -2
- package/package.json +1 -1
package/dist/cardds.css
CHANGED
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
--chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
|
|
91
91
|
--bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
|
|
92
92
|
--header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
|
|
93
|
+
--card-pad-rest: var(--sp-5); /* 20 a card's padding before any variant — .card publishes it as --card-pad; the sheet's peek strip is built from it where no card is in scope */
|
|
94
|
+
--card-gap-rest: var(--sp-4); /* 16 …and its gap */
|
|
93
95
|
--screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
|
|
94
96
|
--map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
|
|
95
97
|
--fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
|
|
@@ -521,8 +523,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
521
523
|
.card {
|
|
522
524
|
--card-bg: var(--card-1-bg); /* the slot, readable by children (avatar rings, fold) */
|
|
523
525
|
--card-ink: var(--card-1-ink);
|
|
524
|
-
--card-pad: var(--
|
|
525
|
-
--card-gap: var(--
|
|
526
|
+
--card-pad: var(--card-pad-rest); /* published so .bleed can cancel it exactly */
|
|
527
|
+
--card-gap: var(--card-gap-rest); /* published so a sheet's peek strip (sheet.css) is exact */
|
|
526
528
|
background: var(--card-bg);
|
|
527
529
|
color: var(--card-ink);
|
|
528
530
|
border: var(--border-w) solid var(--card-border);
|
|
@@ -1105,7 +1107,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1105
1107
|
page. Many sheets stacked as the app's menu are the other system,
|
|
1106
1108
|
.sheet-stack (stack.css): the same material, its own class and file.
|
|
1107
1109
|
|
|
1108
|
-
<article class="card sheet sheet--half"
|
|
1110
|
+
<article class="card sheet sheet--half">
|
|
1109
1111
|
A tap on the handle TOGGLES the sheet between its own state and peek
|
|
1110
1112
|
(Lh 2026-09-16) — never a step-by-step climb; a drag snaps to any of
|
|
1111
1113
|
the listed states, and the state it lands on becomes the one a tap
|
|
@@ -1123,14 +1125,12 @@ button { font: inherit; cursor: pointer; }
|
|
|
1123
1125
|
============================================================ */
|
|
1124
1126
|
|
|
1125
1127
|
.sheet {
|
|
1126
|
-
--_handle: var(--sp-1);
|
|
1127
1128
|
/* the strip a peeked sheet shows: the card's own top padding, the handle,
|
|
1128
1129
|
the gap, one title row (--tap — the head rule below makes every head at
|
|
1129
1130
|
least that tall) and the gap under it, so the cut lands exactly where
|
|
1130
1131
|
the first control starts and nothing of it leaks through. Published
|
|
1131
1132
|
(and registered in tokens.css) so a sheet whose head is taller can
|
|
1132
1133
|
retune it: .my-sheet { --sheet-peek: … } */
|
|
1133
|
-
--sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
|
|
1134
1134
|
--sheet-half: 0.5; /* a share of the sheet's own height */
|
|
1135
1135
|
--sheet-3q: 0.25; /* three quarters showing: slid by a quarter */
|
|
1136
1136
|
/* the sheet's own height, seen from inside it: the box it slides in is the
|
|
@@ -1165,14 +1165,16 @@ button { font: inherit; cursor: pointer; }
|
|
|
1165
1165
|
.sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
|
|
1166
1166
|
.sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
|
|
1167
1167
|
.sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
|
|
1168
|
+
/* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
|
|
1169
|
+
have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
|
|
1170
|
+
inherits it. A sheet whose head is taller retunes it on the box. */
|
|
1171
|
+
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--tap)); }
|
|
1172
|
+
|
|
1168
1173
|
/* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
|
|
1169
1174
|
.sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
|
|
1170
1175
|
changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
|
|
1171
|
-
with `inert
|
|
1172
|
-
|
|
1173
|
-
sheet up to full and sends any other one away; the handle takes it down to peek, never further, so the
|
|
1174
|
-
words and the other buttons are back in reach; the same button again raises it from peek to full.
|
|
1175
|
-
Nothing waits at peek before the first call. No bar on such a screen (actions.css).
|
|
1176
|
+
with `inert` (the React Sheet does). WHO brings it onto the screen is the app's business — a button, a route:
|
|
1177
|
+
cardds ties no component to another. Nothing waits at peek before the first call.
|
|
1176
1178
|
WHAT IT IS FOR: the base offers WAYS IN — sign in · new phone · join — and each button calls a sheet
|
|
1177
1179
|
that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
|
|
1178
1180
|
carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
|
|
@@ -1202,6 +1204,12 @@ button { font: inherit; cursor: pointer; }
|
|
|
1202
1204
|
the cut. A length from the box's container units (--_box), since the
|
|
1203
1205
|
cut is a share of the SHEET's height, not of the body's. */
|
|
1204
1206
|
scroll-padding-bottom: var(--_cover);
|
|
1207
|
+
/* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
|
|
1208
|
+
scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
|
|
1209
|
+
foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
|
|
1210
|
+
when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
|
|
1211
|
+
the part that shows. */
|
|
1212
|
+
padding-bottom: var(--_cover);
|
|
1205
1213
|
}
|
|
1206
1214
|
/* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
|
|
1207
1215
|
A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
|
|
@@ -1228,8 +1236,13 @@ button { font: inherit; cursor: pointer; }
|
|
|
1228
1236
|
.sheet:has(> .card__foot) > .sheet__body { margin-bottom: calc(var(--_foot) - var(--card-pad)); } /* the body stops above the strip (the card's own padding is the rest) */
|
|
1229
1237
|
.sheet.is-dragging > .card__foot { transition: none; }
|
|
1230
1238
|
:is(.sheet--peek, .sheet--away) > .card__foot { opacity: 0; visibility: hidden; transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
|
|
1239
|
+
/* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
|
|
1240
|
+
box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
|
|
1241
|
+
the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
|
|
1242
|
+
.sheet > :not(.sheet__body) { flex-shrink: 0; }
|
|
1243
|
+
:is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
|
|
1231
1244
|
.sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
|
|
1232
|
-
.sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
|
|
1245
|
+
.sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
|
|
1233
1246
|
|
|
1234
1247
|
/* grab handle — first child of the sheet. Drawn as a small bar, but it is
|
|
1235
1248
|
the thumb's grab area for the whole strip above the title: a pseudo widens
|
|
@@ -1390,6 +1403,12 @@ button { font: inherit; cursor: pointer; }
|
|
|
1390
1403
|
/* raised: the sheet floats above a footnote strip on the scrim. Its bottom
|
|
1391
1404
|
edge shows, so it gets its corners and border back; full still stops at
|
|
1392
1405
|
the top bar's line, because the top edge never moved. */
|
|
1406
|
+
/* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
|
|
1407
|
+
foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
|
|
1408
|
+
scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
|
|
1409
|
+
the sheet gives it up itself, as --_lift — the same token a raised sheet floats on. */
|
|
1410
|
+
.sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
|
|
1411
|
+
.sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
|
|
1393
1412
|
.sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--r-card); border-bottom: var(--border-w) solid var(--card-border); }
|
|
1394
1413
|
.sheet-stage__foot {
|
|
1395
1414
|
position: absolute;
|
|
@@ -1480,8 +1499,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
1480
1499
|
:has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
|
|
1481
1500
|
/* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
|
|
1482
1501
|
move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
|
|
1483
|
-
(sheet.css), and a form lives whole in ONE sheet, never item by item on the base.
|
|
1484
|
-
|
|
1502
|
+
(sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
|
|
1503
|
+
own screens are composed (the guide); the framework does not police it — a component never changes
|
|
1504
|
+
because another one is there. */
|
|
1485
1505
|
|
|
1486
1506
|
/* the reading block: a .base-content (base.css) — the same words-on-the-base
|
|
1487
1507
|
block as anywhere else (it was .step__card, and a .card before 2026-09-16;
|
|
@@ -1500,11 +1520,10 @@ button { font: inherit; cursor: pointer; }
|
|
|
1500
1520
|
color: var(--on-surface);
|
|
1501
1521
|
}
|
|
1502
1522
|
/* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
|
|
1503
|
-
(card.css).
|
|
1504
|
-
|
|
1505
|
-
|
|
1523
|
+
(card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
|
|
1524
|
+
(the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
|
|
1525
|
+
.step:has(> .sheet--peek) > :is(.base-content, .pages) { padding-bottom: var(--sheet-peek); }
|
|
1506
1526
|
.step > :is(.base-content, .pages) {
|
|
1507
|
-
padding-bottom: var(--sheet-room, 0rem);
|
|
1508
1527
|
flex: 0 1 auto;
|
|
1509
1528
|
}
|
|
1510
1529
|
.step > :is(.base-content, .pages) {
|
|
@@ -1879,14 +1898,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
1879
1898
|
<button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
|
|
1880
1899
|
</div> — or the same two in an .action-bar__tier--pager under a control */
|
|
1881
1900
|
.action-bar--pager { align-items: center; }
|
|
1882
|
-
/* The bar belongs to the BASE
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
|
|
1886
|
-
past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
|
|
1887
|
-
is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
|
|
1888
|
-
the eye cannot see. */
|
|
1889
|
-
:has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
|
|
1901
|
+
/* The bar belongs to the BASE: the move of a page of words — a pager turning them, or one wide button. A screen
|
|
1902
|
+
with a sheet finishes on the sheet's own foot instead (sheet.css). Guidance for composing, not a rule the
|
|
1903
|
+
CSS enforces: a component never changes because another one is there. */
|
|
1890
1904
|
/* the page dots: under the card in a .screen__centre group; tight gaps */
|
|
1891
1905
|
.pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
|
|
1892
1906
|
.pager__at > * {
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export interface CalendarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
export interface CalendarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title' | 'onSelect'> {
|
|
3
3
|
/** the month shown ("June") */
|
|
4
4
|
title: ReactNode;
|
|
5
5
|
/** days in the month */
|
|
6
6
|
days?: number;
|
|
7
7
|
/** weekday column of the 1st (1 = first column) */
|
|
8
8
|
start?: number;
|
|
9
|
-
/** the
|
|
10
|
-
selected?: number;
|
|
9
|
+
/** CONTROLLED: the picked day, or `null` for none. The app owns it — pair it with `onSelect`. (`undefined` = uncontrolled.) */
|
|
10
|
+
selected?: number | null;
|
|
11
|
+
/** UNCONTROLLED: the day picked on arrival; the calendar then keeps its own */
|
|
12
|
+
defaultSelected?: number;
|
|
13
|
+
/** a day was tapped */
|
|
14
|
+
onSelect?: (day: number) => void;
|
|
15
|
+
/** the head's two buttons: the app turns the month (it owns `title`, `days`, `start`) */
|
|
16
|
+
onPrev?: () => void;
|
|
17
|
+
onNext?: () => void;
|
|
18
|
+
/** their accessible names */
|
|
19
|
+
prevLabel?: string;
|
|
20
|
+
nextLabel?: string;
|
|
11
21
|
/** softly marked days */
|
|
12
22
|
marks?: number[];
|
|
13
23
|
/** strongly marked days */
|
|
@@ -19,5 +29,6 @@ export interface CalendarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'ti
|
|
|
19
29
|
/** replace the default prev/next icon buttons */
|
|
20
30
|
nav?: ReactNode;
|
|
21
31
|
}
|
|
22
|
-
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head.
|
|
23
|
-
|
|
32
|
+
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head. It shows ONE month and knows no dates:
|
|
33
|
+
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). */
|
|
34
|
+
export declare function Calendar({ title, days, start, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel, nextLabel, marks, marks2, disabled, weekdays, nav, className, ...rest }: CalendarProps): import("react").JSX.Element;
|
package/dist/choice/Calendar.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
4
|
-
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head.
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head. It shows ONE month and knows no dates:
|
|
6
|
+
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). */
|
|
7
|
+
export function Calendar({ title, days = 30, start = 1, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel = 'Previous', nextLabel = 'Next', marks = [], marks2 = [], disabled = [], weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], nav, className, ...rest }) {
|
|
8
|
+
const [inner, setInner] = useState(defaultSelected);
|
|
9
|
+
const day = selected !== undefined ? selected : inner;
|
|
10
|
+
const pick = (d) => { if (selected === undefined)
|
|
11
|
+
setInner(d); onSelect?.(d); };
|
|
12
|
+
return (_jsxs("div", { className: cx('calendar', className), ...rest, children: [_jsxs("div", { className: "calendar__head", children: [_jsx("span", { className: "t-title", children: title }), nav ?? (_jsxs("span", { className: "icon-row", children: [_jsx(IconBtn, { sm: true, icon: "chevron-left", label: prevLabel, onClick: onPrev }), _jsx(IconBtn, { sm: true, icon: "chevron-right", label: nextLabel, onClick: onNext })] }))] }), _jsxs("div", { className: "calendar__grid", children: [weekdays.map((d) => _jsx("span", { className: "calendar__wd", children: d }, d)), Array.from({ length: days }, (_, i) => i + 1).map((d) => (_jsx("button", { type: "button", className: cx('calendar__day', marks.includes(d) && 'calendar__day--mark', marks2.includes(d) && 'calendar__day--mark2'), "aria-selected": d === day ? 'true' : undefined, onClick: () => pick(d), disabled: disabled.includes(d), style: d === 1 ? { '--start': start } : undefined, children: d }, d)))] })] }));
|
|
7
13
|
}
|
package/dist/choice/Slider.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export interface SliderProps extends Omit<ComponentPropsWithoutRef<'input'>, 'cl
|
|
|
8
8
|
style?: React.CSSProperties;
|
|
9
9
|
}
|
|
10
10
|
/** Slider — one value along a hairline rail with a 48px knob; the whole range is thumb-tall. */
|
|
11
|
-
export declare function Slider({ label, glyph, className, style, ...input }: SliderProps): import("react").JSX.Element;
|
|
11
|
+
export declare function Slider({ label, glyph, className, style, onInput, ...input }: SliderProps): import("react").JSX.Element;
|
package/dist/choice/Slider.js
CHANGED
|
@@ -2,13 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
/** Slider — one value along a hairline rail with a 48px knob; the whole range is thumb-tall. */
|
|
5
|
-
export function Slider({ label, glyph, className, style, ...input }) {
|
|
5
|
+
export function Slider({ label, glyph, className, style, onInput, ...input }) {
|
|
6
6
|
const range = useRef(null);
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const box = useRef(null);
|
|
8
|
+
// the knob's place along the rail, 0..1, published on the slider as --slider-at so the glyph can ride the knob (choice.css)
|
|
9
|
+
const place = () => {
|
|
10
|
+
const r = range.current;
|
|
11
|
+
if (!r || !box.current)
|
|
12
|
+
return;
|
|
13
|
+
const min = Number(r.min) || 0, max = Number(r.max) || 100;
|
|
14
|
+
box.current.style.setProperty('--slider-at', String(max > min ? Math.min(1, Math.max(0, (Number(r.value) - min) / (max - min))) : 0));
|
|
15
|
+
};
|
|
16
|
+
useEffect(place); // after every render: a controlled value, a new min / max
|
|
17
|
+
return (_jsxs("label", { ref: box, className: cx('slider', className), style: style, children: [label != null && _jsx("span", { className: "slider__label", children: label }), glyph != null && _jsx("span", { className: "slider__glyph", "aria-hidden": "true", children: glyph }), _jsx("input", { ref: range, className: "slider__range", type: "range", onInput: (e) => { place(); onInput?.(e); }, ...input })] }));
|
|
14
18
|
}
|
package/dist/forms/Pin.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
-
export interface PinProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'> {
|
|
2
|
+
export interface PinProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue'> {
|
|
3
3
|
/** how many digits (4 or 6) */
|
|
4
4
|
length?: number;
|
|
5
|
-
/** digits
|
|
5
|
+
/** CONTROLLED: the digits so far, left to right ("03"). The app owns it — pair it with `onChange`. */
|
|
6
6
|
value?: string;
|
|
7
|
+
/** UNCONTROLLED: the digits it starts with; the Pin then keeps its own */
|
|
8
|
+
defaultValue?: string;
|
|
7
9
|
/** accessible name prefix for each cell ("Digit" → "Digit 1") */
|
|
8
10
|
label?: string;
|
|
9
|
-
/** the code so far, after every
|
|
11
|
+
/** the code so far, after every change; `complete` once every cell holds a digit */
|
|
10
12
|
onChange?: (code: string, complete: boolean) => void;
|
|
11
13
|
/** focus the first empty cell on mount */
|
|
12
14
|
autoFocus?: boolean;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Pin — big code entry, one digit per pill; a filled cell turns accent. Lives in a Sheet.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* Its own behaviour, inside it: typing a digit moves to the next cell, Backspace in an empty cell moves back and clears, the
|
|
19
|
+
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
20
|
+
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
18
21
|
*/
|
|
19
|
-
export declare function Pin({ length, value, label, onChange, autoFocus, className, ...rest }: PinProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Pin({ length, value, defaultValue, label, onChange, autoFocus, className, ...rest }: PinProps): import("react").JSX.Element;
|
package/dist/forms/Pin.js
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
/**
|
|
5
5
|
* Pin — big code entry, one digit per pill; a filled cell turns accent. Lives in a Sheet.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Its own behaviour, inside it: typing a digit moves to the next cell, Backspace in an empty cell moves back and clears, the
|
|
7
|
+
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
8
|
+
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
8
9
|
*/
|
|
9
|
-
export function Pin({ length = 6, value = '', label = 'Digit', onChange, autoFocus, className, ...rest }) {
|
|
10
|
+
export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, className, ...rest }) {
|
|
10
11
|
const ref = useRef(null);
|
|
12
|
+
const [inner, setInner] = useState(defaultValue);
|
|
13
|
+
const code = (value ?? inner).replace(/\D/g, '').slice(0, length);
|
|
14
|
+
const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
|
|
15
|
+
const set = (next) => {
|
|
16
|
+
next = next.slice(0, length);
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
setInner(next);
|
|
19
|
+
if (next !== code)
|
|
20
|
+
onChange?.(next, next.length === length);
|
|
21
|
+
};
|
|
11
22
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const on = (e) => { const d = e.detail; onChange(d.code, d.complete); };
|
|
16
|
-
el.addEventListener('cardds:pin', on);
|
|
17
|
-
return () => el.removeEventListener('cardds:pin', on);
|
|
18
|
-
}, [onChange]);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!autoFocus)
|
|
21
|
-
return;
|
|
22
|
-
const cells = ref.current?.querySelectorAll('.pin__cell');
|
|
23
|
-
if (cells)
|
|
24
|
-
([...cells].find((c) => !c.value) ?? cells[cells.length - 1])?.focus();
|
|
23
|
+
if (autoFocus)
|
|
24
|
+
cell(Math.min(code.length, length - 1))?.focus();
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- on mount only
|
|
25
26
|
}, [autoFocus]);
|
|
26
|
-
|
|
27
|
+
const input = (i) => (e) => {
|
|
28
|
+
const digits = e.target.value.replace(/\D/g, '');
|
|
29
|
+
const at = Math.min(i, code.length); // a code has no gaps: a digit typed further along lands in the first empty cell
|
|
30
|
+
if (!digits) {
|
|
31
|
+
set(code.slice(0, at) + code.slice(at + 1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
set(code.slice(0, at) + digits + code.slice(at + digits.length)); // a paste (or a typed run) spills over the cells that follow
|
|
35
|
+
cell(Math.min(at + digits.length, length - 1))?.focus();
|
|
36
|
+
};
|
|
37
|
+
const key = (i) => (e) => {
|
|
38
|
+
if (e.key === 'Backspace' && !e.currentTarget.value && i > 0) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
set(code.slice(0, i - 1) + code.slice(i));
|
|
41
|
+
cell(i - 1)?.focus();
|
|
42
|
+
}
|
|
43
|
+
else if (e.key === 'ArrowLeft' && i > 0) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
cell(i - 1)?.focus();
|
|
46
|
+
}
|
|
47
|
+
else if (e.key === 'ArrowRight' && i < length - 1) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
cell(i + 1)?.focus();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return (_jsx("div", { ref: ref, className: cx('pin', className), ...rest, children: Array.from({ length }, (_, i) => (
|
|
53
|
+
// no maxLength: the browser would cut a pasted code before this ever saw it. A cell holds one digit — typing into a full one
|
|
54
|
+
// replaces it, so its digit is selected whenever the cell takes the focus
|
|
55
|
+
_jsx("input", { className: "pin__cell", inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, autoComplete: i === 0 ? 'one-time-code' : 'off', onChange: input(i), onKeyDown: key(i), onFocus: (e) => e.currentTarget.select() }, i))) }));
|
|
27
56
|
}
|
package/dist/media/Postcard.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
export interface PostcardProps extends ComponentPropsWithoutRef<'div'> {
|
|
3
3
|
/** the photo on the front; none = the wireframe box */
|
|
4
4
|
photo?: string | null;
|
|
@@ -7,14 +7,18 @@ export interface PostcardProps extends ComponentPropsWithoutRef<'div'> {
|
|
|
7
7
|
name: ReactNode;
|
|
8
8
|
/** the words on the back */
|
|
9
9
|
caption?: ReactNode;
|
|
10
|
-
/**
|
|
10
|
+
/** CONTROLLED: the back is showing. The app owns it — pair it with `onFlip`. */
|
|
11
11
|
flipped?: boolean;
|
|
12
|
+
/** UNCONTROLLED: start turned over; the card then keeps its own side */
|
|
13
|
+
defaultFlipped?: boolean;
|
|
14
|
+
/** a face was tapped: the side it asks to show */
|
|
15
|
+
onFlip?: (flipped: boolean) => void;
|
|
12
16
|
frontLabel?: string;
|
|
13
17
|
backLabel?: string;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Postcard — a member's introduction as a card in the hand: the photo (or the wireframe box) with the name in front,
|
|
17
|
-
* the caption on the back; a tap on either face turns it over (
|
|
21
|
+
* the caption on the back; a tap on either face turns it over (`--motion-flip`). Which side shows is a state: `flipped` + `onFlip`, or `defaultFlipped`.
|
|
18
22
|
* A voice note, a like, a reply go OUTSIDE it, in a Row under it — a face is a button and holds no button.
|
|
19
23
|
*/
|
|
20
|
-
export declare function Postcard({ photo, alt, name, caption, flipped, frontLabel, backLabel, className, ...rest }: PostcardProps): import("react").JSX.Element;
|
|
24
|
+
export declare function Postcard({ photo, alt, name, caption, flipped, defaultFlipped, onFlip, frontLabel, backLabel, className, ...rest }: PostcardProps): import("react").JSX.Element;
|
package/dist/media/Postcard.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
4
|
/**
|
|
4
5
|
* Postcard — a member's introduction as a card in the hand: the photo (or the wireframe box) with the name in front,
|
|
5
|
-
* the caption on the back; a tap on either face turns it over (
|
|
6
|
+
* the caption on the back; a tap on either face turns it over (`--motion-flip`). Which side shows is a state: `flipped` + `onFlip`, or `defaultFlipped`.
|
|
6
7
|
* A voice note, a like, a reply go OUTSIDE it, in a Row under it — a face is a button and holds no button.
|
|
7
8
|
*/
|
|
8
|
-
export function Postcard({ photo, alt = '', name, caption, flipped, frontLabel = 'พลิกดูด้านหลัง', backLabel = 'พลิกดูด้านหน้า', className, ...rest }) {
|
|
9
|
-
|
|
9
|
+
export function Postcard({ photo, alt = '', name, caption, flipped, defaultFlipped = false, onFlip, frontLabel = 'พลิกดูด้านหลัง', backLabel = 'พลิกดูด้านหน้า', className, ...rest }) {
|
|
10
|
+
const [inner, setInner] = useState(defaultFlipped);
|
|
11
|
+
const back = flipped ?? inner;
|
|
12
|
+
const turn = () => { if (flipped === undefined)
|
|
13
|
+
setInner(!back); onFlip?.(!back); };
|
|
14
|
+
return (_jsxs("div", { className: cx('postcard', back && 'is-flipped', className), ...rest, children: [_jsxs("button", { className: "postcard__face postcard__front", type: "button", "aria-label": frontLabel, onClick: turn, children: [photo ? _jsx("img", { src: photo, alt: alt }) : alt ? _jsx("div", { className: "ph", role: "img", "aria-label": alt }) : _jsx("div", { className: "ph" }), _jsx("span", { className: "postcard__label", children: name })] }), _jsxs("button", { className: "postcard__face postcard__back", type: "button", "aria-label": backLabel, onClick: turn, children: [caption != null && _jsx("p", { className: "postcard__caption", children: caption }), _jsx("span", { className: "postcard__label", children: name })] })] }));
|
|
10
15
|
}
|
package/dist/numbers/Picker.d.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
-
export interface PickerProps extends ComponentPropsWithoutRef<'div'> {
|
|
2
|
+
export interface PickerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue'> {
|
|
3
|
+
/** CONTROLLED: the chosen PickerItem's `value`. The app owns it — pair it with `onChange`. */
|
|
4
|
+
value?: string;
|
|
5
|
+
/** UNCONTROLLED: the value it starts on; the picker then keeps its own */
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
/** the member settled on a value — scrolled the rail to it, or tapped it */
|
|
8
|
+
onChange?: (value: string) => void;
|
|
3
9
|
/** the unit under the rail ("hours") */
|
|
4
10
|
unit?: ReactNode;
|
|
5
11
|
label?: string;
|
|
6
12
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Picker — a horizontal snap picker, the centre value big between two accent rules; children are PickerItems.
|
|
15
|
+
* It holds a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`. The value IS the item standing in the centre
|
|
16
|
+
* of the rail: the picker scrolls the chosen one there, and when the member scrolls (or taps an item) it asks for the one that
|
|
17
|
+
* settled in the centre. The CSS lights the centred item by scroll position; the app never reads the DOM to know what was picked.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }: PickerProps): import("react").JSX.Element;
|
|
20
|
+
export interface PickerItemProps extends Omit<ComponentPropsWithoutRef<'button'>, 'value'> {
|
|
21
|
+
/** what choosing this item reports; default = its own text */
|
|
22
|
+
value?: string;
|
|
11
23
|
}
|
|
12
|
-
/** PickerItem — one value on the Picker rail. */
|
|
13
|
-
export declare function PickerItem({
|
|
24
|
+
/** PickerItem — one value on the Picker rail; a tap brings it to the centre. */
|
|
25
|
+
export declare function PickerItem({ value, className, children, onClick, ...rest }: PickerItemProps): import("react").JSX.Element;
|
package/dist/numbers/Picker.js
CHANGED
|
@@ -1,18 +1,56 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const PickerContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Picker — a horizontal snap picker, the centre value big between two accent rules; children are PickerItems.
|
|
7
|
+
* It holds a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`. The value IS the item standing in the centre
|
|
8
|
+
* of the rail: the picker scrolls the chosen one there, and when the member scrolls (or taps an item) it asks for the one that
|
|
9
|
+
* settled in the centre. The CSS lights the centred item by scroll position; the app never reads the DOM to know what was picked.
|
|
10
|
+
*/
|
|
11
|
+
export function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }) {
|
|
12
|
+
const [inner, setInner] = useState(defaultValue);
|
|
13
|
+
const current = value ?? inner;
|
|
6
14
|
const rail = useRef(null);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
const mounted = useRef(false);
|
|
16
|
+
const settle = useRef(null);
|
|
17
|
+
const pick = (next) => {
|
|
18
|
+
if (next === current)
|
|
19
|
+
return;
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
setInner(next);
|
|
22
|
+
onChange?.(next);
|
|
23
|
+
};
|
|
24
|
+
const items = () => [...(rail.current?.querySelectorAll(':scope > .picker__item') ?? [])];
|
|
25
|
+
const column = () => (rail.current ? rail.current.clientWidth / 3 : 0); // the rail is three columns wide; item i stands in the centre at i × one column
|
|
26
|
+
/* the chosen value stands in the centre — at once on arrival, sliding after */
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
const r = rail.current, i = items().findIndex((el) => el.dataset.value === current);
|
|
29
|
+
if (r && i >= 0 && Math.abs(r.scrollLeft - i * column()) > 1)
|
|
30
|
+
r.scrollTo({ left: i * column(), behavior: mounted.current ? 'smooth' : 'auto' });
|
|
31
|
+
mounted.current = true;
|
|
32
|
+
});
|
|
33
|
+
useEffect(() => () => { if (settle.current)
|
|
34
|
+
clearTimeout(settle.current); }, []);
|
|
35
|
+
/* the rail came to rest: whatever stands in the centre is what the member chose */
|
|
36
|
+
const scrolled = () => {
|
|
37
|
+
if (settle.current)
|
|
38
|
+
clearTimeout(settle.current);
|
|
39
|
+
settle.current = setTimeout(() => {
|
|
40
|
+
const r = rail.current, col = column();
|
|
41
|
+
if (!r || !col)
|
|
42
|
+
return;
|
|
43
|
+
const at = items()[Math.round(r.scrollLeft / col)]?.dataset.value;
|
|
44
|
+
if (at !== undefined)
|
|
45
|
+
pick(at);
|
|
46
|
+
}, 120);
|
|
47
|
+
};
|
|
48
|
+
return (_jsx(PickerContext.Provider, { value: { value: current, pick }, children: _jsxs("div", { className: cx('picker', className), "aria-label": label, "data-value": current, ...rest, children: [_jsx("div", { className: "picker__rail", role: "listbox", ref: rail, onScroll: scrolled, children: children }), unit != null && _jsx("span", { className: "picker__unit", children: unit })] }) }));
|
|
14
49
|
}
|
|
15
|
-
/** PickerItem — one value on the Picker rail. */
|
|
16
|
-
export function PickerItem({
|
|
17
|
-
|
|
50
|
+
/** PickerItem — one value on the Picker rail; a tap brings it to the centre. */
|
|
51
|
+
export function PickerItem({ value, className, children, onClick, ...rest }) {
|
|
52
|
+
const picker = useContext(PickerContext);
|
|
53
|
+
const own = value ?? (typeof children === 'string' || typeof children === 'number' ? String(children) : undefined);
|
|
54
|
+
return (_jsx("button", { type: "button", role: "option", "data-value": own, "aria-selected": own !== undefined && picker?.value === own ? 'true' : 'false', className: cx('picker__item', className), onClick: (e) => { onClick?.(e); if (!e.defaultPrevented && own !== undefined)
|
|
55
|
+
picker?.pick(own); }, ...rest, children: children }));
|
|
18
56
|
}
|
|
@@ -6,12 +6,12 @@ export interface BaseContentProps extends ComponentPropsWithoutRef<'section'> {
|
|
|
6
6
|
* maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
|
|
7
7
|
* Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
|
|
8
8
|
* In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
|
|
9
|
-
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of WAYS IN
|
|
10
|
+
* (sign in · new phone · join) — plain `Btn`s; what each one opens (a `Sheet` holding that way's whole form) is the APP's wiring, not
|
|
11
|
+
* cardds's. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
|
|
12
12
|
*/
|
|
13
13
|
export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
|
|
14
14
|
export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
|
|
15
15
|
}
|
|
16
|
-
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds
|
|
16
|
+
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
|
|
17
17
|
export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
|
|
@@ -5,14 +5,14 @@ import { cx } from '../cx.js';
|
|
|
5
5
|
* maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
|
|
6
6
|
* Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
|
|
7
7
|
* In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
|
|
8
|
-
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of WAYS IN
|
|
9
|
+
* (sign in · new phone · join) — plain `Btn`s; what each one opens (a `Sheet` holding that way's whole form) is the APP's wiring, not
|
|
10
|
+
* cardds's. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
|
|
11
11
|
*/
|
|
12
12
|
export function BaseContent({ className, ...rest }) {
|
|
13
13
|
return _jsx("section", { className: cx('base-content', className), ...rest });
|
|
14
14
|
}
|
|
15
|
-
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds
|
|
15
|
+
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
|
|
16
16
|
export function CtaPack({ className, ...rest }) {
|
|
17
17
|
return _jsx("div", { className: cx('cta-pack', className), ...rest });
|
|
18
18
|
}
|
package/dist/sheets/Drawer.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
export interface ModalProps extends ComponentPropsWithoutRef<'article'> {
|
|
3
|
+
/** CONTROLLED: on the screen or not (default `true`). The app owns it. Set it `false` and the modal leaves the way it came — its
|
|
4
|
+
* opening played backwards — then calls `onClosed`, where the app stops rendering it (and its `SheetStage ask`). */
|
|
5
|
+
open?: boolean;
|
|
6
|
+
/** the leaving has finished (at once, when motion is reduced) — unmount here */
|
|
7
|
+
onClosed?: () => void;
|
|
3
8
|
}
|
|
4
|
-
/**
|
|
5
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls
|
|
11
|
+
* when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the drawer comes
|
|
12
|
+
* out below while the lift moves up.
|
|
13
|
+
* A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
|
|
14
|
+
* records the answer and sets `open={false}`; the modal's one behaviour is how it leaves.
|
|
15
|
+
*/
|
|
16
|
+
export declare function Modal({ open, onClosed, className, ...rest }: ModalProps): import("react").JSX.Element;
|
|
6
17
|
export interface LiftProps extends ComponentPropsWithoutRef<'div'> {
|
|
7
18
|
}
|
|
8
19
|
/** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
|
|
@@ -13,7 +24,7 @@ export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'titl
|
|
|
13
24
|
/** the fine print under it */
|
|
14
25
|
caption?: ReactNode;
|
|
15
26
|
}
|
|
16
|
-
/** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons)
|
|
27
|
+
/** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons) — plain buttons with the app's `onClick`; the Drawer does nothing with them. */
|
|
17
28
|
export declare function Drawer({ title, caption, className, children, ...rest }: DrawerProps): import("react").JSX.Element;
|
|
18
29
|
export interface BtnTrioProps extends ComponentPropsWithoutRef<'div'> {
|
|
19
30
|
}
|