@usableapp/cardds 0.7.103 → 0.7.104
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 +27 -0
- package/css/actions.css +102 -36
- package/css/base.css +139 -15
- package/css/card.css +308 -79
- package/css/choice.css +25 -33
- package/css/finish.css +28 -27
- package/css/forms.css +6 -14
- package/css/journey.css +4 -4
- package/css/layover.css +26 -24
- package/css/lists.css +8 -6
- package/css/media.css +13 -74
- package/css/numbers.css +37 -32
- package/css/palette.css +1 -1
- package/css/people.css +3 -8
- package/css/post.css +23 -19
- package/css/sheet.css +84 -56
- package/css/stack.css +39 -215
- package/css/stamp.css +41 -29
- package/css/step.css +1 -107
- package/css/sticky.css +43 -41
- package/css/theme.template.css +12 -0
- package/css/tokens.css +100 -26
- package/dist/actions/Chip.d.ts +1 -1
- package/dist/actions/Chip.js +3 -2
- package/dist/cardds.css +1053 -868
- package/dist/cards/Card.d.ts +3 -3
- package/dist/cards/Card.js +1 -1
- package/dist/cards/CardFeed.js +7 -9
- package/dist/catalog.json +1 -1
- package/dist/layover/Snackbar.js +2 -2
- package/dist/lists/Bubble.d.ts +1 -1
- package/dist/lists/Bubble.js +1 -1
- package/dist/lists/Row.js +5 -7
- package/dist/numbers/Picker.js +11 -18
- package/dist/people/Avatar.d.ts +1 -1
- package/dist/people/Avatar.js +3 -2
- package/dist/post/PostDesk.js +6 -9
- package/dist/post/PostLetter.js +4 -4
- package/dist/pressable.d.ts +9 -0
- package/dist/pressable.js +6 -0
- package/dist/room.d.ts +5 -0
- package/dist/room.js +9 -0
- package/dist/scaffold/Panes.js +8 -14
- package/dist/sheets/Sheet.d.ts +1 -1
- package/dist/sheets/Sheet.js +2 -3
- package/dist/sheets/SheetSteps.js +3 -3
- package/dist/sheets/StackSheet.d.ts +1 -1
- package/dist/sheets/sheetStepsPile.js +2 -3
- package/dist/sheets/stackCard.js +67 -115
- package/dist/state.d.ts +21 -0
- package/dist/state.js +28 -1
- package/dist/sticky/Sticky.js +3 -7
- package/dist/templates/Home.d.ts +1 -1
- package/dist/templates/Splash.js +2 -2
- package/dist/templates/Walkthrough.js +4 -5
- package/docs/reference.md +71 -9
- package/lib/catalog.mjs +23 -7
- package/lint/rules.mjs +7 -5
- package/lint/values.mjs +144 -0
- package/package.json +6 -4
package/dist/sheets/stackCard.js
CHANGED
|
@@ -71,6 +71,67 @@ const armSwallow = (swallow) => {
|
|
|
71
71
|
swallow.current = true;
|
|
72
72
|
requestAnimationFrame(() => { swallow.current = false; });
|
|
73
73
|
};
|
|
74
|
+
/* the finger let go short of the commit (or was cancelled): the card eases back from the frame just read */
|
|
75
|
+
const releaseCard = (card, live, prop) => {
|
|
76
|
+
live.current = null;
|
|
77
|
+
card.classList.remove('is-dragging');
|
|
78
|
+
readFrame(card);
|
|
79
|
+
card.style.removeProperty(prop);
|
|
80
|
+
};
|
|
81
|
+
/* a finger on a head, the open sheet's (down, `--_pull`, folds it) or a peeked one's (up, `--_lift`, opens it): the card follows it up to
|
|
82
|
+
`cap`; let go past the commit, `settle` asks the app, and a card the app left where it was (`unmoved`) eases back */
|
|
83
|
+
function pullHead(e, { card, head, doc, held, live, swallow, way, prop, cap, settle, unmoved }) {
|
|
84
|
+
const slop = pxFromRem(SLOP_REM);
|
|
85
|
+
const commit = pxFromRem(COMMIT_REM);
|
|
86
|
+
const pull = { pointerId: e.pointerId, startY: e.clientY, last: 0, cap: Math.max(cap, commit), commit, slop, moved: false };
|
|
87
|
+
held.current = pull;
|
|
88
|
+
const onMove = (ev) => {
|
|
89
|
+
if (ev.pointerId !== pull.pointerId || held.current !== pull)
|
|
90
|
+
return;
|
|
91
|
+
const d = along(way, pull.startY, ev.clientY);
|
|
92
|
+
pull.last = d;
|
|
93
|
+
if (d < pull.slop)
|
|
94
|
+
return;
|
|
95
|
+
pull.moved = true;
|
|
96
|
+
ev.preventDefault();
|
|
97
|
+
try {
|
|
98
|
+
if (!head.hasPointerCapture(ev.pointerId))
|
|
99
|
+
head.setPointerCapture(ev.pointerId);
|
|
100
|
+
}
|
|
101
|
+
catch { /* the pointer is already gone */ }
|
|
102
|
+
live.current = { prop, px: `${Math.min(d, pull.cap)}px` };
|
|
103
|
+
card.classList.add('is-dragging');
|
|
104
|
+
card.style.setProperty(prop, live.current.px);
|
|
105
|
+
};
|
|
106
|
+
const onUp = (ev) => {
|
|
107
|
+
if (ev.pointerId !== pull.pointerId || held.current !== pull)
|
|
108
|
+
return;
|
|
109
|
+
const d = Math.max(pull.last, along(way, pull.startY, ev.clientY));
|
|
110
|
+
const moved = pull.moved || d >= pull.slop;
|
|
111
|
+
held.current = null;
|
|
112
|
+
doc.stop();
|
|
113
|
+
if (moved && d >= pull.commit) {
|
|
114
|
+
card.classList.remove('is-dragging');
|
|
115
|
+
readFrame(card);
|
|
116
|
+
live.current = null;
|
|
117
|
+
flushSync(settle);
|
|
118
|
+
if (card.isConnected && unmoved() && card.style.getPropertyValue(prop))
|
|
119
|
+
releaseCard(card, live, prop);
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
releaseCard(card, live, prop);
|
|
123
|
+
if (moved)
|
|
124
|
+
armSwallow(swallow);
|
|
125
|
+
};
|
|
126
|
+
const onCancel = (ev) => {
|
|
127
|
+
if (ev.pointerId !== pull.pointerId || held.current !== pull)
|
|
128
|
+
return;
|
|
129
|
+
held.current = null;
|
|
130
|
+
doc.stop();
|
|
131
|
+
releaseCard(card, live, prop);
|
|
132
|
+
};
|
|
133
|
+
doc.listen(onMove, onUp, onCancel);
|
|
134
|
+
}
|
|
74
135
|
/**
|
|
75
136
|
* The card's own pull, refresh and focus. Listeners sit on this card's head and body.
|
|
76
137
|
* `--_pull` / `--_lift` are written while a finger is down, the same way a single Sheet writes `--_drag`.
|
|
@@ -113,68 +174,14 @@ export function useStackCard(ref, place, swallow) {
|
|
|
113
174
|
const scroller = card.querySelector(':scope > .sheet__body') ?? card;
|
|
114
175
|
const doc = docPointer();
|
|
115
176
|
const primary = (e) => e.isPrimary && e.button === 0;
|
|
116
|
-
const release = (prop) => {
|
|
117
|
-
live.current = null;
|
|
118
|
-
card.classList.remove('is-dragging');
|
|
119
|
-
readFrame(card);
|
|
120
|
-
card.style.removeProperty(prop);
|
|
121
|
-
};
|
|
122
177
|
const onHeadDown = (e) => {
|
|
123
178
|
e.stopPropagation();
|
|
124
179
|
if (!head || !stack || !primary(e) || headPull.current || bodyPull.current)
|
|
125
180
|
return;
|
|
126
181
|
if (!pullsHead(e.target, head))
|
|
127
182
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const pull = { pointerId: e.pointerId, startY: e.clientY, last: 0, cap: Math.max(restPullPx(card, stack), commit), commit, slop, moved: false };
|
|
131
|
-
headPull.current = pull;
|
|
132
|
-
const onMove = (ev) => {
|
|
133
|
-
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
134
|
-
return;
|
|
135
|
-
const dy = along('down', pull.startY, ev.clientY);
|
|
136
|
-
pull.last = dy;
|
|
137
|
-
if (dy < pull.slop)
|
|
138
|
-
return;
|
|
139
|
-
pull.moved = true;
|
|
140
|
-
ev.preventDefault();
|
|
141
|
-
try {
|
|
142
|
-
if (head && !head.hasPointerCapture(ev.pointerId))
|
|
143
|
-
head.setPointerCapture(ev.pointerId);
|
|
144
|
-
}
|
|
145
|
-
catch { /* the pointer is already gone */ }
|
|
146
|
-
live.current = { prop: '--_pull', px: `${Math.min(dy, pull.cap)}px` };
|
|
147
|
-
card.classList.add('is-dragging');
|
|
148
|
-
card.style.setProperty('--_pull', live.current.px);
|
|
149
|
-
};
|
|
150
|
-
const onUp = (ev) => {
|
|
151
|
-
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
152
|
-
return;
|
|
153
|
-
const dy = Math.max(pull.last, along('down', pull.startY, ev.clientY));
|
|
154
|
-
const moved = pull.moved || dy >= pull.slop;
|
|
155
|
-
headPull.current = null;
|
|
156
|
-
doc.stop();
|
|
157
|
-
if (moved && dy >= pull.commit) {
|
|
158
|
-
card.classList.remove('is-dragging');
|
|
159
|
-
readFrame(card);
|
|
160
|
-
live.current = null;
|
|
161
|
-
flushSync(() => placeRef.current?.onClose());
|
|
162
|
-
if (card.isConnected && card.classList.contains('is-open') && card.style.getPropertyValue('--_pull'))
|
|
163
|
-
release('--_pull');
|
|
164
|
-
}
|
|
165
|
-
else
|
|
166
|
-
release('--_pull');
|
|
167
|
-
if (moved)
|
|
168
|
-
armSwallow(swallow);
|
|
169
|
-
};
|
|
170
|
-
const onCancel = (ev) => {
|
|
171
|
-
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
172
|
-
return;
|
|
173
|
-
headPull.current = null;
|
|
174
|
-
doc.stop();
|
|
175
|
-
release('--_pull');
|
|
176
|
-
};
|
|
177
|
-
doc.listen(onMove, onUp, onCancel);
|
|
183
|
+
pullHead(e, { card, head, doc, held: headPull, live, swallow, way: 'down', prop: '--_pull', cap: restPullPx(card, stack),
|
|
184
|
+
settle: () => placeRef.current?.onClose(), unmoved: () => card.classList.contains('is-open') });
|
|
178
185
|
};
|
|
179
186
|
const onBodyDown = (e) => {
|
|
180
187
|
const ask = placeRef.current;
|
|
@@ -279,7 +286,7 @@ export function useStackCard(ref, place, swallow) {
|
|
|
279
286
|
scroller.style.removeProperty('--_refresh');
|
|
280
287
|
setPulling(false);
|
|
281
288
|
if (live.current?.prop === '--_pull')
|
|
282
|
-
|
|
289
|
+
releaseCard(card, live, '--_pull');
|
|
283
290
|
head?.removeEventListener('pointerdown', onHeadDown);
|
|
284
291
|
scroller.removeEventListener('pointerdown', onBodyDown);
|
|
285
292
|
};
|
|
@@ -295,12 +302,6 @@ export function useStackCard(ref, place, swallow) {
|
|
|
295
302
|
if (!head || !stack)
|
|
296
303
|
return;
|
|
297
304
|
const doc = docPointer();
|
|
298
|
-
const release = () => {
|
|
299
|
-
live.current = null;
|
|
300
|
-
card.classList.remove('is-dragging');
|
|
301
|
-
readFrame(card);
|
|
302
|
-
card.style.removeProperty('--_lift');
|
|
303
|
-
};
|
|
304
305
|
const onDown = (e) => {
|
|
305
306
|
if (!e.isPrimary || e.button !== 0 || headPull.current)
|
|
306
307
|
return;
|
|
@@ -329,64 +330,15 @@ export function useStackCard(ref, place, swallow) {
|
|
|
329
330
|
return;
|
|
330
331
|
}
|
|
331
332
|
e.stopPropagation();
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const travel = card.getBoundingClientRect().top - stack.getBoundingClientRect().top;
|
|
335
|
-
const pull = { pointerId: e.pointerId, startY: e.clientY, last: 0, cap: Math.max(travel, commit), commit, slop, moved: false };
|
|
336
|
-
headPull.current = pull;
|
|
337
|
-
const onMove = (ev) => {
|
|
338
|
-
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
339
|
-
return;
|
|
340
|
-
const upPx = along('up', pull.startY, ev.clientY);
|
|
341
|
-
pull.last = upPx;
|
|
342
|
-
if (upPx < pull.slop)
|
|
343
|
-
return;
|
|
344
|
-
pull.moved = true;
|
|
345
|
-
ev.preventDefault();
|
|
346
|
-
try {
|
|
347
|
-
if (!head.hasPointerCapture(ev.pointerId))
|
|
348
|
-
head.setPointerCapture(ev.pointerId);
|
|
349
|
-
}
|
|
350
|
-
catch { /* the pointer is already gone */ }
|
|
351
|
-
live.current = { prop: '--_lift', px: `${Math.min(upPx, pull.cap)}px` };
|
|
352
|
-
card.classList.add('is-dragging');
|
|
353
|
-
card.style.setProperty('--_lift', live.current.px);
|
|
354
|
-
};
|
|
355
|
-
const onUp = (ev) => {
|
|
356
|
-
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
357
|
-
return;
|
|
358
|
-
const upPx = Math.max(pull.last, along('up', pull.startY, ev.clientY));
|
|
359
|
-
const moved = pull.moved || upPx >= pull.slop;
|
|
360
|
-
headPull.current = null;
|
|
361
|
-
doc.stop();
|
|
362
|
-
if (moved && upPx >= pull.commit) {
|
|
363
|
-
card.classList.remove('is-dragging');
|
|
364
|
-
readFrame(card);
|
|
365
|
-
live.current = null;
|
|
366
|
-
flushSync(() => placeRef.current?.onOpen());
|
|
367
|
-
if (card.isConnected && !card.classList.contains('is-open') && card.style.getPropertyValue('--_lift'))
|
|
368
|
-
release();
|
|
369
|
-
}
|
|
370
|
-
else
|
|
371
|
-
release();
|
|
372
|
-
if (moved)
|
|
373
|
-
armSwallow(swallow);
|
|
374
|
-
};
|
|
375
|
-
const onCancel = (ev) => {
|
|
376
|
-
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
377
|
-
return;
|
|
378
|
-
headPull.current = null;
|
|
379
|
-
doc.stop();
|
|
380
|
-
release();
|
|
381
|
-
};
|
|
382
|
-
doc.listen(onMove, onUp, onCancel);
|
|
333
|
+
pullHead(e, { card, head, doc, held: headPull, live, swallow, way: 'up', prop: '--_lift', cap: card.getBoundingClientRect().top - stack.getBoundingClientRect().top,
|
|
334
|
+
settle: () => placeRef.current?.onOpen(), unmoved: () => !card.classList.contains('is-open') });
|
|
383
335
|
};
|
|
384
336
|
head.addEventListener('pointerdown', onDown);
|
|
385
337
|
return () => {
|
|
386
338
|
doc.stop();
|
|
387
339
|
headPull.current = null;
|
|
388
340
|
if (live.current?.prop === '--_lift')
|
|
389
|
-
|
|
341
|
+
releaseCard(card, live, '--_lift');
|
|
390
342
|
head.removeEventListener('pointerdown', onDown);
|
|
391
343
|
};
|
|
392
344
|
}, [place?.open, place?.before, place?.after, ref, swallow]);
|
package/dist/state.d.ts
CHANGED
|
@@ -25,6 +25,27 @@ export declare function watchSize(targets: Array<Element | null | undefined>, fn
|
|
|
25
25
|
export declare const warnOnce: (key: string, text: string) => void;
|
|
26
26
|
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
27
27
|
export declare const reducedMotion: () => boolean;
|
|
28
|
+
/** `--motion-sheet` in ms, read from `cs` (the document's by default); 350 where it reads as none */
|
|
29
|
+
export declare const sheetMs: (cs?: CSSStyleDeclaration) => number;
|
|
30
|
+
/** cancel the moves tagged `id` on `els`, so what is read next is where they rest */
|
|
31
|
+
export declare const stopMoves: (els: HTMLElement[], id: string) => void;
|
|
32
|
+
/** a reordered element glides from `dy` px off to its place, at the `--motion-sheet` and `--ease-settle` of its list (`cs`); tagged `id` for stopMoves */
|
|
33
|
+
export declare function glide(el: HTMLElement, dy: number, cs: CSSStyleDeclaration, id: string): Animation;
|
|
34
|
+
/** how far a press travelled along `axis` from `from` to `to` — 0 when it is no swipe: shorter than `min` px, or not `ratio` times its travel across */
|
|
35
|
+
export declare function swipeOf(from: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
}, to: {
|
|
39
|
+
clientX: number;
|
|
40
|
+
clientY: number;
|
|
41
|
+
}, axis: 'x' | 'y', min: number, ratio?: number): number;
|
|
42
|
+
/** a rail that comes to rest: `settled()` on each scroll, and `then` runs `ms` after the last one; `cancel` drops the wait, and so does unmounting */
|
|
43
|
+
export declare function useSettled(then: () => void, ms?: number): {
|
|
44
|
+
settled: () => void;
|
|
45
|
+
cancel: () => void;
|
|
46
|
+
};
|
|
47
|
+
/** the bottom of what the member sees — the keyboard's top while it is up (Infinity where the browser has no visualViewport) */
|
|
48
|
+
export declare const visibleBottom: () => number;
|
|
28
49
|
/** give `box` the cursor once it has settled: after its own `translate` transition (600ms at most), or after `wait` ms. It lands on the field
|
|
29
50
|
* marked `data-autofocus`, else the first field inside `scope` (a selector under `box`; default: all of it), else `fallback`. A field in
|
|
30
51
|
* `box` that already has the cursor keeps it. Inside `[data-standby="off"]` no field is looked for: `fallback` only. Nothing moves the page (`preventScroll`). Returns the cancel — `true` when it had already
|
package/dist/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
export function useControlled(value, initial, onChange) {
|
|
3
3
|
const [own, setOwn] = useState(initial);
|
|
4
4
|
const shown = value !== undefined ? value : own;
|
|
@@ -63,6 +63,33 @@ export const warnOnce = (key, text) => { if (!warned.has(key)) {
|
|
|
63
63
|
} };
|
|
64
64
|
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
65
65
|
export const reducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
66
|
+
/** `--motion-sheet` in ms, read from `cs` (the document's by default); 350 where it reads as none */
|
|
67
|
+
export const sheetMs = (cs = getComputedStyle(document.documentElement)) => (parseFloat(cs.getPropertyValue('--motion-sheet')) || 0.35) * 1000;
|
|
68
|
+
/** cancel the moves tagged `id` on `els`, so what is read next is where they rest */
|
|
69
|
+
export const stopMoves = (els, id) => els.forEach((el) => el.getAnimations().forEach((a) => { if (a.id === id)
|
|
70
|
+
a.cancel(); }));
|
|
71
|
+
/** a reordered element glides from `dy` px off to its place, at the `--motion-sheet` and `--ease-settle` of its list (`cs`); tagged `id` for stopMoves */
|
|
72
|
+
export function glide(el, dy, cs, id) {
|
|
73
|
+
const a = el.animate([{ translate: `0 ${dy}px` }, { translate: '0 0' }], { duration: sheetMs(cs), easing: cs.getPropertyValue('--ease-settle').trim() || 'ease' });
|
|
74
|
+
a.id = id;
|
|
75
|
+
return a;
|
|
76
|
+
}
|
|
77
|
+
/** how far a press travelled along `axis` from `from` to `to` — 0 when it is no swipe: shorter than `min` px, or not `ratio` times its travel across */
|
|
78
|
+
export function swipeOf(from, to, axis, min, ratio = 0) {
|
|
79
|
+
const dx = to.clientX - from.x, dy = to.clientY - from.y;
|
|
80
|
+
const [along, across] = axis === 'x' ? [dx, dy] : [dy, dx];
|
|
81
|
+
return Math.abs(along) < min || Math.abs(along) < ratio * Math.abs(across) ? 0 : along;
|
|
82
|
+
}
|
|
83
|
+
/** a rail that comes to rest: `settled()` on each scroll, and `then` runs `ms` after the last one; `cancel` drops the wait, and so does unmounting */
|
|
84
|
+
export function useSettled(then, ms = 120) {
|
|
85
|
+
const wait = useRef(null);
|
|
86
|
+
const cancel = () => { if (wait.current)
|
|
87
|
+
clearTimeout(wait.current); };
|
|
88
|
+
useEffect(() => cancel, []);
|
|
89
|
+
return { settled: () => { cancel(); wait.current = setTimeout(then, ms); }, cancel };
|
|
90
|
+
}
|
|
91
|
+
/** the bottom of what the member sees — the keyboard's top while it is up (Infinity where the browser has no visualViewport) */
|
|
92
|
+
export const visibleBottom = () => { const vv = window.visualViewport; return vv ? vv.offsetTop + vv.height : Infinity; };
|
|
66
93
|
/* THE RULE (Lh 2026-09-29): a part of the screen that has just become ACTIVE — a sheet come up, a step opened, a stack sheet opened, a
|
|
67
94
|
modal asking — has its field waiting with the cursor. ONE mechanic for every such part; the component only says WHEN it became active.
|
|
68
95
|
THE SWITCH (Lh 2026-10-06): under `data-standby="off"` (on any box around the part) no field is given the cursor — the part's
|
package/dist/sticky/Sticky.js
CHANGED
|
@@ -2,9 +2,9 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { pressable } from '../pressable.js';
|
|
5
|
-
import { setRefs
|
|
5
|
+
import { setRefs } from '../state.js';
|
|
6
6
|
import { word } from '../words.js';
|
|
7
|
-
import {
|
|
7
|
+
import { holdRoomWhile } from '../room.js';
|
|
8
8
|
import { fitSticky } from './fit.js';
|
|
9
9
|
/**
|
|
10
10
|
* Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
|
|
@@ -31,11 +31,7 @@ export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt,
|
|
|
31
31
|
if (!note || !paper)
|
|
32
32
|
return;
|
|
33
33
|
const measure = () => setSmall(fitSticky(note) !== 'full');
|
|
34
|
-
room.
|
|
35
|
-
const body = paper.querySelector('.sticky__body');
|
|
36
|
-
const stop = watchSize([body], measure);
|
|
37
|
-
document.fonts?.ready.then(measure);
|
|
38
|
-
return () => { room.current?.stop(); room.current = null; stop(); };
|
|
34
|
+
return holdRoomWhile(room, [paper.querySelector('.sticky__body')], measure, paper, fieldOf, () => fitSticky(note) === 'over', measure);
|
|
39
35
|
}, []);
|
|
40
36
|
const notched = corner != null;
|
|
41
37
|
const { tap, Box, boxProps } = pressable(notched ? undefined : onClick, 'div', rest);
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface HomeSheet {
|
|
|
9
9
|
icon?: ReactNode;
|
|
10
10
|
/** the tone slot — see `Tone` (default: 2, 3, 1, 2, 3 by place). The head shows it */
|
|
11
11
|
tone?: Tone;
|
|
12
|
-
/**
|
|
12
|
+
/** content on the theme's paper (`--action-bg`, the ground of its fields), the head keeps `tone`. The same look as `Sheet paper` */
|
|
13
13
|
paper?: boolean;
|
|
14
14
|
/** the sheet's pages as icon tabs in its head (a count shows as a small circle over the icon) — see `StackSheet` */
|
|
15
15
|
tabs?: StackSheetTab[];
|
package/dist/templates/Splash.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { reducedMotion, useLatest } from '../state.js';
|
|
3
|
+
import { reducedMotion, sheetMs, useLatest } from '../state.js';
|
|
4
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
5
5
|
import { cx } from '../cx.js';
|
|
6
6
|
/**
|
|
@@ -24,7 +24,7 @@ export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }
|
|
|
24
24
|
if (!leaving)
|
|
25
25
|
return;
|
|
26
26
|
const still = reducedMotion();
|
|
27
|
-
const ms = still ? 0 :
|
|
27
|
+
const ms = still ? 0 : sheetMs();
|
|
28
28
|
const t = setTimeout(() => left.current?.(), ms);
|
|
29
29
|
return () => clearTimeout(t);
|
|
30
30
|
}, [leaving]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
-
import { useControlled } from '../state.js';
|
|
3
|
+
import { swipeOf, useControlled } from '../state.js';
|
|
4
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
5
5
|
import { Step } from '../sheets/Step.js';
|
|
6
6
|
import { Page } from '../scaffold/Page.js';
|
|
@@ -40,10 +40,9 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
|
|
|
40
40
|
start.current = null;
|
|
41
41
|
if (!s0)
|
|
42
42
|
return;
|
|
43
|
-
const dx = e
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
go(dx < 0 ? at + 1 : at - 1);
|
|
43
|
+
const dx = swipeOf(s0, e, 'x', 48, 1.5);
|
|
44
|
+
if (dx)
|
|
45
|
+
go(dx < 0 ? at + 1 : at - 1);
|
|
47
46
|
},
|
|
48
47
|
onPointerCancel: () => { start.current = null; },
|
|
49
48
|
};
|
package/docs/reference.md
CHANGED
|
@@ -341,9 +341,9 @@ whose markup has rules, because the sheet's box is the step itself:
|
|
|
341
341
|
padding, the same distance from the edge, so the title stays put. `refreshing`
|
|
342
342
|
(off unless the app sets it) changes the mark's colour while the app reloads
|
|
343
343
|
what the sheet shows, as a stack sheet's does; the sheet does not fetch. `paper` makes
|
|
344
|
-
the content
|
|
344
|
+
the content the theme's paper (`--action-bg`) while the head keeps the sheet's colour (`tone`, as a card).
|
|
345
345
|
The same prop on a `StackSheet`, or on a `Card` in a stack: the head keeps the
|
|
346
|
-
tone, the body and the foot are
|
|
346
|
+
tone, the body and the foot are the paper, and the gap under the head stays the tone.
|
|
347
347
|
While that sheet is closed, the corners the next sheet does not cover show the
|
|
348
348
|
card's colour. That colour fades in as the sheet returns, and is done when it stops.
|
|
349
349
|
A sheet with no head (the dim page sheet) is a body alone.
|
|
@@ -693,8 +693,8 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
|
|
|
693
693
|
narrower, so at the standard size only `name` shows, and it is larger.
|
|
694
694
|
- **`StampPlace`** — an empty place the size of a stamp, its edge a ring of round dots: a word inside, `onClick` for "make
|
|
695
695
|
yours", `faint` for a gap nobody is asked to fill. The edge is a drawn stroke, so the dots are yours: `--stamp-place-dot`
|
|
696
|
-
is how wide each is (default `--rule-w
|
|
697
|
-
(default
|
|
696
|
+
is how wide each is (default `--rule-w`), `--stamp-place-gap` the distance from one dot's centre to the next
|
|
697
|
+
(default three `--sp-0`), and a `--stamp-place-dash` above 0 stretches each dot into a round-ended dash. Its corner is
|
|
698
698
|
`--stamp-place-radius`. The dots' outer edge is the box's edge whatever their width.
|
|
699
699
|
- **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
|
|
700
700
|
the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. Each stamp stands in
|
|
@@ -719,8 +719,8 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
|
|
|
719
719
|
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
|
|
720
720
|
its top right corner for `removeFor` ms (default 3000). A press on the cross, or Delete / Backspace on the focused
|
|
721
721
|
stamp, asks, and the app takes the `StampOn` away. A stamp with `onRemove` and no `onAtChange` shows the cross on a
|
|
722
|
-
press. The cross is a 24 disc (`--stamp-on-remove-size`, `--stamp-on-remove-bg`
|
|
723
|
-
|
|
722
|
+
press. The cross is a 24 disc (`--stamp-on-remove-size`, `--stamp-on-remove-bg` `--ink-onlight`, `--stamp-on-remove-ink`
|
|
723
|
+
`--ink-ondark`) in a 44 target, centred on the corner and upright whatever the stamp's `turn`. Its name is cardds's word
|
|
724
724
|
("Remove" / "เอาออก"), or `removeLabel`. A press or a drag on a removable stamp never reaches what lies under it.
|
|
725
725
|
A stamp that is still arriving cannot be taken off yet: its cross waits until it has landed.
|
|
726
726
|
|
|
@@ -741,7 +741,7 @@ struck over its edge) proves the trip. Three components, one for each thing a me
|
|
|
741
741
|
the older `Postcard` (a member's cover card that turns over) is another component and shares nothing with these.
|
|
742
742
|
|
|
743
743
|
- **`PostLetter`**: reading one that arrived. Two upright 2:3 sheets, one in front. The picture side is `picture` (the
|
|
744
|
-
app's img or svg) in a
|
|
744
|
+
app's img or svg) in a margin of its paper (`--post-paper-bg`) with `caption` under it; the written side is the address (`kicker`, `to` lines) over
|
|
745
745
|
the message (`children`, set large, `sign` at its end), with `stamp` and `postmark` on the corner. The tab over the
|
|
746
746
|
card, or a tap on the card, turns it: the sheet in front swings to its edge and the other swings in. `face` +
|
|
747
747
|
`onFaceChange`, or `defaultFace`. `voice` is a voice note's time as the app words it; with it the picture side has a
|
|
@@ -910,6 +910,34 @@ question and hunts for its answer. Rules 11–13 serve that:
|
|
|
910
910
|
the card's ground, a note is a `Banner` (a light band, a rule above and
|
|
911
911
|
below, edge to edge), a count is words (`Text caption muted`), never a
|
|
912
912
|
chip. A chip or tile that looks pressable and is not lies.
|
|
913
|
+
15. **A component adjusts another only through its knobs.** A component that
|
|
914
|
+
holds another (a sheet holding a card, a stack holding a sheet) sets the
|
|
915
|
+
child's knobs (`.sheet { --card-pad: … }`), never the child's own
|
|
916
|
+
property (`.sheet .card__head { color: … }`). The child's file is the one
|
|
917
|
+
place its look is written, so what a story shows is what a project gets.
|
|
918
|
+
Missing the knob? Add it in the child's file. No `!important` either, but
|
|
919
|
+
`[hidden]`'s. A component belongs to the file that writes its bare class
|
|
920
|
+
(`.card` → `css/card.css`). A knob the holder sets is still the child's
|
|
921
|
+
knob: it is read, with its default, in the child's file and never
|
|
922
|
+
declared there. Where a child SITS in a holder that places it (its
|
|
923
|
+
position, inset, grid cell, layer, margins, how it scrolls or moves in
|
|
924
|
+
each of the holder's states) is not a look the holder retunes: the
|
|
925
|
+
child's own file writes it, as `.screen--fill > .action-bar { … }` in
|
|
926
|
+
`css/actions.css` and the stack's sheets in `css/card.css`. A part or a
|
|
927
|
+
variant of a block (`.card--cover`, `.screen__centre`) is written in the
|
|
928
|
+
block's file too. One kind of rule names many components and is no
|
|
929
|
+
override: a **trait**, one behaviour every control shares, kept as one
|
|
930
|
+
list so a new control joins it there. There are five, each in its own
|
|
931
|
+
file and writing only its own properties: press (cursor, touch-action,
|
|
932
|
+
the transition, scale, opacity, filter) and the focus ring (outline) in
|
|
933
|
+
`css/actions.css`; the button reset (border, background, font, colour,
|
|
934
|
+
padding), the hidden scrollbar and user-select in `css/base.css`. A trait
|
|
935
|
+
is a zero-specificity `:where()` list (the user-select list is a plain
|
|
936
|
+
list of single selectors), so a component's own rule always wins.
|
|
937
|
+
`tests/overrides.spec.js` fails on any override; `tests/overrides.json`,
|
|
938
|
+
which held the 184 there were on 2026-10-20, is empty and stays so.
|
|
939
|
+
One rule that can do neither is named, with why, in `KEPT`
|
|
940
|
+
(`tests/helpers.js`).
|
|
913
941
|
|
|
914
942
|
## A project's theme
|
|
915
943
|
|
|
@@ -1093,8 +1121,11 @@ Every length token resolves to rem — a multiplier × one of four rem bases
|
|
|
1093
1121
|
`--r-base` × 2, `--fs-h1` = `--fs-base` × 1.2; see "A project's theme") — so the
|
|
1094
1122
|
whole system scales with the root font size. `base.css` keeps it at `100%` (the browser's setting, 16px by
|
|
1095
1123
|
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
1096
|
-
column, its 48px taps and its type in proportion on 320–360 screens.
|
|
1097
|
-
|
|
1124
|
+
column, its 48px taps and its type in proportion on 320–360 screens. No
|
|
1125
|
+
length is written in px, the edges included (Lh 2026-10-20: "units are always rem"): `--border-w`,
|
|
1126
|
+
`--hairline-w`, `--rule-w`, the focus ring and one device pixel, `--px`, are rem. Two px stay, both the
|
|
1127
|
+
platform's: the initial value of a registered length (`@property`), which may not be relative, and the
|
|
1128
|
+
lengths a script measures off the page (`--kb`, a sheet's peek, a pulled sheet).
|
|
1098
1129
|
An app with its own text-size setting calls `setTextScale(percent)`, which writes
|
|
1099
1130
|
`--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default): the type and its holders (the count
|
|
1100
1131
|
badge, the inline icon) grow, the controls grow half as much (`--tap-base`), spacing and radii keep their size.
|
|
@@ -1109,6 +1140,37 @@ so no component ever writes `px` to consume it (`.action-bar` rides up by it,
|
|
|
1109
1140
|
`.step` grows its room by it). The narrow-phone step is an `em` breakpoint,
|
|
1110
1141
|
so it follows the text size too: at 131% a 390px phone counts as narrow.
|
|
1111
1142
|
|
|
1143
|
+
## A component brings no value of its own
|
|
1144
|
+
|
|
1145
|
+
The gallery is a catalog of components, not of designs: what a story shows must be what a project gets when
|
|
1146
|
+
it imports the piece (Lh 2026-10-20: "a component that slips out of the tokens or the theme is a problem,
|
|
1147
|
+
because that is how a hard-coded look gets made that cannot be taken out and used"). So a component's css
|
|
1148
|
+
reads every value from a token in `css/tokens.css` (or a palette, a finish, a project's theme), and writes none:
|
|
1149
|
+
|
|
1150
|
+
- **colour**: the roles, the inks of each side, the signals, and the nine pastels of a note or a stamp
|
|
1151
|
+
(`--pastel-*`, `--stamp-paper`). A paper sheet's content is the theme's paper, `--action-bg`, the ground of
|
|
1152
|
+
its fields; never a fixed white. A shadow is mixed from the onlight ink (`--shadow-paper`, `--shadow-lift`,
|
|
1153
|
+
`--shadow-fold`), never from black.
|
|
1154
|
+
- **length**: a multiple of a base, in rem (above).
|
|
1155
|
+
- **time**: `--motion-*`. **easing**: `--ease`, `--ease-in`, `--ease-out`, `--ease-in-out`, `--ease-linear`,
|
|
1156
|
+
`--ease-settle` for a row or card that lands where it moved, and `--ease-step` for on or off with nothing between (a caret).
|
|
1157
|
+
- **angle**: a multiple of `--tilt-base` (1deg): a note, a stamp, a letter, a card sent away. A theme that wants
|
|
1158
|
+
everything straight sets it to 0deg. A quarter or half turn is an orientation, not a tilt (an arrow, a face turned over).
|
|
1159
|
+
- **layer**: `--z-under-2` … `--z-over-3`, the order of things inside one piece; `--z-pile`, a sticky pile's top note.
|
|
1160
|
+
- **scale**: `--press-scale` under the finger, `--scale-lift` (a stamp held), `--scale-pick` (a mood's dot picked), `--scale-arrive` (where a stamp's remove button grows from).
|
|
1161
|
+
- **fade**: `--off` (a disabled control) and `--fade-*`, one name per use.
|
|
1162
|
+
- **type**: the eight kinds (display, h1, h2, title, body, label, caption, stat) each have `--face-<kind>`, `--fw-<kind>`,
|
|
1163
|
+
`--fs-<kind>`, `--lh-<kind>` and `--track-<kind>`, and `--type-<kind>` is made of them. A piece set on a size outside the
|
|
1164
|
+
kinds composes its `font:` from the scales: `--fs-*`, weight `--fw-regular` … `--fw-black`, leading `--lh-mark` … `--lh-ruled`.
|
|
1165
|
+
Gallery `Text · Kinds` shows them resolved.
|
|
1166
|
+
|
|
1167
|
+
A knob's default is one of these too, so a knob read from nowhere still shows the theme. `npm run check` reads
|
|
1168
|
+
every declaration of every component file and every literal of an inline `style` in `src/` and `.design-sync/previews/`
|
|
1169
|
+
(`lint/values.mjs`, before the tests run) and fails on a value written in place, naming the file, the line, the kind and
|
|
1170
|
+
the value: a knob's fallback (`z-index: var(--x, 5)`), a number inside `calc()` in a leading or a tracking, `scale` and
|
|
1171
|
+
`scale()`, `filter: opacity()`, a fade in %, a unit in capitals and every CSS colour name included; a number on a length
|
|
1172
|
+
in a `style` object is px. A sign or an identity (`* -1`, `0`, `1`, `100%`) is not a value. `tests/tokens.spec.js` holds it at none.
|
|
1173
|
+
|
|
1112
1174
|
## Icons & touch targets
|
|
1113
1175
|
|
|
1114
1176
|
Icons come from `icons.svg` (Lucide, ISC — monochrome stroke, currentColor):
|
package/lib/catalog.mjs
CHANGED
|
@@ -83,10 +83,20 @@ export function get(cat, name, { html = false, all = false } = {}) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const words = (text) => text.replace(/([a-z\d])([A-Z])/g, '$1 $2').toLowerCase().match(/[\p{L}\p{M}\p{N}]+/gu) ?? [];
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// the words of a request that carry no intention: "the user switches something on or off" is asked by "switches" and "off", and "on", "user", "something" matched every row that says them
|
|
87
|
+
const STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'of', 'to', 'in', 'on', 'an', 'a', 'at', 'by', 'as', 'is', 'are', 'be', 'it', 'its', 'can', 'my', 'your', 'their', 'some', 'user', 'users', 'member', 'someone', 'something']);
|
|
88
|
+
// a request says a value of the roles' closed vocabulary (docs/roles.md) in its own words: "choose" is the job pick, "date" represents time, "read" is show.
|
|
89
|
+
// Such a word also matches every component whose role has the value; "or" ("a or b", "on or off") is a choice, and matches nothing but the role
|
|
90
|
+
const SAYS = {
|
|
91
|
+
pick: ['choose', 'pick', 'select', 'switch', 'tick', 'or'], write: ['fill', 'type', 'write', 'enter'], show: ['read', 'see', 'show'], act: ['press', 'tap', 'send'], navigate: ['go', 'navigate'],
|
|
92
|
+
number: ['number', 'count', 'amount', 'total'], time: ['date', 'day', 'time', 'when'], place: ['place', 'where'], person: ['person', 'who'], glance: ['glance'],
|
|
93
|
+
};
|
|
94
|
+
const ROLE_ONLY = new Set(['or']);
|
|
95
|
+
const tokens = (query) => [...new Set(words(String(query)).filter((t) => (t.length > 1 || ROLE_ONLY.has(t)) && !STOP.has(t)))];
|
|
96
|
+
const stem = (t) => (/(?:ch|sh|x|ss)es$/.test(t) ? t.slice(0, -2) : t.length > 3 && t.endsWith('s') ? t.slice(0, -1) : t);
|
|
97
|
+
const hits = (text, token) => !ROLE_ONLY.has(token) && words(text).some((w) => w.startsWith(token) || w.startsWith(stem(token)));
|
|
98
|
+
/** the role's values (what it represents, its density, its job) said by the token, as the value or in a request's words for it */
|
|
99
|
+
const plays = (role, token) => [...role.represents, ...role.density, ...role.job].some((v) => v === stem(token) || SAYS[v]?.includes(stem(token)));
|
|
90
100
|
const isName = (name, token) => name.toLowerCase() === token || name.toLowerCase() === stem(token);
|
|
91
101
|
|
|
92
102
|
const TOP = 8; // answers per kind; the components that match every axis are never cut
|
|
@@ -135,13 +145,18 @@ export function find(cat, query, axes = {}) {
|
|
|
135
145
|
const roles = new Map(cat.components.map((c) => [c.name, roleOf(c)]));
|
|
136
146
|
const toks = tokens(query);
|
|
137
147
|
const axisWords = [...(asked.want.represents ?? []), ...(asked.want.interact || [])]; // the axis values as words: a screen's own description may say them
|
|
138
|
-
|
|
148
|
+
// every three words of the request in a row, function words kept: a note that says "a or b" or "at a glance" as asked is the piece the request describes
|
|
149
|
+
const all = words(String(query));
|
|
150
|
+
const phrases = all.slice(2).map((w, i) => ` ${all[i]} ${all[i + 1]} ${w} `);
|
|
151
|
+
const rank = (items, fields, level, said = () => 0, saysIt = () => '') => { // a field is [text, weight, match = hits]; saysIt is the text whose phrases count
|
|
139
152
|
const scored = items.map((item) => {
|
|
140
153
|
let matched = 0, score = 0;
|
|
141
154
|
for (const t of toks) {
|
|
142
155
|
const w = fields(item).filter(([text, , match = hits]) => match(text, t)).reduce((sum, [, weight]) => sum + weight, 0);
|
|
143
156
|
if (w) { matched++; score += w; }
|
|
144
157
|
}
|
|
158
|
+
const own = ` ${words(saysIt(item)).join(' ')} `;
|
|
159
|
+
score += 3 * phrases.filter((p) => own.includes(p)).length;
|
|
145
160
|
return { item, matched, score, level: active ? level(item) : 0, said: active ? said(item) : 0 };
|
|
146
161
|
});
|
|
147
162
|
const byWords = (a, b) => b.matched - a.matched || b.score - a.score;
|
|
@@ -159,8 +174,9 @@ export function find(cat, query, axes = {}) {
|
|
|
159
174
|
const said = (s) => axisWords.filter((v) => hits(s.description, v)).length;
|
|
160
175
|
const [screenAxis, screenRest] = tier(rank(screens, (s) => [[s.id, 5], [s.description, 3], [s.uses.join(' '), 1]], level, said), TOP);
|
|
161
176
|
// a component is found by its name (the name that IS the word counts as much again as one that contains it: "card" is Card before FeedCard, whatever
|
|
162
|
-
// their other words say), its description, its role's note (what it is FOR, in the words of a request: "a setting turned on or off")
|
|
163
|
-
|
|
177
|
+
// their other words say), its description, its role's note (what it is FOR, in the words of a request: "a setting turned on or off"), the values of its role
|
|
178
|
+
// a word says ("choose" → pick, "number"), what it stands in for, then its prop names
|
|
179
|
+
const components = rank(cat.components, (c) => [[c.name, 6, isName], [c.name, 6], [c.description, 2], [c.role?.note ?? '', 2], ...(c.role ? [[c.role, 2, plays]] : []), [(c.role?.standsInFor ?? []).map((s) => `${s.name} ${s.condition}`).join(' '), 1], [c.props.map((p) => p.name).join(' '), 1]], (c) => Number(matches(roles.get(c.name), asked.want)), undefined, (c) => `${c.description} ${c.role?.note ?? ''}`);
|
|
164
180
|
const [componentAxis, componentRest] = tier(components, Math.max(TOP, components.axis.length));
|
|
165
181
|
// a screen drawn comes before the components when it answers the words as well as the best component does; one that answers FEWER of them
|
|
166
182
|
// comes after (a question by intention, "a setting turned on or off", is answered by Toggle, not by eight screens that say "off" or "or")
|
package/lint/rules.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export const CONTROL_NAMES = {
|
|
|
15
15
|
Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], Sticky: ['unreadLabel'], StampOn: ['removeLabel'], Card: ['removeLabel', 'toBackLabel', 'toFrontLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
|
|
18
|
+
export const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
|
|
19
19
|
|
|
20
20
|
/** the classes a selector names; a bare state word (`is-open`) is every project's too — it only matters beside a cardds class, which is caught */
|
|
21
21
|
export const selectorClasses = (selector) => [...selector.matchAll(/(?<!\\)\.(-?[a-zA-Z_][\w-]*)/g)].map((m) => m[1]).filter((c) => !/^(is|has)-/.test(c));
|
|
@@ -72,7 +72,7 @@ function elementAt(text, start) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** the expression of a `{…}` attribute value from its opening brace */
|
|
75
|
-
function braceAt(text, start) {
|
|
75
|
+
export function braceAt(text, start) {
|
|
76
76
|
let depth = 0, quote = null;
|
|
77
77
|
for (let i = start; i < text.length; i++) {
|
|
78
78
|
const c = text[i];
|
|
@@ -98,7 +98,7 @@ function classWords(text, at) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/** a style object's or attribute's `key: value` entries from its body (`at` = where the body starts in the text): split on the commas / semicolons that are not inside a string, bracket or call */
|
|
101
|
-
function* styleEntries(body, at, sep) {
|
|
101
|
+
export function* styleEntries(body, at, sep) {
|
|
102
102
|
let depth = 0, quote = null, from = 0;
|
|
103
103
|
for (let i = 0; i <= body.length; i++) {
|
|
104
104
|
const c = body[i];
|
|
@@ -114,6 +114,9 @@ function* styleEntries(body, at, sep) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** comments out, lines kept; a string is stepped over whole, so `accept="image/*"` opens no comment */
|
|
118
|
+
export const blankComments = (source) => source.replace(/"[^"\n]*"|'[^'\n]*'|`[^`]*`|\/\*[\s\S]*?\*\/|(?<![:'"`\\])\/\/[^\n]*/g, (c) => /^["'`]/.test(c) ? c : c.replace(/[^\n]/g, ' '));
|
|
119
|
+
|
|
117
120
|
/**
|
|
118
121
|
* @param {string} text a source file
|
|
119
122
|
* @param {string} file its name, for the findings
|
|
@@ -122,8 +125,7 @@ function* styleEntries(body, at, sep) {
|
|
|
122
125
|
export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
123
126
|
const findings = [];
|
|
124
127
|
const lines = source.split('\n');
|
|
125
|
-
|
|
126
|
-
let text = source.replace(/"[^"\n]*"|'[^'\n]*'|`[^`]*`|\/\*[\s\S]*?\*\/|(?<![:'"`\\])\/\/[^\n]*/g, (c) => /^["'`]/.test(c) ? c : c.replace(/[^\n]/g, ' '));
|
|
128
|
+
let text = blankComments(source);
|
|
127
129
|
const add = (rule, level, idx, message) => {
|
|
128
130
|
const line = lineOf(text, idx);
|
|
129
131
|
if (!/cardds-ignore-next-line/.test(lines[line - 2] ?? '')) findings.push({ rule, level, file, line, message }); // `// cardds-ignore-next-line` — a deliberate case (a story that shows the wrong way)
|