@usableapp/cardds 0.7.90 → 0.7.93

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.
@@ -6,9 +6,11 @@ export interface StampBookProps extends ComponentProps<'div'> {
6
6
  strip?: boolean;
7
7
  /** the page with NO ground — no fill, no padding, no round: the stamps lie on what holds the book, three across, each its own size in the middle of its third. Put the name INSIDE the stamp (`Stamp name`), not under it */
8
8
  bare?: boolean;
9
+ /** the THIRD SIZE: each stamp as wide as its third (4:5), the gaps between them one `--screen-pad` both ways — three stamps and two gaps fill the row, nothing at its sides. With `bare` at 375: 103.67 × 129.58. The one place a stamp is not `--stamp-size` */
10
+ fill?: boolean;
9
11
  }
10
- /** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. The book places its stamps and never sizes them: each is the one size of a stamp, in the middle of its third. */
11
- export declare function StampBook({ places, strip, bare, className, children, ...rest }: StampBookProps): import("react").JSX.Element;
12
+ /** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. The book places its stamps and sizes them only with `fill`: otherwise each is the one size of a stamp, in the middle of its third. */
13
+ export declare function StampBook({ places, strip, bare, fill, className, children, ...rest }: StampBookProps): import("react").JSX.Element;
12
14
  export interface StampCellProps extends Omit<ComponentProps<'span'>, 'onClick'> {
13
15
  /** a short line under the stamp — who it is from */
14
16
  label?: ReactNode;
@@ -4,10 +4,10 @@ import { cx } from '../cx.js';
4
4
  import { pressable } from '../pressable.js';
5
5
  import { Icon } from '../type/Icon.js';
6
6
  import { StampPlace } from './Stamp.js';
7
- /** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. The book places its stamps and never sizes them: each is the one size of a stamp, in the middle of its third. */
8
- export function StampBook({ places = 0, strip, bare, className, children, ...rest }) {
7
+ /** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. The book places its stamps and sizes them only with `fill`: otherwise each is the one size of a stamp, in the middle of its third. */
8
+ export function StampBook({ places = 0, strip, bare, fill, className, children, ...rest }) {
9
9
  const free = Math.max(0, places - Children.toArray(children).length); // toArray drops null / undefined / booleans: `{owned && <StampCell/>}` that is false holds no place (Children.count counts it)
10
- return (_jsxs("div", { className: cx('stamp-book', strip && 'stamp-book--strip', bare && 'stamp-book--bare', className), ...rest, children: [children, Array.from({ length: free }, (_, i) => _jsx(StampPlace, { faint: true, "aria-hidden": "true" }, i))] }));
10
+ return (_jsxs("div", { className: cx('stamp-book', strip && 'stamp-book--strip', bare && 'stamp-book--bare', fill && !strip && 'stamp-book--fill', className), ...rest, children: [children, Array.from({ length: free }, (_, i) => _jsx(StampPlace, { faint: true, "aria-hidden": "true" }, i))] }));
11
11
  }
12
12
  /** StampCell — one place of a StampBook: a Stamp (the child) and a short label under it. */
13
13
  export function StampCell({ label, onClick, on, className, children, ...rest }) {
@@ -1,4 +1,4 @@
1
- import type { ComponentProps, ReactNode } from 'react';
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
2
  import type { StickyTone } from './tones.js';
3
3
  export interface StickyProps extends Omit<ComponentProps<'div'>, 'onClick'> {
4
4
  /** the whole note is a button: it renders a `<button>` and keeps its fold. Not with `corner` (a button cannot hold a button) */
@@ -24,8 +24,10 @@ export interface StickyProps extends Omit<ComponentProps<'div'>, 'onClick'> {
24
24
  }
25
25
  /**
26
26
  * Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
27
- * Every note is that one square whatever it says: words it cannot hold are cut, never the note grown.
27
+ * Every note is that one square whatever it says, never grown: words it cannot hold are written one size smaller (`--sticky-small-font`, the label size:
28
+ * six lines instead of four and a half); words that do not fit even then are cut where their room ends. The note measures its own words.
29
+ * A `StickyField` on it stops at that room: the note refuses the input that would not fit, and the app's `onChange` is not called for it.
28
30
  * THE FOLD SAYS UNREAD: the bottom-right corner folded over = not read yet; `flat` = read. A folded note says so to a screen reader too (`unreadLabel`).
29
31
  * The outer box holds the tone and the shadow; the paper inside is the part that is cut (a clip would cut the shadow too).
30
32
  */
31
- export declare function Sticky({ tone, flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ...rest }: StickyProps): import("react").JSX.Element;
33
+ export declare function Sticky({ tone, flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ref, ...rest }: StickyProps): import("react").JSX.Element;
@@ -1,16 +1,77 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
3
  import { cx } from '../cx.js';
3
4
  import { pressable } from '../pressable.js';
5
+ import { setRefs, watchSize } from '../state.js';
4
6
  import { word } from '../words.js';
7
+ import { fitSticky } from './fit.js';
5
8
  /**
6
9
  * Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
7
- * Every note is that one square whatever it says: words it cannot hold are cut, never the note grown.
10
+ * Every note is that one square whatever it says, never grown: words it cannot hold are written one size smaller (`--sticky-small-font`, the label size:
11
+ * six lines instead of four and a half); words that do not fit even then are cut where their room ends. The note measures its own words.
12
+ * A `StickyField` on it stops at that room: the note refuses the input that would not fit, and the app's `onChange` is not called for it.
8
13
  * THE FOLD SAYS UNREAD: the bottom-right corner folded over = not read yet; `flat` = read. A folded note says so to a screen reader too (`unreadLabel`).
9
14
  * The outer box holds the tone and the shadow; the paper inside is the part that is cut (a clip would cut the shadow too).
10
15
  */
11
- export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ...rest }) {
16
+ export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ref, ...rest }) {
17
+ const own = useRef(null);
18
+ const [small, setSmall] = useState(false);
19
+ const last = useRef('');
20
+ const composing = useRef(false);
21
+ const fieldOf = () => own.current?.querySelector('.sticky-field') ?? null;
22
+ useLayoutEffect(() => {
23
+ const note = own.current;
24
+ if (!note)
25
+ return;
26
+ setSmall(fitSticky(note) !== 'full');
27
+ const field = fieldOf();
28
+ if (field && !composing.current)
29
+ last.current = field.value;
30
+ }, [children, head, date, foot]); // a chat re-renders every note on each keystroke in its answer field; only a note whose words changed is measured again (a field is an element, new every render)
31
+ useEffect(() => {
32
+ const note = own.current, paper = note?.querySelector('.sticky__paper');
33
+ if (!note || !paper)
34
+ return;
35
+ const measure = () => setSmall(fitSticky(note) !== 'full');
36
+ /* A native listener on the paper runs before React's own at the root, so stopping the event here keeps the app's `onChange` from
37
+ hearing an input the note refused. `dropped` is for a composition: the app has heard it already, so it is told the words went back. */
38
+ const refuse = (field, dropped) => {
39
+ const added = field.value.length - last.current.length, at = field.selectionStart - added;
40
+ if (dropped)
41
+ Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(field, last.current);
42
+ else
43
+ field.value = last.current; // the prototype's setter, so React's value tracker sees the change and the input dispatched next reaches onChange
44
+ field.setSelectionRange(Math.max(0, at), Math.max(0, at));
45
+ if (dropped)
46
+ field.dispatchEvent(new Event('input', { bubbles: true }));
47
+ };
48
+ const check = (e, dropped) => {
49
+ const field = fieldOf();
50
+ if (!field || e.target !== field)
51
+ return measure();
52
+ if (fitSticky(note) === 'over' && field.value.length > last.current.length) {
53
+ if (!dropped)
54
+ e.stopPropagation();
55
+ refuse(field, dropped);
56
+ fitSticky(note);
57
+ }
58
+ else
59
+ last.current = field.value;
60
+ measure();
61
+ };
62
+ const onInput = (e) => (e.isComposing ? measure() : check(e, false));
63
+ const onComposing = () => { composing.current = true; };
64
+ const onComposed = (e) => { composing.current = false; check(e, true); };
65
+ paper.addEventListener('input', onInput);
66
+ paper.addEventListener('compositionstart', onComposing);
67
+ paper.addEventListener('compositionend', onComposed);
68
+ const body = paper.querySelector('.sticky__body');
69
+ const stop = watchSize([body], measure);
70
+ document.fonts?.ready.then(measure);
71
+ return () => { paper.removeEventListener('input', onInput); paper.removeEventListener('compositionstart', onComposing); paper.removeEventListener('compositionend', onComposed); stop(); };
72
+ }, []);
12
73
  const notched = corner != null;
13
74
  const { tap, Box, boxProps } = pressable(notched ? undefined : onClick, 'div', rest);
14
75
  const Part = tap ? 'span' : 'div';
15
- return (_jsxs(Box, { className: cx('sticky', `sticky--${tone}`, flat && 'sticky--flat', notched && 'sticky--corner', tilt && `sticky--tilt-${tilt}`, tap && 'sticky--tap', className), ...boxProps, children: [_jsxs(Part, { className: "sticky__paper", children: [(head != null || date != null) && _jsxs(Part, { className: "sticky__head", children: [head, date] }), _jsx(Part, { className: "sticky__body", children: children }), foot != null && _jsx(Part, { className: "sticky__foot", children: foot }), !flat && !notched && _jsx("span", { className: "sticky__fold", "aria-hidden": "true" }), !flat && !notched && _jsx("span", { className: "sticky__state t-sr", children: unreadLabel ?? word('unread') })] }), stuck != null && _jsx(Part, { className: "sticky__stuck", children: stuck }), notched && _jsx("div", { className: "sticky__corner", children: corner })] }));
76
+ return (_jsxs(Box, { ref: setRefs(own, ref), className: cx('sticky', `sticky--${tone}`, small && 'sticky--small', flat && 'sticky--flat', notched && 'sticky--corner', tilt && `sticky--tilt-${tilt}`, tap && 'sticky--tap', className), ...boxProps, children: [_jsxs(Part, { className: "sticky__paper", children: [(head != null || date != null) && _jsxs(Part, { className: "sticky__head", children: [head, date] }), _jsx(Part, { className: "sticky__body", children: children }), foot != null && _jsx(Part, { className: "sticky__foot", children: foot }), !flat && !notched && _jsx("span", { className: "sticky__fold", "aria-hidden": "true" }), !flat && !notched && _jsx("span", { className: "sticky__state t-sr", children: unreadLabel ?? word('unread') })] }), stuck != null && _jsx(Part, { className: "sticky__stuck", children: stuck }), notched && _jsx("div", { className: "sticky__corner", children: corner })] }));
16
77
  }
@@ -3,5 +3,9 @@ export interface StickyFieldProps extends ComponentProps<'textarea'> {
3
3
  /** what is being written — the field's accessible name (it has no visible label) */
4
4
  label: string;
5
5
  }
6
- /** StickyField — the words written ON a Sticky: a bare textarea in the note's own type and ink, filling the note. */
6
+ /**
7
+ * StickyField — the words written ON a Sticky: a bare textarea in the note's own type and ink, filling the note.
8
+ * A note being written stops at its paper's room: the `Sticky` it is on refuses the input that would not fit (even one size smaller), and the
9
+ * app's `onChange` is not called for it. `maxLength` is still the app's to set on top.
10
+ */
7
11
  export declare function StickyField({ label, className, ...rest }: StickyFieldProps): import("react").JSX.Element;
@@ -1,6 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** StickyField — the words written ON a Sticky: a bare textarea in the note's own type and ink, filling the note. */
3
+ /**
4
+ * StickyField — the words written ON a Sticky: a bare textarea in the note's own type and ink, filling the note.
5
+ * A note being written stops at its paper's room: the `Sticky` it is on refuses the input that would not fit (even one size smaller), and the
6
+ * app's `onChange` is not called for it. `maxLength` is still the app's to set on top.
7
+ */
4
8
  export function StickyField({ label, className, ...rest }) {
5
9
  return _jsx("textarea", { className: cx('sticky-field', className), "aria-label": label, ...rest });
6
10
  }
@@ -0,0 +1,6 @@
1
+ export type StickyFit = 'full' | 'small' | 'over';
2
+ /** How a note's words sit on it, measured: `full` at the note's size, `small` once written one size smaller, `over` when even that is too much.
3
+ * Leaves `sticky--small` on the note as the answer says (on for `small` and `over`). A textarea scrolls inside itself, so when the
4
+ * words are a `StickyField` it is the field that is measured, not the body round it. A body under a heading on a board is
5
+ * line-clamped to what it shows and never shrinks. */
6
+ export declare function fitSticky(note: HTMLElement): StickyFit;
@@ -0,0 +1,17 @@
1
+ const SMALL = 'sticky--small';
2
+ /** How a note's words sit on it, measured: `full` at the note's size, `small` once written one size smaller, `over` when even that is too much.
3
+ * Leaves `sticky--small` on the note as the answer says (on for `small` and `over`). A textarea scrolls inside itself, so when the
4
+ * words are a `StickyField` it is the field that is measured, not the body round it. A body under a heading on a board is
5
+ * line-clamped to what it shows and never shrinks. */
6
+ export function fitSticky(note) {
7
+ note.classList.remove(SMALL);
8
+ const body = note.querySelector('.sticky__body');
9
+ if (!body || getComputedStyle(body).webkitLineClamp !== 'none')
10
+ return 'full';
11
+ const words = body.querySelector(':scope > .sticky-field') ?? body;
12
+ const cut = () => words.scrollHeight > words.clientHeight;
13
+ if (!cut())
14
+ return 'full';
15
+ note.classList.add(SMALL);
16
+ return cut() ? 'over' : 'small';
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.90",
3
+ "version": "0.7.93",
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.",