@usableapp/cardds 0.7.85 → 0.7.87
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 +33 -0
- package/README.md +44 -1
- package/bin/cardds.mjs +8 -3
- package/css/actions.css +12 -7
- package/css/card.css +1 -2
- package/css/choice.css +2 -2
- package/css/finish.css +97 -11
- package/css/forms.css +1 -2
- package/css/numbers.css +1 -2
- package/css/post.css +1 -2
- package/css/stack.css +1 -1
- package/css/stamp.css +44 -4
- package/css/step.css +6 -0
- package/css/sticky.css +45 -2
- package/css/tokens.css +6 -0
- package/dist/cardds.css +217 -35
- package/dist/cards/CardFeed.js +2 -9
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lists/Row.js +2 -9
- package/dist/pressable.d.ts +8 -0
- package/dist/pressable.js +6 -0
- package/dist/scaffold/Screen.js +2 -9
- package/dist/sheets/StackSheet.js +3 -7
- package/dist/sheets/sheetMeasure.d.ts +1 -0
- package/dist/sheets/sheetMeasure.js +14 -9
- package/dist/sheets/stackCard.js +41 -48
- package/dist/stamp/Stamp.js +3 -6
- package/dist/stamp/StampBook.js +2 -3
- package/dist/stamp/StampOn.d.ts +21 -3
- package/dist/stamp/StampOn.js +75 -18
- package/dist/stamp/underNotes.d.ts +3 -0
- package/dist/stamp/underNotes.js +4 -0
- package/dist/state.d.ts +5 -1
- package/dist/state.js +8 -0
- package/dist/sticky/Sticky.d.ts +2 -2
- package/dist/sticky/Sticky.js +2 -3
- package/dist/sticky/StickyPile.d.ts +20 -0
- package/dist/sticky/StickyPile.js +31 -0
- package/dist/words.d.ts +1 -1
- package/dist/words.js +2 -2
- package/lib/catalog.mjs +24 -7
- package/lib/mcp.mjs +3 -3
- package/lint/rules.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
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.87 — 2026-10-12
|
|
6
|
+
|
|
7
|
+
Nothing here breaks an existing screen: no class and no markup changed, six knobs are added and none removed; three mechanics that two files each wrote twice are now one rule each, and every screen without `data-finish` draws as in 0.7.86.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Four finishes of a card's paper, four values of one attribute: `<html data-finish="lit | print | sticker | ink">`, beside any palette or none** (Lh 2026-10-12, ruled from the timebank home with each injected; teeth, tape, ticket and slab rejected). Before → after: `data-finish="lit"` was a trial (not in the README) and the only value → `lit` (light from above) is released as it was, and three more: `print` (printed paper: an ink line in the card's own ink, a hard misregistered offset, grain over the card, a dotted rule under a lone card's head), `sticker` (a die-cut sticker: a white cut line, a short hard drop, a lone card leans, buttons with a bottom edge that press down), `ink` (a poster / comic print: a thick line and a hard offset in the surface's ink). No attribute = nothing moves. Under every finish the wireframe's two borders step back and a card in a closed stack's base stays flat (one generic `[data-finish]` block, as `[data-palette]`). Six knobs: `--print-off` · `--print-grain` · `--sticker-cut` · `--sticker-tilt` (0deg = no lean) · `--ink-w` · `--ink-off`. README "Finish". Gallery: the finish is its own switch beside the palette (key `f`; `lit` is no longer a palette), stories `Card · Finishes` and `Home · Finish`.
|
|
11
|
+
- **The catalog knows the finishes.** `npx cardds find "printed paper"` (or `"sticker"`, `"finish"`) answers the finish first, kind `finish`, with its knobs; `cardds list` shows the four after the components; `find_components` over the MCP the same. Read from css/finish.css's own header, never a kept list; one field `finishes` on dist/catalog.json.
|
|
12
|
+
- **`cardds get` / `get_component` now list a component's knobs (joined by its classes, so a knob named for an old name is found), what it composes and what composes it; `find` matches the role's note and stands-in, so a question by intention finds the piece.** `knobs` is each row of docs/knobs.md whose selector names a class the component emits (`--action-note-*` under `BarNote`, `--tile-badge-*` under `JourneyBadge`, `--modal-drawer-*` under `ModalAsk`, `--band-*` under `NumberBand`), with its default, what it sets and the selector; `composes` is the catalog names the component's own source renders or calls (`Fab` → `IconBtn`, `Icon`), `composedBy` the inverse. `find "turned on or off"` is `Toggle`, `find "digit by digit"` is `Pin`; and the component NAMED by a word (`Card`, `Sheet`, `Stamp`) now ranks before the family that contains it (`FeedCard`, `SheetStack`), whatever their other words say; and a screen that answers fewer of a question's words than the best component comes after the components, so `find "a setting turned on or off"` is `Toggle` first, not eight screens that say "off" (a screen that answers every word, as `Snackbar · Success` does for "good news in passing", still comes first). The catalog's `classes` now include the `className="…"` literals a component writes, not only its `cx()` arguments (BEM parts such as `.snackbar__body` were missing, and 230 of the 640 knobs would have joined nothing). dist/catalog.json 1.78 → 1.87 MB.
|
|
13
|
+
- **tests/twins.spec.js: what is written twice under two names fails the suite** — two keyframes with one body, two rules with one declaration set (three declarations or more; the pairs that are not twins are named in the test with why), or two pieces in one role cell of docs/roles.md with no ruling written on either row (no "stands in for", no note). The census of 2026-10-12 found these with three hand scripts; now the suite does. Plain node over css/*.css and docs/roles.md; a block the reader cannot close throws naming the file, and the rules read are counted against the files' own braces.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **The postmark is struck as a stamp is.** Before → after: on a `PostDesk` the postmark slid in from up-left, turning, in `--motion-press` → it is shown large and faint and pressed down past its size to rest, as a `StampOn arrive` is, in `--stamp-on-arrive-time` (default `--motion-sheet`); the one `@keyframes strike` lives in tokens.css and both read it (Lh 2026-10-12: one strike). Where it rests is unchanged.
|
|
17
|
+
- **One `turn` keyframe for the ring that turns while waiting.** Before → after: `Spinner` read `spinner-turn` (numbers.css) and a busy `FormNote` read `form-note-turn` (forms.css), two keyframes with one body → both read `@keyframes turn` in tokens.css, with the system's other shared motion (`--motion-*`); same 0.9s, linear, infinite, still with reduced motion.
|
|
18
|
+
- **One rule for a pick's hidden native input.** Before → after: `.sticky-pick__swatch > input` (sticky.css) repeated the six declarations of `:is(.chip--pick, .mood__opt, .tile--pick) > input` (choice.css) → the swatch is a fourth holder in choice.css's `:is()` list and sticky.css has no copy; same specificity (`:is`), same look and press.
|
|
19
|
+
- **One rule for the scrolling screen that becomes a column.** Before → after: actions.css made a non-fill `.screen` a full-height flex column when its last child is an `.action-bar`, and card.css made it one when it holds a `.screen__centre`, the same three declarations twice → one rule in step.css, `.screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre)`; the bar's sticky row (actions.css) and the centre's flex (card.css) are as before.
|
|
20
|
+
- **`StackSheet`: a long title's run is measured by the Sheet's measurer.** Before → after: the stack measured its heading's inner span by its rounded `scrollWidth` and set `--_over` / `--_run` on its own → one `runOver` in sheetMeasure.ts measures the words themselves (a Range, ceiled) for the `Sheet` and the `StackSheet` alike, so the stack's run reaches its last letter by the Sheet's rule — measured on screen · Home at 2.2rem: `--_over` 37 → 38 px, `--_run` 4.2 → 4.3 s; 30 px a second plus the pauses, as before (the stack never ran the 6 s fallback). `--stack-sheet-run-time` and `--sheet-title-run-time` unchanged; the two keyframes stay.
|
|
21
|
+
- **`useStackCard`: one `docPointer()` and one `armSwallow()` serve both pulls** (the open sheet's fold and reload, the peeked head's lift) — they were written twice in stackCard.ts. Nothing changes for a screen.
|
|
22
|
+
- **`setRefs(own, ref)` in src/state.ts hands a node to a component's own ref and the caller's** — `Rows`, `CardFeed` and `Screen` wrote it three times. Nothing changes for a screen.
|
|
23
|
+
- **`pressable()` in src/pressable.ts: a box that is a `<button>` when it can be pressed** — `Stamp`, `StampPlace`, `StampCell` and `Sticky` wrote it four times; every story's rendered html is byte-identical (454 stories diffed). Nothing changes for a screen.
|
|
24
|
+
- **Tooling: the style snapshot holds every animation still before it reads a style** — one that loops (a spinner's turn, a grip's dissolve, a title's run) at its first frame, one that ends (a modal's drop, a sheet's slide) at its end, and the Boot story is sampled once its splash has left. Before, `snapshot:check` against the tree's own CSS named four rows that were time, not style (a spinner mid-turn in three stories, the splash mid-fade); now `snapshot:write` then `snapshot:check` on one tree reports identical.
|
|
25
|
+
|
|
26
|
+
## 0.7.86 — 2026-10-11
|
|
27
|
+
|
|
28
|
+
Nothing here breaks an existing screen: every prop is new and optional, and a screen that does not use them draws and behaves as in 0.7.85.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`StampOn onRemove`: a stuck stamp can be taken off.** When a drag ends or an arrow key moves the stamp, a cross stands on its top right corner for `removeFor` ms (default 3000). A press on the cross, or Delete / Backspace on the focused stamp, calls `onRemove`. The stamp only asks: the app takes the `StampOn` away, as it owns `at`. A stamp with `onRemove` and no `onAtChange` cannot be dragged, so a press on it shows the cross. A new drag hides the cross while the finger holds. The cross is a 24 disc in a 44 × 44 target, centred on the stamp's corner, and stays upright whatever the stamp's `turn`. Its name is `removeLabel`, default cardds's word `remove` ("Remove" / "เอาออก" by the page's language). Knobs: `--stamp-on-remove-size` (`--sp-6`, 24), `--stamp-on-remove-bg` (#141414), `--stamp-on-remove-ink` (#ffffff). A press or a drag on a removable stamp or its cross never reaches a pressable card or pile under it. Without `onRemove` a `StampOn` is as before.
|
|
32
|
+
- **`StampOn main`: a stuck stamp at the stamp's main size.** `--stamp-size`, 96 × 120 (90 × 112.5 on a screen 360 wide), to cover what it lies on. The default stays the small size, `--stamp-sm-size`, 48 × 60 (45 × 56.25 at 360).
|
|
33
|
+
- **`StickyPile`: one to six notes in a pile.** A feed item of its own, with no card frame. Its children are `Sticky` notes, the first on top. Each note under the top one lies 24 right and 8 down from the one above (`--sticky-pile-step-x`, `--sp-6`; `--sticky-pile-step-y`, `--sp-2`), turned by `StickyBoard`'s four angles by its place, so the top note's turn is the same for every count. Only the top note draws its `foot`. Measured at 375: one note 184 × 184, three 232 × 200, six 304 × 224, with `--sticky-pile-pad` (`--sp-2`, 8) of room round the notes. The pile stands in the middle of its column and is never wider than it: a column too narrow shortens the steps. A seventh note is not drawn (a console warning, once). `onClick` makes the whole pile one button under its notes, one Tab stop, with a ring round the pile; it needs `label`, the button's name, and warns once without it. The pile only says it was pressed. A `StampOn` on the top note keeps its drag and its cross; the stamps on the notes under it are inert whatever the app passed. A pile's notes wear their stamp at one size: pass `main` to all or to none.
|
|
34
|
+
- A feed card that holds a `StampOn` is still not pressed whole: a `.stamp-on` counts as a control of the card. Unchanged.
|
|
35
|
+
- **`StampOn onRemove` with `arrive`: no cross until the stamp has landed.** A stamp that is still being struck on is not stuck yet, so it shows no cross, takes no focus and answers no Delete; once `onArrived` fires it is a removable stamp as any other. `main` with `arrive` strikes the 96 × 120 stamp.
|
|
36
|
+
|
|
5
37
|
## 0.7.85 — 2026-10-11
|
|
6
38
|
|
|
7
39
|
### Added
|
|
@@ -11,6 +43,7 @@
|
|
|
11
43
|
|
|
12
44
|
### Fixed
|
|
13
45
|
- **`ChipRow bleed`: a chip keeps its words on one line** — before → after: a chip of whole sentences shrank to fit the row and its words broke onto three or four lines, so the strip never scrolled → each chip is as wide as its words and the strip scrolls sideways. A row of one-word chips is unchanged. Story `Chip · ChipRowBleedSentences`.
|
|
46
|
+
|
|
14
47
|
## 0.7.84 — 2026-10-11
|
|
15
48
|
|
|
16
49
|
### Changed — breaking for a screen that draws a `Stamp`
|
package/README.md
CHANGED
|
@@ -146,12 +146,18 @@ npx cardds find [words…] [--represents person] [--shape group] [--interact pic
|
|
|
146
146
|
# by the AXES first (docs/roles.md: what it stands for · one or a group · what the member can do), words second;
|
|
147
147
|
# the screens already drawn, then components, then what is used with them (nothing → a hint, not a list)
|
|
148
148
|
npx cardds scaffold <id> # a whole file from a screen: tsx (the code) or --html (a static document linking cardds.css)
|
|
149
|
-
npx cardds get <Name> [--html] # one component: its role, props (a ReactNode prop is a slot) + variations with code
|
|
149
|
+
npx cardds get <Name> [--html] # one component: its role, props (a ReactNode prop is a slot), what it composes, its knobs + variations with code
|
|
150
150
|
npx cardds list # every name by what it is for: one · group-brief · group-detail, the show-only first, then by what the member can do
|
|
151
151
|
npx cardds check <dir> # the rules, before a screen is called done
|
|
152
152
|
npx cardds guide # the few rules no checker sees, in one short text
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
+
`get` lists the component's **knobs** — each `--name` with its default, what it sets and the selector that reads it, joined by the classes the
|
|
156
|
+
component emits, so a knob named for an old name (`--action-note-*`) is found under the component that has it now (`BarNote`). It says what the
|
|
157
|
+
component **composes** (the catalog names its own source renders or calls: `Fab` → `IconBtn`, `Icon`) and what it is **composed by** (the inverse).
|
|
158
|
+
`find` matches the role's note and what a component stands in for as well as its name and description, so a question by intention ("a setting
|
|
159
|
+
turned on or off", "a code digit by digit") finds the piece (`Toggle`, `Pin`).
|
|
160
|
+
|
|
155
161
|
`list` · `get` · `find` · `scaffold` take `--json` (the function's result) and `--root <path>` (a cardds checkout: rebuilt first, so never stale). The same
|
|
156
162
|
five answers over MCP — `list_components` · `get_component` · `find_components` · `scaffold_screen` · `check_source`:
|
|
157
163
|
|
|
@@ -607,6 +613,16 @@ A sticky note is a square of paper with a few words on it. The family is in `src
|
|
|
607
613
|
`defaultWriting`. While writing, the note stands at the top, the screen behind is washed pale and takes no touch,
|
|
608
614
|
and `tools` (the paper chooser, the send button) and `tray` (the writer's stamp) stand under it. A touch outside or
|
|
609
615
|
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`.
|
|
616
|
+
- **`StickyPile`** — one to six notes lying one over another, as the newest notes of a feed do. It is a feed item of
|
|
617
|
+
its own, with no card frame. The first child is on top, so pass the newest first. Each note under it lies a step
|
|
618
|
+
right and down (`--sticky-pile-step-x`, 24, and `--sticky-pile-step-y`, 8), turned by the board's four angles by
|
|
619
|
+
its place, so the top note turns the same whatever the count. Only the top note draws its `foot`. At 375 a pile of
|
|
620
|
+
one is 184 × 184, of three 232 × 200, of six 304 × 224 (`--sticky-pile-pad`, 8, is the room round the notes for
|
|
621
|
+
their turn). The pile stands in the middle of its column and is never wider than it: a narrow column shortens the
|
|
622
|
+
steps. A seventh note is not drawn, and the console says so once. With `onClick` the whole pile is one button, and
|
|
623
|
+
`label` is its name (required with `onClick`): one Tab stop, a ring round the pile. Where the press goes is the
|
|
624
|
+
app's. A `StampOn` on the top note keeps its own touch; the stamps on the notes under it are inert. A pile's notes
|
|
625
|
+
all wear their stamp at one size.
|
|
610
626
|
- **`StickyField`** is the bare textarea on a note. **`StickyPick`** is the paper chooser: `value` + `onChange`,
|
|
611
627
|
`tones` for the ones on offer.
|
|
612
628
|
|
|
@@ -654,6 +670,14 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
|
|
|
654
670
|
onto its place, and `onArrived` tells the app when it has landed (at once where motion is off), so the app does what
|
|
655
671
|
follows. A `Card` and a `Sheet` are positioned boxes, so a `StampOn` written inside one lies on it.
|
|
656
672
|
`--stamp-on-arrive-time` sets how long the strike takes.
|
|
673
|
+
`main` makes it the stamp's main size, 96 × 120 (`--stamp-size`; 90 × 112.5 on a screen 360 wide), to cover what
|
|
674
|
+
it lies on. With `onRemove` the stamp can be taken off: when a drag ends or an arrow key moves it, a cross stands on
|
|
675
|
+
its top right corner for `removeFor` ms (default 3000). A press on the cross, or Delete / Backspace on the focused
|
|
676
|
+
stamp, asks, and the app takes the `StampOn` away. A stamp with `onRemove` and no `onAtChange` shows the cross on a
|
|
677
|
+
press. The cross is a 24 disc (`--stamp-on-remove-size`, `--stamp-on-remove-bg` #141414, `--stamp-on-remove-ink`
|
|
678
|
+
#ffffff) in a 44 target, centred on the corner and upright whatever the stamp's `turn`. Its name is cardds's word
|
|
679
|
+
("Remove" / "เอาออก"), or `removeLabel`. A press or a drag on a removable stamp never reaches what lies under it.
|
|
680
|
+
A stamp that is still arriving cannot be taken off yet: its cross waits until it has landed.
|
|
657
681
|
|
|
658
682
|
What an app stores for a stamp: the picture ids (1 to 4, in order), the ground tone, the name, the small line. Making
|
|
659
683
|
one is the app's screen: `ChipPick` checkboxes that each show a picture, the app refuses a fifth, and the app draws
|
|
@@ -917,6 +941,25 @@ Copy any block in `palette.css`, rename, retune:
|
|
|
917
941
|
}
|
|
918
942
|
```
|
|
919
943
|
|
|
944
|
+
## Finish
|
|
945
|
+
|
|
946
|
+
The finish of a card's paper — a style apart from colour, beside any palette or none:
|
|
947
|
+
|
|
948
|
+
```html
|
|
949
|
+
<html data-palette="orchard" data-finish="print"> <!-- or on any ancestor; no attribute = the flat look -->
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
Four values, one attribute (`css/finish.css`; the header names each and its knobs):
|
|
953
|
+
|
|
954
|
+
| `data-finish` | the paper | knobs (set in `theme.css`, as any knob) |
|
|
955
|
+
|---|---|---|
|
|
956
|
+
| `lit` | lit from above: a lit top edge, a hairline ring, a layered shadow that grows with height; filled buttons stand up and go down under the finger; fields sunk | the shadows every component reads (`--shadow-card`, `--shadow-float`, `--shadow-sheet`) |
|
|
957
|
+
| `print` | printed paper: an ink line in the card's own ink, a hard misregistered offset, grain over the card, a dotted rule under a lone card's head | `--print-off` (the offset, 3px) · `--print-grain` (the grain's opacity, 0.32) |
|
|
958
|
+
| `sticker` | a die-cut sticker: a white cut line, a short hard drop, a lone card leans (never one in a stack, a list or a feed), buttons with a bottom edge that press down | `--sticker-cut` (5px) · `--sticker-tilt` (0.8deg; `0deg` = straight) |
|
|
959
|
+
| `ink` | a poster / comic print: a thick line in the surface's ink round every boundary and control, a hard offset in the same ink | `--ink-w` (2.5px) · `--ink-off` (5px) |
|
|
960
|
+
|
|
961
|
+
Under every finish the wireframe's two borders step back (`--card-border`, `--action-border`; ink puts its own line there), a card in a closed stack's base stays flat, and the information inside a card stays flat — only a container or a control gets an edge. Colours are mixes of the roles, so a finish sits in any palette. The gallery's nav has a finish switch (key `f`); `Card · Finishes` shows the four side by side, `Home · Finish` a home under `print`.
|
|
962
|
+
|
|
920
963
|
## Scale: everything is rem
|
|
921
964
|
|
|
922
965
|
Every length token resolves to rem — a multiplier × one of four rem bases
|
package/bin/cardds.mjs
CHANGED
|
@@ -97,22 +97,27 @@ const asText = {
|
|
|
97
97
|
? [...row('helpers: ', cs.filter((c) => HELPERS.has(c.name))), ...row('structure: ', cs.filter((c) => !HELPERS.has(c.name)))]
|
|
98
98
|
: [...row('show ', cs.filter((c) => !c.interact)), ...catalog.KINDS.flatMap((k) => row(k.padEnd(10), cs.filter((c) => c.interact && c.interact.includes(k))))];
|
|
99
99
|
return catalog.SHAPES.map((shape) => [shape, components.filter((c) => c.shape === shape)])
|
|
100
|
-
.filter(([, cs]) => cs.length).flatMap(([shape, cs]) => [shape, ...section(shape, cs)])
|
|
100
|
+
.filter(([, cs]) => cs.length).flatMap(([shape, cs]) => [shape, ...section(shape, cs)])
|
|
101
|
+
.concat(components.finishes.length ? ['finish (data-finish, the paper — beside any palette)', ...components.finishes.map((f) => ` ${f.name.padEnd(8)}${catalog.sentence(f.is)}${f.knobs.length ? ` · knobs ${f.knobs.map((k) => k.name).join(' ')}` : ''}`)] : []).join('\n');
|
|
101
102
|
},
|
|
102
103
|
get: (c, { html }) => [
|
|
103
104
|
`${c.name} (${c.group})${c.element ? ` — forwards the <${c.element}> attributes` : ''}`,
|
|
104
105
|
one(c.description),
|
|
105
106
|
...(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('; ')}` : ''}`] : []),
|
|
107
|
+
...(c.composes.length ? [`composes: ${c.composes.join(' · ')}`] : []),
|
|
108
|
+
...(c.composedBy.length ? [`composed by: ${c.composedBy.join(' · ')}`] : []),
|
|
106
109
|
...(c.params ? ['\nparameters', ...(c.params.length ? [] : [' (none)']), ...c.params.map((p) => ` ${p.name}${p.optional && !p.default ? '?' : ''}: ${p.type}${p.default ? ` = ${p.default}` : ''}`)] : [
|
|
107
110
|
'\nprops',
|
|
108
111
|
...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`)]),
|
|
112
|
+
`\nknobs (${c.knobs.length})${c.knobs.length ? '' : ' — nothing to retune from a theme'}`,
|
|
113
|
+
...c.knobs.map((k) => ` ${k.name}: ${k.default} — ${k.sets} on ${k.on}`),
|
|
109
114
|
`\nvariations (${c.variations.length})`,
|
|
110
115
|
...c.variations.map((v) => `\n ${v.id} — ${one(v.description)}\n${indent(v.tsx)}${html ? `\n html:\n${indent(v.html, ' ')}` : ''}`),
|
|
111
116
|
...(c.alsoUsedIn ? [`\nalso used in: ${c.alsoUsedIn.join(' ')}\n(--all prints their code too)`] : []),
|
|
112
117
|
].join('\n'),
|
|
113
118
|
find: (r) => [
|
|
114
119
|
...(r.fallback ? [`nothing matches "${r.query}" — ${r.hint} (${r.screens} screens drawn)`]
|
|
115
|
-
: r.results.length ? r.results.map((x) => `${x.kind
|
|
120
|
+
: r.results.length ? r.results.map((x) => `${x.kind.padEnd(10)}${x.id ?? x.name}${x.role ? ` [${[...x.role.represents, x.role.shape, x.role.interact ? x.role.interact.join('+') : 'show'].join('·')}]` : ''} — ${x.summary}${x.kind === 'finish' && x.knobs.length ? ` · knobs ${x.knobs.join(' ')}` : ''}`) : [r.hint]),
|
|
116
121
|
...(r.usedTogether?.length ? [`\nused together: ${r.usedTogether.map((u) => `${u.name} (${u.stories})`).join(' ')}`] : []),
|
|
117
122
|
].join('\n'),
|
|
118
123
|
scaffold: (r) => typeof r === 'string' ? r.trimEnd() : r.html ? r.html.trimEnd() : r.map((x) => `${x.id} — ${x.summary}`).join('\n'),
|
|
@@ -130,7 +135,7 @@ function ask(cmd, argv) {
|
|
|
130
135
|
if (cmd === 'mcp') return import('../lib/mcp.mjs').then((m) => m.serve({ root }));
|
|
131
136
|
const cat = catalog.load({ root });
|
|
132
137
|
const q = argv.join(' ');
|
|
133
|
-
const result = cmd === 'list' ? catalog.list(cat) : cmd === 'get' ? catalog.get(cat, q, { html, all }) : cmd === 'find' ? catalog.find(cat, q, axes) : catalog.scaffold(cat, q || undefined, { format: html ? 'html' : 'tsx', css });
|
|
138
|
+
const result = cmd === 'list' ? Object.assign(catalog.list(cat), { finishes: catalog.finishes(cat) }) : cmd === 'get' ? catalog.get(cat, q, { html, all }) : cmd === 'find' ? catalog.find(cat, q, axes) : catalog.scaffold(cat, q || undefined, { format: html ? 'html' : 'tsx', css });
|
|
134
139
|
if (result.error) { console.error(`cardds ${cmd}: ${result.error}${result.closest ? ` — closest: ${result.closest.join(', ')}` : ''}`); return 1; }
|
|
135
140
|
if (result.warning && !json) console.error(`cardds scaffold: ${result.warning}`);
|
|
136
141
|
console.log(json ? JSON.stringify(result) : asText[cmd](result, { html }));
|
package/css/actions.css
CHANGED
|
@@ -273,10 +273,9 @@
|
|
|
273
273
|
display: flex;
|
|
274
274
|
gap: var(--sp-2);
|
|
275
275
|
}
|
|
276
|
-
/* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
|
|
276
|
+
/* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall (step.css), the bar its
|
|
277
277
|
last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
|
|
278
278
|
it takes the room it needs in the column, so nothing is added to the content for it */
|
|
279
|
-
.screen:not(.screen--fill):has(> .action-bar:last-child) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
280
279
|
.screen:not(.screen--fill) > .action-bar:last-child {
|
|
281
280
|
position: sticky; bottom: var(--kb, 0rem);
|
|
282
281
|
max-width: none;
|
|
@@ -382,7 +381,7 @@
|
|
|
382
381
|
button's, the card itself is never focused or disabled. */
|
|
383
382
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
384
383
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
385
|
-
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach) {
|
|
384
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove) {
|
|
386
385
|
cursor: pointer;
|
|
387
386
|
touch-action: manipulation;
|
|
388
387
|
transition: scale var(--motion-press) ease-out;
|
|
@@ -401,9 +400,15 @@
|
|
|
401
400
|
}
|
|
402
401
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
403
402
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
404
|
-
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
403
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
405
404
|
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
406
405
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
406
|
+
/* a pile of notes pressed whole (Lh 2026-10-11): the button lies invisible UNDER the notes, so it is the notes that answer — DIM, as a card does (the group dims as one, the
|
|
407
|
+
notes do not show through one another); the ring is the button's own, round the whole pile */
|
|
408
|
+
.sticky-pile__press { cursor: pointer; touch-action: manipulation; }
|
|
409
|
+
.sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) ease-out; }
|
|
410
|
+
.sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
|
|
411
|
+
.sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
|
|
407
412
|
/* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
|
|
408
413
|
it darkens a touch instead (--press-shade) */
|
|
409
414
|
:where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) ease-out; }
|
|
@@ -411,17 +416,17 @@
|
|
|
411
416
|
/* the focus ring: on the control, or on the label that wraps a hidden input (chip, tile, file button, a row that is a label) */
|
|
412
417
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
413
418
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
414
|
-
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):focus-visible,
|
|
419
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):focus-visible,
|
|
415
420
|
:where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
416
421
|
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .feed-card--small, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
417
422
|
/* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
|
|
418
423
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
419
424
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
420
|
-
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):is(:disabled, [aria-disabled="true"]),
|
|
425
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):is(:disabled, [aria-disabled="true"]),
|
|
421
426
|
:where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
|
|
422
427
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
423
428
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
424
|
-
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):not(.link):is(:disabled, [aria-disabled="true"]),
|
|
429
|
+
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):not(.link):is(:disabled, [aria-disabled="true"]),
|
|
425
430
|
:where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
|
|
426
431
|
.sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
|
|
427
432
|
@media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
|
package/css/card.css
CHANGED
|
@@ -212,12 +212,11 @@
|
|
|
212
212
|
actions.css) and scrolls up like any page — nothing is capped, nothing
|
|
213
213
|
shrinks, no scroll box inside a box.
|
|
214
214
|
So on a .screen that is NOT --fill the group is in flow: the column is at
|
|
215
|
-
least one phone tall, the group takes the room after the top bar and
|
|
215
|
+
least one phone tall (step.css), the group takes the room after the top bar and
|
|
216
216
|
centres its children in it. The screen's own inset stays the inset.
|
|
217
217
|
<div class="screen"><header class="topbar">…</header>
|
|
218
218
|
<div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
|
|
219
219
|
</div><div class="action-bar">…</div> */
|
|
220
|
-
.screen:not(.screen--fill):has(> .screen__centre) { min-height: 100dvh; display: flex; flex-direction: column; }
|
|
221
220
|
.screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
|
|
222
221
|
|
|
223
222
|
/* ---- the card that changes slides: out one side, the next in from the other ----
|
package/css/choice.css
CHANGED
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
.chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); }
|
|
85
85
|
.chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
|
|
86
86
|
/* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
|
|
87
|
-
a chip, a mood dot, a tile (media.css) */
|
|
88
|
-
:is(.chip--pick, .mood__opt, .tile--pick) > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
|
|
87
|
+
a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css) — the native input lies over its whole box, unseen */
|
|
88
|
+
:is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch) > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
|
|
89
89
|
:is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
90
90
|
/* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
|
|
91
91
|
.chip--sign { padding-right: var(--sp-2); }
|
package/css/finish.css
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
/* ============================================================
|
|
2
|
-
cardds/finish.css — FINISH:
|
|
3
|
-
look read flat, "not finished"), apart from colour: palette.css says WHICH colours, this says
|
|
4
|
-
|
|
5
|
-
No attribute = nothing here applies — the flat look, unchanged.
|
|
2
|
+
cardds/finish.css — FINISH: the finish of a card's paper. A style of its own (Lh 2026-10-02: the plain
|
|
3
|
+
look read flat, "not finished"), apart from colour: palette.css says WHICH colours, this says what the
|
|
4
|
+
material is and how it is lit. Apply with <html data-finish="…"> (or on any ancestor) beside any palette,
|
|
5
|
+
or none, or "own". No attribute = nothing here applies — the flat look, unchanged.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
FINISHES — four values of the one attribute (Lh 2026-10-12, from the timebank home with each injected:
|
|
8
|
+
four finishes, variations of one concept, never new pieces; teeth · tape · ticket · slab were rejected):
|
|
9
|
+
lit = lit from above: a lit top edge, a hairline ring and a layered shadow that grows with height; filled buttons stand up and go down under the finger; fields and pin cells are sunk.
|
|
10
|
+
print = printed paper: an ink line in the card's own ink, a hard misregistered offset instead of a blurred shadow, paper grain over the card, a dotted rule under the head as on a printed form; information inside stays flat.
|
|
11
|
+
sticker = a die-cut sticker: a white cut line round the card, a short hard drop, a slight tilt on a lone card, buttons with a bottom edge that press down.
|
|
12
|
+
ink = a poster / comic print: a thick line in the surface's ink round every boundary and control, a hard offset in the same ink.
|
|
13
|
+
KNOBS (set in a theme, as any knob; docs/knobs.md lists them):
|
|
14
|
+
print --print-off (the misregistration: how far the second pass missed) · --print-grain (the grain's opacity; a ::after, so the knob walk cannot probe it)
|
|
15
|
+
sticker --sticker-cut (the paper left round the print) · --sticker-tilt (a lone card's lean; 0deg for none — a card in a stack or a list never leans)
|
|
16
|
+
ink --ink-w (the line) · --ink-off (the offset)
|
|
17
|
+
|
|
18
|
+
Information inside a card stays flat under every finish: only a container or a control gets an edge.
|
|
19
|
+
Colours are mixes of the roles (--on-surface, --card-ink, --accent, currentColor), never plain black,
|
|
20
|
+
so a finish sits in any palette; the one plain white is the sticker's backing paper and the lit edge's light.
|
|
21
|
+
============================================================ */
|
|
22
|
+
|
|
23
|
+
/* every finish: the finish IS the edge, so the wireframe's two borders step back (as [data-palette] in palette.css); ink puts its own line there below */
|
|
24
|
+
[data-finish] { --card-border: transparent; --action-border: transparent; }
|
|
25
|
+
|
|
26
|
+
/* ============================================================
|
|
27
|
+
lit — the light comes from above:
|
|
8
28
|
- a thing that stands UP (card, sheet, button) has a lit top edge, a hairline ring and a shadow
|
|
9
29
|
that grows with its height: card 1 · sheet / float 2 — the layers cardds already has;
|
|
10
|
-
- a thing that is SUNK (a field, a pin cell) has its shadow inside, under its top edge
|
|
11
|
-
- information inside a card stays flat: only a container or a control gets an edge.
|
|
30
|
+
- a thing that is SUNK (a field, a pin cell) has its shadow inside, under its top edge.
|
|
12
31
|
Shadows are mixed from the ink (--on-surface), never plain black, so they sit in the palette.
|
|
13
32
|
============================================================ */
|
|
14
|
-
|
|
15
33
|
[data-finish="lit"] {
|
|
16
34
|
--_shade: var(--on-surface);
|
|
17
35
|
--finish-edge: inset 0 1px 0 rgb(255 255 255 / 0.55); /* the lit top edge */
|
|
@@ -37,9 +55,6 @@
|
|
|
37
55
|
--finish-sheen: linear-gradient(to bottom, rgb(255 255 255 / 0.16), rgb(255 255 255 / 0) 60%);
|
|
38
56
|
--finish-sunk: inset 0 1px 2px color-mix(in srgb, var(--_shade) 14%, transparent), 0 1px 0 rgb(255 255 255 / 0.5);
|
|
39
57
|
|
|
40
|
-
/* the shadow is the edge: the wireframe's lines step back (as in every colour palette) */
|
|
41
|
-
--card-border: transparent;
|
|
42
|
-
--action-border: transparent;
|
|
43
58
|
--outline: color-mix(in srgb, currentColor 14%, transparent);
|
|
44
59
|
}
|
|
45
60
|
|
|
@@ -80,3 +95,74 @@
|
|
|
80
95
|
|
|
81
96
|
/* ---- sunk: what the member types into ---- */
|
|
82
97
|
[data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
|
|
98
|
+
|
|
99
|
+
/* ============================================================
|
|
100
|
+
print — printed paper (riso / zine): the second pass missed by --print-off, so the card's shadow is a
|
|
101
|
+
hard offset in its own ink; a line in that ink round the card; grain over the whole card; the head
|
|
102
|
+
underlined by a dotted rule as on a printed form. A lone card's head is a form's head; a stacked
|
|
103
|
+
sheet's, a row card's, a cover's (controls over a picture) and a folded feed card's (the head is the
|
|
104
|
+
whole card) have no rule.
|
|
105
|
+
============================================================ */
|
|
106
|
+
[data-finish="print"] {
|
|
107
|
+
--_ink: color-mix(in srgb, currentColor 70%, transparent);
|
|
108
|
+
--outline: var(--_ink);
|
|
109
|
+
}
|
|
110
|
+
[data-finish="print"] .card {
|
|
111
|
+
border: var(--border-w) solid var(--_ink);
|
|
112
|
+
box-shadow: var(--print-off, 3px) var(--print-off, 3px) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
|
|
113
|
+
isolation: isolate; /* the grain's blend stays inside the card */
|
|
114
|
+
}
|
|
115
|
+
/* the grain: a ::after, not ::before — .card--cover's scrim is its ::before. Over everything in the card (z-index 1, the foot's own level), touches pass through it */
|
|
116
|
+
[data-finish="print"] .card::after {
|
|
117
|
+
content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 1;
|
|
118
|
+
opacity: var(--print-grain, 0.32); mix-blend-mode: multiply;
|
|
119
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.3 0 0 0 0 0.25 0 0 0 0 0.2 0 0 0 0.9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
120
|
+
}
|
|
121
|
+
/* a dark card (the brand tone, a cover): multiply would only blacken it, so the grain lightens, fainter */
|
|
122
|
+
[data-finish="print"] :is(.card--3, .card--cover)::after { mix-blend-mode: screen; opacity: calc(var(--print-grain, 0.32) * 0.56); }
|
|
123
|
+
[data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .card__head {
|
|
124
|
+
padding-bottom: var(--sp-2);
|
|
125
|
+
border-bottom: var(--rule-w) dotted color-mix(in srgb, currentColor 45%, transparent);
|
|
126
|
+
}
|
|
127
|
+
[data-finish="print"] :is(.btn, .chip) {
|
|
128
|
+
border-color: var(--_ink);
|
|
129
|
+
box-shadow: 2px 2px 0 0 color-mix(in srgb, currentColor 35%, transparent);
|
|
130
|
+
}
|
|
131
|
+
[data-finish="print"] .btn--primary { box-shadow: 2px 2px 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
|
|
132
|
+
[data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 2px 2px; }
|
|
133
|
+
[data-finish="print"] .card__thumb { border-style: dashed; }
|
|
134
|
+
|
|
135
|
+
/* ============================================================
|
|
136
|
+
sticker — a die-cut sticker on a sheet: --sticker-cut of white paper left round the print, a short hard
|
|
137
|
+
drop under it, a lone card leans by --sticker-tilt (the next one the other way); chunky buttons with a
|
|
138
|
+
bottom edge that press down. A card in a stack, in a list, in a feed, a sheet or a modal never leans:
|
|
139
|
+
those are placed by their holder, a lean would cut or cross them.
|
|
140
|
+
============================================================ */
|
|
141
|
+
[data-finish="sticker"] { --outline: color-mix(in srgb, currentColor 18%, transparent); }
|
|
142
|
+
[data-finish="sticker"] .card {
|
|
143
|
+
box-shadow:
|
|
144
|
+
0 0 0 var(--sticker-cut, 5px) #fff,
|
|
145
|
+
0 calc(var(--sticker-cut, 5px) + 4px) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
|
|
146
|
+
0 calc(var(--sticker-cut, 5px) + 12px) 20px -8px color-mix(in srgb, var(--on-surface) 22%, transparent);
|
|
147
|
+
}
|
|
148
|
+
[data-finish="sticker"] .card:not(.sheet, .modal, .sheet-stack *, .card-list > *, .card-feed > *) { rotate: var(--sticker-tilt, 0.8deg); }
|
|
149
|
+
[data-finish="sticker"] .card:not(.sheet, .modal, .sheet-stack *, .card-list > *, .card-feed > *):nth-of-type(even) { rotate: calc(-1 * var(--sticker-tilt, 0.8deg)); }
|
|
150
|
+
[data-finish="sticker"] .card-list { gap: var(--sp-5); }
|
|
151
|
+
[data-finish="sticker"] .btn {
|
|
152
|
+
box-shadow: 0 3px 0 0 color-mix(in srgb, currentColor 30%, transparent);
|
|
153
|
+
border-color: color-mix(in srgb, currentColor 25%, transparent);
|
|
154
|
+
}
|
|
155
|
+
[data-finish="sticker"] .btn--primary { box-shadow: 0 3px 0 0 color-mix(in srgb, var(--btn-primary-bg, var(--accent)) 55%, var(--on-surface)); }
|
|
156
|
+
[data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 3px; }
|
|
157
|
+
[data-finish="sticker"] .chip { box-shadow: 0 0 0 3px #fff; }
|
|
158
|
+
|
|
159
|
+
/* ============================================================
|
|
160
|
+
ink — a poster / comic print: --ink-w of the surface's ink round every boundary and control, and a hard
|
|
161
|
+
offset of --ink-off in the same ink. The two wireframe borders come back as that line.
|
|
162
|
+
============================================================ */
|
|
163
|
+
[data-finish="ink"] { --card-border: var(--on-surface); --action-border: var(--on-surface); --outline: currentColor; }
|
|
164
|
+
[data-finish="ink"] .card { border: var(--ink-w, 2.5px) solid var(--on-surface); box-shadow: var(--ink-off, 5px) var(--ink-off, 5px) 0 0 var(--on-surface); }
|
|
165
|
+
[data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 2.5px) solid currentColor; box-shadow: 3px 3px 0 0 currentColor; }
|
|
166
|
+
[data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 3px 3px 0 0 var(--on-surface); }
|
|
167
|
+
[data-finish="ink"] .toggle { box-shadow: 2px 2px 0 0 var(--on-surface); }
|
|
168
|
+
[data-finish="ink"] .card__thumb { border-width: var(--ink-w, 2.5px); }
|
package/css/forms.css
CHANGED
|
@@ -78,5 +78,4 @@
|
|
|
78
78
|
.form-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin: 0; font: var(--form-note-font, var(--type-caption)); color: var(--form-note-ink, var(--card-muted)); text-align: center; }
|
|
79
79
|
.form-note > .icon { flex: none; }
|
|
80
80
|
.form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
|
|
81
|
-
@media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation:
|
|
82
|
-
@keyframes form-note-turn { to { rotate: 1turn; } }
|
|
81
|
+
@media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
|
package/css/numbers.css
CHANGED
|
@@ -257,8 +257,7 @@
|
|
|
257
257
|
The ring turns; words beside it are optional and take the caption's look. Still when motion is reduced. */
|
|
258
258
|
.spinner { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); vertical-align: middle; font: var(--type-caption); color: var(--spinner-ink, currentColor); }
|
|
259
259
|
.spinner > .icon { flex: none; }
|
|
260
|
-
@media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation:
|
|
261
|
-
@keyframes spinner-turn { to { rotate: 1turn; } }
|
|
260
|
+
@media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
|
|
262
261
|
|
|
263
262
|
/* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
|
|
264
263
|
timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
|
package/css/post.css
CHANGED
|
@@ -234,10 +234,9 @@
|
|
|
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; }
|
|
237
|
-
.post-desk .postmark { animation:
|
|
237
|
+
.post-desk .postmark { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; } /* struck as a StampOn is (`strike`: tokens.css), in the same time */
|
|
238
238
|
}
|
|
239
239
|
@keyframes post-desk-stick { from { translate: 0 calc(-1 * var(--sp-8)); rotate: 14deg; opacity: 0; } }
|
|
240
|
-
@keyframes post-desk-strike { from { translate: calc(-1 * var(--sp-3)) calc(-1 * var(--sp-6)); rotate: -24deg; opacity: 0; } }
|
|
241
240
|
/* the other half's showing edge brings it across: a tap-wide strip out to the screen's edge */
|
|
242
241
|
.post-desk__reach { position: absolute; inset-block: 0; width: var(--tap); border: 0; background: transparent; }
|
|
243
242
|
.post-desk__reach--address { inset-inline-end: calc(-1 * var(--screen-pad)); }
|
package/css/stack.css
CHANGED
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
}
|
|
322
322
|
/* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
|
|
323
323
|
line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
|
|
324
|
-
:is([data-palette], [data-finish
|
|
324
|
+
:is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
|
|
325
325
|
|
|
326
326
|
/* ============================================================
|
|
327
327
|
.sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
|
package/css/stamp.css
CHANGED
|
@@ -213,14 +213,54 @@
|
|
|
213
213
|
}
|
|
214
214
|
/* the SMALL stamp: stuck on a surface (a note). 48 × 60 whatever the surface's size. The stamp carries the size and the `.stamp-on` box is as wide as its stamp */
|
|
215
215
|
.stamp-on > .stamp { width: var(--stamp-sm-size, var(--tap)); max-width: none; }
|
|
216
|
+
/* --main: the same stuck stamp at the stamp's MAIN size, 96 × 120 (--stamp-size), to cover what it lies on (the owner's call, 2026-10-11: the home's note).
|
|
217
|
+
After the rule above, at the same weight: `main` is the one place a stuck stamp reads --stamp-size. */
|
|
218
|
+
.stamp-on--main > .stamp { width: var(--stamp-size, calc(var(--tap) * 2)); }
|
|
216
219
|
.stamp-on--move { will-change: transform; } /* its own layer: Safari left the stamp's shadow behind as a trail along the drag when it was painted with the note (seen on the simulator, 2026-10-07) */
|
|
217
220
|
.stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
|
|
218
221
|
.stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
219
222
|
.stamp-on.is-held { cursor: grabbing; z-index: 1; scale: 1.06; } /* lifted off the paper while the finger holds it */
|
|
220
223
|
@media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
|
|
225
|
+
/* ---- the cross that takes a stamp off (`StampOn onRemove`): a small dark disc with an x on the stamp's top right corner ----
|
|
226
|
+
<div class="stamp-on stamp-on--move">…<button class="stamp-on__remove" aria-label="Remove"><span class="stamp-on__remove-disc"><svg class="icon">x</svg></span></button></div>
|
|
227
|
+
The button is the touch target — at least --tap-floor (44) square, invisible, centred on the corner — and the disc inside it is what is seen
|
|
228
|
+
(24). It is NOT turned with the stamp: a hand turns a stamp, a button stays square to the screen, so the cross counter-turns by the stamp's own turn. Like the
|
|
229
|
+
stamp it brings its ground and its ink (a dark disc, a pale x and a pale ring: seen on any paper and over any picture). */
|
|
230
|
+
.stamp-on__remove-disc {
|
|
231
|
+
display: grid;
|
|
232
|
+
place-items: center;
|
|
233
|
+
width: var(--stamp-on-remove-size, var(--sp-6));
|
|
234
|
+
aspect-ratio: 1;
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
background-color: var(--stamp-on-remove-bg, #141414);
|
|
237
|
+
box-shadow: 0 0 0 var(--sp-0) currentColor, 0 var(--sp-0) var(--sp-1) rgb(0 0 0 / 0.35);
|
|
238
|
+
}
|
|
239
|
+
.stamp-on__remove {
|
|
240
|
+
--_hit: max(var(--tap-floor), var(--stamp-on-remove-size, var(--sp-6)));
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: 0;
|
|
243
|
+
right: 0;
|
|
244
|
+
translate: 50% -50%;
|
|
245
|
+
rotate: calc(var(--stamp-on-turn, 0) * -1deg);
|
|
246
|
+
display: grid;
|
|
247
|
+
place-items: center;
|
|
248
|
+
width: var(--_hit);
|
|
249
|
+
height: var(--_hit);
|
|
250
|
+
padding: 0;
|
|
251
|
+
border: 0;
|
|
252
|
+
border-radius: 50%;
|
|
253
|
+
background: none;
|
|
254
|
+
color: var(--stamp-on-remove-ink, #ffffff);
|
|
255
|
+
font: inherit;
|
|
256
|
+
-webkit-tap-highlight-color: transparent;
|
|
257
|
+
}
|
|
258
|
+
.stamp-on__remove-disc > .icon { width: 60%; height: 60%; }
|
|
259
|
+
@media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) ease-out; } }
|
|
260
|
+
@keyframes stamp-on-remove-in { from { opacity: 0; scale: 0.6; } }
|
|
261
|
+
|
|
262
|
+
/* --arrive: the stamp sticks ITSELF (StampOn `arrive`) — struck onto its place (`strike`: tokens.css). The component reads the
|
|
263
|
+
animation's name to tell the app it has landed. Knob: --stamp-on-arrive-time */
|
|
223
264
|
@media (prefers-reduced-motion: no-preference) {
|
|
224
|
-
.stamp-on--arrive { animation:
|
|
265
|
+
.stamp-on--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; }
|
|
225
266
|
}
|
|
226
|
-
@keyframes stamp-on-strike { from { scale: 2; opacity: 0; } 60% { scale: 0.92; opacity: 1; } }
|
package/css/step.css
CHANGED
|
@@ -110,6 +110,12 @@
|
|
|
110
110
|
padding-bottom: var(--bottom-h, 0rem); /* exactly the bottom layer (measured by Screen): a page scrolls ONLY when the layer would cover its words, and then just far enough to clear them — at 100% nothing scrolls (Lh 2026-09-23) */
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* a SCROLLING (not fill) screen becomes a column at least one phone tall when it holds a bottom or a centre:
|
|
114
|
+
an .action-bar as its last child (Lh 2026-09-24, 0.7.0: the bar is the column's last row — at the foot when the words are short,
|
|
115
|
+
sticky to the viewport's foot while they scroll; actions.css) or a .screen__centre (Lh 2026-09-12: the group is in flow, takes the
|
|
116
|
+
room after the top bar and centres its children in it; a long one makes the page longer and runs under the bar; card.css) */
|
|
117
|
+
.screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
118
|
+
|
|
113
119
|
/* ============================================================
|
|
114
120
|
TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
|
|
115
121
|
in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
|
package/css/sticky.css
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
|
|
95
95
|
}
|
|
96
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 the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css), and may lie over the words. */
|
|
97
|
+
A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css; `StampOn main` is the main size), and may lie over the words. */
|
|
98
98
|
.sticky__stuck { position: absolute; inset: 0; pointer-events: none; }
|
|
99
99
|
.sticky--flat > .sticky__paper { clip-path: none; }
|
|
100
100
|
.sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
box-shadow: inset 0 0 0 var(--border-w) var(--ink-tint); /* a pale tone still has an edge on a pale ground */
|
|
183
183
|
cursor: pointer;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
/* its hidden input: the picks' one rule, choice.css */
|
|
186
186
|
.sticky-pick__swatch:has(> :checked) { outline: var(--rule-w) solid currentColor; outline-offset: var(--sp-0); }
|
|
187
187
|
.sticky-pick__swatch:has(> :focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
188
188
|
|
|
@@ -285,6 +285,49 @@
|
|
|
285
285
|
.sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
|
|
286
286
|
.sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
|
|
287
287
|
|
|
288
|
+
/* ---- sticky pile: one to six notes lying one over another — the newest on top (Lh 2026-10-11) ----
|
|
289
|
+
<div class="sticky-pile sticky-pile--tap" style="--_n: 3">
|
|
290
|
+
<div class="sticky-pile__notes">
|
|
291
|
+
<div class="sticky">…</div><div class="sticky">…</div><div class="sticky">…</div>
|
|
292
|
+
<button class="sticky-pile__press" aria-label="Three new notes"></button>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
The FIRST note is on top; each one under it is shifted a step right and down (--sticky-pile-step-x 24, --sticky-pile-step-y 8) so a strip of it peeks
|
|
296
|
+
out. All lie in ONE grid cell. The turn of each is the board's, by its place among the children (4n + 1 … 4n): the first, the top, is always −3°, whatever the count.
|
|
297
|
+
The notes under the top one draw no foot. The pile is centred in its column (the root is the column's width and a size container) and never
|
|
298
|
+
wider than it: the steps are shortened to what the column leaves. --_n is the count, written by the component: data, not a knob.
|
|
299
|
+
The room round the notes (--sticky-pile-pad) is for the turned corners and the shadow.
|
|
300
|
+
--tap: the whole pile is the button, a layer UNDER the notes (the last child): the notes let a touch through to it (pointer-events), a stamp on the top note keeps its own.
|
|
301
|
+
The press look is actions.css's (DIM); the ring is the button's own, round the whole pile. */
|
|
302
|
+
.sticky-pile {
|
|
303
|
+
--_size: var(--sticky-size, calc(var(--sp-12) * 3.5));
|
|
304
|
+
--_pad: var(--sticky-pile-pad, var(--sp-2));
|
|
305
|
+
--_step-x: min(var(--sticky-pile-step-x, var(--sp-6)), (100cqw - var(--_size) - 2 * var(--_pad)) / max(var(--_n) - 1, 1));
|
|
306
|
+
--_step-y: var(--sticky-pile-step-y, var(--sp-2));
|
|
307
|
+
container-type: inline-size;
|
|
308
|
+
display: grid;
|
|
309
|
+
justify-items: center;
|
|
310
|
+
}
|
|
311
|
+
.sticky-pile__notes {
|
|
312
|
+
position: relative;
|
|
313
|
+
display: grid;
|
|
314
|
+
grid-template: var(--_size) / var(--_size);
|
|
315
|
+
padding: var(--_pad) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-x)) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-y)) var(--_pad);
|
|
316
|
+
}
|
|
317
|
+
.sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(7 - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
|
|
318
|
+
.sticky-pile__notes > .sticky:nth-child(2) { --_depth: 1; }
|
|
319
|
+
.sticky-pile__notes > .sticky:nth-child(3) { --_depth: 2; }
|
|
320
|
+
.sticky-pile__notes > .sticky:nth-child(4) { --_depth: 3; }
|
|
321
|
+
.sticky-pile__notes > .sticky:nth-child(5) { --_depth: 4; }
|
|
322
|
+
.sticky-pile__notes > .sticky:nth-child(6) { --_depth: 5; }
|
|
323
|
+
.sticky-pile__notes > .sticky:nth-child(n + 2) .sticky__foot { display: none; }
|
|
324
|
+
.sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
|
|
325
|
+
.sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
|
|
326
|
+
.sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
|
|
327
|
+
.sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
|
|
328
|
+
.sticky-pile__press { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; appearance: none; background: none; -webkit-tap-highlight-color: transparent; }
|
|
329
|
+
.sticky-pile--tap .sticky-pile__notes > .sticky { pointer-events: none; }
|
|
330
|
+
|
|
288
331
|
/* ---- sticky answer: the note you answer on — it waits at the screen's foot and comes to the top to be written on ----
|
|
289
332
|
<div class="sticky-answer sticky-answer--writing">
|
|
290
333
|
<div class="sticky-answer__scrim"></div>
|
package/css/tokens.css
CHANGED
|
@@ -265,3 +265,9 @@
|
|
|
265
265
|
@property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
|
|
266
266
|
@property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
|
|
267
267
|
@property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
|
|
268
|
+
|
|
269
|
+
/* the ring that turns while waiting: Spinner, FormNote busy */
|
|
270
|
+
@keyframes turn { to { rotate: 1turn; } }
|
|
271
|
+
/* a mark struck onto paper — shown large and faint, pressed a little past its size, then at rest: StampOn `arrive`, the post desk's
|
|
272
|
+
postmark (Lh 2026-10-12: one strike for both; the postmark used to slide in from up-left, turning). Both read --stamp-on-arrive-time */
|
|
273
|
+
@keyframes strike { from { scale: 2; opacity: 0; } 60% { scale: 0.92; opacity: 1; } }
|