@usableapp/cardds 0.7.107 → 0.7.108

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.
@@ -1,4 +1,5 @@
1
1
  import { type ComponentProps, type ReactNode } from 'react';
2
+ import { type Span } from './span.js';
2
3
  export interface CalendarProps extends Omit<ComponentProps<'div'>, 'title' | 'onSelect'> {
3
4
  /** the month shown ("June") */
4
5
  title: ReactNode;
@@ -12,6 +13,12 @@ export interface CalendarProps extends Omit<ComponentProps<'div'>, 'title' | 'on
12
13
  defaultSelected?: number;
13
14
  /** a day was tapped */
14
15
  onSelect?: (day: number) => void;
16
+ /** CONTROLLED, a run of days instead of one: `[first, last]` (the same day twice for one), or `null` for none; pair it with `onSpanChange`. Given (or `defaultSpan` / `onSpanChange`), the calendar is a span pick: a drag across the days covers them, or a tap on the first and a tap on the last; `selected` is not read */
17
+ span?: Span | null;
18
+ /** UNCONTROLLED: the run covered on arrival; the calendar then keeps its own */
19
+ defaultSpan?: Span;
20
+ /** the member covered a run: the finger lifted after a drag, or the second tap closed it */
21
+ onSpanChange?: (span: Span) => void;
15
22
  /** the previous-month button was tapped: the app turns the month (it owns `title`, `days`, `start`) */
16
23
  onPrev?: () => void;
17
24
  /** the next-month button was tapped: the app turns the month, as with `onPrev` */
@@ -24,7 +31,7 @@ export interface CalendarProps extends Omit<ComponentProps<'div'>, 'title' | 'on
24
31
  marks?: number[];
25
32
  /** strongly marked days */
26
33
  marks2?: number[];
27
- /** days that cannot be picked */
34
+ /** days that cannot be picked (a span never covers one: the run stops before it) */
28
35
  disabled?: number[];
29
36
  /** weekday headers, 7 */
30
37
  weekdays?: string[];
@@ -32,5 +39,7 @@ export interface CalendarProps extends Omit<ComponentProps<'div'>, 'title' | 'on
32
39
  nav?: ReactNode;
33
40
  }
34
41
  /** 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:
35
- * 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). */
36
- 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;
42
+ * the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`).
43
+ * A run of days instead of one (the asker says from when to when, Lh 2026-10-21): `span` + `onSpanChange`, or `defaultSpan` — a drag across
44
+ * the days covers them, or a tap on the first day and a tap on the last; the whole run is one highlight, as a finger drags over words. On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). */
45
+ export declare function Calendar({ title, days, start, selected, defaultSelected, onSelect, span, defaultSpan, onSpanChange, onPrev, onNext, prevLabel, nextLabel, marks, marks2, disabled, weekdays, nav, className, ...rest }: CalendarProps): import("react").JSX.Element;
@@ -4,12 +4,24 @@ import { cx } from '../cx.js';
4
4
  import { IconBtn } from '../actions/IconBtn.js';
5
5
  import { word } from '../words.js';
6
6
  import { calendarDay } from './calendarDay.js';
7
+ import { useSpan } from './span.js';
7
8
  /** 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:
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). */
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
+ * the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`).
10
+ * A run of days instead of one (the asker says from when to when, Lh 2026-10-21): `span` + `onSpanChange`, or `defaultSpan` — a drag across
11
+ * the days covers them, or a tap on the first day and a tap on the last; the whole run is one highlight, as a finger drags over words. On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). */
12
+ export function Calendar({ title, days = 30, start = 1, selected, defaultSelected, onSelect, span, defaultSpan, onSpanChange, onPrev, onNext, prevLabel = word('previous'), nextLabel = word('next'), marks = [], marks2 = [], disabled = [], weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], nav, className, ...rest }) {
10
13
  const [day, pick] = useControlled(selected, defaultSelected, onSelect);
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
+ const spanning = span !== undefined || defaultSpan !== undefined || onSpanChange !== undefined;
15
+ // the hook counts cells from 0; the app counts days from 1
16
+ const run = useSpan({
17
+ span: span ? [span[0] - 1, span[1] - 1] : span, defaultSpan: defaultSpan && [defaultSpan[0] - 1, defaultSpan[1] - 1],
18
+ onSpanChange: onSpanChange && ((s) => onSpanChange([s[0] + 1, s[1] + 1])),
19
+ blocked: (i) => i < 0 || i >= days || disabled.includes(i + 1),
20
+ });
21
+ return (_jsxs("div", { className: cx('calendar', spanning && 'calendar--span', 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", ...(spanning ? run.box : {}), children: [weekdays.map((d) => _jsx("span", { className: "calendar__wd", children: d }, d)), Array.from({ length: days }, (_, i) => i + 1).map((d) => (calendarDay({
22
+ key: d, num: d, mark: marks.includes(d), mark2: marks2.includes(d), selected: !spanning && d === day, disabled: disabled.includes(d),
23
+ style: d === 1 ? { '--start': start } : undefined,
24
+ onPick: () => (spanning ? run.tap(d - 1) : pick(d)),
25
+ ...(spanning ? { index: d - 1, span: run.at(d - 1) } : {}),
14
26
  })))] })] }));
15
27
  }
@@ -28,6 +28,12 @@ export interface CalendarWeeksProps extends Omit<ComponentProps<'div'>, 'onSelec
28
28
  defaultSelected?: string;
29
29
  /** a day was tapped: its `key` */
30
30
  onSelect?: (key: string) => void;
31
+ /** CONTROLLED, a run of days instead of one: `[first, last]` by `key` (the same key twice for one day), or `null` for none; pair it with `onSpanChange`. Given (or `defaultSpan` / `onSpanChange`), the weeks are a span pick: a tap on the first day and a tap on the last covers the days between, across the weeks; a drag along a row covers it too (up and down, the box scrolls); `selected` is not read */
32
+ span?: [string, string] | null;
33
+ /** UNCONTROLLED: the run covered on arrival; the weeks then keep their own */
34
+ defaultSpan?: [string, string];
35
+ /** the member covered a run: the finger lifted after a drag, or the second tap closed it */
36
+ onSpanChange?: (span: [string, string]) => void;
31
37
  /** weekday heads, 7 */
32
38
  weekdays?: string[];
33
39
  /** given, the label is a button: the app opens its own month / year pick. Not given, the label is plain words */
@@ -37,5 +43,7 @@ export interface CalendarWeeksProps extends Omit<ComponentProps<'div'>, 'onSelec
37
43
  * 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
44
  * with `key`, `num`, `today`, `disabled`, `mark`) and owns the day (`selected` + `onSelect`, or `defaultSelected`); a tap on the label (`onLabelClick`)
39
45
  * 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;
46
+ * 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.
47
+ * A run of days instead of one (the asker says from when to when, Lh 2026-10-21): `span` + `onSpanChange`, or `defaultSpan` — a tap on the first day
48
+ * and a tap on the last covers the days between, across the weeks; a drag along a row covers it too, while a finger moving up or down still scrolls the box. On a sheet, never in a Card. */
49
+ export declare function CalendarWeeks({ weeks, show, selected, defaultSelected, onSelect, span, defaultSpan, onSpanChange, weekdays, onLabelClick, className, ref, ...rest }: CalendarWeeksProps): import("react").JSX.Element;
@@ -3,13 +3,20 @@ import { useLayoutEffect, useRef, useState } from 'react';
3
3
  import { useControlled, setRefs } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  import { calendarDay } from './calendarDay.js';
6
+ import { useSpan, spanByKey } from './span.js';
6
7
  /** 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
8
  * 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
9
  * with `key`, `num`, `today`, `disabled`, `mark`) and owns the day (`selected` + `onSelect`, or `defaultSelected`); a tap on the label (`onLabelClick`)
9
10
  * 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 }) {
11
+ * 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.
12
+ * A run of days instead of one (the asker says from when to when, Lh 2026-10-21): `span` + `onSpanChange`, or `defaultSpan` — a tap on the first day
13
+ * and a tap on the last covers the days between, across the weeks; a drag along a row covers it too, while a finger moving up or down still scrolls the box. On a sheet, never in a Card. */
14
+ export function CalendarWeeks({ weeks, show = 2, selected, defaultSelected, onSelect, span, defaultSpan, onSpanChange, weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], onLabelClick, className, ref, ...rest }) {
12
15
  const [day, pick] = useControlled(selected, defaultSelected, onSelect);
16
+ const spanning = span !== undefined || defaultSpan !== undefined || onSpanChange !== undefined;
17
+ const all = weeks.flatMap((w) => w.days);
18
+ const covered = useSpan({ ...spanByKey(all.map((d) => d.key), span, defaultSpan, onSpanChange), blocked: (i) => !all[i] || !!all[i].disabled });
19
+ let at = 0;
13
20
  const [first, setFirst] = useState(0);
14
21
  const box = useRef(null);
15
22
  // 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
@@ -58,7 +65,14 @@ export function CalendarWeeks({ weeks, show = 2, selected, defaultSelected, onSe
58
65
  return () => { watch.disconnect(); el.removeEventListener('scroll', onScroll); cancelAnimationFrame(frame); };
59
66
  }, [show, run, todayAt]);
60
67
  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
68
+ return (_jsxs("div", { ref: setRefs(box, ref), className: cx('calendar', 'calendar--weeks', spanning && 'calendar--span', className), ...rest, ...(spanning ? covered.box : {}), children: [_jsxs("div", { className: "calendar__weeks-head", children: [onLabelClick
62
69
  ? _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)))] }));
70
+ : _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) => {
71
+ const i = at++;
72
+ return calendarDay({
73
+ key: d.key, num: d.num, mark: d.mark === 'soft', mark2: d.mark === 'strong', selected: !spanning && d.key === day, today: d.today, disabled: d.disabled,
74
+ onPick: () => (spanning ? covered.tap(i) : pick(d.key)),
75
+ ...(spanning ? { index: i, span: covered.at(i) } : {}),
76
+ });
77
+ }) }, w.days[0]?.key ?? i)))] }));
64
78
  }
@@ -0,0 +1,36 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export interface Hour {
3
+ /** the app's id for the hour ("09:00"): what `onSpanChange` and `onSelect` hand back */
4
+ key: string;
5
+ /** what the cell shows ("9", "๙") */
6
+ num: ReactNode;
7
+ /** the hour cannot be picked (already past, outside the asked span) */
8
+ disabled?: boolean;
9
+ /** a soft mark (the asked hours) or a strong one (booked) */
10
+ mark?: 'soft' | 'strong';
11
+ }
12
+ export interface HourGridProps extends Omit<ComponentProps<'div'>, 'onSelect'> {
13
+ /** the hours, in order; the app builds them (which hours, in whose numerals) */
14
+ hours: Hour[];
15
+ /** the words over the grid: what the numbers are ("hour of the day", "น.") */
16
+ label?: ReactNode;
17
+ /** CONTROLLED, from when to when: `[first, last]` by `key` (the same key twice for one hour), or `null` for none; pair it with `onSpanChange`. Given (or `defaultSpan` / `onSpanChange`), a drag across the hours covers them, or a tap on the first and a tap on the last; `selected` is not read */
18
+ span?: [string, string] | null;
19
+ /** UNCONTROLLED: the run covered on arrival; the grid then keeps its own */
20
+ defaultSpan?: [string, string];
21
+ /** the member covered a run: the finger lifted after a drag, or the second tap closed it */
22
+ onSpanChange?: (span: [string, string]) => void;
23
+ /** CONTROLLED, one hour: the picked hour's `key`, or `null` for none; pair it with `onSelect` */
24
+ selected?: string | null;
25
+ /** UNCONTROLLED: the hour picked on arrival */
26
+ defaultSelected?: string;
27
+ /** an hour was tapped: its `key` */
28
+ onSelect?: (key: string) => void;
29
+ }
30
+ /** HourGrid — the hours of a day as Calendar's own 44px round cells, `--hour-grid-cols` to a row (6 by default), with a label over them. From when
31
+ * to when (the asker's hours, Lh 2026-10-21): `span` + `onSpanChange`, or `defaultSpan` — one drag across the hours covers them, or a tap on the
32
+ * first and a tap on the last; the whole run is one highlight, and it never jumps a disabled hour.
33
+ * One hour instead: `selected` + `onSelect`, or `defaultSelected`. The helper answers within the asked hours: the app marks them (`mark: 'soft'`)
34
+ * and disables the rest, and the helper's own span stays inside. It knows no clock: the app builds `hours` (`key`, `num`, `disabled`, `mark`).
35
+ * On a sheet, never in a Card. */
36
+ export declare function HourGrid({ hours, label, span, defaultSpan, onSpanChange, selected, defaultSelected, onSelect, className, ...rest }: HourGridProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useControlled } from '../state.js';
3
+ import { cx } from '../cx.js';
4
+ import { calendarDay } from './calendarDay.js';
5
+ import { useSpan, spanByKey } from './span.js';
6
+ /** HourGrid — the hours of a day as Calendar's own 44px round cells, `--hour-grid-cols` to a row (6 by default), with a label over them. From when
7
+ * to when (the asker's hours, Lh 2026-10-21): `span` + `onSpanChange`, or `defaultSpan` — one drag across the hours covers them, or a tap on the
8
+ * first and a tap on the last; the whole run is one highlight, and it never jumps a disabled hour.
9
+ * One hour instead: `selected` + `onSelect`, or `defaultSelected`. The helper answers within the asked hours: the app marks them (`mark: 'soft'`)
10
+ * and disables the rest, and the helper's own span stays inside. It knows no clock: the app builds `hours` (`key`, `num`, `disabled`, `mark`).
11
+ * On a sheet, never in a Card. */
12
+ export function HourGrid({ hours, label, span, defaultSpan, onSpanChange, selected, defaultSelected, onSelect, className, ...rest }) {
13
+ const spanning = span !== undefined || defaultSpan !== undefined || onSpanChange !== undefined;
14
+ const [hour, pick] = useControlled(selected, defaultSelected, onSelect);
15
+ const run = useSpan({ ...spanByKey(hours.map((h) => h.key), span, defaultSpan, onSpanChange), blocked: (i) => !hours[i] || !!hours[i].disabled });
16
+ return (_jsxs("div", { className: cx('calendar', 'hour-grid', spanning && 'calendar--span', className), ...rest, children: [label != null && _jsx("span", { className: "calendar__label", children: label }), _jsx("div", { className: "calendar__grid", ...(spanning ? run.box : {}), children: hours.map((h, i) => calendarDay({
17
+ key: h.key, num: h.num, mark: h.mark === 'soft', mark2: h.mark === 'strong', selected: !spanning && h.key === hour, disabled: h.disabled,
18
+ onPick: () => (spanning ? run.tap(i) : pick(h.key)),
19
+ ...(spanning ? { index: i, span: run.at(i) } : {}),
20
+ })) })] }));
21
+ }
@@ -1,6 +1,9 @@
1
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 }: {
2
+ import type { SpanAt } from './span.js';
3
+ /** one `.calendar__day` cell: the button Calendar, CalendarWeeks and HourGrid all draw, so the cell and every `--calendar-day-*` knob are one.
4
+ * In a span (`index` given): the cell carries its order for the span mechanic, and `span` says where it stands under the one highlight
5
+ * (`aria-pressed` says whether it is covered); a key on the cell (not a pointer, which the grid handles) is a tap */
6
+ export declare function calendarDay({ key, num, mark, mark2, selected, today, disabled, style, onPick, index, span }: {
4
7
  key: string | number;
5
8
  num: ReactNode;
6
9
  mark?: boolean;
@@ -10,4 +13,6 @@ export declare function calendarDay({ key, num, mark, mark2, selected, today, di
10
13
  disabled?: boolean;
11
14
  style?: CSSProperties;
12
15
  onPick: () => void;
16
+ index?: number;
17
+ span?: SpanAt;
13
18
  }): import("react").JSX.Element;
@@ -1,6 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
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));
3
+ /** one `.calendar__day` cell: the button Calendar, CalendarWeeks and HourGrid all draw, so the cell and every `--calendar-day-*` knob are one.
4
+ * In a span (`index` given): the cell carries its order for the span mechanic, and `span` says where it stands under the one highlight
5
+ * (`aria-pressed` says whether it is covered); a key on the cell (not a pointer, which the grid handles) is a tap */
6
+ export function calendarDay({ key, num, mark, mark2, selected, today, disabled, style, onPick, index, span }) {
7
+ return (_jsx("button", { type: "button", className: cx('calendar__day', mark && 'calendar__day--mark', mark2 && 'calendar__day--mark2', span && `calendar__day--span-${span}`), "aria-selected": selected ? 'true' : undefined, "aria-pressed": index === undefined ? undefined : span ? 'true' : 'false', "aria-current": today ? 'date' : undefined, onClick: index === undefined ? onPick : (e) => { if (e.detail === 0)
8
+ onPick(); }, disabled: disabled, style: style, "data-span": index, children: num }, key));
6
9
  }
@@ -0,0 +1,34 @@
1
+ import { type PointerEvent as ReactPointerEvent } from 'react';
2
+ /** a run of cells, by their index in the holder's order: `[first, last]`, the same index twice for one cell */
3
+ export type Span = [number, number];
4
+ /** where a cell stands in the span: the one cell of a one-cell span, the first, one between, the last — or nowhere */
5
+ export type SpanAt = 'one' | 'start' | 'in' | 'end' | undefined;
6
+ /** the span mechanic, written once for Calendar, CalendarWeeks and HourGrid (Lh 2026-10-21: the asker says from when to when).
7
+ * The holder numbers its cells in order (`data-span` = the index), spreads `box` on the grid, and asks `at(i)` for each cell's class.
8
+ * One drag across the cells covers them: the first cell under the finger is one end, the last the other, whichever way the finger
9
+ * went. Two taps do the same without a drag: a tap makes a one-cell span, a second tap on another cell closes it (a third starts over).
10
+ * A cell that is `blocked` (disabled) is never covered: the run stops before it, so a span never jumps a day that cannot be picked.
11
+ * Controlled like an input: `span` + `onSpanChange`, or `defaultSpan`; while the finger is down the cells show the live run and the
12
+ * app is asked once, when it lifts. A pointer the browser takes away (the box scrolled instead) covers nothing. */
13
+ export declare function useSpan({ span, defaultSpan, onSpanChange, blocked }: {
14
+ span: Span | null | undefined;
15
+ defaultSpan?: Span;
16
+ onSpanChange?: (span: Span) => void;
17
+ blocked: (i: number) => boolean;
18
+ }): {
19
+ span: Span | null;
20
+ at: (i: number) => SpanAt;
21
+ tap: (i: number) => void;
22
+ box: {
23
+ onPointerDown: (e: ReactPointerEvent<HTMLElement>) => void;
24
+ onPointerMove: (e: ReactPointerEvent<HTMLElement>) => void;
25
+ onPointerUp: () => void;
26
+ onPointerCancel: () => void;
27
+ };
28
+ };
29
+ /** the same span by the holder's keys (CalendarWeeks, HourGrid): useSpan's `span`, `defaultSpan` and `onSpanChange` from a `[key, key]` span, the ask handed back in keys */
30
+ export declare function spanByKey(keys: string[], span: [string, string] | null | undefined, defaultSpan: [string, string] | undefined, onSpanChange: ((span: [string, string]) => void) | undefined): {
31
+ span: Span | null | undefined;
32
+ defaultSpan: Span | undefined;
33
+ onSpanChange: ((s: Span) => void) | undefined;
34
+ };
@@ -0,0 +1,80 @@
1
+ import { useRef, useState } from 'react';
2
+ import { useControlled } from '../state.js';
3
+ const sorted = (a, b) => (a <= b ? [a, b] : [b, a]);
4
+ /** the span mechanic, written once for Calendar, CalendarWeeks and HourGrid (Lh 2026-10-21: the asker says from when to when).
5
+ * The holder numbers its cells in order (`data-span` = the index), spreads `box` on the grid, and asks `at(i)` for each cell's class.
6
+ * One drag across the cells covers them: the first cell under the finger is one end, the last the other, whichever way the finger
7
+ * went. Two taps do the same without a drag: a tap makes a one-cell span, a second tap on another cell closes it (a third starts over).
8
+ * A cell that is `blocked` (disabled) is never covered: the run stops before it, so a span never jumps a day that cannot be picked.
9
+ * Controlled like an input: `span` + `onSpanChange`, or `defaultSpan`; while the finger is down the cells show the live run and the
10
+ * app is asked once, when it lifts. A pointer the browser takes away (the box scrolled instead) covers nothing. */
11
+ export function useSpan({ span, defaultSpan, onSpanChange, blocked }) {
12
+ const [held, request] = useControlled(span, defaultSpan ?? null, onSpanChange);
13
+ const [live, setLive] = useState(null);
14
+ const drag = useRef(null);
15
+ // the farthest cell toward `to` the run from `from` reaches before a blocked cell
16
+ const reach = (from, to) => {
17
+ const step = to < from ? -1 : 1;
18
+ let at = from;
19
+ while (at !== to && !blocked(at + step))
20
+ at += step;
21
+ return at;
22
+ };
23
+ const cellAt = (box, x, y) => {
24
+ const cell = document.elementFromPoint(x, y)?.closest('[data-span]');
25
+ return cell && box.contains(cell) ? Number(cell.dataset.span) : null;
26
+ };
27
+ // a tap on `i` (a finger that did not move, or a key on the cell): the second end of a one-cell span, else a new one-cell span
28
+ const tap = (i) => {
29
+ const one = held && held[0] === held[1] ? held[0] : null;
30
+ request(one !== null && one !== i ? sorted(one, reach(one, i)) : [i, i]);
31
+ };
32
+ const onPointerDown = (e) => {
33
+ const cell = e.target.closest('[data-span]');
34
+ if (!cell || !e.currentTarget.contains(cell))
35
+ return;
36
+ const i = Number(cell.dataset.span);
37
+ if (blocked(i))
38
+ return;
39
+ e.currentTarget.setPointerCapture(e.pointerId);
40
+ drag.current = { from: i, to: i, moved: false };
41
+ setLive([i, i]);
42
+ };
43
+ const onPointerMove = (e) => {
44
+ const d = drag.current;
45
+ if (!d)
46
+ return;
47
+ const i = cellAt(e.currentTarget, e.clientX, e.clientY);
48
+ if (i === null || i === d.to)
49
+ return;
50
+ d.moved = true;
51
+ d.to = reach(d.from, i);
52
+ setLive(sorted(d.from, d.to));
53
+ };
54
+ const onPointerUp = () => {
55
+ const d = drag.current;
56
+ if (!d)
57
+ return;
58
+ drag.current = null;
59
+ setLive(null);
60
+ if (d.moved)
61
+ request(sorted(d.from, d.to));
62
+ else
63
+ tap(d.from);
64
+ };
65
+ const onPointerCancel = () => { drag.current = null; setLive(null); };
66
+ const shown = live ?? held ?? null;
67
+ const at = (i) => {
68
+ if (!shown)
69
+ return undefined;
70
+ if (shown[0] === shown[1])
71
+ return i === shown[0] ? 'one' : undefined;
72
+ return i === shown[0] ? 'start' : i === shown[1] ? 'end' : i > shown[0] && i < shown[1] ? 'in' : undefined;
73
+ };
74
+ return { span: shown, at, tap, box: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel } };
75
+ }
76
+ /** the same span by the holder's keys (CalendarWeeks, HourGrid): useSpan's `span`, `defaultSpan` and `onSpanChange` from a `[key, key]` span, the ask handed back in keys */
77
+ export function spanByKey(keys, span, defaultSpan, onSpanChange) {
78
+ const of = (s) => (s ? [keys.indexOf(s[0]), keys.indexOf(s[1])] : s);
79
+ return { span: of(span), defaultSpan: of(defaultSpan) ?? undefined, onSpanChange: onSpanChange && ((s) => onSpanChange([keys[s[0]], keys[s[1]]])) };
80
+ }
package/dist/index.d.ts CHANGED
@@ -53,6 +53,8 @@ export * from './choice/Check.js';
53
53
  export * from './choice/DayStrip.js';
54
54
  export * from './choice/Calendar.js';
55
55
  export * from './choice/CalendarWeeks.js';
56
+ export * from './choice/HourGrid.js';
57
+ export type { Span, SpanAt } from './choice/span.js';
56
58
  export * from './choice/Mood.js';
57
59
  export * from './choice/Slider.js';
58
60
  export * from './numbers/Ring.js';
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ export * from './choice/Check.js';
57
57
  export * from './choice/DayStrip.js';
58
58
  export * from './choice/Calendar.js';
59
59
  export * from './choice/CalendarWeeks.js';
60
+ export * from './choice/HourGrid.js';
60
61
  export * from './choice/Mood.js';
61
62
  export * from './choice/Slider.js';
62
63
  export * from './numbers/Ring.js';
@@ -8,7 +8,8 @@ export interface JourneyBadgeProps extends ComponentProps<'span'> {
8
8
  sub?: ReactNode;
9
9
  /** no fill: a date box beside a title (Jun / 18) */
10
10
  plain?: boolean;
11
- /** pinned to a CardMedia's corner (rating, price) */
11
+ /** pinned to a CardMedia's corner (rating, price, a distance). On a CardMedia: the paper (`--card-1-bg`), its corner concentric with the media's;
12
+ * a theme's `--tile-badge-bg` / `--tile-badge-radius` still win. Ruled (Lh 2026-10-21) */
12
13
  corner?: boolean;
13
14
  /** 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
15
  * three beside a two-line title (day · number · month). Write it as the head's last child, after the title block */
@@ -15,9 +15,11 @@ export interface KvRowProps extends ComponentProps<'div'> {
15
15
  /** KvRow — one key · value line (48px tall); the value may hold a Chip, an Avatar, an Icon. */
16
16
  export declare function KvRow({ label, className, children, ...rest }: KvRowProps): import("react").JSX.Element;
17
17
  export interface KvGridProps extends ComponentProps<'div'> {
18
+ /** the one number in the middle of the room it stands in (a turning card's back, a Centre): the grid takes its holder's height and stands its cells in the middle of it, each cell's value and caption centred */
19
+ centre?: boolean;
18
20
  }
19
- /** KvGrid — value-over-label cells split by a rule (Sex | Weight); children are KVCells. */
20
- export declare function KvGrid({ className, ...rest }: KvGridProps): import("react").JSX.Element;
21
+ /** KvGrid — value-over-label cells split by a rule (Sex | Weight); children are KVCells. `centre`: one cell at the middle of a card's back (a code the member copies). */
22
+ export declare function KvGrid({ centre, className, ...rest }: KvGridProps): import("react").JSX.Element;
21
23
  export interface KvCellProps extends ComponentProps<'div'> {
22
24
  /** an icon before the value */
23
25
  icon?: IconName;
package/dist/lists/Kv.js CHANGED
@@ -9,9 +9,9 @@ export function Kv({ lined, className, ...rest }) {
9
9
  export function KvRow({ label, className, children, ...rest }) {
10
10
  return (_jsxs("div", { className: cx('kv__row', className), ...rest, children: [_jsx("dt", { className: "kv__key", children: label }), _jsx("dd", { className: "kv__val", children: children })] }));
11
11
  }
12
- /** KvGrid — value-over-label cells split by a rule (Sex | Weight); children are KVCells. */
13
- export function KvGrid({ className, ...rest }) {
14
- return _jsx("div", { className: cx('kv-grid', className), ...rest });
12
+ /** KvGrid — value-over-label cells split by a rule (Sex | Weight); children are KVCells. `centre`: one cell at the middle of a card's back (a code the member copies). */
13
+ export function KvGrid({ centre, className, ...rest }) {
14
+ return _jsx("div", { className: cx('kv-grid', centre && 'kv-grid--centre', className), ...rest });
15
15
  }
16
16
  /** KvCell — an icon, a big value, a caption label. */
17
17
  export function KvCell({ icon, value, label, className, ...rest }) {
package/docs/reference.md CHANGED
@@ -608,7 +608,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
608
608
  control the member fills in or picks stands on a sheet: a `Sheet`'s
609
609
  `Body`, a `SheetStep`, or a stack sheet. That is `Field`, `Composer`
610
610
  and `ComposerArea`, `Pin`, `Dropdown`, `Slider`, `Toggle`, `Check`,
611
- `ChipPick`, `Calendar`, `DayStrip` and `Day`, `Picker`, `Mood`,
611
+ `ChipPick`, `Calendar`, `CalendarWeeks`, `HourGrid`, `DayStrip` and `Day`, `Picker`, `Mood`,
612
612
  `AvatarPick`, `FileBtn`, `AddRow`, `MapPick`, a `Tile pick` and a
613
613
  `Tile upload`. A card is for reading and pressing buttons only; never put
614
614
  a form, or one control of it, in a card. `cardds check` rule
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.107",
3
+ "version": "0.7.108",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",