@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 +40 -0
- package/README.md +46 -4
- package/bin/cardds.mjs +21 -10
- package/css/actions.css +2 -0
- package/css/layover.css +5 -0
- package/css/post.css +1 -1
- package/css/stamp.css +15 -10
- package/css/sticky.css +139 -28
- package/dist/actions/Chip.d.ts +4 -2
- package/dist/actions/Chip.js +3 -3
- package/dist/cardds.css +162 -39
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/sheets/Modal.d.ts +1 -1
- package/dist/sheets/Modal.js +1 -1
- package/dist/stamp/Stamp.d.ts +3 -2
- package/dist/stamp/Stamp.js +3 -2
- package/dist/sticky/Sticky.d.ts +8 -6
- package/dist/sticky/Sticky.js +6 -3
- package/dist/sticky/StickyAnswer.d.ts +23 -0
- package/dist/sticky/StickyAnswer.js +35 -0
- package/dist/sticky/StickyBoard.d.ts +12 -0
- package/dist/sticky/StickyBoard.js +13 -0
- package/dist/sticky/StickyThread.d.ts +12 -6
- package/dist/sticky/StickyThread.js +8 -4
- package/dist/words.d.ts +1 -1
- package/dist/words.js +2 -2
- package/lib/catalog.mjs +1 -0
- package/lib/mcp.mjs +1 -1
- package/lib/role.mjs +11 -3
- package/lint/rules.mjs +7 -2
- package/package.json +1 -1
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
|
|
583
|
-
gap nobody is asked to fill. The edge is a drawn stroke, so
|
|
584
|
-
`--stamp-place-gap`
|
|
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 =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
'\
|
|
97
|
-
|
|
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
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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(--
|
|
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(--
|
|
114
|
-
width: calc(100% + var(--
|
|
115
|
-
height: calc(100% + var(--
|
|
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(--
|
|
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(--
|
|
127
|
-
stroke-
|
|
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
|
|
5
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
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,
|
|
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
|
-
.
|
|
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
|
|
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
|
-
|
|
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; } }
|
package/dist/actions/Chip.d.ts
CHANGED
|
@@ -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;
|
package/dist/actions/Chip.js
CHANGED
|
@@ -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. */
|