@usableapp/cardds 0.7.100 → 0.7.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/css/base.css +20 -0
- package/css/card.css +93 -0
- package/css/choice.css +22 -0
- package/css/forms.css +15 -3
- package/css/journey.css +23 -1
- package/css/media.css +1 -10
- package/css/sheet.css +21 -1
- package/css/tokens.css +2 -2
- package/dist/cardds.css +197 -17
- package/dist/cards/Card.d.ts +31 -3
- package/dist/cards/Card.js +33 -8
- package/dist/cards/CardParts.d.ts +9 -3
- package/dist/cards/CardParts.js +41 -3
- package/dist/cards/cardGo.d.ts +2 -0
- package/dist/cards/cardGo.js +6 -1
- package/dist/catalog.json +1 -1
- package/dist/choice/Calendar.js +5 -1
- package/dist/choice/CalendarWeeks.d.ts +41 -0
- package/dist/choice/CalendarWeeks.js +64 -0
- package/dist/choice/calendarDay.d.ts +13 -0
- package/dist/choice/calendarDay.js +6 -0
- package/dist/forms/Composer.d.ts +1 -1
- package/dist/forms/Composer.js +1 -1
- package/dist/forms/Field.d.ts +9 -1
- package/dist/forms/Field.js +35 -24
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/journey/JourneyBadge.d.ts +5 -2
- package/dist/journey/JourneyBadge.js +3 -3
- package/dist/media/MapArea.d.ts +10 -4
- package/dist/media/MapArea.js +8 -3
- package/dist/media/Postcard.d.ts +1 -1
- package/dist/media/Postcard.js +2 -2
- package/dist/post/PostDesk.d.ts +2 -1
- package/dist/post/PostDesk.js +11 -13
- package/dist/room.d.ts +12 -0
- package/dist/room.js +38 -0
- package/dist/sticky/Sticky.js +5 -38
- package/dist/type/Icon.d.ts +1 -1
- package/dist/type/Icon.js +1 -1
- package/dist/type/icons.d.ts +1 -1
- package/dist/type/icons.js +2 -2
- package/dist/words.d.ts +1 -1
- package/dist/words.js +2 -2
- package/icons.svg +1 -0
- package/lint/rules.mjs +2 -2
- package/package.json +1 -1
package/dist/choice/Calendar.js
CHANGED
|
@@ -3,9 +3,13 @@ import { useControlled } from '../state.js';
|
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
5
5
|
import { word } from '../words.js';
|
|
6
|
+
import { calendarDay } from './calendarDay.js';
|
|
6
7
|
/** 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:
|
|
7
8
|
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). */
|
|
8
9
|
export function Calendar({ title, days = 30, start = 1, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel = word('previous'), nextLabel = word('next'), marks = [], marks2 = [], disabled = [], weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], nav, className, ...rest }) {
|
|
9
10
|
const [day, pick] = useControlled(selected, defaultSelected, onSelect);
|
|
10
|
-
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) => (
|
|
11
|
+
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) => (calendarDay({
|
|
12
|
+
key: d, num: d, mark: marks.includes(d), mark2: marks2.includes(d), selected: d === day, disabled: disabled.includes(d),
|
|
13
|
+
style: d === 1 ? { '--start': start } : undefined, onPick: () => pick(d),
|
|
14
|
+
})))] })] }));
|
|
11
15
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
export interface WeekDay {
|
|
3
|
+
/** the app's id for the day ("2026-06-05"): what `onSelect` hands back */
|
|
4
|
+
key: string;
|
|
5
|
+
/** what the cell shows ("5", "๕") */
|
|
6
|
+
num: ReactNode;
|
|
7
|
+
/** the day is today */
|
|
8
|
+
today?: boolean;
|
|
9
|
+
/** the day cannot be picked (already past, full) */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** a soft mark (something is on) or a strong one (booked) */
|
|
12
|
+
mark?: 'soft' | 'strong';
|
|
13
|
+
}
|
|
14
|
+
export interface Week {
|
|
15
|
+
/** the month · year the week belongs to, in the app's words ("มิถุนายน 2569"): the head shows it while the week is the first row in sight */
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
/** 7 days, in the order of `weekdays` */
|
|
18
|
+
days: WeekDay[];
|
|
19
|
+
}
|
|
20
|
+
export interface CalendarWeeksProps extends Omit<ComponentProps<'div'>, 'onSelect'> {
|
|
21
|
+
/** the run of weeks, in order; the app builds every date */
|
|
22
|
+
weeks: Week[];
|
|
23
|
+
/** week rows in sight, counted in rows like CardList `show` (default 2) */
|
|
24
|
+
show?: number;
|
|
25
|
+
/** CONTROLLED: the picked day's `key`, or `null` for none. The app owns it — pair it with `onSelect`. (`undefined` = uncontrolled.) */
|
|
26
|
+
selected?: string | null;
|
|
27
|
+
/** UNCONTROLLED: the day picked on arrival; the weeks then keep their own */
|
|
28
|
+
defaultSelected?: string;
|
|
29
|
+
/** a day was tapped: its `key` */
|
|
30
|
+
onSelect?: (key: string) => void;
|
|
31
|
+
/** weekday heads, 7 */
|
|
32
|
+
weekdays?: string[];
|
|
33
|
+
/** given, the label is a button: the app opens its own month / year pick. Not given, the label is plain words */
|
|
34
|
+
onLabelClick?: () => void;
|
|
35
|
+
}
|
|
36
|
+
/** CalendarWeeks — a run of weeks that scrolls week by week, `show` rows in sight (2 by default), Calendar's own 44px round cells; a sticky head holds
|
|
37
|
+
* the month · year of the first row in sight and the weekday heads. It knows no dates, as Calendar: the app builds `weeks` (each a `label` and 7 days
|
|
38
|
+
* with `key`, `num`, `today`, `disabled`, `mark`) and owns the day (`selected` + `onSelect`, or `defaultSelected`); a tap on the label (`onLabelClick`)
|
|
39
|
+
* is the app's to answer with its month / year pick. It opens at the week that holds today (Lh 2026-10-19): the first row in sight is the week
|
|
40
|
+
* whose day has `today`, at once and without a scroll to watch; with no `today` in the data it opens at the first week. For a day within the coming weeks on a step with little height. On a sheet, never in a Card. */
|
|
41
|
+
export declare function CalendarWeeks({ weeks, show, selected, defaultSelected, onSelect, weekdays, onLabelClick, className, ref, ...rest }: CalendarWeeksProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useControlled, setRefs } from '../state.js';
|
|
4
|
+
import { cx } from '../cx.js';
|
|
5
|
+
import { calendarDay } from './calendarDay.js';
|
|
6
|
+
/** CalendarWeeks — a run of weeks that scrolls week by week, `show` rows in sight (2 by default), Calendar's own 44px round cells; a sticky head holds
|
|
7
|
+
* the month · year of the first row in sight and the weekday heads. It knows no dates, as Calendar: the app builds `weeks` (each a `label` and 7 days
|
|
8
|
+
* with `key`, `num`, `today`, `disabled`, `mark`) and owns the day (`selected` + `onSelect`, or `defaultSelected`); a tap on the label (`onLabelClick`)
|
|
9
|
+
* is the app's to answer with its month / year pick. It opens at the week that holds today (Lh 2026-10-19): the first row in sight is the week
|
|
10
|
+
* whose day has `today`, at once and without a scroll to watch; with no `today` in the data it opens at the first week. For a day within the coming weeks on a step with little height. On a sheet, never in a Card. */
|
|
11
|
+
export function CalendarWeeks({ weeks, show = 2, selected, defaultSelected, onSelect, weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], onLabelClick, className, ref, ...rest }) {
|
|
12
|
+
const [day, pick] = useControlled(selected, defaultSelected, onSelect);
|
|
13
|
+
const [first, setFirst] = useState(0);
|
|
14
|
+
const box = useRef(null);
|
|
15
|
+
// the box opens at today's week whenever the run itself is new (its days, not its identity: an app that rebuilds `weeks` on each render
|
|
16
|
+
// keeps the member's scroll), and never again for a rerender of the same run
|
|
17
|
+
const todayAt = weeks.findIndex((w) => w.days.some((d) => d.today));
|
|
18
|
+
const run = `${weeks.length}|${weeks[0]?.days[0]?.key}|${todayAt}`;
|
|
19
|
+
const landed = useRef(null);
|
|
20
|
+
// the label and the window both come from the rows' layout tops (offsetTop: the box is their offsetParent, and a sheet's transform does not
|
|
21
|
+
// scale them), read against scrollTop on each frame of a scroll: deterministic, no observer callback to wait for
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
const el = box.current;
|
|
24
|
+
if (!el)
|
|
25
|
+
return;
|
|
26
|
+
const rows = () => [...el.querySelectorAll(':scope > .calendar__week')];
|
|
27
|
+
const top = (row) => row.offsetTop;
|
|
28
|
+
const lead = () => rows()[0]?.offsetTop ?? 0; // the head and the gap under it: where a row snaps to
|
|
29
|
+
const track = () => {
|
|
30
|
+
const under = el.scrollTop + lead();
|
|
31
|
+
const all = rows();
|
|
32
|
+
const at = all.findIndex((row) => top(row) + row.offsetHeight / 2 > under);
|
|
33
|
+
setFirst(at < 0 ? all.length - 1 : at);
|
|
34
|
+
};
|
|
35
|
+
const fit = () => {
|
|
36
|
+
const all = rows(), whole = Math.floor(show);
|
|
37
|
+
el.style.setProperty('--_weeks-lead', `${lead()}px`);
|
|
38
|
+
const cut = all[whole];
|
|
39
|
+
if (!cut)
|
|
40
|
+
el.style.removeProperty('--_weeks-h');
|
|
41
|
+
else
|
|
42
|
+
el.style.setProperty('--_weeks-h', `${top(cut) + (show - whole) * cut.offsetHeight}px`);
|
|
43
|
+
if (landed.current !== run) {
|
|
44
|
+
landed.current = run;
|
|
45
|
+
const today = all[todayAt];
|
|
46
|
+
if (today)
|
|
47
|
+
el.scrollTo({ top: top(today) - lead(), behavior: 'instant' });
|
|
48
|
+
}
|
|
49
|
+
track();
|
|
50
|
+
};
|
|
51
|
+
let frame = 0;
|
|
52
|
+
const onScroll = () => { if (!frame)
|
|
53
|
+
frame = requestAnimationFrame(() => { frame = 0; track(); }); };
|
|
54
|
+
fit();
|
|
55
|
+
const watch = new ResizeObserver(fit);
|
|
56
|
+
watch.observe(el);
|
|
57
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
58
|
+
return () => { watch.disconnect(); el.removeEventListener('scroll', onScroll); cancelAnimationFrame(frame); };
|
|
59
|
+
}, [show, run, todayAt]);
|
|
60
|
+
const label = weeks[Math.min(first, weeks.length - 1)]?.label;
|
|
61
|
+
return (_jsxs("div", { ref: setRefs(box, ref), className: cx('calendar', 'calendar--weeks', className), ...rest, children: [_jsxs("div", { className: "calendar__weeks-head", children: [onLabelClick
|
|
62
|
+
? _jsx("button", { type: "button", className: "calendar__label", onClick: onLabelClick, children: label })
|
|
63
|
+
: _jsx("span", { className: "calendar__label", children: label }), _jsx("div", { className: "calendar__grid", children: weekdays.map((d) => _jsx("span", { className: "calendar__wd", children: d }, d)) })] }), weeks.map((w, i) => (_jsx("div", { className: "calendar__grid calendar__week", children: w.days.map((d) => calendarDay({ key: d.key, num: d.num, mark: d.mark === 'soft', mark2: d.mark === 'strong', selected: d.key === day, today: d.today, disabled: d.disabled, onPick: () => pick(d.key) })) }, w.days[0]?.key ?? i)))] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
/** one `.calendar__day` cell: the button Calendar and CalendarWeeks both draw, so the cell and every `--calendar-day-*` knob are one */
|
|
3
|
+
export declare function calendarDay({ key, num, mark, mark2, selected, today, disabled, style, onPick }: {
|
|
4
|
+
key: string | number;
|
|
5
|
+
num: ReactNode;
|
|
6
|
+
mark?: boolean;
|
|
7
|
+
mark2?: boolean;
|
|
8
|
+
selected?: boolean;
|
|
9
|
+
today?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
onPick: () => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../cx.js';
|
|
3
|
+
/** one `.calendar__day` cell: the button Calendar and CalendarWeeks both draw, so the cell and every `--calendar-day-*` knob are one */
|
|
4
|
+
export function calendarDay({ key, num, mark, mark2, selected, today, disabled, style, onPick }) {
|
|
5
|
+
return (_jsx("button", { type: "button", className: cx('calendar__day', mark && 'calendar__day--mark', mark2 && 'calendar__day--mark2'), "aria-selected": selected ? 'true' : undefined, "aria-current": today ? 'date' : undefined, onClick: onPick, disabled: disabled, style: style, children: num }, key));
|
|
6
|
+
}
|
package/dist/forms/Composer.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ComposerProps extends Omit<ComponentProps<'textarea'>, 'classNa
|
|
|
13
13
|
/** inline style for the wrapper, not the textarea */
|
|
14
14
|
style?: React.CSSProperties;
|
|
15
15
|
}
|
|
16
|
-
/** Composer — a big write-in area (display type) with a bottom bar: icon · hint · send. The write-in step's sheet content. On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). A second thing to write in the same Composer
|
|
16
|
+
/** Composer — a big write-in area (display type) with a bottom bar: icon · hint · send. The write-in step's sheet content. On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). A second thing to write in the same Composer is a `ComposerArea` child: the same words, behind a dashed rule. Two framed write-ins (the ask sheet's request and its reason) are two `Field rows`, not a Composer with a `ComposerArea` (Lh 2026-10-19): the Composer is bare, the one big write-in. The bar is drawn only when it holds a lead, a hint or a send. */
|
|
17
17
|
export declare function Composer({ hint, lead, send, children, className, style, ...textarea }: ComposerProps): import("react").JSX.Element;
|
|
18
18
|
export interface ComposerAreaProps extends Omit<ComponentProps<'textarea'>, 'className' | 'children'> {
|
|
19
19
|
}
|
package/dist/forms/Composer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
import { useStepCursor } from '../sheets/stepCursor.js';
|
|
4
|
-
/** Composer — a big write-in area (display type) with a bottom bar: icon · hint · send. The write-in step's sheet content. On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). A second thing to write in the same Composer
|
|
4
|
+
/** Composer — a big write-in area (display type) with a bottom bar: icon · hint · send. The write-in step's sheet content. On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). A second thing to write in the same Composer is a `ComposerArea` child: the same words, behind a dashed rule. Two framed write-ins (the ask sheet's request and its reason) are two `Field rows`, not a Composer with a `ComposerArea` (Lh 2026-10-19): the Composer is bare, the one big write-in. The bar is drawn only when it holds a lead, a hint or a send. */
|
|
5
5
|
export function Composer({ hint, lead, send, children, className, style, ...textarea }) {
|
|
6
6
|
const cursor = useStepCursor();
|
|
7
7
|
return (_jsxs("div", { className: cx('composer', className), style: style, children: [_jsx("textarea", { className: "composer__input", ...textarea, ...cursor }), children, (lead != null || hint != null || send != null) && (_jsxs("div", { className: "composer__bar", children: [lead, hint != null && _jsx("span", { className: "composer__hint", children: hint }), send] }))] }));
|
package/dist/forms/Field.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export interface FieldProps extends Omit<ComponentProps<'input'>, 'className' |
|
|
|
24
24
|
* → 081-234-5678). Only digits are kept; a mark comes only once a digit follows it, so Backspace never sticks on a dash. The
|
|
25
25
|
* app's `onChange` reads the shaped value (the digits alone: `value.replace(/\D/g, '')`). Lh 2026-09-25 */
|
|
26
26
|
mask?: string;
|
|
27
|
+
/** several lines tall (the request on the help-ask sheet, and under it its reason): the Field draws a `textarea` of this many
|
|
28
|
+
* lines in the same frame. The placeholder is still the label; `error`, `required` and `icon` / `action` work as in one line.
|
|
29
|
+
* `mask`, `format` and `digits` are for one line: with `rows` they are ignored (warns once). The change events come from a
|
|
30
|
+
* `textarea` (`e.currentTarget.value` is the same). Lh 2026-10-19 */
|
|
31
|
+
rows?: number;
|
|
27
32
|
/** classes for the field's label wrapper, not the input */
|
|
28
33
|
className?: string;
|
|
29
34
|
/** inline style for the label wrapper, not the input */
|
|
@@ -36,5 +41,8 @@ export declare function applyMask(v: string, mask: string): string;
|
|
|
36
41
|
* It stands on a Sheet's `SheetBody`, in a `SheetStep`, or on a stack sheet, 1–3 per step; a card is for reading and pressing buttons
|
|
37
42
|
* (`cardds check` rule `field-in-card` flags a Field, or any control the member fills or picks, inside a `<Card>`).
|
|
38
43
|
* The LABEL IS THE PLACEHOLDER. 48px tall (56px with an action inside).
|
|
44
|
+
* Variation `rows={n}`: several lines in the same frame, for what the member writes in words (the request and its reason on the ask
|
|
45
|
+
* sheet: `<Field rows={4} />` over `<Field rows={2} />`). A frame says "tap me", so a framed write-in is a Field, never a Composer:
|
|
46
|
+
* the Composer is the one big write-in on the paper, bare (Lh 2026-10-19, timebank's ask sheet).
|
|
39
47
|
*/
|
|
40
|
-
export declare function Field({ icon, action, label, error, format, digits, formatError, mask, className, style, onBlur, onInput, onChange, ...input }: FieldProps): import("react").JSX.Element;
|
|
48
|
+
export declare function Field({ icon, action, label, error, format: formatProp, digits: digitsProp, formatError, mask: maskProp, rows, className, style, onBlur, onInput, onChange, ...input }: FieldProps): import("react").JSX.Element;
|
package/dist/forms/Field.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
-
import { watchSize } from '../state.js';
|
|
3
|
+
import { warnOnce, watchSize } from '../state.js';
|
|
4
4
|
import { formatOk, useCheck } from './check.js';
|
|
5
5
|
import { cx } from '../cx.js';
|
|
6
6
|
import { Icon } from '../type/Icon.js';
|
|
@@ -24,8 +24,15 @@ export function applyMask(v, mask) {
|
|
|
24
24
|
* It stands on a Sheet's `SheetBody`, in a `SheetStep`, or on a stack sheet, 1–3 per step; a card is for reading and pressing buttons
|
|
25
25
|
* (`cardds check` rule `field-in-card` flags a Field, or any control the member fills or picks, inside a `<Card>`).
|
|
26
26
|
* The LABEL IS THE PLACEHOLDER. 48px tall (56px with an action inside).
|
|
27
|
+
* Variation `rows={n}`: several lines in the same frame, for what the member writes in words (the request and its reason on the ask
|
|
28
|
+
* sheet: `<Field rows={4} />` over `<Field rows={2} />`). A frame says "tap me", so a framed write-in is a Field, never a Composer:
|
|
29
|
+
* the Composer is the one big write-in on the paper, bare (Lh 2026-10-19, timebank's ask sheet).
|
|
27
30
|
*/
|
|
28
|
-
export function Field({ icon, action, label, error, format, digits, formatError, mask, className, style, onBlur, onInput, onChange, ...input }) {
|
|
31
|
+
export function Field({ icon, action, label, error, format: formatProp, digits: digitsProp, formatError, mask: maskProp, rows, className, style, onBlur, onInput, onChange, ...input }) {
|
|
32
|
+
const lines = rows != null;
|
|
33
|
+
if (lines && (formatProp != null || digitsProp != null || maskProp != null))
|
|
34
|
+
warnOnce('field-rows-one-line', 'cardds Field: `mask`, `format` and `digits` are for one line — with `rows` they are ignored');
|
|
35
|
+
const [format, digits, mask] = lines ? [undefined, undefined, undefined] : [formatProp, digitsProp, maskProp];
|
|
29
36
|
const ref = useRef(null);
|
|
30
37
|
const [bad, setBad] = useState(false); // the format check failed, and has not been fixed since
|
|
31
38
|
const wrong = (error != null && error !== false && error !== '') || bad;
|
|
@@ -44,8 +51,8 @@ export function Field({ icon, action, label, error, format, digits, formatError,
|
|
|
44
51
|
and `--_ph-fit` (the share that fits, ≤ 1) scales the placeholder's size — again whenever the box or the font changes */
|
|
45
52
|
useLayoutEffect(() => {
|
|
46
53
|
const el = ref.current, text = input.placeholder;
|
|
47
|
-
if (!el)
|
|
48
|
-
return;
|
|
54
|
+
if (!el || lines)
|
|
55
|
+
return; // a textarea's placeholder wraps: it never needs to shrink
|
|
49
56
|
if (!text) {
|
|
50
57
|
el.style.removeProperty('--_ph-fit');
|
|
51
58
|
return;
|
|
@@ -66,7 +73,7 @@ export function Field({ icon, action, label, error, format, digits, formatError,
|
|
|
66
73
|
fit();
|
|
67
74
|
document.fonts?.ready.then(fit);
|
|
68
75
|
return watchSize([el], fit);
|
|
69
|
-
}, [input.placeholder]);
|
|
76
|
+
}, [input.placeholder, lines]);
|
|
70
77
|
useEffect(() => {
|
|
71
78
|
if (wrong) {
|
|
72
79
|
ref.current?.focus();
|
|
@@ -74,23 +81,27 @@ export function Field({ icon, action, label, error, format, digits, formatError,
|
|
|
74
81
|
}
|
|
75
82
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- when the app's error comes or changes
|
|
76
83
|
}, [error]);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
const onBlurField = (e) => { onBlur?.(e); if (!checked)
|
|
85
|
+
return; const w = verdict(false); setBad(w != null); say(w); };
|
|
86
|
+
const onInputField = (e) => { onInput?.(e); if (bad && verdict(false) == null) {
|
|
87
|
+
setBad(false);
|
|
88
|
+
say(null);
|
|
89
|
+
} };
|
|
90
|
+
const onChangeField = (e) => {
|
|
91
|
+
if (mask) {
|
|
92
|
+
const el = e.currentTarget, before = el.value.slice(0, el.selectionStart ?? el.value.length).replace(/\D/g, '').length;
|
|
93
|
+
const shaped = applyMask(el.value, mask);
|
|
94
|
+
if (shaped !== el.value) {
|
|
95
|
+
el.value = shaped;
|
|
96
|
+
let at = 0;
|
|
97
|
+
for (let n = 0; at < shaped.length && n < before; at++)
|
|
98
|
+
if (/\d/.test(shaped[at]))
|
|
99
|
+
n++; // the caret stays after the same digit
|
|
100
|
+
el.setSelectionRange(at, at);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
onChange?.(e);
|
|
104
|
+
};
|
|
105
|
+
const shared = { ref, 'aria-label': label, 'aria-invalid': wrong || undefined, ...input, ...cursor, onBlur: onBlurField, onInput: onInputField, onChange: onChangeField };
|
|
106
|
+
return (_jsxs("label", { className: cx('field', lines && 'field--rows', action != null && 'field--action', wrong && 'field--error', className), style: style, children: [icon && _jsx(Icon, { name: icon, size: "sm" }), lines ? _jsx("textarea", { rows: rows, ...shared }) : _jsx("input", { ...shared }), action] }));
|
|
96
107
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export * from './forms/Composer.js';
|
|
|
57
57
|
export * from './choice/Check.js';
|
|
58
58
|
export * from './choice/DayStrip.js';
|
|
59
59
|
export * from './choice/Calendar.js';
|
|
60
|
+
export * from './choice/CalendarWeeks.js';
|
|
60
61
|
export * from './choice/Mood.js';
|
|
61
62
|
export * from './choice/Slider.js';
|
|
62
63
|
export * from './numbers/Ring.js';
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export * from './forms/Composer.js';
|
|
|
61
61
|
export * from './choice/Check.js';
|
|
62
62
|
export * from './choice/DayStrip.js';
|
|
63
63
|
export * from './choice/Calendar.js';
|
|
64
|
+
export * from './choice/CalendarWeeks.js';
|
|
64
65
|
export * from './choice/Mood.js';
|
|
65
66
|
export * from './choice/Slider.js';
|
|
66
67
|
export * from './numbers/Ring.js';
|
|
@@ -10,6 +10,9 @@ export interface JourneyBadgeProps extends ComponentProps<'span'> {
|
|
|
10
10
|
plain?: boolean;
|
|
11
11
|
/** pinned to a CardMedia's corner (rating, price) */
|
|
12
12
|
corner?: boolean;
|
|
13
|
+
/** a date in a card's head: as tall as the title block beside it, no taller — two lines beside a one-line title (day over number · month),
|
|
14
|
+
* three beside a two-line title (day · number · month). Write it as the head's last child, after the title block */
|
|
15
|
+
stretch?: boolean;
|
|
13
16
|
}
|
|
14
|
-
/** JourneyBadge — a tiny label over a big value: gate, seat, date, rating. */
|
|
15
|
-
export declare function JourneyBadge({ label, value, sub, plain, corner, className, ...rest }: JourneyBadgeProps): import("react").JSX.Element;
|
|
17
|
+
/** JourneyBadge — a tiny label over a big value: gate, seat, date, rating. `stretch`: a date in a head that takes its form from the title's height. */
|
|
18
|
+
export declare function JourneyBadge({ label, value, sub, plain, corner, stretch, className, ...rest }: JourneyBadgeProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/** JourneyBadge — a tiny label over a big value: gate, seat, date, rating. */
|
|
4
|
-
export function JourneyBadge({ label, value, sub, plain, corner, className, ...rest }) {
|
|
5
|
-
return (_jsxs("span", { className: cx('tile-badge', plain && 'tile-badge--plain', corner && 'tile-badge--corner', className), ...rest, children: [_jsx("span", { className: "tile-badge__label", children: label }), _jsx("span", { className: "tile-badge__value", children: value }), sub != null && _jsx("span", { className: "tile-badge__label", children: sub })] }));
|
|
3
|
+
/** JourneyBadge — a tiny label over a big value: gate, seat, date, rating. `stretch`: a date in a head that takes its form from the title's height. */
|
|
4
|
+
export function JourneyBadge({ label, value, sub, plain, corner, stretch, className, ...rest }) {
|
|
5
|
+
return (_jsxs("span", { className: cx('tile-badge', plain && 'tile-badge--plain', corner && 'tile-badge--corner', stretch && 'tile-badge--stretch', className), ...rest, children: [_jsx("span", { className: "tile-badge__label", children: label }), _jsx("span", { className: "tile-badge__value", children: value }), sub != null && _jsx("span", { className: "tile-badge__label", children: sub })] }));
|
|
6
6
|
}
|
package/dist/media/MapArea.d.ts
CHANGED
|
@@ -7,9 +7,13 @@ export interface MapAreaProps extends ComponentProps<'div'> {
|
|
|
7
7
|
hint?: ReactNode;
|
|
8
8
|
/** the tile provider's credit, bottom right (OSM requires it) — a caption, never the engine's own control */
|
|
9
9
|
credit?: ReactNode;
|
|
10
|
-
/** the
|
|
10
|
+
/** the shape of the centre mark. `ring` (default) is the rough area the member's neighbours may see; `pin` is an EXACT point, a clear pin
|
|
11
|
+
* at the centre, for a meeting place (Lh 2026-10-19: a meeting position is a pin, never the ring) */
|
|
12
|
+
mark?: 'ring' | 'pin';
|
|
13
|
+
/** the accessible name of the centre mark, whatever its shape (what the centre means): the rough area for a ring, the meeting point for a pin */
|
|
11
14
|
ringLabel?: string;
|
|
12
|
-
/** the
|
|
15
|
+
/** the centre mark's element, whatever its shape — the engine reads its position to know which point is the choice: a ring's CENTRE
|
|
16
|
+
* (the middle of its box), a pin's TIP (the bottom-centre of its box) */
|
|
13
17
|
ringRef?: Ref<HTMLSpanElement>;
|
|
14
18
|
/** FIRST, the map engine's mount — ONE element, filled to the box (the app hands it to Leaflet or the like); after it, the
|
|
15
19
|
* places the member may pick (`MapPick`), over the map */
|
|
@@ -20,7 +24,9 @@ export interface MapAreaProps extends ComponentProps<'div'> {
|
|
|
20
24
|
* offline) the box shows the wireframe street pattern, never a hole. Two places: behind a stage sheet (the default), or
|
|
21
25
|
* `inline`, in flow inside a card or a sheet's body — as a SheetBody's only child it fills the body, so the open sheet
|
|
22
26
|
* becomes the map while its head stays. The ring marks the rough area: behind a stage it is centred in the band a half
|
|
23
|
-
* sheet leaves open, inline at the centre of its own box. Dragging the map underneath is that choice, never a pin;
|
|
27
|
+
* sheet leaves open, inline at the centre of its own box. Dragging the map underneath is that choice, never a pin to place;
|
|
24
28
|
* `ringRef` is how the engine learns where that is. Places to pick go after the mount, as `MapPick`s.
|
|
29
|
+
* Variation `mark="pin"`: for a meeting place the centre is an exact point, so a clear pin stands there instead of the ring,
|
|
30
|
+
* its tip on the point the ring's centre would be (the engine reads the pin's bottom-centre). The ring stays for a rough area.
|
|
25
31
|
*/
|
|
26
|
-
export declare function MapArea({ inline, hint, credit, ringLabel, ringRef, className, children, ...rest }: MapAreaProps): import("react").JSX.Element;
|
|
32
|
+
export declare function MapArea({ inline, hint, credit, mark, ringLabel, ringRef, className, children, ...rest }: MapAreaProps): import("react").JSX.Element;
|
package/dist/media/MapArea.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { Icon } from '../type/Icon.js';
|
|
3
4
|
/**
|
|
4
5
|
* MapArea — the box a map engine draws in: the first child is the engine's mount and fills it; before tiles arrive (or
|
|
5
6
|
* offline) the box shows the wireframe street pattern, never a hole. Two places: behind a stage sheet (the default), or
|
|
6
7
|
* `inline`, in flow inside a card or a sheet's body — as a SheetBody's only child it fills the body, so the open sheet
|
|
7
8
|
* becomes the map while its head stays. The ring marks the rough area: behind a stage it is centred in the band a half
|
|
8
|
-
* sheet leaves open, inline at the centre of its own box. Dragging the map underneath is that choice, never a pin;
|
|
9
|
+
* sheet leaves open, inline at the centre of its own box. Dragging the map underneath is that choice, never a pin to place;
|
|
9
10
|
* `ringRef` is how the engine learns where that is. Places to pick go after the mount, as `MapPick`s.
|
|
11
|
+
* Variation `mark="pin"`: for a meeting place the centre is an exact point, so a clear pin stands there instead of the ring,
|
|
12
|
+
* its tip on the point the ring's centre would be (the engine reads the pin's bottom-centre). The ring stays for a rough area.
|
|
10
13
|
*/
|
|
11
|
-
export function MapArea({ inline, hint, credit, ringLabel = 'ย่านของคุณโดยประมาณ', ringRef, className, children, ...rest }) {
|
|
12
|
-
return (_jsxs("div", { className: cx('map', inline && 'map--inline', className), ...rest, children: [children,
|
|
14
|
+
export function MapArea({ inline, hint, credit, mark = 'ring', ringLabel = mark === 'pin' ? 'จุดนัด' : 'ย่านของคุณโดยประมาณ', ringRef, className, children, ...rest }) {
|
|
15
|
+
return (_jsxs("div", { className: cx('map', inline && 'map--inline', className), ...rest, children: [children, mark === 'pin'
|
|
16
|
+
? _jsx("span", { ref: ringRef, className: "map__pin", role: "img", "aria-label": ringLabel, children: _jsx(Icon, { name: "map-pin" }) })
|
|
17
|
+
: _jsx("span", { ref: ringRef, className: "map__ring", role: "img", "aria-label": ringLabel }), hint != null && _jsx("span", { className: "map__hint", children: hint }), credit != null && _jsx("span", { className: "map__credit", children: credit })] }));
|
|
13
18
|
}
|
package/dist/media/Postcard.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface PostcardProps extends ComponentProps<'div'> {
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Postcard — a member's introduction, the shape of a cover card: the photo fills it (or the wireframe box), the band at the
|
|
34
|
-
* bottom holds the name, its line and the heart, and turns WITH the face; a tap on the photo turns it over (`--motion-flip`) to the back: the name a fifth down the paper, its line and the caption after it — or, for a voice note, `voice` (the play control) at the middle; the app plays it.
|
|
34
|
+
* bottom holds the name, its line and the heart, and turns WITH the face; a tap on the photo turns it over (the shared flip mechanic, `.flip`, at `--motion-flip`) to the back: the name a fifth down the paper, its line and the caption after it — or, for a voice note, `voice` (the play control) at the middle; the app plays it.
|
|
35
35
|
* Which side shows is a state: `flipped` + `onFlip`, or `defaultFlipped`. The heart is the member's own like: `liked` + `onLike`.
|
|
36
36
|
* Who else liked it, a voice note, a reply go OUTSIDE it — a Row under it (`Row more` expands the list).
|
|
37
37
|
*/
|
package/dist/media/Postcard.js
CHANGED
|
@@ -4,7 +4,7 @@ import { cx } from '../cx.js';
|
|
|
4
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
5
5
|
/**
|
|
6
6
|
* Postcard — a member's introduction, the shape of a cover card: the photo fills it (or the wireframe box), the band at the
|
|
7
|
-
* bottom holds the name, its line and the heart, and turns WITH the face; a tap on the photo turns it over (`--motion-flip`) to the back: the name a fifth down the paper, its line and the caption after it — or, for a voice note, `voice` (the play control) at the middle; the app plays it.
|
|
7
|
+
* bottom holds the name, its line and the heart, and turns WITH the face; a tap on the photo turns it over (the shared flip mechanic, `.flip`, at `--motion-flip`) to the back: the name a fifth down the paper, its line and the caption after it — or, for a voice note, `voice` (the play control) at the middle; the app plays it.
|
|
8
8
|
* Which side shows is a state: `flipped` + `onFlip`, or `defaultFlipped`. The heart is the member's own like: `liked` + `onLike`.
|
|
9
9
|
* Who else liked it, a voice note, a reply go OUTSIDE it — a Row under it (`Row more` expands the list).
|
|
10
10
|
*/
|
|
@@ -12,5 +12,5 @@ export function Postcard({ photo, alt = '', name, line, caption, voice, flipped,
|
|
|
12
12
|
const [back, flip] = useControlled(flipped, defaultFlipped, onFlip);
|
|
13
13
|
const turn = () => flip(!back);
|
|
14
14
|
const words = (_jsxs("div", { className: "postcard__words", children: [_jsx("span", { className: "t-h1", children: name }), line != null && _jsx("span", { className: "t-body", children: line })] }));
|
|
15
|
-
return (_jsxs("div", { className: cx('postcard', back && 'is-flipped', className), ...rest, children: [_jsxs("div", { className: "postcard__face postcard__front", children: [_jsx("button", { className: "postcard__turn", 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" }) }), _jsxs("div", { className: "postcard__band", children: [words, (liked !== undefined || onLike) && _jsx(IconBtn, { invert: true, icon: "heart", label: likeLabel, "aria-pressed": !!liked, onClick: () => onLike?.(!liked) })] })] }), _jsxs("div", { className: "postcard__face postcard__back", children: [_jsxs("button", { className: "postcard__turn", type: "button", "aria-label": backLabel, onClick: turn, children: [words, caption != null && _jsx("p", { className: "postcard__caption", children: caption })] }), voice != null && _jsx("div", { className: "postcard__voice", children: voice })] })] }));
|
|
15
|
+
return (_jsxs("div", { className: cx('postcard flip', back && 'is-flipped', className), ...rest, children: [_jsxs("div", { className: "postcard__face flip__face flip__face--front postcard__front", children: [_jsx("button", { className: "postcard__turn", 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" }) }), _jsxs("div", { className: "postcard__band", children: [words, (liked !== undefined || onLike) && _jsx(IconBtn, { invert: true, icon: "heart", label: likeLabel, "aria-pressed": !!liked, onClick: () => onLike?.(!liked) })] })] }), _jsxs("div", { className: "postcard__face flip__face flip__face--back postcard__back", children: [_jsxs("button", { className: "postcard__turn", type: "button", "aria-label": backLabel, onClick: turn, children: [words, caption != null && _jsx("p", { className: "postcard__caption", children: caption })] }), voice != null && _jsx("div", { className: "postcard__voice", children: voice })] })] }));
|
|
16
16
|
}
|
package/dist/post/PostDesk.d.ts
CHANGED
|
@@ -53,7 +53,8 @@ export interface PostDeskProps extends ComponentProps<'div'> {
|
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* PostDesk — a postcard being WRITTEN, stamped and sent. The paper is landscape and twice the screen; the phone looks at one half:
|
|
56
|
-
* the message on ruled lines (typing stops when the half is full — the paper is the limit
|
|
56
|
+
* the message on ruled lines (typing stops when the half is full — the paper is the limit: an addition that would not fit is refused
|
|
57
|
+
* where it was typed, the caret stays, the app's `onMessageChange` never hears it and `onFull` is called; the same rule as a `StickyField` on a `Sticky`), then the address with the stamp box.
|
|
57
58
|
* The other half's showing edge, or a sideways drag, slides the paper. The stamp box and the stamps under the card choose one of
|
|
58
59
|
* the sender's own stamps. Sending is the SCREEN's button, never the desk's: the app sets `state="struck"` — the postmark lands,
|
|
59
60
|
* the desk asks for `sent`, and the card leaves upward; `sent` stands in its place. Every state is the app's (`x` + `onXChange`, or `defaultX`).
|
package/dist/post/PostDesk.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { reducedMotion, useControlled, useLatest, watchSize } from '../state.js';
|
|
5
|
+
import { holdRoom } from '../room.js';
|
|
5
6
|
import { word } from '../words.js';
|
|
6
7
|
import { StampPlace } from '../stamp/Stamp.js';
|
|
7
8
|
const STRIKE = 700; // ms the struck card is seen before it asks to leave
|
|
@@ -20,7 +21,8 @@ function linesOf(ta) {
|
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* PostDesk — a postcard being WRITTEN, stamped and sent. The paper is landscape and twice the screen; the phone looks at one half:
|
|
23
|
-
* the message on ruled lines (typing stops when the half is full — the paper is the limit
|
|
24
|
+
* the message on ruled lines (typing stops when the half is full — the paper is the limit: an addition that would not fit is refused
|
|
25
|
+
* where it was typed, the caret stays, the app's `onMessageChange` never hears it and `onFull` is called; the same rule as a `StickyField` on a `Sticky`), then the address with the stamp box.
|
|
24
26
|
* The other half's showing edge, or a sideways drag, slides the paper. The stamp box and the stamps under the card choose one of
|
|
25
27
|
* the sender's own stamps. Sending is the SCREEN's button, never the desk's: the app sets `state="struck"` — the postmark lands,
|
|
26
28
|
* the desk asks for `sent`, and the card leaves upward; `sent` stands in its place. Every state is the app's (`x` + `onXChange`, or `defaultX`).
|
|
@@ -46,10 +48,15 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
|
|
|
46
48
|
told.current = `${left}/${fits}`;
|
|
47
49
|
lines.current?.(left, fits);
|
|
48
50
|
};
|
|
49
|
-
|
|
51
|
+
const full = useLatest(onFull);
|
|
52
|
+
const room = useRef(null);
|
|
53
|
+
useLayoutEffect(() => { measure(); room.current?.sync(); }, [text]);
|
|
50
54
|
useEffect(() => {
|
|
55
|
+
const ta = paper.current;
|
|
56
|
+
room.current = holdRoom(ta.parentElement, () => paper.current, () => ta.scrollHeight > ta.clientHeight + 1, (refused) => refused && full.current?.());
|
|
51
57
|
document.fonts?.ready.then(measure);
|
|
52
|
-
|
|
58
|
+
const stop = watchSize([ta], measure);
|
|
59
|
+
return () => { room.current?.stop(); room.current = null; stop(); };
|
|
53
60
|
}, []);
|
|
54
61
|
useEffect(() => {
|
|
55
62
|
if (at !== 'struck')
|
|
@@ -57,15 +64,6 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
|
|
|
57
64
|
const t = setTimeout(() => askState('sent'), reducedMotion() ? 0 : STRIKE);
|
|
58
65
|
return () => clearTimeout(t);
|
|
59
66
|
}, [at, askState]);
|
|
60
|
-
const write = (e) => {
|
|
61
|
-
const ta = e.currentTarget;
|
|
62
|
-
if (ta.scrollHeight > ta.clientHeight + 1 && ta.value.length >= text.length) {
|
|
63
|
-
ta.value = text;
|
|
64
|
-
onFull?.();
|
|
65
|
-
return;
|
|
66
|
-
} // taking words away is never refused
|
|
67
|
-
askText(ta.value);
|
|
68
|
-
};
|
|
69
67
|
const from = useRef(null);
|
|
70
68
|
const drop = (e) => {
|
|
71
69
|
const dx = from.current == null ? 0 : e.clientX - from.current;
|
|
@@ -73,7 +71,7 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
|
|
|
73
71
|
if (writing && Math.abs(dx) >= SWIPE)
|
|
74
72
|
askSide(dx < 0 ? 'address' : 'message');
|
|
75
73
|
};
|
|
76
|
-
return (_jsxs("div", { className: cx('post-desk', `post-desk--${side}`, `post-desk--${at}`, className), ...rest, children: [_jsxs("div", { className: "post-desk__window", onPointerDown: (e) => { from.current = e.clientX; }, onPointerUp: drop, children: [_jsxs("div", { className: "post-desk__card post-paper", children: [_jsx("section", { className: "post-desk__half post-desk__half--message", "aria-label": word('message'), inert: !writing || side !== 'message', children: _jsx("textarea", { ref: paper, className: "post-desk__write", "aria-label": word('message'), placeholder: placeholder, value: text, onChange:
|
|
74
|
+
return (_jsxs("div", { className: cx('post-desk', `post-desk--${side}`, `post-desk--${at}`, className), ...rest, children: [_jsxs("div", { className: "post-desk__window", onPointerDown: (e) => { from.current = e.clientX; }, onPointerUp: drop, children: [_jsxs("div", { className: "post-desk__card post-paper", children: [_jsx("section", { className: "post-desk__half post-desk__half--message", "aria-label": word('message'), inert: !writing || side !== 'message', children: _jsx("textarea", { ref: paper, className: "post-desk__write", "aria-label": word('message'), placeholder: placeholder, value: text, onChange: (e) => askText(e.currentTarget.value) }) }), _jsxs("section", { className: "post-desk__half post-desk__half--address", "aria-label": word('address'), inert: !writing || side !== 'address', children: [_jsx("div", { className: "post-desk__corner", children: _jsxs("div", { className: "post-corner", children: [!writing && postmark, _jsx("button", { className: "post-corner__stamp post-desk__slot", type: "button", "aria-pressed": chosen != null, "aria-label": word(chosen ? 'stampOff' : 'stampOn'), onClick: () => askStamp(chosen ? null : stamps[0]?.id ?? null), children: chosen ? chosen.stamp : _jsx(StampPlace, { "aria-hidden": "true", children: word('stampOn') }) })] }) }), _jsx("div", { className: "post-desk__lines", children: address })] })] }), writing && _jsx("button", { className: `post-desk__reach post-desk__reach--${side === 'message' ? 'address' : 'message'}`, type: "button", "aria-label": word(side === 'message' ? 'toAddress' : 'toMessage'), onClick: () => askSide(side === 'message' ? 'address' : 'message') }), at === 'sent' && _jsx("div", { className: "post-desk__gone", children: sent })] }), _jsx("p", { className: "post-desk__note", "aria-live": "polite", children: note }), writing && side === 'address' && stamps.length > 0 && (_jsx("div", { className: "post-desk__stamps", role: "group", "aria-label": word('stamps'), children: stamps.map((s) => (_jsx("button", { className: "post-desk__pick", type: "button", "aria-pressed": s.id === stuck, "aria-label": s.label, onClick: () => askStamp(s.id), children: s.stamp }, s.id))) }))] }));
|
|
77
75
|
}
|
|
78
76
|
/** PostDeskLine — one address line of a PostDesk (`address`): a small word and a bare input on a rule. The value is the app's, like any input's. */
|
|
79
77
|
export function PostDeskLine({ label, type = 'text', autoComplete = 'off', className, ...rest }) {
|
package/dist/room.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A field that stops at its paper's room. An input that adds to the field's words while `over()` says they no longer fit is refused:
|
|
2
|
+
* the words go back to what they were, the caret stays where it was, and the app's `onChange` never hears it. Taking words away is
|
|
3
|
+
* never refused. `after(refused)` runs after every input on the paper, refused or not, so the holder can measure again.
|
|
4
|
+
* The listeners sit on `paper`, an element round the field: a native listener there runs before React's own at the root, so stopping
|
|
5
|
+
* the event keeps `onChange` from hearing it. A composition (Thai, Japanese) has reached the app already when it ends, so its words
|
|
6
|
+
* are put back through the prototype's setter and an input is sent, which tells the app they went back.
|
|
7
|
+
* Call `sync()` after the app sets the field's value itself (a cleared field after send); `stop()` takes the listeners off. */
|
|
8
|
+
export interface Room {
|
|
9
|
+
sync(): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function holdRoom(paper: HTMLElement, fieldOf: () => HTMLTextAreaElement | null, over: () => boolean, after?: (refused: boolean) => void): Room;
|
package/dist/room.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function holdRoom(paper, fieldOf, over, after = () => { }) {
|
|
2
|
+
let last = fieldOf()?.value ?? '', composing = false;
|
|
3
|
+
const refuse = (field, dropped) => {
|
|
4
|
+
const added = field.value.length - last.length, at = field.selectionStart - added;
|
|
5
|
+
if (dropped)
|
|
6
|
+
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(field, last);
|
|
7
|
+
else
|
|
8
|
+
field.value = last; // the prototype's setter, so React's value tracker sees the change and the input dispatched next reaches onChange
|
|
9
|
+
field.setSelectionRange(Math.max(0, at), Math.max(0, at));
|
|
10
|
+
if (dropped)
|
|
11
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
12
|
+
};
|
|
13
|
+
const check = (e, dropped) => {
|
|
14
|
+
const field = fieldOf();
|
|
15
|
+
if (!field || e.target !== field)
|
|
16
|
+
return after(false);
|
|
17
|
+
const refused = field.value.length >= last.length && over();
|
|
18
|
+
if (refused) {
|
|
19
|
+
if (!dropped)
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
refuse(field, dropped);
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
last = field.value;
|
|
25
|
+
after(refused);
|
|
26
|
+
};
|
|
27
|
+
const onInput = (e) => (e.isComposing ? after(false) : check(e, false));
|
|
28
|
+
const onComposing = () => { composing = true; };
|
|
29
|
+
const onComposed = (e) => { composing = false; check(e, true); };
|
|
30
|
+
paper.addEventListener('input', onInput);
|
|
31
|
+
paper.addEventListener('compositionstart', onComposing);
|
|
32
|
+
paper.addEventListener('compositionend', onComposed);
|
|
33
|
+
return {
|
|
34
|
+
sync() { const field = fieldOf(); if (field && !composing)
|
|
35
|
+
last = field.value; },
|
|
36
|
+
stop() { paper.removeEventListener('input', onInput); paper.removeEventListener('compositionstart', onComposing); paper.removeEventListener('compositionend', onComposed); },
|
|
37
|
+
};
|
|
38
|
+
}
|