@usableapp/cardds 0.7.63 → 0.7.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
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.64 — 2026-10-06
6
+
7
+ ### Added
8
+ - **Sticky notes** (Lh 2026-10-06) — a new family, nothing existing moves. `Sticky` is a paper note: a flat pastel block, straight, no pattern, its bottom-right corner folded, a soft shadow. `tone` picks one of nine papers (`STICKY_TONES`: cotton · blush · tea-leaf · custard · salmon · iceberg · cocoa · apricot · water; default custard); a theme retunes a paper with `--sticky-<tone>-bg`. `flat` drops the fold, `square` makes the 1:1 note you write on, `head` and `foot` are the small lines above and under the words, `corner` stands a control (an `IconBtn`) on the bottom-right corner.
9
+ - **`Sticky date` + `StickyDate`** — the day big, the month under it, at the note's top-right: `<Sticky date={<StickyDate day="14" month="ก.พ." />}>`. cardds does not format dates: you pass the words. A picture in `head` beside a date is a small square (`--sticky-pic-size`).
10
+ - **`Sticky tilt`** — `"left"` | `"right"` turns the note 5° (`--sticky-tilt`). A note is straight everywhere else: this is for the one note a start screen hands over (gallery `StickyWelcome`).
11
+ - **`StickyField`** (a bare textarea on a note, `label` names it) · **`StickyPick`** (the tone chooser, `value` + `onChange`) · **`StickyThread`** + **`StickyPost`** (notes as a chat or a feed: `who` · `name` · `time` above the notes, `side="right"` for mine, `rail` draws the dotted line under the avatars).
12
+
13
+ ### Fixed
14
+ - **`Callout`: the tail joins the card cleanly** — its two strokes ended past the card's border as small nubs. The tail is about 2px shorter. Nothing to change in a screen.
15
+
5
16
  ## 0.7.63 — 2026-10-06
6
17
 
7
18
  ### Added
package/cardds.css CHANGED
@@ -14,6 +14,7 @@
14
14
  @import url("css/people.css"); /* avatar, avatar stack, chooser row */
15
15
  @import url("css/choice.css"); /* check, toggle, chip grid, days, calendar, mood, pin, composer */
16
16
  @import url("css/lists.css"); /* row, rows, key-value, kv grid, timeline, legend, link */
17
+ @import url("css/sticky.css"); /* sticky note, its field, tone chooser, thread of posts, pile */
17
18
  @import url("css/media.css"); /* cover card, quote, tile grid, fold, mosaic, wave */
18
19
  @import url("css/layover.css"); /* ask drawer, popover menu, banner, float bar, callout */
19
20
  @import url("css/finish.css"); /* light, shadow, edges — only under data-finish="lit" */
package/css/layover.css CHANGED
@@ -279,11 +279,14 @@
279
279
  content: "";
280
280
  position: absolute;
281
281
  left: 50%;
282
- bottom: calc(-1 * var(--sp-2));
282
+ bottom: 0;
283
283
  width: var(--sp-3);
284
284
  height: var(--sp-3);
285
- translate: -50% 0;
285
+ /* centred on the card's padding edge and cut to its lower half, so the tail's
286
+ strokes end flush on the card's border instead of poking past it */
287
+ translate: -50% 50%;
286
288
  rotate: 45deg;
289
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
287
290
  background: inherit;
288
291
  border: inherit;
289
292
  border-top: 0;
package/css/sticky.css ADDED
@@ -0,0 +1,187 @@
1
+ /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, a thread of posts.
2
+ A note is its own paper: it brings its colour and its ink, whatever card or palette it lies on. */
3
+
4
+ /* ---- sticky: a paper note — a flat pastel block, straight, the bottom-right corner folded over ----
5
+ Straight unless asked (--tilt-left / --tilt-right: the one note of a start screen), never patterned. The corner is CUT on the diagonal and a flap of a darker shade lies over it.
6
+ <div class="sticky sticky--custard">
7
+ <div class="sticky__paper">
8
+ <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>
10
+ </div>
11
+ </div>
12
+ Two boxes on purpose: the OUTER one holds the tone and the shadow, the PAPER is the one that is cut — a clip-path
13
+ on the outer box would cut its own shadow off. The outer box's ground is kept out of the cut corner by painting
14
+ only its content box, which stops one fold short of the right and bottom edges; the paper's negative margins run
15
+ back out to the full size. The paper takes the tone by `inherit`. */
16
+ .sticky {
17
+ color: var(--sticky-ink, #141414);
18
+ --_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
+ 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
+ --_fold: var(--sticky-fold, var(--sp-6));
21
+ --_min: calc(var(--tap) * 3);
22
+ --_cap: calc(var(--tap) * 7);
23
+ position: relative;
24
+ display: grid;
25
+ box-sizing: border-box;
26
+ width: fit-content; /* hugs its words… */
27
+ min-width: var(--sticky-min-w, var(--_min)); /* …but a word alone is still a note */
28
+ max-width: var(--sticky-max-w, 100%);
29
+ padding: 0 var(--_fold) var(--_fold) 0; /* = the line above, written whole */
30
+ background-clip: content-box;
31
+ filter: drop-shadow(var(--sticky-drop, 0 var(--sp-1) var(--sp-2) rgb(0 0 0 / 0.16))); /* drop-shadow follows the cut; a box-shadow would draw the square */
32
+ }
33
+ /* the tones — the names in src/sticky/tones.ts. A swatch of the chooser wears the same class. */
34
+ .sticky--cotton { background-color: var(--sticky-cotton-bg, #f3efe2); }
35
+ .sticky--blush { background-color: var(--sticky-blush-bg, #f6d5da); }
36
+ .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, #d5e8c5); }
37
+ .sticky--custard { background-color: var(--sticky-custard-bg, #f6e9a0); }
38
+ .sticky--salmon { background-color: var(--sticky-salmon-bg, #f3c7b3); }
39
+ .sticky--iceberg { background-color: var(--sticky-iceberg-bg, #cfe0ee); }
40
+ .sticky--cocoa { background-color: var(--sticky-cocoa-bg, #dccbbd); }
41
+ .sticky--apricot { background-color: var(--sticky-apricot-bg, #f6d49a); }
42
+ .sticky--water { background-color: var(--sticky-water-bg, #bcd6f3); }
43
+
44
+ .sticky__paper {
45
+ position: relative;
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: var(--sticky-gap, var(--sp-3));
49
+ box-sizing: border-box;
50
+ min-width: 0;
51
+ margin: 0 calc(-1 * var(--_fold)) calc(-1 * var(--_fold)) 0;
52
+ padding: var(--sticky-pad-y, var(--sp-4)) var(--sticky-pad-x, var(--sp-4));
53
+ border-radius: var(--sticky-radius, var(--sp-1));
54
+ background-color: inherit;
55
+ clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_fold)), calc(100% - var(--_fold)) 100%, 0 100%);
56
+ }
57
+ .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)); }
58
+ .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
59
+ .sticky__head:has(> .sticky-date) { align-items: start; }
60
+ .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 */
61
+ .sticky__body { flex: 1 1 auto; min-width: 0; font: var(--sticky-font, var(--type-title)); overflow-wrap: anywhere; }
62
+ .sticky__foot { font: var(--sticky-foot-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); padding-inline-end: var(--_fold); } /* the line stops before the fold */
63
+ /* 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 */
64
+ .sticky__fold {
65
+ position: absolute;
66
+ right: 0;
67
+ bottom: 0;
68
+ width: var(--_fold);
69
+ height: var(--_fold);
70
+ border-top-left-radius: var(--sp-1);
71
+ background-color: var(--sticky-fold-bg, color-mix(in srgb, currentColor 14%, transparent)); /* a darker shade of whatever the paper is */
72
+ box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
73
+ }
74
+ .sticky--flat > .sticky__paper { clip-path: none; }
75
+ .sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
76
+
77
+ /* --tilt: the note turned a little on its middle. Straight is the rule; this is for the one note a start screen hands over. */
78
+ .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 */
79
+ .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, 5deg)); }
80
+ .sticky--tilt-right { rotate: var(--sticky-tilt, 5deg); }
81
+
82
+ /* ---- sticky date: the day, big, the month under it — at the end of the note's head (its top-right corner) ----
83
+ <div class="sticky__head">…<time class="sticky-date" datetime="2026-02-14"><span class="sticky-date__day">14</span><span class="sticky-date__month">ก.พ.</span></time></div> */
84
+ .sticky-date { display: grid; justify-items: end; margin-inline-start: auto; text-align: end; color: var(--sticky-ink, #141414); }
85
+ .sticky-date__day { font: var(--sticky-date-day-font, var(--type-stat)); }
86
+ .sticky-date__month { font: var(--sticky-date-month-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
87
+
88
+ /* --square: the note to write on — 1:1, as wide as its column up to a cap, in the middle */
89
+ .sticky--square { width: 100%; max-width: var(--sticky-square-max-w, var(--_cap)); aspect-ratio: 1; margin-inline: auto; }
90
+
91
+ /* --corner: a control stands ON the bottom-right corner, half outside. The paper is notched round it (a mask, so the
92
+ shadow follows the notch and no ground colour is needed); the note has no fold. */
93
+ .sticky__corner {
94
+ position: absolute;
95
+ right: 0;
96
+ bottom: 0;
97
+ translate: 50% 50%;
98
+ display: grid;
99
+ place-items: center;
100
+ width: var(--sticky-corner-size, var(--tap-sm));
101
+ aspect-ratio: 1;
102
+ }
103
+ /* the half of the control that stands outside is the note's own room: it never runs past the column, nor onto what follows */
104
+ .sticky--corner {
105
+ --_corner: var(--sticky-corner-size, var(--tap-sm)); /* the same default as .sticky__corner above: the paper's notch needs it too */
106
+ max-width: calc(var(--sticky-max-w, 100%) - var(--_corner) / 2);
107
+ margin: 0 calc(var(--_corner) / 2) calc(var(--_corner) / 2) 0;
108
+ }
109
+ .sticky--corner > .sticky__paper {
110
+ --_notch: calc(var(--_corner) / 2 + var(--sp-1));
111
+ clip-path: none;
112
+ -webkit-mask: radial-gradient(circle at 100% 100%, transparent calc(var(--_notch) - var(--border-w)), #000 var(--_notch));
113
+ mask: radial-gradient(circle at 100% 100%, transparent calc(var(--_notch) - var(--border-w)), #000 var(--_notch));
114
+ }
115
+
116
+ /* ---- sticky field: the words written ON a note — a bare textarea in the note's type and ink, filling it ----
117
+ <div class="sticky__body"><textarea class="sticky-field" aria-label="Note" placeholder="Add text to this note"></textarea></div> */
118
+ .sticky__body:has(> .sticky-field) { display: grid; }
119
+ .sticky-field {
120
+ display: block;
121
+ box-sizing: border-box;
122
+ width: 100%;
123
+ min-height: var(--sticky-field-min-h, var(--_min));
124
+ resize: none;
125
+ border: 0;
126
+ margin: 0;
127
+ padding: 0;
128
+ background: transparent;
129
+ color: inherit;
130
+ font: inherit;
131
+ outline: none; /* the caret is the focus mark, as in the composer */
132
+ }
133
+ .sticky-field::placeholder { color: var(--sticky-muted-ink, var(--_muted)); opacity: 1; }
134
+
135
+ /* ---- sticky pick: the tone chooser — round swatches at tap size, the chosen one ringed ----
136
+ <div class="sticky-pick" role="radiogroup" aria-label="Note colour">
137
+ <label class="sticky-pick__swatch sticky--blush"><input type="radio" name="…" aria-label="blush"></label>…
138
+ </div> */
139
+ .sticky-pick { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sticky-pick-gap, var(--sp-3)); }
140
+ .sticky-pick__swatch {
141
+ position: relative;
142
+ flex: none;
143
+ width: var(--sticky-pick-size, var(--tap));
144
+ aspect-ratio: 1;
145
+ border-radius: var(--r-chip);
146
+ color: var(--sticky-pick-on-ink, var(--on-surface)); /* the ring's colour */
147
+ box-shadow: inset 0 0 0 var(--border-w) var(--ink-tint); /* a pale tone still has an edge on a pale ground */
148
+ cursor: pointer;
149
+ }
150
+ .sticky-pick__swatch > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
151
+ .sticky-pick__swatch:has(> :checked) { outline: var(--rule-w) solid currentColor; outline-offset: var(--sp-0); }
152
+ .sticky-pick__swatch:has(> :focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
153
+
154
+ /* ---- sticky thread: a chat or a feed told in notes ----
155
+ <div class="sticky-thread sticky-thread--rail">
156
+ <div class="sticky-post">
157
+ <span class="sticky-post__who"><span class="avatar">…</span></span>
158
+ <div class="sticky-post__head"><span class="sticky-post__name">Noi</span><span class="sticky-post__time">8:15</span></div>
159
+ <span class="sticky-post__rail"></span>
160
+ <div class="sticky-post__notes"><div class="sticky">…</div></div>
161
+ </div>
162
+ <div class="sticky-post sticky-post--right">…</div>
163
+ </div>
164
+ Two columns: the avatar, then everything else — so the notes start under the NAME. --right swaps the columns. */
165
+ .sticky-thread { display: grid; gap: var(--sticky-thread-gap, var(--sp-6)); --_thread-gap: var(--sticky-thread-gap, var(--sp-6)); } /* --_thread-gap: the same value, for the rail to run through */
166
+ .sticky-post {
167
+ display: grid;
168
+ grid-template-columns: auto minmax(0, 1fr);
169
+ align-items: center;
170
+ column-gap: var(--sticky-post-col-gap, var(--sp-3));
171
+ row-gap: var(--sticky-post-row-gap, var(--sp-2));
172
+ }
173
+ .sticky-post:not(:has(> .sticky-post__who)) { column-gap: 0; } /* nobody pictured: no empty column's gap */
174
+ .sticky-post__who { grid-area: 1 / 1; display: grid; }
175
+ .sticky-post__head { grid-area: 1 / 2; display: flex; align-items: baseline; gap: var(--sticky-post-head-gap, var(--sp-2)); min-width: 0; }
176
+ .sticky-post__name { min-width: 0; font: var(--sticky-post-name-font, var(--type-label)); overflow-wrap: anywhere; }
177
+ .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; }
178
+ .sticky-post__notes { grid-area: 2 / 2; display: grid; justify-items: start; gap: var(--sticky-post-notes-gap, var(--sp-3)); min-width: 0; }
179
+ .sticky-post__notes > .sticky { max-width: var(--sticky-post-max-w, 85%); } /* a note leaves the far side open, as a bubble does */
180
+ .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 */
181
+ .sticky-thread--rail > .sticky-post:not(:last-child) > .sticky-post__rail { display: block; }
182
+ .sticky-post--right { grid-template-columns: minmax(0, 1fr) auto; }
183
+ .sticky-post--right > .sticky-post__who { grid-column: 2; }
184
+ .sticky-post--right > .sticky-post__head { grid-column: 1; flex-direction: row-reverse; }
185
+ .sticky-post--right > .sticky-post__head > .sticky-post__time { margin-inline: 0 auto; }
186
+ .sticky-post--right > .sticky-post__notes { grid-column: 1; justify-items: end; }
187
+ .sticky-post--right > .sticky-post__rail { grid-column: 2; }
package/dist/cardds.css CHANGED
@@ -3597,6 +3597,195 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3597
3597
  .bullets__pic.ph { border-radius: var(--bullets-pic-radius, 0rem); }
3598
3598
  .bullets__text { flex: 1; min-width: 0; }
3599
3599
 
3600
+ /* ---- css/sticky.css — sticky note, its field, tone chooser, thread of posts, pile ---- */
3601
+ /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, a thread of posts.
3602
+ A note is its own paper: it brings its colour and its ink, whatever card or palette it lies on. */
3603
+
3604
+ /* ---- sticky: a paper note — a flat pastel block, straight, the bottom-right corner folded over ----
3605
+ Straight unless asked (--tilt-left / --tilt-right: the one note of a start screen), never patterned. The corner is CUT on the diagonal and a flap of a darker shade lies over it.
3606
+ <div class="sticky sticky--custard">
3607
+ <div class="sticky__paper">
3608
+ <div class="sticky__head">12 Oct</div><div class="sticky__body">…</div><div class="sticky__foot">From Noi</div>
3609
+ <span class="sticky__fold"></span>
3610
+ </div>
3611
+ </div>
3612
+ Two boxes on purpose: the OUTER one holds the tone and the shadow, the PAPER is the one that is cut — a clip-path
3613
+ on the outer box would cut its own shadow off. The outer box's ground is kept out of the cut corner by painting
3614
+ only its content box, which stops one fold short of the right and bottom edges; the paper's negative margins run
3615
+ back out to the full size. The paper takes the tone by `inherit`. */
3616
+ .sticky {
3617
+ color: var(--sticky-ink, #141414);
3618
+ --_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) */
3619
+ 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 */
3620
+ --_fold: var(--sticky-fold, var(--sp-6));
3621
+ --_min: calc(var(--tap) * 3);
3622
+ --_cap: calc(var(--tap) * 7);
3623
+ position: relative;
3624
+ display: grid;
3625
+ box-sizing: border-box;
3626
+ width: fit-content; /* hugs its words… */
3627
+ min-width: var(--sticky-min-w, var(--_min)); /* …but a word alone is still a note */
3628
+ max-width: var(--sticky-max-w, 100%);
3629
+ padding: 0 var(--_fold) var(--_fold) 0; /* = the line above, written whole */
3630
+ background-clip: content-box;
3631
+ filter: drop-shadow(var(--sticky-drop, 0 var(--sp-1) var(--sp-2) rgb(0 0 0 / 0.16))); /* drop-shadow follows the cut; a box-shadow would draw the square */
3632
+ }
3633
+ /* the tones — the names in src/sticky/tones.ts. A swatch of the chooser wears the same class. */
3634
+ .sticky--cotton { background-color: var(--sticky-cotton-bg, #f3efe2); }
3635
+ .sticky--blush { background-color: var(--sticky-blush-bg, #f6d5da); }
3636
+ .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, #d5e8c5); }
3637
+ .sticky--custard { background-color: var(--sticky-custard-bg, #f6e9a0); }
3638
+ .sticky--salmon { background-color: var(--sticky-salmon-bg, #f3c7b3); }
3639
+ .sticky--iceberg { background-color: var(--sticky-iceberg-bg, #cfe0ee); }
3640
+ .sticky--cocoa { background-color: var(--sticky-cocoa-bg, #dccbbd); }
3641
+ .sticky--apricot { background-color: var(--sticky-apricot-bg, #f6d49a); }
3642
+ .sticky--water { background-color: var(--sticky-water-bg, #bcd6f3); }
3643
+
3644
+ .sticky__paper {
3645
+ position: relative;
3646
+ display: flex;
3647
+ flex-direction: column;
3648
+ gap: var(--sticky-gap, var(--sp-3));
3649
+ box-sizing: border-box;
3650
+ min-width: 0;
3651
+ margin: 0 calc(-1 * var(--_fold)) calc(-1 * var(--_fold)) 0;
3652
+ padding: var(--sticky-pad-y, var(--sp-4)) var(--sticky-pad-x, var(--sp-4));
3653
+ border-radius: var(--sticky-radius, var(--sp-1));
3654
+ background-color: inherit;
3655
+ clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_fold)), calc(100% - var(--_fold)) 100%, 0 100%);
3656
+ }
3657
+ .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)); }
3658
+ .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
3659
+ .sticky__head:has(> .sticky-date) { align-items: start; }
3660
+ .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 */
3661
+ .sticky__body { flex: 1 1 auto; min-width: 0; font: var(--sticky-font, var(--type-title)); overflow-wrap: anywhere; }
3662
+ .sticky__foot { font: var(--sticky-foot-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); padding-inline-end: var(--_fold); } /* the line stops before the fold */
3663
+ /* 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 */
3664
+ .sticky__fold {
3665
+ position: absolute;
3666
+ right: 0;
3667
+ bottom: 0;
3668
+ width: var(--_fold);
3669
+ height: var(--_fold);
3670
+ border-top-left-radius: var(--sp-1);
3671
+ background-color: var(--sticky-fold-bg, color-mix(in srgb, currentColor 14%, transparent)); /* a darker shade of whatever the paper is */
3672
+ box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
3673
+ }
3674
+ .sticky--flat > .sticky__paper { clip-path: none; }
3675
+ .sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
3676
+
3677
+ /* --tilt: the note turned a little on its middle. Straight is the rule; this is for the one note a start screen hands over. */
3678
+ .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 */
3679
+ .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, 5deg)); }
3680
+ .sticky--tilt-right { rotate: var(--sticky-tilt, 5deg); }
3681
+
3682
+ /* ---- sticky date: the day, big, the month under it — at the end of the note's head (its top-right corner) ----
3683
+ <div class="sticky__head">…<time class="sticky-date" datetime="2026-02-14"><span class="sticky-date__day">14</span><span class="sticky-date__month">ก.พ.</span></time></div> */
3684
+ .sticky-date { display: grid; justify-items: end; margin-inline-start: auto; text-align: end; color: var(--sticky-ink, #141414); }
3685
+ .sticky-date__day { font: var(--sticky-date-day-font, var(--type-stat)); }
3686
+ .sticky-date__month { font: var(--sticky-date-month-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
3687
+
3688
+ /* --square: the note to write on — 1:1, as wide as its column up to a cap, in the middle */
3689
+ .sticky--square { width: 100%; max-width: var(--sticky-square-max-w, var(--_cap)); aspect-ratio: 1; margin-inline: auto; }
3690
+
3691
+ /* --corner: a control stands ON the bottom-right corner, half outside. The paper is notched round it (a mask, so the
3692
+ shadow follows the notch and no ground colour is needed); the note has no fold. */
3693
+ .sticky__corner {
3694
+ position: absolute;
3695
+ right: 0;
3696
+ bottom: 0;
3697
+ translate: 50% 50%;
3698
+ display: grid;
3699
+ place-items: center;
3700
+ width: var(--sticky-corner-size, var(--tap-sm));
3701
+ aspect-ratio: 1;
3702
+ }
3703
+ /* the half of the control that stands outside is the note's own room: it never runs past the column, nor onto what follows */
3704
+ .sticky--corner {
3705
+ --_corner: var(--sticky-corner-size, var(--tap-sm)); /* the same default as .sticky__corner above: the paper's notch needs it too */
3706
+ max-width: calc(var(--sticky-max-w, 100%) - var(--_corner) / 2);
3707
+ margin: 0 calc(var(--_corner) / 2) calc(var(--_corner) / 2) 0;
3708
+ }
3709
+ .sticky--corner > .sticky__paper {
3710
+ --_notch: calc(var(--_corner) / 2 + var(--sp-1));
3711
+ clip-path: none;
3712
+ -webkit-mask: radial-gradient(circle at 100% 100%, transparent calc(var(--_notch) - var(--border-w)), #000 var(--_notch));
3713
+ mask: radial-gradient(circle at 100% 100%, transparent calc(var(--_notch) - var(--border-w)), #000 var(--_notch));
3714
+ }
3715
+
3716
+ /* ---- sticky field: the words written ON a note — a bare textarea in the note's type and ink, filling it ----
3717
+ <div class="sticky__body"><textarea class="sticky-field" aria-label="Note" placeholder="Add text to this note"></textarea></div> */
3718
+ .sticky__body:has(> .sticky-field) { display: grid; }
3719
+ .sticky-field {
3720
+ display: block;
3721
+ box-sizing: border-box;
3722
+ width: 100%;
3723
+ min-height: var(--sticky-field-min-h, var(--_min));
3724
+ resize: none;
3725
+ border: 0;
3726
+ margin: 0;
3727
+ padding: 0;
3728
+ background: transparent;
3729
+ color: inherit;
3730
+ font: inherit;
3731
+ outline: none; /* the caret is the focus mark, as in the composer */
3732
+ }
3733
+ .sticky-field::placeholder { color: var(--sticky-muted-ink, var(--_muted)); opacity: 1; }
3734
+
3735
+ /* ---- sticky pick: the tone chooser — round swatches at tap size, the chosen one ringed ----
3736
+ <div class="sticky-pick" role="radiogroup" aria-label="Note colour">
3737
+ <label class="sticky-pick__swatch sticky--blush"><input type="radio" name="…" aria-label="blush"></label>…
3738
+ </div> */
3739
+ .sticky-pick { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sticky-pick-gap, var(--sp-3)); }
3740
+ .sticky-pick__swatch {
3741
+ position: relative;
3742
+ flex: none;
3743
+ width: var(--sticky-pick-size, var(--tap));
3744
+ aspect-ratio: 1;
3745
+ border-radius: var(--r-chip);
3746
+ color: var(--sticky-pick-on-ink, var(--on-surface)); /* the ring's colour */
3747
+ box-shadow: inset 0 0 0 var(--border-w) var(--ink-tint); /* a pale tone still has an edge on a pale ground */
3748
+ cursor: pointer;
3749
+ }
3750
+ .sticky-pick__swatch > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
3751
+ .sticky-pick__swatch:has(> :checked) { outline: var(--rule-w) solid currentColor; outline-offset: var(--sp-0); }
3752
+ .sticky-pick__swatch:has(> :focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
3753
+
3754
+ /* ---- sticky thread: a chat or a feed told in notes ----
3755
+ <div class="sticky-thread sticky-thread--rail">
3756
+ <div class="sticky-post">
3757
+ <span class="sticky-post__who"><span class="avatar">…</span></span>
3758
+ <div class="sticky-post__head"><span class="sticky-post__name">Noi</span><span class="sticky-post__time">8:15</span></div>
3759
+ <span class="sticky-post__rail"></span>
3760
+ <div class="sticky-post__notes"><div class="sticky">…</div></div>
3761
+ </div>
3762
+ <div class="sticky-post sticky-post--right">…</div>
3763
+ </div>
3764
+ Two columns: the avatar, then everything else — so the notes start under the NAME. --right swaps the columns. */
3765
+ .sticky-thread { display: grid; gap: var(--sticky-thread-gap, var(--sp-6)); --_thread-gap: var(--sticky-thread-gap, var(--sp-6)); } /* --_thread-gap: the same value, for the rail to run through */
3766
+ .sticky-post {
3767
+ display: grid;
3768
+ grid-template-columns: auto minmax(0, 1fr);
3769
+ align-items: center;
3770
+ column-gap: var(--sticky-post-col-gap, var(--sp-3));
3771
+ row-gap: var(--sticky-post-row-gap, var(--sp-2));
3772
+ }
3773
+ .sticky-post:not(:has(> .sticky-post__who)) { column-gap: 0; } /* nobody pictured: no empty column's gap */
3774
+ .sticky-post__who { grid-area: 1 / 1; display: grid; }
3775
+ .sticky-post__head { grid-area: 1 / 2; display: flex; align-items: baseline; gap: var(--sticky-post-head-gap, var(--sp-2)); min-width: 0; }
3776
+ .sticky-post__name { min-width: 0; font: var(--sticky-post-name-font, var(--type-label)); overflow-wrap: anywhere; }
3777
+ .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; }
3778
+ .sticky-post__notes { grid-area: 2 / 2; display: grid; justify-items: start; gap: var(--sticky-post-notes-gap, var(--sp-3)); min-width: 0; }
3779
+ .sticky-post__notes > .sticky { max-width: var(--sticky-post-max-w, 85%); } /* a note leaves the far side open, as a bubble does */
3780
+ .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 */
3781
+ .sticky-thread--rail > .sticky-post:not(:last-child) > .sticky-post__rail { display: block; }
3782
+ .sticky-post--right { grid-template-columns: minmax(0, 1fr) auto; }
3783
+ .sticky-post--right > .sticky-post__who { grid-column: 2; }
3784
+ .sticky-post--right > .sticky-post__head { grid-column: 1; flex-direction: row-reverse; }
3785
+ .sticky-post--right > .sticky-post__head > .sticky-post__time { margin-inline: 0 auto; }
3786
+ .sticky-post--right > .sticky-post__notes { grid-column: 1; justify-items: end; }
3787
+ .sticky-post--right > .sticky-post__rail { grid-column: 2; }
3788
+
3600
3789
  /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
3601
3790
  /* ============================================================
3602
3791
  cardds/media.css — media & display inside cards:
@@ -4161,11 +4350,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4161
4350
  content: "";
4162
4351
  position: absolute;
4163
4352
  left: 50%;
4164
- bottom: calc(-1 * var(--sp-2));
4353
+ bottom: 0;
4165
4354
  width: var(--sp-3);
4166
4355
  height: var(--sp-3);
4167
- translate: -50% 0;
4356
+ /* centred on the card's padding edge and cut to its lower half, so the tail's
4357
+ strokes end flush on the card's border instead of poking past it */
4358
+ translate: -50% 50%;
4168
4359
  rotate: 45deg;
4360
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
4169
4361
  background: inherit;
4170
4362
  border: inherit;
4171
4363
  border-top: 0;
package/dist/index.d.ts CHANGED
@@ -72,6 +72,12 @@ export * from './lists/Timeline.js';
72
72
  export * from './lists/Legend.js';
73
73
  export * from './lists/Bullets.js';
74
74
  export * from './lists/Bubble.js';
75
+ export * from './sticky/tones.js';
76
+ export * from './sticky/Sticky.js';
77
+ export * from './sticky/StickyDate.js';
78
+ export * from './sticky/StickyField.js';
79
+ export * from './sticky/StickyPick.js';
80
+ export * from './sticky/StickyThread.js';
75
81
  export * from './media/Quote.js';
76
82
  export * from './media/Postcard.js';
77
83
  export * from './media/MapArea.js';
package/dist/index.js CHANGED
@@ -76,6 +76,12 @@ export * from './lists/Timeline.js';
76
76
  export * from './lists/Legend.js';
77
77
  export * from './lists/Bullets.js';
78
78
  export * from './lists/Bubble.js';
79
+ export * from './sticky/tones.js';
80
+ export * from './sticky/Sticky.js';
81
+ export * from './sticky/StickyDate.js';
82
+ export * from './sticky/StickyField.js';
83
+ export * from './sticky/StickyPick.js';
84
+ export * from './sticky/StickyThread.js';
79
85
  export * from './media/Quote.js';
80
86
  export * from './media/Postcard.js';
81
87
  export * from './media/MapArea.js';
@@ -0,0 +1,25 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ import type { StickyTone } from './tones.js';
3
+ export interface StickyProps extends ComponentProps<'div'> {
4
+ /** the paper's colour — one of STICKY_TONES (default custard) */
5
+ tone?: StickyTone;
6
+ /** no folded corner */
7
+ flat?: boolean;
8
+ /** 1:1, filling its column up to a cap (the write screen); otherwise the note hugs its words */
9
+ square?: boolean;
10
+ /** a row at the top of the note — a date, a small picture, a label */
11
+ head?: ReactNode;
12
+ /** a StickyDate at the end of the head — the note's top-right corner */
13
+ date?: ReactNode;
14
+ /** turned a little on its middle (the one note of a start screen); a note is straight otherwise */
15
+ tilt?: 'left' | 'right';
16
+ /** a small muted line at the bottom ("From …") */
17
+ foot?: ReactNode;
18
+ /** a control (an IconBtn) standing on the bottom-right corner, half outside the note; the note then has no fold */
19
+ corner?: ReactNode;
20
+ }
21
+ /**
22
+ * Sticky — a paper note: a flat pastel block, straight, its bottom-right corner folded over, a soft shadow under it.
23
+ * The outer box holds the tone and the shadow; the paper inside is the part that is cut (a clip would cut the shadow too).
24
+ */
25
+ export declare function Sticky({ tone, flat, square, head, date, tilt, foot, corner, className, children, ...rest }: StickyProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /**
4
+ * Sticky — a paper note: a flat pastel block, straight, its bottom-right corner folded over, a soft shadow under it.
5
+ * The outer box holds the tone and the shadow; the paper inside is the part that is cut (a clip would cut the shadow too).
6
+ */
7
+ export function Sticky({ tone = 'custard', flat, square, head, date, tilt, foot, corner, className, children, ...rest }) {
8
+ const notched = corner != null;
9
+ return (_jsxs("div", { className: cx('sticky', `sticky--${tone}`, flat && 'sticky--flat', square && 'sticky--square', notched && 'sticky--corner', tilt && `sticky--tilt-${tilt}`, className), ...rest, children: [_jsxs("div", { className: "sticky__paper", children: [(head != null || date != null) && _jsxs("div", { className: "sticky__head", children: [head, date] }), _jsx("div", { className: "sticky__body", children: children }), foot != null && _jsx("div", { className: "sticky__foot", children: foot }), !flat && !notched && _jsx("span", { className: "sticky__fold", "aria-hidden": "true" })] }), notched && _jsx("div", { className: "sticky__corner", children: corner })] }));
10
+ }
@@ -0,0 +1,9 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface StickyDateProps extends ComponentProps<'time'> {
3
+ /** the day of the month, shown big */
4
+ day: ReactNode;
5
+ /** the month, short, under the day — the app's own words ("ก.พ.", "Feb") */
6
+ month: ReactNode;
7
+ }
8
+ /** StickyDate — a date for a Sticky's `date`: the day big, the month under it, at the note's top-right corner. */
9
+ export declare function StickyDate({ day, month, className, ...rest }: StickyDateProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** StickyDate — a date for a Sticky's `date`: the day big, the month under it, at the note's top-right corner. */
4
+ export function StickyDate({ day, month, className, ...rest }) {
5
+ return (_jsxs("time", { className: cx('sticky-date', className), ...rest, children: [_jsx("span", { className: "sticky-date__day", children: day }), _jsx("span", { className: "sticky-date__month", children: month })] }));
6
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentProps } from 'react';
2
+ export interface StickyFieldProps extends ComponentProps<'textarea'> {
3
+ /** what is being written — the field's accessible name (it has no visible label) */
4
+ label: string;
5
+ }
6
+ /** StickyField — the words written ON a Sticky: a bare textarea in the note's own type and ink, filling the note. */
7
+ export declare function StickyField({ label, className, ...rest }: StickyFieldProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** StickyField — the words written ON a Sticky: a bare textarea in the note's own type and ink, filling the note. */
4
+ export function StickyField({ label, className, ...rest }) {
5
+ return _jsx("textarea", { className: cx('sticky-field', className), "aria-label": label, ...rest });
6
+ }
@@ -0,0 +1,14 @@
1
+ import type { ComponentProps } from 'react';
2
+ import type { StickyTone } from './tones.js';
3
+ export interface StickyPickProps extends Omit<ComponentProps<'div'>, 'onChange'> {
4
+ /** CONTROLLED: the chosen tone — with `onChange` */
5
+ value: StickyTone;
6
+ /** the tone the member tapped */
7
+ onChange?: (tone: StickyTone) => void;
8
+ /** the tones on offer, in order (default: all of STICKY_TONES) */
9
+ tones?: readonly StickyTone[];
10
+ /** the group's accessible name */
11
+ label: string;
12
+ }
13
+ /** StickyPick — the tone chooser: a row of round swatches at tap size, the chosen one ringed. Native radios: the arrows move the choice. */
14
+ export declare function StickyPick({ value, onChange, tones, label, className, ...rest }: StickyPickProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { STICKY_TONES } from './tones.js';
5
+ /** StickyPick — the tone chooser: a row of round swatches at tap size, the chosen one ringed. Native radios: the arrows move the choice. */
6
+ export function StickyPick({ value, onChange, tones = STICKY_TONES, label, className, ...rest }) {
7
+ const name = useId();
8
+ return (_jsx("div", { className: cx('sticky-pick', className), role: "radiogroup", "aria-label": label, ...rest, children: tones.map((tone) => (_jsx("label", { className: cx('sticky-pick__swatch', `sticky--${tone}`), children: _jsx("input", { type: "radio", name: name, value: tone, "aria-label": tone, checked: tone === value, onChange: () => onChange?.(tone) }) }, tone))) }));
9
+ }
@@ -0,0 +1,22 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface StickyThreadProps extends ComponentProps<'div'> {
3
+ /** a dotted line running down under the avatars, post to post (a feed) */
4
+ rail?: boolean;
5
+ }
6
+ /** StickyThread — a stack of StickyPosts: a chat or a feed told in notes. */
7
+ export declare function StickyThread({ rail, className, ...rest }: StickyThreadProps): import("react").JSX.Element;
8
+ export interface StickyPostProps extends ComponentProps<'div'> {
9
+ /** who wrote it (an Avatar) */
10
+ who?: ReactNode;
11
+ /** their name, beside the avatar */
12
+ name?: ReactNode;
13
+ /** when, at the far end of the head row */
14
+ time?: ReactNode;
15
+ /** whose: theirs on the left (default), mine on the right */
16
+ side?: 'left' | 'right';
17
+ }
18
+ /**
19
+ * StickyPost — one person's turn: a head row (avatar · name · time at the far end) and, under the name, their Sticky notes.
20
+ * `side="right"` mirrors it. The rail mark is always there; only a `StickyThread rail` draws it.
21
+ */
22
+ export declare function StickyPost({ who, name, time, side, className, children, ...rest }: StickyPostProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** StickyThread — a stack of StickyPosts: a chat or a feed told in notes. */
4
+ export function StickyThread({ rail, className, ...rest }) {
5
+ return _jsx("div", { className: cx('sticky-thread', rail && 'sticky-thread--rail', className), ...rest });
6
+ }
7
+ /**
8
+ * StickyPost — one person's turn: a head row (avatar · name · time at the far end) and, under the name, their Sticky notes.
9
+ * `side="right"` mirrors it. The rail mark is always there; only a `StickyThread rail` draws it.
10
+ */
11
+ export function StickyPost({ who, name, time, side = 'left', className, children, ...rest }) {
12
+ return (_jsxs("div", { className: cx('sticky-post', side === 'right' && 'sticky-post--right', className), ...rest, children: [who != null && _jsx("span", { className: "sticky-post__who", children: who }), _jsxs("div", { className: "sticky-post__head", children: [name != null && _jsx("span", { className: "sticky-post__name", children: name }), time != null && _jsx("span", { className: "sticky-post__time", children: time })] }), _jsx("span", { className: "sticky-post__rail", "aria-hidden": "true" }), _jsx("div", { className: "sticky-post__notes", children: children })] }));
13
+ }
@@ -0,0 +1,2 @@
1
+ export declare const STICKY_TONES: readonly ["cotton", "blush", "tea-leaf", "custard", "salmon", "iceberg", "cocoa", "apricot", "water"];
2
+ export type StickyTone = (typeof STICKY_TONES)[number];
@@ -0,0 +1,2 @@
1
+ /* the sticky notes' stock tones — the one list. css/sticky.css has a `.sticky--<tone>` class per name; nothing else knows a tone. */
2
+ export const STICKY_TONES = ['cotton', 'blush', 'tea-leaf', 'custard', 'salmon', 'iceberg', 'cocoa', 'apricot', 'water'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.63",
3
+ "version": "0.7.64",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",