@stapel/listings-react 0.28.0 → 0.28.1
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 +45 -0
- package/dist/default/notice.d.ts.map +1 -1
- package/dist/default/notice.js +46 -4
- package/dist/default/notice.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +1 -1
- package/nav-manifest.json +1 -1
- package/package.json +3 -3
- package/src/analytics/generated/events.json +1 -1
- package/src/default/notice.ts +56 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @stapel/listings-react
|
|
2
2
|
|
|
3
|
+
## 0.28.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ef75b1b: listings: a confirmation toast no longer outlives the surface that spoke it — and the suite now says so per test
|
|
8
|
+
|
|
9
|
+
**The defect, as CI met it.** `@stapel/listings-react#test` was failing two runs
|
|
10
|
+
in three on inputs whose turbo hash had not moved: all tests passed, and the job
|
|
11
|
+
then died on an unhandled `ReferenceError: window is not defined` thrown out of
|
|
12
|
+
react-dom, blamed on `test/favoriteFeedback.test.tsx` and `test/detail.test.tsx`.
|
|
13
|
+
Neither file was at fault, and no `--pool` setting was going to fix it.
|
|
14
|
+
|
|
15
|
+
**Root cause.** `useNotice()` raises its confirmation through a holder mounted
|
|
16
|
+
OUTSIDE the component tree — antd's `<App>` holder where a host mounted one, and
|
|
17
|
+
otherwise a React root antd's static `message` entry renders into the document on
|
|
18
|
+
first use. Nothing unmounts either when the surface that spoke unmounts. And a
|
|
19
|
+
standing notice is not an idle DOM node: `@rc-component/notification` counts its
|
|
20
|
+
`NOTICE_SECONDS` down with a **`requestAnimationFrame` loop**
|
|
21
|
+
(`useNoticeTimer`), and `@rc-component/util`'s `raf` captured
|
|
22
|
+
`window.requestAnimationFrame` at module load. So the listing page's heart toast
|
|
23
|
+
kept stepping frames after the test that pressed it, after `cleanup()`, and past
|
|
24
|
+
the end of the file — and the frame that landed once vitest had torn the jsdom
|
|
25
|
+
environment down called into a `window` that no longer existed.
|
|
26
|
+
|
|
27
|
+
**The fix, in the source.** `useNotice` now keeps the handle antd returns for
|
|
28
|
+
each notice it raises and **retires them on unmount** (each handle also retires
|
|
29
|
+
itself the moment its notice closes on its own, so the set holds only what is
|
|
30
|
+
still on screen). Both arms, contextual and static.
|
|
31
|
+
|
|
32
|
+
**Behaviour change, and it is the intent.** A person who presses the heart and
|
|
33
|
+
immediately navigates away no longer gets "Saved" floating over the next page,
|
|
34
|
+
about a listing they have left. The toast has always been the AMPLIFIER of a
|
|
35
|
+
gesture on a surface and never the record of it — the heart's own fill and the
|
|
36
|
+
share menu's own sentence are the record — so an amplifier with nothing left to
|
|
37
|
+
amplify is closed rather than left running. A host that wants a confirmation to
|
|
38
|
+
survive the surface should raise it from a component that survives too.
|
|
39
|
+
|
|
40
|
+
**The gate.** `test/vitest.setup.ts` now asserts, after `cleanup()` and per test,
|
|
41
|
+
that the test left behind **no pending animation frame** and **no unhandled
|
|
42
|
+
rejection**, both read-and-reset so a leaking test reddens itself and not the
|
|
43
|
+
twenty after it. Verified to fail on the unfixed source, on exactly the two
|
|
44
|
+
files CI named, and to pass on the fixed one. `favoriteFeedback.test.tsx` also
|
|
45
|
+
now awaits the write its detail-page gesture starts, instead of ending with a
|
|
46
|
+
POST in flight.
|
|
47
|
+
|
|
3
48
|
## 0.28.0
|
|
4
49
|
|
|
5
50
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../src/default/notice.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../src/default/notice.ts"],"names":[],"mappings":"AA0DA,2EAA2E;AAC3E,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAU5C;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,wBAAgB,SAAS,IAAI,MAAM,CAuClC"}
|
package/dist/default/notice.js
CHANGED
|
@@ -30,8 +30,30 @@
|
|
|
30
30
|
* the heart's state is on the heart. This is the AMPLIFIER, never the record.
|
|
31
31
|
* A future `SkinNotice` in the substrate replaces the body of this function
|
|
32
32
|
* and nothing else; the callers already speak in resolved sentences.
|
|
33
|
+
*
|
|
34
|
+
* ── An amplifier may not outlive what it amplifies ────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* Both arms hand the notice to a holder MOUNTED OUTSIDE this component tree —
|
|
37
|
+
* antd's `<App>` holder in the first arm, and in the second a React root antd
|
|
38
|
+
* renders into the document the first time the static entry is called. Neither
|
|
39
|
+
* is unmounted by unmounting the surface that spoke, and a standing notice is
|
|
40
|
+
* not an idle DOM node: `@rc-component/notification` counts its two seconds
|
|
41
|
+
* down with a `requestAnimationFrame` LOOP (`useNoticeTimer`), which is live
|
|
42
|
+
* work driven from a root nothing in this package owns.
|
|
43
|
+
*
|
|
44
|
+
* So the notices raised here are RETIRED when the surface that raised them
|
|
45
|
+
* goes. Two things follow, and both are the intent:
|
|
46
|
+
*
|
|
47
|
+
* - a person who presses the heart and immediately navigates away does not
|
|
48
|
+
* get "Saved" floating over the next page, about a listing they left;
|
|
49
|
+
* - nothing this pair started keeps running after the tree it started in is
|
|
50
|
+
* gone. Measured as CI flake: the detail page's heart toast kept stepping
|
|
51
|
+
* its rAF loop past the end of the test file that raised it, and the frame
|
|
52
|
+
* that landed after the environment was torn down threw
|
|
53
|
+
* `ReferenceError: window is not defined` out of react-dom — from a suite
|
|
54
|
+
* in which every test had passed.
|
|
33
55
|
*/
|
|
34
|
-
import { useCallback } from "react";
|
|
56
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
35
57
|
import { App, message as staticMessage } from "antd";
|
|
36
58
|
/**
|
|
37
59
|
* How long a confirmation stands, in seconds.
|
|
@@ -46,9 +68,29 @@ export const NOTICE_SECONDS = 2;
|
|
|
46
68
|
export function useNotice() {
|
|
47
69
|
const app = App.useApp();
|
|
48
70
|
const contextual = app.message.success;
|
|
71
|
+
// The notices this surface has raised and that have not closed themselves
|
|
72
|
+
// yet — see the header. A notice retires itself the moment it closes, so
|
|
73
|
+
// this holds at most the handful still on screen.
|
|
74
|
+
const standing = useRef(new Set());
|
|
75
|
+
useEffect(() => () => {
|
|
76
|
+
const open = standing.current;
|
|
77
|
+
standing.current = new Set();
|
|
78
|
+
for (const close of open)
|
|
79
|
+
close();
|
|
80
|
+
}, []);
|
|
81
|
+
const hold = useCallback((raised) => {
|
|
82
|
+
standing.current.add(raised);
|
|
83
|
+
const retire = () => {
|
|
84
|
+
standing.current.delete(raised);
|
|
85
|
+
};
|
|
86
|
+
// Closed by its own timer, by our unmount, or by a host calling
|
|
87
|
+
// `message.destroy()` — every ending resolves this, and a rejection is an
|
|
88
|
+
// ending too. Nothing is awaited: the notice is already on screen.
|
|
89
|
+
raised.then(retire, retire);
|
|
90
|
+
}, []);
|
|
49
91
|
return useCallback((text) => {
|
|
50
92
|
if (typeof contextual === "function") {
|
|
51
|
-
contextual(text, NOTICE_SECONDS);
|
|
93
|
+
hold(contextual(text, NOTICE_SECONDS));
|
|
52
94
|
return;
|
|
53
95
|
}
|
|
54
96
|
// No `<App>` above us. antd's static entry renders its own holder into
|
|
@@ -56,7 +98,7 @@ export function useNotice() {
|
|
|
56
98
|
// and is a no-op on a server where there is no document to render into.
|
|
57
99
|
if (typeof document === "undefined")
|
|
58
100
|
return;
|
|
59
|
-
staticMessage.success(text, NOTICE_SECONDS);
|
|
60
|
-
}, [contextual]);
|
|
101
|
+
hold(staticMessage.success(text, NOTICE_SECONDS));
|
|
102
|
+
}, [contextual, hold]);
|
|
61
103
|
}
|
|
62
104
|
//# sourceMappingURL=notice.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notice.js","sourceRoot":"","sources":["../../src/default/notice.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"notice.js","sourceRoot":"","sources":["../../src/default/notice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AAarD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;IACzB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;IACvC,0EAA0E;IAC1E,yEAAyE;IACzE,kDAAkD;IAClD,MAAM,QAAQ,GAAG,MAAM,CAAoB,IAAI,GAAG,EAAE,CAAC,CAAC;IACtD,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC;QAC9B,QAAQ,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,KAAK,MAAM,KAAK,IAAI,IAAI;YAAE,KAAK,EAAE,CAAC;IACpC,CAAC,EACD,EAAE,CACH,CAAC;IACF,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,MAAoB,EAAQ,EAAE;QACtD,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7B,MAAM,MAAM,GAAG,GAAS,EAAE;YACxB,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC,CAAC;QACF,gEAAgE;QAChE,0EAA0E;QAC1E,mEAAmE;QACnE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,OAAO,WAAW,CAChB,CAAC,IAAY,EAAQ,EAAE;QACrB,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,cAAc,CAAiB,CAAC,CAAC;YACvD,OAAO;QACT,CAAC;QACD,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO;QAC5C,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,cAAc,CAAiB,CAAC,CAAC;IACpE,CAAC,EACD,CAAC,UAAU,EAAE,IAAI,CAAC,CACnB,CAAC;AACJ,CAAC"}
|
package/llms.txt
CHANGED
package/manifest.json
CHANGED
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/listings-react",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Headless React pair for stapel-listings: a typed client over the draft twin, the composer that turns a category schema into a submittable listing, and the owner's dashboard. Two independent axes are rendered as two — lifecycle `status` decides visibility and `moderation_status` decides nothing about it, so an edit to a live listing stays live and says 'under review' instead of vanishing. Feature values are drawn and mirrored by @stapel/attributes-react; photos arrive as an injected upload bag whose `refs` ARE `images_draft` and whose `settled` gates the submit; a publish refusal is routed onto the control that caused it by slug. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -103,12 +103,12 @@
|
|
|
103
103
|
"typescript": "^5.8.3",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
105
|
"@stapel/attributes-react": "^0.16.5",
|
|
106
|
-
"@stapel/core": "^0.
|
|
106
|
+
"@stapel/core": "^0.26.0",
|
|
107
107
|
"@stapel/currencies-react": "^0.4.0",
|
|
108
108
|
"@stapel/image": "^0.4.2",
|
|
109
109
|
"@stapel/showcase": "^0.3.0",
|
|
110
110
|
"@stapel/tokens": "^0.8.0",
|
|
111
|
-
"@stapel/tokens-antd": "^0.
|
|
111
|
+
"@stapel/tokens-antd": "^0.18.0"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|
|
114
114
|
"node": ">=22"
|
package/src/default/notice.ts
CHANGED
|
@@ -30,13 +30,43 @@
|
|
|
30
30
|
* the heart's state is on the heart. This is the AMPLIFIER, never the record.
|
|
31
31
|
* A future `SkinNotice` in the substrate replaces the body of this function
|
|
32
32
|
* and nothing else; the callers already speak in resolved sentences.
|
|
33
|
+
*
|
|
34
|
+
* ── An amplifier may not outlive what it amplifies ────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* Both arms hand the notice to a holder MOUNTED OUTSIDE this component tree —
|
|
37
|
+
* antd's `<App>` holder in the first arm, and in the second a React root antd
|
|
38
|
+
* renders into the document the first time the static entry is called. Neither
|
|
39
|
+
* is unmounted by unmounting the surface that spoke, and a standing notice is
|
|
40
|
+
* not an idle DOM node: `@rc-component/notification` counts its two seconds
|
|
41
|
+
* down with a `requestAnimationFrame` LOOP (`useNoticeTimer`), which is live
|
|
42
|
+
* work driven from a root nothing in this package owns.
|
|
43
|
+
*
|
|
44
|
+
* So the notices raised here are RETIRED when the surface that raised them
|
|
45
|
+
* goes. Two things follow, and both are the intent:
|
|
46
|
+
*
|
|
47
|
+
* - a person who presses the heart and immediately navigates away does not
|
|
48
|
+
* get "Saved" floating over the next page, about a listing they left;
|
|
49
|
+
* - nothing this pair started keeps running after the tree it started in is
|
|
50
|
+
* gone. Measured as CI flake: the detail page's heart toast kept stepping
|
|
51
|
+
* its rAF loop past the end of the test file that raised it, and the frame
|
|
52
|
+
* that landed after the environment was torn down threw
|
|
53
|
+
* `ReferenceError: window is not defined` out of react-dom — from a suite
|
|
54
|
+
* in which every test had passed.
|
|
33
55
|
*/
|
|
34
|
-
import { useCallback } from "react";
|
|
56
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
35
57
|
import { App, message as staticMessage } from "antd";
|
|
36
58
|
|
|
37
59
|
/** Say one short sentence. Resolved copy — this is the skin, not a bag. */
|
|
38
60
|
export type Notice = (text: string) => void;
|
|
39
61
|
|
|
62
|
+
/**
|
|
63
|
+
* What antd hands back for a raised notice: CALL it to close the notice early,
|
|
64
|
+
* `then` it to learn that it closed on its own. Typed structurally rather than
|
|
65
|
+
* imported (`antd/es/message/interface`) so this file keeps to antd's public
|
|
66
|
+
* entry — the shape is antd's documented `MessageType` either way.
|
|
67
|
+
*/
|
|
68
|
+
type RaisedNotice = (() => void) & PromiseLike<unknown>;
|
|
69
|
+
|
|
40
70
|
/**
|
|
41
71
|
* How long a confirmation stands, in seconds.
|
|
42
72
|
*
|
|
@@ -51,18 +81,40 @@ export const NOTICE_SECONDS = 2;
|
|
|
51
81
|
export function useNotice(): Notice {
|
|
52
82
|
const app = App.useApp();
|
|
53
83
|
const contextual = app.message.success;
|
|
84
|
+
// The notices this surface has raised and that have not closed themselves
|
|
85
|
+
// yet — see the header. A notice retires itself the moment it closes, so
|
|
86
|
+
// this holds at most the handful still on screen.
|
|
87
|
+
const standing = useRef<Set<RaisedNotice>>(new Set());
|
|
88
|
+
useEffect(
|
|
89
|
+
() => () => {
|
|
90
|
+
const open = standing.current;
|
|
91
|
+
standing.current = new Set();
|
|
92
|
+
for (const close of open) close();
|
|
93
|
+
},
|
|
94
|
+
[]
|
|
95
|
+
);
|
|
96
|
+
const hold = useCallback((raised: RaisedNotice): void => {
|
|
97
|
+
standing.current.add(raised);
|
|
98
|
+
const retire = (): void => {
|
|
99
|
+
standing.current.delete(raised);
|
|
100
|
+
};
|
|
101
|
+
// Closed by its own timer, by our unmount, or by a host calling
|
|
102
|
+
// `message.destroy()` — every ending resolves this, and a rejection is an
|
|
103
|
+
// ending too. Nothing is awaited: the notice is already on screen.
|
|
104
|
+
raised.then(retire, retire);
|
|
105
|
+
}, []);
|
|
54
106
|
return useCallback(
|
|
55
107
|
(text: string): void => {
|
|
56
108
|
if (typeof contextual === "function") {
|
|
57
|
-
contextual(text, NOTICE_SECONDS);
|
|
109
|
+
hold(contextual(text, NOTICE_SECONDS) as RaisedNotice);
|
|
58
110
|
return;
|
|
59
111
|
}
|
|
60
112
|
// No `<App>` above us. antd's static entry renders its own holder into
|
|
61
113
|
// the document, which is exactly right for a host that never opted in,
|
|
62
114
|
// and is a no-op on a server where there is no document to render into.
|
|
63
115
|
if (typeof document === "undefined") return;
|
|
64
|
-
staticMessage.success(text, NOTICE_SECONDS);
|
|
116
|
+
hold(staticMessage.success(text, NOTICE_SECONDS) as RaisedNotice);
|
|
65
117
|
},
|
|
66
|
-
[contextual]
|
|
118
|
+
[contextual, hold]
|
|
67
119
|
);
|
|
68
120
|
}
|