@usableapp/cardds 0.7.81 → 0.7.83

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 CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.83 — 2026-10-11
6
+
7
+ ### Changed — breaking for a screen that draws a `Sticky`
8
+ - **Every `Sticky` is one square of one size** (Lh 2026-10-10) — before → after: a note hugged its words, was at least 211 tall, grew with more words, and had 4px round corners → every note is a square of 168 with square corners, whatever it says. Words it cannot hold are cut where their room ends, with no ellipsis. To bring a screen up to date: shorten what a note says, or open the note to show the rest. One knob sets the side: `--sticky-size` (on `:root` for every note, on an element for the notes inside it). `--sticky-radius` still rounds the corners; its default is now 0.
9
+ - **`Sticky square` is gone** — before → after: `square` made a note 1:1 and as wide as its column → every note is the one square; delete the prop. No alias: a screen that still passes it fails its typecheck.
10
+ - **Seven knobs are gone**: `--sticky-min-w`, `--sticky-max-w`, `--sticky-min-h`, `--sticky-square-max-w`, `--sticky-tilt-max-w`, `--sticky-post-max-w`, and `--sticky-field-min-h` (the field fills the note). Setting one now does nothing; use `--sticky-size`.
11
+ - **The fold means unread** — before → after: the folded corner was a look, `flat` the plain square → a folded corner says the note has not been read, `flat` says it has. A folded note also carries a hidden word for a screen reader ("Unread" / "ยังไม่ได้อ่าน" by `<html lang>`; `unreadLabel` for the app's own; `setWords` for another language). To bring a screen up to date: pass `flat` on every note that is not new.
12
+ - **A stamp on a note is 48 wide** — before → after: a `StampOn` in `Sticky stuck` was 27% of the note → 48, whatever the note's size (`--sticky-stamp-w`). `--stamp-on-w` still sets it; the note's own knob wins.
13
+ - **A lone note stands in the middle** of a `Page` or a `Centre` (it used to be the `square` note's own doing).
14
+
15
+ ### Changed
16
+ - **`npx cardds check` says how many files it read, and refuses to call nothing "clean"** (Lh 2026-10-10) — before → after: `cardds check docs` (a folder with no `.js .jsx .ts .tsx`) or `cardds check src scr` (a typo next to a real folder) printed `cardds check: clean` and exited 0, because a path that was not there was dropped and a folder with nothing in it had nothing to flag → the line ends with the count (`cardds check: clean (103 files read)`), a path that is not there is `…: no such folder or file` and exit 2 before any file is read, a folder with no source in it is exit 2, and a single file is a path too. Findings still exit 1. A script that ran `cardds check <dir>` on an empty or misspelt folder now fails where it passed.
17
+
18
+ ### Fixed
19
+ - **A `BtnRow` in a `ModalAsk` fills the card** (Lh 2026-10-11) — before → after: the row was as wide as its buttons' words, so two word buttons were squeezed and their words wrapped (99 × 87 each at 375) → the row fills the ask, 24 under the question, and each button keeps its words on one line (146.5 × 60). A `Btn quiet` in a `ModalAsk` is edged in the ask's own ink at full strength (it was 35%): light on the dark end, dark on `ModalAsk light`. A `BtnTrio` is unchanged. Knobs: `--modal-drawer-answers-gap`, `--modal-drawer-quiet-border`. Story `Modal · WordAnswers`. A project that forced this from outside can delete its own rule.
20
+ - **`cardds check` and the ESLint rule missed two cardds classes** — before → after: `raw-class` read the class list from `css/*.css` and lost every class that only ever stands second in a compound selector (`.field.field--error`), so `className="field--error"` and `className="pin--error"` were not flagged → both are in the list (494 of 494). A screen that wrote either by hand now gets the `raw-class` error: use `Field error` / `Pin error`.
21
+ - **`cardds get <Name>` left out props and parameters** — before → after: `Countdown` showed none of the `Link` props it takes (`disabled`, `muted`, `icon`, `sm`) and `Home`, `Splash`, `Walkthrough`, `CallSheet` none of the `Screen` ones (`as`, `palette`, `ready`), because only the component's own interface was read; the ten functions an app calls (`slideTo`, `slideScreen`, `useLayer`, `useOptionalLayer`, `word`, `setWords`, `setTextScale`, `controlsFor`, `registerIcons`, `applyMask`) showed an empty `props` → inherited props are listed (`Omit<…>` taken off), and a function lists its `parameters` (`slideTo`: `direction`, `update`, `scope = document`; `--json` has `params`).
22
+ - **`cardds get slideTo` showed no variation though three stories call it** — before → after: a story counted as using a component only when it rendered it as a tag, so `slideTo`, `slideScreen`, `useLayer` and `useLayers` were used by no story → a function a story calls counts (7 story–function pairs found), and `get` / `find` show those stories.
23
+ - **The MCP server kept an old catalog after `package.json` or `gallery/vite.config.ts` changed** — both are read by the catalog build (the version; how the stories render), neither moved the stamp that makes the server load again.
24
+ - **A line of `docs/roles.md` the reader could not parse was dropped** — the table is read line by line now and a line that is not a row names itself in the error (nothing in the table is wrong today).
25
+
26
+ ### Added
27
+ - **`StickyBoard`** — notes stuck on a page: rows that wrap by the width (two on a phone, three on a wider screen), edge to edge, each turned by one of four angles in turn, the later note over the earlier where they cross. Knobs: `--sticky-board-lap`, `--sticky-board-turn-1` … `-4`, `--sticky-board-drop`, `--sticky-board-foot-inset`. A note's `foot` there lies over the note's foot and takes no room from the words.
28
+ - **`StickyThread lapped`** — a chat with no head rows: theirs at the left, mine at the right, each turn lying over a corner of the one before, the time close under its note. `StickyPost who` is not drawn and `name` is only heard. Knobs: `--sticky-thread-lap` (76 down), `--sticky-thread-lap-x` (32 across), `--sticky-thread-same-gap`, `--sticky-thread-time-gap`, `--sticky-thread-tilt`. The head-row way and `rail` are unchanged.
29
+ - **`StickyAnswer`** — the note you answer on, in its two places: waiting at the screen's foot, and at the top while written on, with `tools` and `tray` under it. `writing` + `onWritingChange(writing, 'focus' | 'outside' | 'escape')`, or `defaultWriting`. Knobs: `--sticky-answer-show`, `-end`, `-tilt`, `-top`, `-gap`, `-tools-gap`, `-tray-gap`, `-scrim-bg`.
30
+ - **`Chip sm`** — a small label, low and tight (a name on a note). Before: a chip was 38 tall whatever its padding knobs said, because its height is the `--chip-h` token. Never a tap target. Knobs: `--chip-sm-pad-y`, `--chip-sm-pad-x`, `--chip-sm-gap`.
31
+ - **A `Chip` in a note's `foot` or `head`** is a label on the paper: a pale ground (`--sticky-chip-bg`), no edge. In the foot it stands at the start and is cut with an ellipsis past `--sticky-foot-chip-max-w` (62%) of the row; what follows it stands at the end and is never cut.
32
+ - Gallery: `StickyBoard`, `StickyAnswer`, `StickyThread · Lapped`, `Sticky · Unread / LongWords / Size`, `Chip · Small`, and the screens `StickyScreens · ChatPage`, `Chat` and `EmptyChat` (a chat with no note yet: an `EmptyState` on the ground, the answer note at the foot).
33
+
34
+ ## 0.7.82 — 2026-10-10
35
+
36
+ ### Changed
37
+ - **The edge of a `StampPlace` is round dots, close together** (Lh 2026-10-10: the dashes stood too far apart) — before → after: a stroke of 8px dashes with a gap of 12px → 2px round dots with 4px clear between them (a pitch of 6px from one centre to the next), still round at `--stamp-place-radius`, the same colours (`--stamp-place-border`, `--stamp-place-faint-border`, a card's muted ink), the same 4:5 size, the words and icon inside where they were. The ring closes wherever its length ends, so one gap, at the top left, is a little shorter or longer than the rest. A project that set `--stamp-place-dash` and `--stamp-place-gap` for 0.7.77's dashes keeps them, now with round ends (each dash is longer by one dot, each gap shorter by one dot); unset them for dots.
38
+
39
+ ### Added
40
+ - **`--stamp-place-dot`** — how wide each dot of a `StampPlace`'s edge is (default `--rule-w`). The place's clear border, the edge's inset and its corner follow it, so the dots' outer edge stays the box's edge and the words stay where they are.
41
+
42
+ ### Fixed
43
+ - **`docs/knobs.md` listed 13 knobs too few** — a knob whose default is a `calc()` with a `var()` inside (`--btn-form-pad-x`, `--card-head-stamp-w`, `--stack-back-size`…) was missed by `npm run knobs`, so the doc and the knob test never saw it. The script now reads one more level of brackets.
44
+
5
45
  ## 0.7.81 — 2026-10-10
6
46
 
7
47
  ### Added
package/README.md CHANGED
@@ -569,6 +569,45 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
569
569
 
570
570
  Live: every screen in the gallery (`npm run dev`).
571
571
 
572
+ ## Sticky notes
573
+
574
+ A sticky note is a square of paper with a few words on it. The family is in `src/sticky/`, its CSS in `css/sticky.css`.
575
+
576
+ - **One note, one size.** Every `Sticky` is one square, 168 by default, with square corners. It never grows with its
577
+ words: words it cannot hold are cut where their room ends, with no ellipsis. One knob sets the side,
578
+ `--sticky-size`; set it on an element to resize the notes inside that element only.
579
+ - **The fold says unread.** A folded bottom-right corner means the note has not been read. `flat` means it has. A
580
+ folded note also says so to a screen reader after its words: cardds's own word by the page's language, or
581
+ `unreadLabel`.
582
+ - **`Sticky`** — `tone` is one of the nine papers in `STICKY_TONES`. `head`, `date`, `foot` are small rows above and
583
+ under the words. `onClick` makes the whole note a button. `stuck` holds what is stuck on the paper (`StampOn`).
584
+ `tilt` turns the one note a start screen hands over. A `Chip sm` in the `foot` is a name label on the paper: it
585
+ stands at the start, what follows it (a time) at the end, on one line.
586
+ - **`StickyBoard`** — notes stuck on a page. Rows wrap by the width: two on a phone, three from 504 of room, a screen about 540 wide.
587
+ The notes stand edge to edge and each is turned by one of four angles in turn (`--sticky-board-turn-1` to `-4`), so
588
+ no two notes that touch lean alike. Later is on top, always: the right note over the left, the lower over the
589
+ upper. `--sticky-board-lap` (0) is how far each lies over the one before. Where two notes do not fit side by side,
590
+ each lies over the one before by what is missing, up to half the fold; past that there is one note in a row. A
591
+ note's `foot` is laid over the note's foot and takes no room from the words, so a note reads the same on the page
592
+ as where it is opened.
593
+ - **`StickyThread`** — notes as a conversation, in two ways. With head rows: each `StickyPost` shows `who`, `name`
594
+ and `time` above its notes, and `rail` joins the avatars for a feed. `lapped`: a chat with no head rows. Theirs
595
+ stand at the left and mine (`side="right"`) at the right, each turn lying over a corner of the one before
596
+ (`--sticky-thread-lap` down, `--sticky-thread-lap-x` across), the time close under its note, the name only heard.
597
+ Two turns from one side stand `--sticky-thread-same-gap` apart.
598
+ - **`StickyAnswer`** — the note you answer on. Its child is a `Sticky flat` holding a `StickyField`. At rest it waits
599
+ at the screen's foot near the right, three quarters of it in sight (`--sticky-answer-show`). A touch on it puts the
600
+ cursor in the field and the holder asks to write: `writing` + `onWritingChange(writing, reason)`, or
601
+ `defaultWriting`. While writing, the note stands at the top, the screen behind is washed pale and takes no touch,
602
+ and `tools` (the paper chooser, the send button) and `tray` (the writer's stamp) stand under it. A touch outside or
603
+ Escape asks to stop. The words, the paper and the stamps on the note are the app's. It is a child of the `Screen`.
604
+ - **`StickyField`** is the bare textarea on a note. **`StickyPick`** is the paper chooser: `value` + `onChange`,
605
+ `tones` for the ones on offer.
606
+
607
+ Whose colour a note wears is the app's rule. The gallery's `StickyScreens` shows one: a neighbour's note wears the
608
+ ground of that neighbour's stamp, and my note wears the paper I picked while writing, from a writing set plus the
609
+ ground of my own stamp.
610
+
572
611
  ## Stamps
573
612
 
574
613
  A stamp is a small paper picture a member keeps and sticks on a note or a postcard for a neighbour. Sending never uses
@@ -579,9 +618,11 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
579
618
  same list and the same `--sticky-<tone>-bg` knobs as a note's paper). `name` is the big line on the picture's foot,
580
619
  `words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. It fills its column up to
581
620
  `--stamp-max-w`, and every inner length follows its width, so one stamp reads in a tray and in a sheet.
582
- - **`StampPlace`** — an empty dashed place the size of a stamp: a word inside, `onClick` for "make yours", `faint` for a
583
- gap nobody is asked to fill. The edge is a drawn stroke, so its dashes are yours: `--stamp-place-dash` and
584
- `--stamp-place-gap` (defaults 8px and 12px), its corner is `--stamp-place-radius`.
621
+ - **`StampPlace`** — an empty place the size of a stamp, its edge a ring of round dots: a word inside, `onClick` for "make
622
+ yours", `faint` for a gap nobody is asked to fill. The edge is a drawn stroke, so the dots are yours: `--stamp-place-dot`
623
+ is how wide each is (default `--rule-w`, 2px), `--stamp-place-gap` the distance from one dot's centre to the next
624
+ (default 6px), and a `--stamp-place-dash` above 0 stretches each dot into a round-ended dash. Its corner is
625
+ `--stamp-place-radius`. The dots' outer edge is the box's edge whatever their width.
585
626
  - **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
586
627
  the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. `strip` is one line
587
628
  of small stamps that scrolls sideways, the tray a member picks from. A `false` or `null` child holds no place.
@@ -593,7 +634,8 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
593
634
  - **`StampOn`** — a stamp stuck on a surface. `at` is the stamp's middle as a share of the surface (`{ x, y }`, 0 to 1),
594
635
  `turn` its angle in degrees. With `onAtChange` the member drags it (the arrow keys move it too) and it asks with the
595
636
  new place when the finger lets go; without it the stamp is still. The surface is the nearest positioned box:
596
- `Sticky stuck={…}` is one.
637
+ `Sticky stuck={…}` is one. On a note a stamp is 48 wide whatever the note's size (`--sticky-stamp-w`), and it may lie
638
+ over the words.
597
639
 
598
640
  What an app stores for a stamp: the picture ids (1 to 4, in order), the ground tone, the name, the small line. Making
599
641
  one is the app's screen: `ChipPick` checkboxes that each show a picture, the app refuses a fifth, and the app draws
package/bin/cardds.mjs CHANGED
@@ -16,6 +16,7 @@ const cwd = process.cwd();
16
16
  const SKIP = new Set(['node_modules', 'dist', 'build', '.git', '.next', 'coverage']);
17
17
 
18
18
  function* sources(dir) {
19
+ if (statSync(dir).isFile()) { yield dir; return; }
19
20
  for (const name of readdirSync(dir)) {
20
21
  if (SKIP.has(name) || name.startsWith('.')) continue;
21
22
  const p = join(dir, name);
@@ -24,17 +25,26 @@ function* sources(dir) {
24
25
  }
25
26
  }
26
27
 
28
+ // A check that read nothing has proved nothing: a path that is not there, or a folder with no source in it, is exit 2 — never "clean".
27
29
  function check(dirs) {
28
- const roots = (dirs.length ? dirs : ['src']).map((d) => join(cwd, d)).filter((d) => existsSync(d));
29
- if (!roots.length) { console.error('cardds check: no such folder — `npx cardds check <dir>`'); return 2; }
30
- let errors = 0, warns = 0;
31
- for (const root of roots) for (const file of sources(root)) {
32
- for (const f of checkSource(readFileSync(file, 'utf8'), relative(cwd, file))) {
33
- f.level === 'error' ? errors++ : warns++;
34
- console.log(`${f.file}:${f.line} ${f.level === 'error' ? 'error' : 'warn '} [${f.rule}] ${f.message}`);
30
+ const roots = dirs.length ? dirs : ['src'];
31
+ const missing = roots.filter((d) => !existsSync(join(cwd, d)));
32
+ if (missing.length) { for (const d of missing) console.error(`cardds check: ${d}: no such folder or file — \`npx cardds check <dir>\``); return 2; }
33
+ let errors = 0, warns = 0, read = 0;
34
+ for (const dir of roots) {
35
+ let here = 0;
36
+ for (const file of sources(join(cwd, dir))) {
37
+ here++;
38
+ for (const f of checkSource(readFileSync(file, 'utf8'), relative(cwd, file))) {
39
+ f.level === 'error' ? errors++ : warns++;
40
+ console.log(`${f.file}:${f.line} ${f.level === 'error' ? 'error' : 'warn '} [${f.rule}] ${f.message}`);
41
+ }
35
42
  }
43
+ if (!here) { console.error(`cardds check: ${dir}: no .js .jsx .ts .tsx file to read (node_modules, dist, build and dot-folders are skipped)`); return 2; }
44
+ read += here;
36
45
  }
37
- console.log(errors + warns ? `\ncardds check: ${errors} error(s), ${warns} warning(s)` : 'cardds check: clean');
46
+ const files = `${read} file${read === 1 ? '' : 's'} read`;
47
+ console.log(errors + warns ? `\ncardds check: ${errors} error(s), ${warns} warning(s) (${files})` : `cardds check: clean (${files})`);
38
48
  return errors ? 1 : 0;
39
49
  }
40
50
 
@@ -93,8 +103,9 @@ const asText = {
93
103
  `${c.name} (${c.group})${c.element ? ` — forwards the <${c.element}> attributes` : ''}`,
94
104
  one(c.description),
95
105
  ...(c.role ? [`role: represents ${[c.role.represents, c.role.count, c.role.density, c.role.job].flat().join(' · ')}${c.role.standsInFor.length ? `; stands in for: ${c.role.standsInFor.map((s) => (s.condition ? `${s.name} (${s.condition})` : s.name)).join('; ')}` : ''}`] : []),
96
- '\nprops',
97
- ...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`),
106
+ ...(c.params ? ['\nparameters', ...(c.params.length ? [] : [' (none)']), ...c.params.map((p) => ` ${p.name}${p.optional && !p.default ? '?' : ''}: ${p.type}${p.default ? ` = ${p.default}` : ''}`)] : [
107
+ '\nprops',
108
+ ...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`)]),
98
109
  `\nvariations (${c.variations.length})`,
99
110
  ...c.variations.map((v) => `\n ${v.id} — ${one(v.description)}\n${indent(v.tsx)}${html ? `\n html:\n${indent(v.html, ' ')}` : ''}`),
100
111
  ...(c.alsoUsedIn ? [`\nalso used in: ${c.alsoUsedIn.join(' ')}\n(--all prints their code too)`] : []),
package/css/actions.css CHANGED
@@ -195,6 +195,8 @@
195
195
  .dot--warn { background: var(--signal-warn); }
196
196
  .dot--error { background: var(--signal-error); }
197
197
  .dot--info { background: var(--signal-info); }
198
+ /* --sm: a small LABEL (a name on a note, a tag in a tight head) — low and tight; never a tap target */
199
+ .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); }
198
200
  .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
199
201
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (README 13) */
200
202
  .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); }
package/css/layover.css CHANGED
@@ -83,6 +83,11 @@
83
83
  .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
84
84
  .modal__drawer .t-caption { opacity: var(--dim); }
85
85
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
86
+ /* the answers as WORD buttons (a BtnRow of Btns; Lh 2026-10-11): the row fills the ask with room over it and each button keeps its words on one
87
+ line — left to its words' width the row squeezed two buttons into round blobs, their words on two lines. A quiet button is an outline only,
88
+ so on the ask its edge is the ask's ink at full strength (light on the dark end, dark on --light). The BtnTrio of round buttons is not touched. */
89
+ .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
90
+ .modal__drawer .btn--quiet { border-color: var(--modal-drawer-quiet-border, currentColor); }
86
91
  .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
87
92
  .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
88
93
  @media (prefers-reduced-motion: no-preference) {
package/css/post.css CHANGED
@@ -230,7 +230,7 @@
230
230
  .post-desk__line > span { font: var(--type-caption); color: var(--_muted); }
231
231
  .post-desk__line > input { width: 100%; min-width: 0; min-height: var(--tap); border: 0; outline: none; background: transparent; color: inherit; font: var(--type-title); }
232
232
  .post-desk__line > input::placeholder { color: var(--_muted); }
233
- /* the stamp box: empty it is a dashed place to press; pressed, the chosen stamp lies in it */
233
+ /* the stamp box: empty it is a place of dots to press; pressed, the chosen stamp lies in it */
234
234
  .post-desk__slot { border: 0; background: transparent; color: inherit; }
235
235
  @media (prefers-reduced-motion: no-preference) {
236
236
  .post-desk__slot > .stamp { animation: post-desk-stick var(--motion-quick) ease-out both; }
package/css/stamp.css CHANGED
@@ -91,28 +91,32 @@
91
91
 
92
92
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
93
93
  <span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
94
- The dashed edge is a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose, a stroke's are two
95
- knobs (--stamp-place-dash, --stamp-place-gap) and its corners still follow --stamp-place-radius. The element keeps a clear border
96
- of the same width so what is inside sits where it did; the svg lies on that border, over it, and takes no part in the layout. */
94
+ The edge is a row of round dots drawn on a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose,
95
+ a stroke's are knobs and its corners still follow --stamp-place-radius. A dot is a zero-length dash under a round cap, --stamp-place-dot
96
+ wide; --stamp-place-gap is the distance from one dot's centre to the next, and a --stamp-place-dash above 0 makes round-ended dashes
97
+ of it. The pattern closes wherever the perimeter ends, so the last gap is shorter or longer than the rest by less than one pitch.
98
+ The element keeps a clear border of the same width so what is inside sits where it did; the svg lies on that border, over it, and
99
+ takes no part in the layout. */
97
100
  .stamp-place {
98
101
  position: relative;
99
102
  display: grid;
100
103
  place-content: center;
101
104
  justify-items: center;
102
105
  padding: var(--sp-1);
103
- border: var(--rule-w) solid transparent;
106
+ border: var(--_dot) solid transparent;
104
107
  border-radius: var(--stamp-place-radius, var(--sp-2));
105
108
  background: none;
106
109
  color: var(--stamp-place-ink, var(--on-surface-muted));
107
110
  font: var(--stamp-place-font, var(--type-caption));
108
111
  text-align: center;
109
112
  --_edge: var(--stamp-place-border, var(--on-surface-muted));
113
+ --_dot: var(--stamp-place-dot, var(--rule-w));
110
114
  }
111
115
  .stamp-place__edge {
112
116
  position: absolute;
113
- inset: calc(var(--rule-w) / -2) auto auto calc(var(--rule-w) / -2);
114
- width: calc(100% + var(--rule-w));
115
- height: calc(100% + var(--rule-w));
117
+ inset: calc(var(--_dot) / -2) auto auto calc(var(--_dot) / -2);
118
+ width: calc(100% + var(--_dot));
119
+ height: calc(100% + var(--_dot));
116
120
  max-width: none; /* base.css caps every svg at its box */
117
121
  overflow: visible; /* the stroke is centred on the rect's own edge: half of it falls outside the svg */
118
122
  pointer-events: none;
@@ -120,11 +124,12 @@
120
124
  .stamp-place__edge > rect {
121
125
  width: 100%;
122
126
  height: 100%;
123
- rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--rule-w) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
127
+ rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--_dot) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
124
128
  fill: none;
125
129
  stroke: var(--_edge);
126
- stroke-width: var(--rule-w);
127
- stroke-dasharray: var(--stamp-place-dash, var(--sp-2)) var(--stamp-place-gap, var(--sp-3));
130
+ stroke-width: var(--_dot);
131
+ stroke-linecap: round;
132
+ stroke-dasharray: var(--stamp-place-dash, 0) var(--stamp-place-gap, calc(var(--sp-0) * 3));
128
133
  }
129
134
  .card .stamp-place, .post-paper .stamp-place { --_edge: var(--stamp-place-border, var(--card-muted)); color: var(--stamp-place-ink, var(--card-muted)); }
130
135
  .stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
package/css/sticky.css CHANGED
@@ -1,12 +1,14 @@
1
- /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, a thread of posts.
1
+ /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, notes stuck on a page, a thread of posts, the answer note.
2
2
  A note is its own paper: it brings its colour and its ink, whatever card or palette it lies on. */
3
3
 
4
- /* ---- sticky: a paper note — a flat pastel block, straight, the bottom-right corner folded over ----
5
- Straight unless asked (--tilt-left / --tilt-right: the one note of a start screen), never patterned. The corner is CUT on the diagonal and a flap of a darker shade lies over it.
4
+ /* ---- sticky: a paper note — a flat pastel SQUARE of one size, the bottom-right corner folded over ----
5
+ ONE SIZE (Lh 2026-10-10): every note is --sticky-size square (168), whatever it says; words it cannot hold are cut where the words' room ends,
6
+ no ellipsis. Square corners. THE FOLD SAYS UNREAD: a folded corner = not read yet, --flat = read.
7
+ Straight unless asked (--tilt-left / --tilt-right, or a holder that turns its notes), never patterned. The corner is CUT on the diagonal and a flap of a darker shade lies over it.
6
8
  <div class="sticky sticky--custard">
7
9
  <div class="sticky__paper">
8
10
  <div class="sticky__head">12 Oct</div><div class="sticky__body">…</div><div class="sticky__foot">From Noi</div>
9
- <span class="sticky__fold"></span>
11
+ <span class="sticky__fold"></span><span class="sticky__state t-sr">Unread</span>
10
12
  </div>
11
13
  </div>
12
14
  Two boxes on purpose: the OUTER one holds the tone and the shadow, the PAPER is the one that is cut — a clip-path
@@ -18,16 +20,13 @@
18
20
  --_muted: color-mix(in srgb, currentColor 62%, transparent); /* muted words: the ink sunk into the paper (currentColor is read where it is used: there it is still the note's ink) */
19
21
  padding-inline-end: var(--sticky-fold, var(--sp-6)); /* the knob, read by a real property first (the knob test probes that one); --_fold hands the same value to the paper and the flap */
20
22
  --_fold: var(--sticky-fold, var(--sp-6));
21
- --_min: calc(var(--tap) * 3);
22
- --_cap: calc(var(--tap) * 7);
23
- --_tall: calc(var(--tap) * 4.4);
24
23
  position: relative;
25
24
  display: grid;
25
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr); /* the paper is the note's size, never its words' */
26
26
  box-sizing: border-box;
27
- width: fit-content; /* hugs its words… */
28
- min-width: var(--sticky-min-w, var(--_min)); /* …but a word alone is still a note */
29
- max-width: var(--sticky-max-w, 100%);
30
- min-height: var(--sticky-min-h, var(--_tall)); /* every note is one height, the height of a note being written (Lh 2026-10-07); more words still grow it */
27
+ flex: none;
28
+ width: var(--sticky-size, calc(var(--sp-12) * 3.5)); /* 168: two side by side on a phone */
29
+ aspect-ratio: 1;
31
30
  padding: 0 var(--_fold) var(--_fold) 0; /* = the line above, written whole */
32
31
  background-clip: content-box;
33
32
  isolation: isolate; /* the two layers below stay under this note, over whatever is behind it */
@@ -41,7 +40,7 @@
41
40
  position: absolute;
42
41
  z-index: -1;
43
42
  pointer-events: none;
44
- border-radius: var(--sticky-radius, var(--sp-1));
43
+ border-radius: var(--sticky-radius, 0rem);
45
44
  background-image: linear-gradient(-45deg, transparent calc(var(--_fold) * 0.7071), var(--_ink) 0); /* the folded corner, cut */
46
45
  }
47
46
  .sticky::after { --_ink: var(--sticky-edge-bg, rgb(0 0 0 / 0.14)); inset: calc(-1 * var(--border-w)); }
@@ -69,18 +68,20 @@
69
68
  gap: var(--sticky-gap, var(--sp-3));
70
69
  box-sizing: border-box;
71
70
  min-width: 0;
71
+ min-height: 0;
72
72
  margin: 0 calc(-1 * var(--_fold)) calc(-1 * var(--_fold)) 0;
73
73
  padding: var(--sticky-pad-y, var(--sp-4)) var(--sticky-pad-x, var(--sp-4));
74
- border-radius: var(--sticky-radius, var(--sp-1));
74
+ border-radius: var(--sticky-radius, 0rem);
75
75
  background-color: inherit;
76
76
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_fold)), calc(100% - var(--_fold)) 100%, 0 100%);
77
77
  }
78
- .sticky__head { display: flex; align-items: center; gap: var(--sticky-head-gap, var(--sp-2)); font: var(--sticky-head-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
78
+ .sticky__head { flex: none; display: flex; align-items: center; gap: var(--sticky-head-gap, var(--sp-2)); font: var(--sticky-head-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
79
79
  .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
80
80
  .sticky__head:has(> .sticky-date) { align-items: start; }
81
81
  .sticky__head:has(> .sticky-date) > .ph { flex: none; width: var(--sticky-pic-size, calc(var(--tap) * 1.5)); min-height: 0; aspect-ratio: 1; } /* beside a date the picture is a small square at the start */
82
- .sticky__body { flex: 1 1 auto; min-width: 0; font: var(--sticky-font, var(--type-title)); overflow-wrap: anywhere; }
83
- .sticky__foot { font: var(--sticky-foot-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); padding-inline-end: var(--_fold); } /* the line stops before the fold */
82
+ .sticky__body { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: clip; /* words past the note's room are cut, the foot keeps its place */ font: var(--sticky-font, var(--type-title)); overflow-wrap: anywhere; }
83
+ .sticky__paper > :not(.sticky__body) > .chip { border-color: transparent; background-color: var(--sticky-chip-bg, rgb(255 255 255 / 0.85)); color: var(--sticky-ink, #141414); } /* a chip on a note is a label ON the paper (whose note it is): a pale ground nearly opaque (it may lie over words), no edge, the note's ink */
84
+ .sticky__foot { flex: none; font: var(--sticky-foot-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); padding-inline-end: var(--_fold); } /* the line stops before the fold */
84
85
  /* the flap: a square in the corner — the paper's cut takes its outer half, what is left is the triangle lying on the note */
85
86
  .sticky__fold {
86
87
  position: absolute;
@@ -92,12 +93,16 @@
92
93
  background-color: var(--sticky-fold-bg, color-mix(in srgb, currentColor 14%, transparent)); /* a darker shade of whatever the paper is */
93
94
  box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
94
95
  }
95
- /* things stuck ON the note (a stamp, placed by hand): a layer over the whole paper. A tap between them still reaches the words under it. */
96
+ /* things stuck ON the note (a stamp, placed by hand): a layer over the whole paper. A tap between them still reaches the words under it.
97
+ A stamp on a note is one width whatever the note's (48 × 60, the stamp of the album's strip), and may lie over the words. */
96
98
  .sticky__stuck { position: absolute; inset: 0; pointer-events: none; }
99
+ .sticky__stuck > .stamp-on { width: var(--sticky-stamp-w, var(--stamp-on-w, var(--sp-12))); } /* the stamp's own knob still reaches it; the note's wins */
97
100
  .sticky--flat > .sticky__paper { clip-path: none; }
98
101
  .sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
99
102
 
100
- /* --tilt: the note turned a little on its middle. Straight is the rule; this is for the one note a start screen hands over. */
103
+ /* --tilt: the note turned a little on its middle — the one note a start screen hands over. Notes on a board or in a lapped thread are turned by their holder. */
104
+ /* a note that stands alone in a page's column or in a centre stands in its middle */
105
+ .page > .sticky, .screen__centre > .sticky { margin-inline: auto; }
101
106
  .sticky--tilt-left, .sticky--tilt-right { margin-block: var(--sp-4); } /* the turned corners and the shadow need room: a box that scrolls (a centre) would cut them */
102
107
  .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, 5deg)); }
103
108
  .sticky--tilt-right { rotate: var(--sticky-tilt, 5deg); }
@@ -108,9 +113,6 @@
108
113
  .sticky-date__day { font: var(--sticky-date-day-font, var(--type-stat)); }
109
114
  .sticky-date__month { font: var(--sticky-date-month-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
110
115
 
111
- /* --square: the note to write on — 1:1, as wide as its column up to a cap, in the middle */
112
- .sticky--square { width: 100%; max-width: var(--sticky-square-max-w, var(--_cap)); aspect-ratio: 1; margin-inline: auto; }
113
-
114
116
  /* --corner: a control stands ON the bottom-right corner, half outside. The paper is notched round it (a mask, so no
115
117
  ground colour is needed; the edge and the shadow are notched the same way); the note has no fold. */
116
118
  .sticky__corner {
@@ -126,7 +128,6 @@
126
128
  /* the half of the control that stands outside is the note's own room: it never runs past the column, nor onto what follows */
127
129
  .sticky--corner {
128
130
  --_corner: var(--sticky-corner-size, var(--tap-sm)); /* the same default as .sticky__corner above: the paper's notch needs it too */
129
- max-width: calc(var(--sticky-max-w, 100%) - var(--_corner) / 2);
130
131
  margin: 0 calc(var(--_corner) / 2) calc(var(--_corner) / 2) 0;
131
132
  }
132
133
  .sticky--corner::before, .sticky--corner::after { background-image: radial-gradient(circle at 100% 100%, transparent calc(var(--_corner) / 2 + var(--sp-1)), var(--_ink) 0); }
@@ -137,16 +138,17 @@
137
138
  mask: radial-gradient(circle at 100% 100%, transparent calc(var(--_notch) - var(--border-w)), #000 var(--_notch));
138
139
  }
139
140
 
140
- /* a tilted square is sized by the SCREEN, not by a length: three quarters of its width inside the inset, about two thirds of the glass (Lh 2026-10-06) — the turned corners
141
- keep clear of the edges on any phone. cqw reads the fill screen (the size container). After --square, so it wins. */
142
- .sticky--square.sticky--tilt-left, .sticky--square.sticky--tilt-right { max-width: var(--sticky-tilt-max-w, 75cqw); }
143
-
144
141
  /* --tap: the whole note is the button (`onClick`). The fold stays; the ring follows the note's turn. */
145
142
  .sticky--tap { appearance: none; border: 0; font: inherit; text-align: start; cursor: pointer; -webkit-tap-highlight-color: transparent; }
146
143
  .sticky--tap .sticky__body, .sticky--tap .sticky__foot { display: block; }
147
144
  .sticky--tap:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
148
145
  .sticky--tap:active > .sticky__paper { translate: 0 var(--sp-0); }
149
146
 
147
+ /* a name label in the foot (a Chip, `sm`) stands at the START, what follows it (the time) at the end, on one line: a long name is cut
148
+ with an ellipsis past its share of the row and never wraps; the time is never cut (a long one may run a little onto the fold) */
149
+ .sticky__paper > .sticky__foot:has(> .chip) { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); white-space: nowrap; text-align: end; }
150
+ .sticky__foot > .chip { display: block; flex: none; min-width: 0; max-width: var(--sticky-foot-chip-max-w, 62%); overflow: hidden; text-overflow: ellipsis; text-align: start; }
151
+
150
152
  /* ---- sticky field: the words written ON a note — a bare textarea in the note's type and ink, filling it ----
151
153
  <div class="sticky__body"><textarea class="sticky-field" aria-label="Note" placeholder="Add text to this note"></textarea></div> */
152
154
  .sticky__body:has(> .sticky-field) { display: grid; }
@@ -154,7 +156,7 @@
154
156
  display: block;
155
157
  box-sizing: border-box;
156
158
  width: 100%;
157
- min-height: var(--sticky-field-min-h, var(--_min));
159
+ height: 100%; /* the note's whole room: a touch anywhere on the paper writes */
158
160
  resize: none;
159
161
  border: 0;
160
162
  margin: 0;
@@ -210,7 +212,6 @@
210
212
  .sticky-post__name { min-width: 0; font: var(--sticky-post-name-font, var(--type-label)); overflow-wrap: anywhere; }
211
213
  .sticky-post__time { margin-inline-start: auto; font: var(--sticky-post-time-font, var(--type-caption)); color: var(--sticky-post-time-ink, var(--on-surface-muted)); white-space: nowrap; }
212
214
  .sticky-post__notes { grid-area: 2 / 2; display: grid; justify-items: start; gap: var(--sticky-post-notes-gap, var(--sp-3)); min-width: 0; }
213
- .sticky-post__notes > .sticky { max-width: var(--sticky-post-max-w, 85%); } /* a note leaves the far side open, as a bubble does */
214
215
  .sticky-post__rail { display: none; grid-area: 2 / 1; justify-self: center; align-self: stretch; color: var(--sticky-thread-rail-ink, var(--on-surface-muted)); border-inline-start: var(--rule-w) dotted currentColor; margin-block-end: calc(-1 * var(--_thread-gap)); } /* runs on through the gap to the next avatar */
215
216
  .sticky-thread--rail > .sticky-post:not(:last-child) > .sticky-post__rail { display: block; }
216
217
  /* a chat (no rail): a note leans on its writer's own edge of the thread — it starts under the avatar, not under the name, so theirs and mine stand apart (Lh 2026-10-07: both sat in the middle). With a rail the notes keep clear of it, under the name. */
@@ -221,3 +222,113 @@
221
222
  .sticky-post--right > .sticky-post__head > .sticky-post__time { margin-inline: 0 auto; }
222
223
  .sticky-post--right > .sticky-post__notes { grid-column: 1; justify-items: end; }
223
224
  .sticky-post--right > .sticky-post__rail { grid-column: 2; }
225
+
226
+ /* --lapped: a chat with NO head rows (Lh 2026-10-10) — theirs at the left, mine at the right, each turn lying over a corner of the one before
227
+ on the other side; two turns from one side have nothing to lie over and stand a small gap apart. The time stands close under its note, on
228
+ its own side; the avatar is not drawn and the name is only heard (it stands once, in the screen's top bar). The thread is two notes wide
229
+ less what they share, in the middle of its column, so the corners cross on any phone. Later = on top: the order of the turns. */
230
+ .sticky-thread--lapped {
231
+ gap: 0;
232
+ width: 100%;
233
+ max-width: calc(2 * var(--sticky-size, calc(var(--sp-12) * 3.5)) - var(--sticky-thread-lap-x, var(--sp-8))); /* 304: the two sides cross 32 in the middle (38 as they lie turned — the prototype's 39); on a 375 phone each stands 19.5 in from the screen's inset */
234
+ margin-inline: auto;
235
+ padding-block-start: var(--sp-4); /* the first note's turned corner */
236
+ }
237
+ .sticky-thread--lapped > .sticky-post { position: relative; display: grid; grid-template-columns: auto; justify-self: start; justify-items: start; row-gap: var(--sticky-thread-time-gap, var(--sp-0)); margin-block-start: var(--sticky-thread-same-gap, var(--sp-2)); }
238
+ .sticky-thread--lapped > .sticky-post:first-child { margin-block-start: 0; }
239
+ .sticky-thread--lapped > .sticky-post--right { justify-self: end; justify-items: end; }
240
+ .sticky-thread--lapped > .sticky-post:not(.sticky-post--right) + .sticky-post--right,
241
+ .sticky-thread--lapped > .sticky-post--right + .sticky-post:not(.sticky-post--right) { margin-block-start: calc(-1 * var(--sticky-thread-lap, calc(var(--sp-1) * 19))); } /* 76 */
242
+ .sticky-thread--lapped > .sticky-post > .sticky-post__who { display: none; }
243
+ .sticky-thread--lapped > .sticky-post > .sticky-post__notes { grid-area: 1 / 1; }
244
+ .sticky-thread--lapped > .sticky-post > .sticky-post__head { grid-area: 2 / 1; }
245
+ .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__name { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* as .t-sr: heard, not seen */
246
+ .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__time { margin-inline: 0; }
247
+ .sticky-thread--lapped > .sticky-post > .sticky-post__head:not(:has(> .sticky-post__time)) { display: none; }
248
+ /* each note leans a little, one way then the other */
249
+ .sticky-thread--lapped > .sticky-post > .sticky-post__notes > .sticky { rotate: calc(-1 * var(--sticky-thread-tilt, 2deg)); }
250
+ .sticky-thread--lapped > .sticky-post:nth-child(even) > .sticky-post__notes > .sticky { rotate: var(--sticky-thread-tilt, 2deg); }
251
+ /* under an answer note the thread ends with the room that note stands in: the last turn scrolls up clear of it */
252
+ .screen:has(> .sticky-answer) .sticky-thread--lapped { padding-block-end: calc(var(--sticky-size, calc(var(--sp-12) * 3.5)) * var(--sticky-answer-show, 0.75) + var(--sp-6)); }
253
+
254
+ /* ---- sticky board: notes stuck on a page — rows that wrap by the width, each note turned a little ----
255
+ <div class="sticky-board"><button class="sticky sticky--tap …">…</button>…</div>
256
+ As many notes in a row as the width holds (two on a phone, three from 504 of room, a screen about 540 wide), the rows in the middle. The notes stand edge to
257
+ edge both ways (--sticky-board-lap 0): only their turned corners cross. LATER = ON TOP, always: the right one over the left, the lower over
258
+ the upper — the order of the notes, no z-index. Where two notes do not fit side by side, each lies over the one before by what is
259
+ missing, so a phone still shows two — but never by more than half the fold, which would hide the corner that says unread: past that, one in a row.
260
+ The columns are tracks of the note's size less the lap; a note is wider than its track by the lap, and the room at the end is what the last
261
+ of a row runs into — none is cut. The tracks are 50% while the lap is the missing room (padding % and track % read the same width).
262
+ A note here is the SAME note as inside its chat (Lh 2026-10-11): on the paper only what is the note's own — its words, its time at the END
263
+ of the foot, the stamps really stuck on it. Whose it is, is a label of the page: a small Chip at the start of the note's foot.
264
+ The foot takes no room from the words (below). */
265
+ .sticky-board {
266
+ --_size: var(--sticky-size, calc(var(--sp-12) * 3.5));
267
+ --_most: calc(var(--sticky-fold, var(--sp-6)) / 2);
268
+ padding-inline-end: clamp(var(--sticky-board-lap, 0rem), 2 * var(--_size) - 100%, var(--_most));
269
+ --_cell: max(var(--_size) - var(--_most), min(var(--_size) - var(--sticky-board-lap, 0rem), 50%));
270
+ display: grid;
271
+ grid-template-columns: repeat(auto-fill, var(--_cell));
272
+ grid-auto-rows: calc(var(--_size) - var(--sticky-board-lap, 0rem)); /* down the page the lap is the set one: a page is as long as it needs */
273
+ justify-content: center;
274
+ padding-block: var(--sp-2) calc(var(--sticky-board-lap, 0rem) + var(--sticky-board-drop, var(--sp-2)) + var(--sp-2)); /* the turned corners above; below, the last row's overhang and its drop */
275
+ }
276
+ .sticky-board > * { position: relative; }
277
+ .sticky-board > :nth-child(even) { translate: 0 var(--sticky-board-drop, var(--sp-2)); } /* tops of a row NEAR one line, not on it */
278
+ /* the foot is laid OVER the note's foot, out of its flow: the words keep the whole paper, so they break exactly as they do where the note is
279
+ opened (Lh 2026-10-11 — a clamp showed three lines and "…" on the page and four inside). A long note's last line runs under the label and the time. */
280
+ .sticky-board .sticky__paper > .sticky__foot { position: absolute; inset: auto var(--sticky-board-foot-inset, var(--sp-3)) var(--sticky-board-foot-inset, var(--sp-3)); text-align: end; }
281
+ /* how far each is turned: FOUR unlike angles in turn (Lh 2026-10-11). At most three notes meet, so four keep the lean from ever looking the
282
+ same: two in a row, a note lies over the one two before it and those lean opposite ways; three in a row, over the one three before it,
283
+ never the same angle. (A run of seven left one column at 0.75° and −0.5°: its edges read as one straight line and the lean was lost.) */
284
+ .sticky-board > :nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
285
+ .sticky-board > :nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
286
+ .sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
287
+ .sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
288
+
289
+ /* ---- sticky answer: the note you answer on — it waits at the screen's foot and comes to the top to be written on ----
290
+ <div class="sticky-answer sticky-answer--writing">
291
+ <div class="sticky-answer__scrim"></div>
292
+ <div class="sticky-answer__note"><div class="sticky sticky--flat">…<textarea class="sticky-field"></textarea>…</div></div>
293
+ <div class="sticky-answer__under" inert>
294
+ <div class="sticky-answer__tools">…the paper chooser · the send button…</div>
295
+ <div class="sticky-answer__tray">…what else stands under them (the writer's stamp)…</div>
296
+ </div>
297
+ </div>
298
+ A LAYER of the screen (its child), as the snackbar is. AT REST the note stands near the right, three quarters of it above the screen's
299
+ foot (--sticky-answer-show), turned a little; what stands under it is out of sight and inert. WRITING it stands straight in the middle under
300
+ the top bar, the screen behind is washed pale and takes no touch, tools and tray stand under it. The note keeps its one size in both
301
+ places. One `top`, one `right` — they slide between the two places; nothing else moves. */
302
+ .sticky-answer {
303
+ --_size: var(--sticky-size, calc(var(--sp-12) * 3.5));
304
+ position: fixed; inset: 0;
305
+ max-width: var(--screen-w); margin-inline: auto;
306
+ overflow: clip;
307
+ z-index: var(--z-fab);
308
+ pointer-events: none;
309
+ }
310
+ .screen--fill > .sticky-answer { position: absolute; }
311
+ .sticky-answer--writing { z-index: calc(var(--z-top) + 1); }
312
+ .sticky-answer__scrim { position: absolute; inset: 0; background: var(--sticky-answer-scrim-bg, color-mix(in srgb, var(--surface) 82%, transparent)); opacity: 0; visibility: hidden; transition: opacity var(--motion-sheet) ease, visibility 0s var(--motion-sheet); }
313
+ .sticky-answer--writing > .sticky-answer__scrim { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
314
+ .sticky-answer__note {
315
+ position: absolute;
316
+ top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0px)));
317
+ right: var(--sticky-answer-end, var(--screen-pad));
318
+ rotate: var(--sticky-answer-tilt, -3deg);
319
+ pointer-events: auto;
320
+ transition: top var(--motion-sheet) ease, right var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
321
+ }
322
+ .sticky-answer--writing > .sticky-answer__note { top: calc(var(--safe-top) + var(--header-h) + var(--sticky-answer-top, var(--sp-5))); right: calc(50% - var(--_size) / 2); rotate: 0deg; }
323
+ .sticky-answer__under {
324
+ position: absolute; inset-inline: var(--screen-pad);
325
+ top: calc(var(--safe-top) + var(--header-h) + var(--sticky-answer-top, var(--sp-5)) + var(--_size) + var(--sticky-answer-gap, var(--sp-4)));
326
+ display: grid; gap: var(--sticky-answer-gap, var(--sp-4));
327
+ opacity: 0; visibility: hidden;
328
+ transition: opacity var(--motion-sheet-rest) ease, visibility 0s var(--motion-sheet-rest);
329
+ }
330
+ .sticky-answer--writing > .sticky-answer__under { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: var(--motion-sheet-rest), 0s; }
331
+ .sticky-answer__tools { display: flex; align-items: center; justify-content: space-between; gap: var(--sticky-answer-tools-gap, var(--sp-3)); }
332
+ .sticky-answer__tools > .sticky-pick { justify-content: start; }
333
+ .sticky-answer__tray { display: grid; gap: var(--sticky-answer-tray-gap, var(--sp-2)); }
334
+ @media (prefers-reduced-motion: reduce) { .sticky-answer__scrim, .sticky-answer__note, .sticky-answer__under { transition: none; } }
@@ -7,6 +7,8 @@ export interface ChipProps extends ComponentProps<'span'> {
7
7
  muted?: boolean;
8
8
  /** a leading signal dot: `a` (the accent signal), `b` (the secondary), or one of the signal set — ok · warn · error · info */
9
9
  dot?: 'a' | 'b' | DotTone;
10
+ /** a small label: low and tight (a name on a note). Never with `as="button"` or `href` — it is under tap size */
11
+ sm?: boolean;
10
12
  /** the element: span (default), a link, or a button */
11
13
  as?: 'span' | 'a' | 'button';
12
14
  /** makes the chip a link (an `a`) */
@@ -14,8 +16,8 @@ export interface ChipProps extends ComponentProps<'span'> {
14
16
  /** the chip's words */
15
17
  children?: ReactNode;
16
18
  }
17
- /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
18
- export declare function Chip({ on, muted, dot, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
19
+ /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
20
+ export declare function Chip({ on, muted, dot, sm, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
19
21
  ref?: import("react").Ref<HTMLSpanElement> | undefined;
20
22
  key?: import("react").Key | null | undefined;
21
23
  defaultChecked?: boolean | undefined;
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Dot } from './Dot.js';
5
- /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
6
- export function Chip({ on, muted, dot, as, href, className, children, ...rest }) {
5
+ /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
6
+ export function Chip({ on, muted, dot, sm, as, href, className, children, ...rest }) {
7
7
  const el = as ?? (href ? 'a' : 'span');
8
- return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', muted && 'chip--muted', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b', tone: dot === 'a' || dot === 'b' ? undefined : dot }), children);
8
+ return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', muted && 'chip--muted', sm && 'chip--sm', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b', tone: dot === 'a' || dot === 'b' ? undefined : dot }), children);
9
9
  }
10
10
  /** ChipPick — a chip that is a choice: full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
11
11
  * interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */