@wix/web5-core 1.63.26 → 1.63.28

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.
Files changed (41) hide show
  1. package/dist/cjs/context/ComponentDependenciesContext.js +15 -1
  2. package/dist/cjs/context/ComponentDependenciesContext.js.map +1 -1
  3. package/dist/cjs/context/ImageSlotContext.js +221 -0
  4. package/dist/cjs/context/ImageSlotContext.js.map +1 -0
  5. package/dist/cjs/hooks/useImageSlot.js +182 -0
  6. package/dist/cjs/hooks/useImageSlot.js.map +1 -0
  7. package/dist/cjs/image/composeSemantic.js +94 -0
  8. package/dist/cjs/image/composeSemantic.js.map +1 -0
  9. package/dist/cjs/image/imageSlotTypes.js +4 -0
  10. package/dist/cjs/image/imageSlotTypes.js.map +1 -0
  11. package/dist/cjs/index.js +11 -3
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/types/dependencies.js.map +1 -1
  14. package/dist/esm/context/ComponentDependenciesContext.js +13 -0
  15. package/dist/esm/context/ComponentDependenciesContext.js.map +1 -1
  16. package/dist/esm/context/ImageSlotContext.js +208 -0
  17. package/dist/esm/context/ImageSlotContext.js.map +1 -0
  18. package/dist/esm/hooks/useImageSlot.js +177 -0
  19. package/dist/esm/hooks/useImageSlot.js.map +1 -0
  20. package/dist/esm/image/composeSemantic.js +91 -0
  21. package/dist/esm/image/composeSemantic.js.map +1 -0
  22. package/dist/esm/image/imageSlotTypes.js +2 -0
  23. package/dist/esm/image/imageSlotTypes.js.map +1 -0
  24. package/dist/esm/index.js +7 -0
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/types/dependencies.js.map +1 -1
  27. package/dist/types/context/ComponentDependenciesContext.d.ts +10 -0
  28. package/dist/types/context/ComponentDependenciesContext.d.ts.map +1 -1
  29. package/dist/types/context/ImageSlotContext.d.ts +63 -0
  30. package/dist/types/context/ImageSlotContext.d.ts.map +1 -0
  31. package/dist/types/hooks/useImageSlot.d.ts +35 -0
  32. package/dist/types/hooks/useImageSlot.d.ts.map +1 -0
  33. package/dist/types/image/composeSemantic.d.ts +37 -0
  34. package/dist/types/image/composeSemantic.d.ts.map +1 -0
  35. package/dist/types/image/imageSlotTypes.d.ts +112 -0
  36. package/dist/types/image/imageSlotTypes.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +7 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/types/dependencies.d.ts +11 -0
  40. package/dist/types/types/dependencies.d.ts.map +1 -1
  41. package/package.json +2 -2
@@ -3,6 +3,7 @@
3
3
  exports.__esModule = true;
4
4
  exports.ComponentDependenciesProvider = void 0;
5
5
  exports.useComponentDependencies = useComponentDependencies;
6
+ exports.useOptionalComponentDependencies = useOptionalComponentDependencies;
6
7
  var _react = _interopRequireWildcard(require("react"));
7
8
  var _jsxFileName = "/home/builduser/work/c2b6ca0b3fa984ee/packages/web5-core/dist/cjs/context/ComponentDependenciesContext.tsx";
8
9
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
@@ -14,6 +15,19 @@ function useComponentDependencies() {
14
15
  }
15
16
  return ctx;
16
17
  }
18
+
19
+ /**
20
+ * The same context, without the throw.
21
+ *
22
+ * For providers the HOST mounts around every section rather than a component
23
+ * opting in: those sit above trees that may legitimately have no dependencies
24
+ * configured — a layout test, a storybook story — and hard-crashing them would
25
+ * make an unrelated feature's provider a global requirement. A consumer that
26
+ * genuinely needs deps should still use `useComponentDependencies`.
27
+ */
28
+ function useOptionalComponentDependencies() {
29
+ return (0, _react.useContext)(ComponentDependenciesContext);
30
+ }
17
31
  const ComponentDependenciesProvider = ({
18
32
  deps,
19
33
  children
@@ -23,7 +37,7 @@ const ComponentDependenciesProvider = ({
23
37
  __self: void 0,
24
38
  __source: {
25
39
  fileName: _jsxFileName,
26
- lineNumber: 31,
40
+ lineNumber: 44,
27
41
  columnNumber: 5
28
42
  }
29
43
  }, children);
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireWildcard","require","_jsxFileName","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","ComponentDependenciesContext","createContext","useComponentDependencies","ctx","useContext","Error","ComponentDependenciesProvider","deps","children","createElement","Provider","value","__self","__source","fileName","lineNumber","columnNumber","exports"],"sources":["../../../src/context/ComponentDependenciesContext.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n type FC,\n type PropsWithChildren,\n} from 'react';\nimport type { ComponentDependencies } from '../types/dependencies';\n\nconst ComponentDependenciesContext =\n createContext<ComponentDependencies | null>(null);\n\nexport function useComponentDependencies(): ComponentDependencies {\n const ctx = useContext(ComponentDependenciesContext);\n if (!ctx) {\n throw new Error(\n 'useComponentDependencies must be used within a ComponentDependenciesProvider. ' +\n 'Wrap your app with <ComponentDependenciesProvider deps={...}>.',\n );\n }\n return ctx;\n}\n\nexport interface ComponentDependenciesProviderProps extends PropsWithChildren {\n deps: ComponentDependencies;\n}\n\nexport const ComponentDependenciesProvider: FC<\n ComponentDependenciesProviderProps\n> = ({ deps, children }) => {\n return (\n <ComponentDependenciesContext.Provider value={deps}>\n {children}\n </ComponentDependenciesContext.Provider>\n );\n};\n"],"mappings":";;;;;AAAA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AAKe,IAAAC,YAAA;AAAA,SAAAF,wBAAAG,CAAA,EAAAC,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAL,uBAAA,YAAAA,CAAAG,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,uBAAAA,CAAA,yBAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAK,GAAA,CAAAX,CAAA,UAAAM,CAAA,CAAAM,GAAA,CAAAZ,CAAA,GAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,EAAAQ,CAAA,gBAAAP,CAAA,IAAAD,CAAA,gBAAAC,CAAA,OAAAa,cAAA,CAAAC,IAAA,CAAAf,CAAA,EAAAC,CAAA,OAAAM,CAAA,IAAAD,CAAA,GAAAU,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAC,CAAA,OAAAM,CAAA,CAAAK,GAAA,IAAAL,CAAA,CAAAM,GAAA,IAAAP,CAAA,CAAAE,CAAA,EAAAP,CAAA,EAAAM,CAAA,IAAAC,CAAA,CAAAP,CAAA,IAAAD,CAAA,CAAAC,CAAA,WAAAO,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAGf,MAAMkB,4BAA4B,gBAChC,IAAAC,oBAAa,EAA+B,IAAI,CAAC;AAE5C,SAASC,wBAAwBA,CAAA,EAA0B;EAChE,MAAMC,GAAG,GAAG,IAAAC,iBAAU,EAACJ,4BAA4B,CAAC;EACpD,IAAI,CAACG,GAAG,EAAE;IACR,MAAM,IAAIE,KAAK,CACb,gFAAgF,GAC9E,gEACJ,CAAC;EACH;EACA,OAAOF,GAAG;AACZ;AAMO,MAAMG,6BAEZ,GAAGA,CAAC;EAAEC,IAAI;EAAEC;AAAS,CAAC,KAAK;EAC1B,oBACE/B,MAAA,CAAAc,OAAA,CAAAkB,aAAA,CAACT,4BAA4B,CAACU,QAAQ;IAACC,KAAK,EAAEJ,IAAK;IAAAK,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAlC,YAAA;MAAAmC,UAAA;MAAAC,YAAA;IAAA;EAAA,GAChDR,QACoC,CAAC;AAE5C,CAAC;AAACS,OAAA,CAAAX,6BAAA,GAAAA,6BAAA","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireWildcard","require","_jsxFileName","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","ComponentDependenciesContext","createContext","useComponentDependencies","ctx","useContext","Error","useOptionalComponentDependencies","ComponentDependenciesProvider","deps","children","createElement","Provider","value","__self","__source","fileName","lineNumber","columnNumber","exports"],"sources":["../../../src/context/ComponentDependenciesContext.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n type FC,\n type PropsWithChildren,\n} from 'react';\nimport type { ComponentDependencies } from '../types/dependencies';\n\nconst ComponentDependenciesContext =\n createContext<ComponentDependencies | null>(null);\n\nexport function useComponentDependencies(): ComponentDependencies {\n const ctx = useContext(ComponentDependenciesContext);\n if (!ctx) {\n throw new Error(\n 'useComponentDependencies must be used within a ComponentDependenciesProvider. ' +\n 'Wrap your app with <ComponentDependenciesProvider deps={...}>.',\n );\n }\n return ctx;\n}\n\n/**\n * The same context, without the throw.\n *\n * For providers the HOST mounts around every section rather than a component\n * opting in: those sit above trees that may legitimately have no dependencies\n * configured — a layout test, a storybook story — and hard-crashing them would\n * make an unrelated feature's provider a global requirement. A consumer that\n * genuinely needs deps should still use `useComponentDependencies`.\n */\nexport function useOptionalComponentDependencies(): ComponentDependencies | null {\n return useContext(ComponentDependenciesContext);\n}\n\nexport interface ComponentDependenciesProviderProps extends PropsWithChildren {\n deps: ComponentDependencies;\n}\n\nexport const ComponentDependenciesProvider: FC<\n ComponentDependenciesProviderProps\n> = ({ deps, children }) => {\n return (\n <ComponentDependenciesContext.Provider value={deps}>\n {children}\n </ComponentDependenciesContext.Provider>\n );\n};\n"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AAKe,IAAAC,YAAA;AAAA,SAAAF,wBAAAG,CAAA,EAAAC,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAL,uBAAA,YAAAA,CAAAG,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,uBAAAA,CAAA,yBAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAK,GAAA,CAAAX,CAAA,UAAAM,CAAA,CAAAM,GAAA,CAAAZ,CAAA,GAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,EAAAQ,CAAA,gBAAAP,CAAA,IAAAD,CAAA,gBAAAC,CAAA,OAAAa,cAAA,CAAAC,IAAA,CAAAf,CAAA,EAAAC,CAAA,OAAAM,CAAA,IAAAD,CAAA,GAAAU,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAC,CAAA,OAAAM,CAAA,CAAAK,GAAA,IAAAL,CAAA,CAAAM,GAAA,IAAAP,CAAA,CAAAE,CAAA,EAAAP,CAAA,EAAAM,CAAA,IAAAC,CAAA,CAAAP,CAAA,IAAAD,CAAA,CAAAC,CAAA,WAAAO,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAGf,MAAMkB,4BAA4B,gBAChC,IAAAC,oBAAa,EAA+B,IAAI,CAAC;AAE5C,SAASC,wBAAwBA,CAAA,EAA0B;EAChE,MAAMC,GAAG,GAAG,IAAAC,iBAAU,EAACJ,4BAA4B,CAAC;EACpD,IAAI,CAACG,GAAG,EAAE;IACR,MAAM,IAAIE,KAAK,CACb,gFAAgF,GAC9E,gEACJ,CAAC;EACH;EACA,OAAOF,GAAG;AACZ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASG,gCAAgCA,CAAA,EAAiC;EAC/E,OAAO,IAAAF,iBAAU,EAACJ,4BAA4B,CAAC;AACjD;AAMO,MAAMO,6BAEZ,GAAGA,CAAC;EAAEC,IAAI;EAAEC;AAAS,CAAC,KAAK;EAC1B,oBACEhC,MAAA,CAAAc,OAAA,CAAAmB,aAAA,CAACV,4BAA4B,CAACW,QAAQ;IAACC,KAAK,EAAEJ,IAAK;IAAAK,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAnC,YAAA;MAAAoC,UAAA;MAAAC,YAAA;IAAA;EAAA,GAChDR,QACoC,CAAC;AAE5C,CAAC;AAACS,OAAA,CAAAX,6BAAA,GAAAA,6BAAA","ignoreList":[]}
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.useImageSlotCollector = exports.ImageSlotProvider = void 0;
5
+ var _react = _interopRequireWildcard(require("react"));
6
+ var _ComponentDependenciesContext = require("./ComponentDependenciesContext");
7
+ var _diagnosticTypes = require("../component/diagnosticTypes");
8
+ var _jsxFileName = "/home/builduser/work/c2b6ca0b3fa984ee/packages/web5-core/dist/cjs/context/ImageSlotContext.tsx";
9
+ /**
10
+ * The section-scoped image slot collector.
11
+ *
12
+ * A section declares the holes in its layout; it never makes a request. This
13
+ * gathers every declaration made inside one section's subtree, fires ONE
14
+ * `ResolveImageSet` through the injected port, and hands each declaration its
15
+ * answer back by `slotId`. Sections stay ignorant of each other — no section
16
+ * imports another, no ordering rules, no exclusion lists threaded through
17
+ * props — and the network still sees the joint solve that distinctness depends
18
+ * on.
19
+ *
20
+ * WHY THE BOUNDARY IS THE SECTION
21
+ * -------------------------------
22
+ * The resolver solves a maximum-weight one-to-one assignment across everything
23
+ * in one request, so distinctness and palette cohesion are properties of that
24
+ * solve and stop at its edge. Drawing the boundary at the page would buy
25
+ * cross-section distinctness at the cost of making every section's pictures
26
+ * wait on the slowest declaration on the page; drawing it at the section keeps
27
+ * each section independent and accepts that two sections may land on the same
28
+ * photograph. The most visible case — four identical photos in one grid — is
29
+ * still prevented for free, because those slots share a request.
30
+ *
31
+ * WHY THERE IS NO READINESS POLICY
32
+ * --------------------------------
33
+ * There is nothing to wait for. A section component is never constructed from
34
+ * partial markdown: a declared-but-unstreamed section is a `PageSection` with
35
+ * `isSkeleton: true` and `component: null`, and the real element is built only
36
+ * once its body has finished streaming. So a section's props are final the
37
+ * moment it exists, and the flush is simply "after this section's first render
38
+ * commit" — one effect, not a scheduler.
39
+ *
40
+ * The same fact bounds the win honestly: because only a *mounted* section
41
+ * declares, no section can ask for a picture before its own body has streamed,
42
+ * including a background whose subject needed nothing from that body.
43
+ */
44
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
45
+ const ImageSlotContext = /*#__PURE__*/(0, _react.createContext)(null);
46
+
47
+ /**
48
+ * Module-level so `read` returns a STABLE reference for a slot nobody has
49
+ * published yet. `useImageSlot` reads this store through
50
+ * `useSyncExternalStore`, which compares snapshots with `Object.is` on every
51
+ * render — a fresh `{ status: 'pending' }` per call reads as "the store
52
+ * changed", and React re-renders until it throws "Maximum update depth
53
+ * exceeded". Every slot is pending from mount until its batch answers, so a
54
+ * literal here takes down every section that declares one.
55
+ */
56
+ const PENDING = {
57
+ status: 'pending'
58
+ };
59
+ /**
60
+ * Mounted once per section by the host's section wrapper, so every section in
61
+ * every client package gets a collector without opting in.
62
+ */
63
+ const ImageSlotProvider = ({
64
+ sectionId,
65
+ children
66
+ }) => {
67
+ // Optional on purpose: this is mounted by the host around EVERY section, so
68
+ // it sits above trees that may have no dependencies configured at all (a
69
+ // layout test, a story). Throwing there would make the image feature's
70
+ // provider a global requirement for rendering any section.
71
+ const deps = (0, _ComponentDependenciesContext.useOptionalComponentDependencies)();
72
+ const resolve = deps == null ? void 0 : deps.resolveImageSet;
73
+
74
+ /**
75
+ * Slot state lives in a ref, NOT in React state, and the context value never
76
+ * changes. Holding it in state put it inside the provider's value, so every
77
+ * resolution produced a new value and re-rendered every consumer in the
78
+ * section — free at one slot, twelve re-renders per resolution in a
79
+ * twelve-card grid, which is exactly the shape this boundary exists to serve.
80
+ * Consumers now subscribe to their own id and nothing else.
81
+ */
82
+ const statesRef = (0, _react.useRef)({});
83
+ const listenersRef = (0, _react.useRef)(new Map());
84
+
85
+ // Declarations arriving during this commit. A ref, not state: collecting a
86
+ // declaration must not itself cause a render, or every hook that declares
87
+ // would re-render every sibling before the batch is even sent.
88
+ const pendingRef = (0, _react.useRef)(new Map());
89
+ // Ids already sent. A re-render re-declares the same content-derived id, and
90
+ // that must be a no-op rather than a second request.
91
+ const sentRef = (0, _react.useRef)(new Set());
92
+ const flushScheduled = (0, _react.useRef)(false);
93
+ const aliveRef = (0, _react.useRef)(true);
94
+ (0, _react.useEffect)(() => {
95
+ aliveRef.current = true;
96
+ return () => {
97
+ aliveRef.current = false;
98
+ };
99
+ }, []);
100
+ const publish = (0, _react.useCallback)(next => {
101
+ const changed = [];
102
+ for (const [id, state] of Object.entries(next)) {
103
+ if (statesRef.current[id] !== state) {
104
+ statesRef.current[id] = state;
105
+ changed.push(id);
106
+ }
107
+ }
108
+ for (const id of changed) {
109
+ var _listenersRef$current;
110
+ (_listenersRef$current = listenersRef.current.get(id)) == null || _listenersRef$current.forEach(fn => fn());
111
+ }
112
+ }, []);
113
+ const markUnavailable = (0, _react.useCallback)(batch => {
114
+ if (!aliveRef.current) {
115
+ return;
116
+ }
117
+ publish(Object.fromEntries(batch.map(s => [s.id, {
118
+ status: 'unavailable'
119
+ }])));
120
+ }, [publish]);
121
+ const flush = (0, _react.useCallback)(async () => {
122
+ flushScheduled.current = false;
123
+ const batch = [...pendingRef.current.values()].filter(s => !sentRef.current.has(s.id));
124
+ pendingRef.current.clear();
125
+ if (batch.length === 0) {
126
+ return;
127
+ }
128
+ batch.forEach(s => sentRef.current.add(s.id));
129
+
130
+ // No port means the host has not injected a resolver. Fail the batch closed
131
+ // rather than silently leaving slots pending forever.
132
+ if (!resolve) {
133
+ markUnavailable(batch);
134
+ return;
135
+ }
136
+ try {
137
+ const res = await resolve(batch);
138
+ if (!aliveRef.current) {
139
+ return;
140
+ }
141
+ const bySlotId = new Map(res.slots.map(s => [s.slotId, s]));
142
+ const next = {};
143
+ for (const req of batch) {
144
+ const got = bySlotId.get(req.id);
145
+ // A slot the resolver could not fill comes back with a null url. That
146
+ // is `unavailable` to a layout, not a resolved picture.
147
+ next[req.id] = got && got.imageUrl ? {
148
+ status: 'resolved',
149
+ slot: got
150
+ } : {
151
+ status: 'unavailable'
152
+ };
153
+ }
154
+ publish(next);
155
+ } catch (err) {
156
+ // Failure is contained to this section: one bad request costs this
157
+ // section its pictures and nothing else on the page.
158
+ deps == null || deps.reportDiagnostic == null || deps.reportDiagnostic(_diagnosticTypes.DIAGNOSTIC_TYPES.IMAGE_RESOLUTION_FAILED, sectionId, `ResolveImageSet failed for ${batch.length} slot(s): ${err instanceof Error ? err.message : String(err)}`);
159
+ markUnavailable(batch);
160
+ }
161
+ }, [resolve, deps, sectionId, publish, markUnavailable]);
162
+ const declare = (0, _react.useCallback)(slot => {
163
+ if (sentRef.current.has(slot.id)) {
164
+ return;
165
+ }
166
+ pendingRef.current.set(slot.id, slot);
167
+ // Flush on a microtask, so every declaration made during this commit —
168
+ // the background, the cards, an inline image — lands in the same batch.
169
+ if (!flushScheduled.current) {
170
+ flushScheduled.current = true;
171
+ queueMicrotask(() => {
172
+ void flush();
173
+ });
174
+ }
175
+ }, [flush]);
176
+ const read = (0, _react.useCallback)(slotId => statesRef.current[slotId] ?? PENDING, []);
177
+ const subscribe = (0, _react.useCallback)((slotId, onChange) => {
178
+ let set = listenersRef.current.get(slotId);
179
+ if (!set) {
180
+ set = new Set();
181
+ listenersRef.current.set(slotId, set);
182
+ }
183
+ set.add(onChange);
184
+ return () => {
185
+ var _set;
186
+ (_set = set) == null || _set.delete(onChange);
187
+ if (set && set.size === 0) {
188
+ listenersRef.current.delete(slotId);
189
+ }
190
+ };
191
+ }, []);
192
+
193
+ // Stable for the provider's lifetime: every member is a `useCallback` with no
194
+ // reactive deps, so mounting this around a section costs its consumers
195
+ // nothing after first render.
196
+ const api = (0, _react.useMemo)(() => ({
197
+ declare,
198
+ read,
199
+ subscribe
200
+ }), [declare, read, subscribe]);
201
+ return /*#__PURE__*/_react.default.createElement(ImageSlotContext.Provider, {
202
+ value: api,
203
+ __self: void 0,
204
+ __source: {
205
+ fileName: _jsxFileName,
206
+ lineNumber: 246,
207
+ columnNumber: 5
208
+ }
209
+ }, children);
210
+ };
211
+
212
+ /**
213
+ * Null when no provider is mounted above — a host that has not adopted the
214
+ * section wrapper's collector. `useImageSlot` handles that by resolving each
215
+ * slot on its own, so a section still gets its picture and only loses batching
216
+ * with its siblings.
217
+ */
218
+ exports.ImageSlotProvider = ImageSlotProvider;
219
+ const useImageSlotCollector = () => (0, _react.useContext)(ImageSlotContext);
220
+ exports.useImageSlotCollector = useImageSlotCollector;
221
+ //# sourceMappingURL=ImageSlotContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","_interopRequireWildcard","require","_ComponentDependenciesContext","_diagnosticTypes","_jsxFileName","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","ImageSlotContext","createContext","PENDING","status","ImageSlotProvider","sectionId","children","deps","useOptionalComponentDependencies","resolve","resolveImageSet","statesRef","useRef","listenersRef","Map","pendingRef","sentRef","Set","flushScheduled","aliveRef","useEffect","current","publish","useCallback","next","changed","id","state","entries","push","_listenersRef$current","forEach","fn","markUnavailable","batch","fromEntries","map","s","flush","values","filter","clear","length","add","res","bySlotId","slots","slotId","req","got","imageUrl","slot","err","reportDiagnostic","DIAGNOSTIC_TYPES","IMAGE_RESOLUTION_FAILED","Error","message","String","declare","queueMicrotask","read","subscribe","onChange","_set","delete","size","api","useMemo","createElement","Provider","value","__self","__source","fileName","lineNumber","columnNumber","exports","useImageSlotCollector","useContext"],"sources":["../../../src/context/ImageSlotContext.tsx"],"sourcesContent":["/**\n * The section-scoped image slot collector.\n *\n * A section declares the holes in its layout; it never makes a request. This\n * gathers every declaration made inside one section's subtree, fires ONE\n * `ResolveImageSet` through the injected port, and hands each declaration its\n * answer back by `slotId`. Sections stay ignorant of each other — no section\n * imports another, no ordering rules, no exclusion lists threaded through\n * props — and the network still sees the joint solve that distinctness depends\n * on.\n *\n * WHY THE BOUNDARY IS THE SECTION\n * -------------------------------\n * The resolver solves a maximum-weight one-to-one assignment across everything\n * in one request, so distinctness and palette cohesion are properties of that\n * solve and stop at its edge. Drawing the boundary at the page would buy\n * cross-section distinctness at the cost of making every section's pictures\n * wait on the slowest declaration on the page; drawing it at the section keeps\n * each section independent and accepts that two sections may land on the same\n * photograph. The most visible case — four identical photos in one grid — is\n * still prevented for free, because those slots share a request.\n *\n * WHY THERE IS NO READINESS POLICY\n * --------------------------------\n * There is nothing to wait for. A section component is never constructed from\n * partial markdown: a declared-but-unstreamed section is a `PageSection` with\n * `isSkeleton: true` and `component: null`, and the real element is built only\n * once its body has finished streaming. So a section's props are final the\n * moment it exists, and the flush is simply \"after this section's first render\n * commit\" — one effect, not a scheduler.\n *\n * The same fact bounds the win honestly: because only a *mounted* section\n * declares, no section can ask for a picture before its own body has streamed,\n * including a background whose subject needed nothing from that body.\n */\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n} from 'react';\nimport { useOptionalComponentDependencies } from './ComponentDependenciesContext';\nimport { DIAGNOSTIC_TYPES } from '../component/diagnosticTypes';\nimport type {\n ImageSlotRequest,\n SlotState,\n} from '../image/imageSlotTypes';\n\nexport interface ImageSlotCollector {\n /** Register a slot for this batch. Idempotent per content-derived id. */\n declare: (slot: ImageSlotRequest) => void;\n /** Current state for one slot. Not reactive alone — pair with `subscribe`. */\n read: (slotId: string) => SlotState;\n /** Notify only when THIS slot changes. Returns an unsubscribe. */\n subscribe: (slotId: string, onChange: () => void) => () => void;\n}\n\nconst ImageSlotContext = createContext<ImageSlotCollector | null>(null);\n\n/**\n * Module-level so `read` returns a STABLE reference for a slot nobody has\n * published yet. `useImageSlot` reads this store through\n * `useSyncExternalStore`, which compares snapshots with `Object.is` on every\n * render — a fresh `{ status: 'pending' }` per call reads as \"the store\n * changed\", and React re-renders until it throws \"Maximum update depth\n * exceeded\". Every slot is pending from mount until its batch answers, so a\n * literal here takes down every section that declares one.\n */\nconst PENDING: SlotState = { status: 'pending' };\n\nexport interface ImageSlotProviderProps {\n /** Identifies the section in diagnostics. */\n sectionId: string;\n children: React.ReactNode;\n}\n\n/**\n * Mounted once per section by the host's section wrapper, so every section in\n * every client package gets a collector without opting in.\n */\nexport const ImageSlotProvider: React.FC<ImageSlotProviderProps> = ({\n sectionId,\n children,\n}) => {\n // Optional on purpose: this is mounted by the host around EVERY section, so\n // it sits above trees that may have no dependencies configured at all (a\n // layout test, a story). Throwing there would make the image feature's\n // provider a global requirement for rendering any section.\n const deps = useOptionalComponentDependencies();\n const resolve = deps?.resolveImageSet;\n\n /**\n * Slot state lives in a ref, NOT in React state, and the context value never\n * changes. Holding it in state put it inside the provider's value, so every\n * resolution produced a new value and re-rendered every consumer in the\n * section — free at one slot, twelve re-renders per resolution in a\n * twelve-card grid, which is exactly the shape this boundary exists to serve.\n * Consumers now subscribe to their own id and nothing else.\n */\n const statesRef = useRef<Record<string, SlotState>>({});\n const listenersRef = useRef<Map<string, Set<() => void>>>(new Map());\n\n // Declarations arriving during this commit. A ref, not state: collecting a\n // declaration must not itself cause a render, or every hook that declares\n // would re-render every sibling before the batch is even sent.\n const pendingRef = useRef<Map<string, ImageSlotRequest>>(new Map());\n // Ids already sent. A re-render re-declares the same content-derived id, and\n // that must be a no-op rather than a second request.\n const sentRef = useRef<Set<string>>(new Set());\n const flushScheduled = useRef(false);\n const aliveRef = useRef(true);\n\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n const publish = useCallback((next: Record<string, SlotState>) => {\n const changed: string[] = [];\n for (const [id, state] of Object.entries(next)) {\n if (statesRef.current[id] !== state) {\n statesRef.current[id] = state;\n changed.push(id);\n }\n }\n for (const id of changed) {\n listenersRef.current.get(id)?.forEach((fn) => fn());\n }\n }, []);\n\n const markUnavailable = useCallback(\n (batch: ImageSlotRequest[]) => {\n if (!aliveRef.current) {\n return;\n }\n publish(\n Object.fromEntries(\n batch.map((s) => [s.id, { status: 'unavailable' } as SlotState]),\n ),\n );\n },\n [publish],\n );\n\n const flush = useCallback(async () => {\n flushScheduled.current = false;\n\n const batch = [...pendingRef.current.values()].filter(\n (s) => !sentRef.current.has(s.id),\n );\n pendingRef.current.clear();\n if (batch.length === 0) {\n return;\n }\n batch.forEach((s) => sentRef.current.add(s.id));\n\n // No port means the host has not injected a resolver. Fail the batch closed\n // rather than silently leaving slots pending forever.\n if (!resolve) {\n markUnavailable(batch);\n return;\n }\n\n try {\n const res = await resolve(batch);\n if (!aliveRef.current) {\n return;\n }\n const bySlotId = new Map(res.slots.map((s) => [s.slotId, s]));\n const next: Record<string, SlotState> = {};\n for (const req of batch) {\n const got = bySlotId.get(req.id);\n // A slot the resolver could not fill comes back with a null url. That\n // is `unavailable` to a layout, not a resolved picture.\n next[req.id] =\n got && got.imageUrl\n ? { status: 'resolved', slot: got }\n : { status: 'unavailable' };\n }\n publish(next);\n } catch (err) {\n // Failure is contained to this section: one bad request costs this\n // section its pictures and nothing else on the page.\n deps?.reportDiagnostic?.(\n DIAGNOSTIC_TYPES.IMAGE_RESOLUTION_FAILED,\n sectionId,\n `ResolveImageSet failed for ${batch.length} slot(s): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n markUnavailable(batch);\n }\n }, [resolve, deps, sectionId, publish, markUnavailable]);\n\n const declare = useCallback(\n (slot: ImageSlotRequest) => {\n if (sentRef.current.has(slot.id)) {\n return;\n }\n pendingRef.current.set(slot.id, slot);\n // Flush on a microtask, so every declaration made during this commit —\n // the background, the cards, an inline image — lands in the same batch.\n if (!flushScheduled.current) {\n flushScheduled.current = true;\n queueMicrotask(() => {\n void flush();\n });\n }\n },\n [flush],\n );\n\n const read = useCallback(\n (slotId: string): SlotState => statesRef.current[slotId] ?? PENDING,\n [],\n );\n\n const subscribe = useCallback((slotId: string, onChange: () => void) => {\n let set = listenersRef.current.get(slotId);\n if (!set) {\n set = new Set();\n listenersRef.current.set(slotId, set);\n }\n set.add(onChange);\n return () => {\n set?.delete(onChange);\n if (set && set.size === 0) {\n listenersRef.current.delete(slotId);\n }\n };\n }, []);\n\n // Stable for the provider's lifetime: every member is a `useCallback` with no\n // reactive deps, so mounting this around a section costs its consumers\n // nothing after first render.\n const api = useMemo<ImageSlotCollector>(\n () => ({ declare, read, subscribe }),\n [declare, read, subscribe],\n );\n\n return (\n <ImageSlotContext.Provider value={api}>{children}</ImageSlotContext.Provider>\n );\n};\n\n/**\n * Null when no provider is mounted above — a host that has not adopted the\n * section wrapper's collector. `useImageSlot` handles that by resolving each\n * slot on its own, so a section still gets its picture and only loses batching\n * with its siblings.\n */\nexport const useImageSlotCollector = (): ImageSlotCollector | null =>\n useContext(ImageSlotContext);\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.useImageSlot = useImageSlot;
5
+ var _react = require("react");
6
+ var _ComponentDependenciesContext = require("../context/ComponentDependenciesContext");
7
+ var _ImageSlotContext = require("../context/ImageSlotContext");
8
+ var _composeSemantic = require("../image/composeSemantic");
9
+ /**
10
+ * `useImageSlot` — the whole section-facing surface for images.
11
+ *
12
+ * A section describes the hole in its layout and what the picture should be
13
+ * about. It does not name a request, a batch, or a set boundary; that omission
14
+ * is deliberate and load-bearing, because it is what lets the boundary widen
15
+ * from the section to the page later without touching a single component.
16
+ *
17
+ * Replaces the per-image `useResolvedImageSources` path, whose endpoint was
18
+ * removed and, once restored, answers with an empty list.
19
+ */
20
+
21
+ /** The `web5://image/` prefix an author's token carries. */
22
+ const TOKEN_PREFIX = 'web5://image/';
23
+ const decodeToken = raw => {
24
+ const body = raw.startsWith(TOKEN_PREFIX) ? raw.slice(TOKEN_PREFIX.length) : raw;
25
+ try {
26
+ return decodeURIComponent(body).trim();
27
+ } catch {
28
+ return body.trim();
29
+ }
30
+ };
31
+
32
+ /**
33
+ * Content-derived, never an index and never a React key. A re-render
34
+ * re-declares the same id so it does not re-request, and reordering items
35
+ * cannot remap pictures onto different cards — which an index-based id would
36
+ * do, visibly. Kept short and readable rather than cryptographic: it only has
37
+ * to be stable, and unique within one request.
38
+ */
39
+ const slotIdFor = (role, subjectKey, ratio, width) => {
40
+ let h = 0;
41
+ const material = `${role}|${subjectKey}|${ratio.toFixed(4)}|${width}`;
42
+ for (let i = 0; i < material.length; i++) {
43
+ h = Math.imul(31, h) + material.charCodeAt(i) | 0;
44
+ }
45
+ return `${role}-${(h >>> 0).toString(36)}`;
46
+ };
47
+
48
+ /** Module-level so `getSnapshot` returns a STABLE reference. Returning a fresh
49
+ * `{ status: 'unavailable' }` on every call makes `useSyncExternalStore`
50
+ * believe the store changed on every check, and it re-renders forever. */
51
+ const SETTLED_UNAVAILABLE = {
52
+ status: 'unavailable'
53
+ };
54
+ function useImageSlot(options) {
55
+ var _state$slot$visualMet;
56
+ const {
57
+ role = 'background',
58
+ ratio,
59
+ width = 800,
60
+ subject,
61
+ fallbackUrl,
62
+ enabled = true
63
+ } = options;
64
+
65
+ // Optional for the same reason the provider's is: a section rendered without
66
+ // dependencies configured should lose its picture, not throw.
67
+ const deps = (0, _ComponentDependenciesContext.useOptionalComponentDependencies)();
68
+ const collector = (0, _ImageSlotContext.useImageSlotCollector)();
69
+ const resolve = deps == null ? void 0 : deps.resolveImageSet;
70
+
71
+ /** The declaration, derived purely from props. */
72
+ const request = (0, _react.useMemo)(() => {
73
+ if (!enabled) {
74
+ return null;
75
+ }
76
+ if (subject.from === 'entity') {
77
+ var _subject$entityId;
78
+ const entityId = (_subject$entityId = subject.entityId) == null ? void 0 : _subject$entityId.trim();
79
+ if (!entityId) {
80
+ return null;
81
+ }
82
+ return {
83
+ id: slotIdFor(role, `e:${entityId}`, ratio, width),
84
+ kind: 'IMAGE_SLOT_KIND_ENTITY',
85
+ ratio,
86
+ renderWidthPx: width,
87
+ entityId
88
+ };
89
+ }
90
+ const semantic = subject.from === 'token' ? decodeToken(subject.token ?? '') : (0, _composeSemantic.composeSemantic)({
91
+ title: subject.title,
92
+ lead: subject.lead
93
+ });
94
+
95
+ // Too thin to retrieve on is not a query worth sending. `composeSemantic`
96
+ // returns empty rather than guessing, and a token can be empty too
97
+ // (`![alt](web5://image/)`).
98
+ if (!semantic) {
99
+ return null;
100
+ }
101
+ return {
102
+ id: slotIdFor(role, `s:${semantic}`, ratio, width),
103
+ kind: 'IMAGE_SLOT_KIND_EDITORIAL',
104
+ ratio,
105
+ renderWidthPx: width,
106
+ semantic
107
+ };
108
+ }, [enabled, role, ratio, width, subject]);
109
+
110
+ // Used only when no collector is mounted above — a host that has not adopted
111
+ // the section wrapper's provider. Same answer, no batching with siblings.
112
+ const [soloState, setSoloState] = (0, _react.useState)({
113
+ status: 'pending'
114
+ });
115
+ const soloSentRef = (0, _react.useRef)(null);
116
+ (0, _react.useEffect)(() => {
117
+ if (!request) {
118
+ return;
119
+ }
120
+ if (collector) {
121
+ collector.declare(request);
122
+ return;
123
+ }
124
+ if (soloSentRef.current === request.id) {
125
+ return;
126
+ }
127
+ soloSentRef.current = request.id;
128
+ if (!resolve) {
129
+ setSoloState({
130
+ status: 'unavailable'
131
+ });
132
+ return;
133
+ }
134
+ let alive = true;
135
+ resolve([request]).then(res => {
136
+ if (!alive) {
137
+ return;
138
+ }
139
+ const got = res.slots.find(s => s.slotId === request.id);
140
+ setSoloState(got && got.imageUrl ? {
141
+ status: 'resolved',
142
+ slot: got
143
+ } : {
144
+ status: 'unavailable'
145
+ });
146
+ }).catch(() => {
147
+ if (alive) {
148
+ setSoloState({
149
+ status: 'unavailable'
150
+ });
151
+ }
152
+ });
153
+ return () => {
154
+ alive = false;
155
+ };
156
+ }, [collector, resolve, request]);
157
+
158
+ // Subscribe to THIS slot only. `useSyncExternalStore` is the right shape
159
+ // here: the collector holds slot state outside React so resolving one slot
160
+ // does not re-render its siblings, and this is the supported way to read such
161
+ // a store without tearing. Memoised on the slot id so a re-render does not
162
+ // churn listeners.
163
+ const slotId = request == null ? void 0 : request.id;
164
+ const subscribe = (0, _react.useMemo)(() => onChange => collector && slotId ? collector.subscribe(slotId, onChange) : () => {}, [collector, slotId]);
165
+ const getSnapshot = (0, _react.useMemo)(() => () => collector && slotId ? collector.read(slotId) : SETTLED_UNAVAILABLE, [collector, slotId]);
166
+ const collectorState = (0, _react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
167
+ const state = !request ?
168
+ // Not asking (disabled, or nothing worth retrieving on) is settled, not
169
+ // pending — a layout must not wait for an answer that will never come.
170
+ SETTLED_UNAVAILABLE : collector ? collectorState : soloState;
171
+ const resolvedUrl = state.status === 'resolved' ? state.slot.imageUrl : null;
172
+ return {
173
+ state,
174
+ // Pending deliberately renders nothing rather than the fallback: swapping
175
+ // fallback -> resolved a moment later is a visible flicker, and the layouts
176
+ // that use this already have a ground colour for the pending case.
177
+ url: state.status === 'pending' ? null : resolvedUrl ?? fallbackUrl ?? null,
178
+ backgroundColor: state.status === 'resolved' ? ((_state$slot$visualMet = state.slot.visualMetadata) == null ? void 0 : _state$slot$visualMet.backgroundColor) || undefined : undefined,
179
+ isPending: state.status === 'pending'
180
+ };
181
+ }
182
+ //# sourceMappingURL=useImageSlot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_ComponentDependenciesContext","_ImageSlotContext","_composeSemantic","TOKEN_PREFIX","decodeToken","raw","body","startsWith","slice","length","decodeURIComponent","trim","slotIdFor","role","subjectKey","ratio","width","h","material","toFixed","i","Math","imul","charCodeAt","toString","SETTLED_UNAVAILABLE","status","useImageSlot","options","_state$slot$visualMet","subject","fallbackUrl","enabled","deps","useOptionalComponentDependencies","collector","useImageSlotCollector","resolve","resolveImageSet","request","useMemo","from","_subject$entityId","entityId","id","kind","renderWidthPx","semantic","token","composeSemantic","title","lead","soloState","setSoloState","useState","soloSentRef","useRef","useEffect","declare","current","alive","then","res","got","slots","find","s","slotId","imageUrl","slot","catch","subscribe","onChange","getSnapshot","read","collectorState","useSyncExternalStore","state","resolvedUrl","url","backgroundColor","visualMetadata","undefined","isPending"],"sources":["../../../src/hooks/useImageSlot.ts"],"sourcesContent":["/**\n * `useImageSlot` — the whole section-facing surface for images.\n *\n * A section describes the hole in its layout and what the picture should be\n * about. It does not name a request, a batch, or a set boundary; that omission\n * is deliberate and load-bearing, because it is what lets the boundary widen\n * from the section to the page later without touching a single component.\n *\n * Replaces the per-image `useResolvedImageSources` path, whose endpoint was\n * removed and, once restored, answers with an empty list.\n */\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useOptionalComponentDependencies } from '../context/ComponentDependenciesContext';\nimport { useImageSlotCollector } from '../context/ImageSlotContext';\nimport { composeSemantic } from '../image/composeSemantic';\nimport type {\n ImageSlotRequest,\n ImageSubject,\n SlotState,\n} from '../image/imageSlotTypes';\n\nexport interface UseImageSlotOptions {\n /** What the picture is for. Only affects how a layout reads the result. */\n role?: 'background' | 'inline' | 'card';\n /** width / height of the hole — a constant of the layout, never measured. */\n ratio: number;\n /**\n * Delivered pixel width. Pass a value off the breakpoint ladder, never a\n * measurement: a bucket keeps the slot id stable across a resize, keeps two\n * sections at 798px and 802px one cacheable shape, and gives the CDN a small\n * set of urls it can cache across visitors.\n */\n width?: 400 | 800 | 1200 | 1600;\n subject: ImageSubject;\n /** Rendered instead when the resolver cannot fill the slot — typically an\n * entity's own payload image. */\n fallbackUrl?: string;\n /**\n * Declare nothing when false. For the case where the layout already HAS its\n * picture — a direct url in props, a story fixture — and resolving would be a\n * request whose answer is thrown away. Default true.\n */\n enabled?: boolean;\n}\n\nexport interface ResolvedImage {\n state: SlotState;\n /** What to actually render — the resolved url, else the caller's fallback,\n * else nothing. Saves every layout writing the same three-way check. */\n url: string | null;\n /** Paint the letterbox with this while an image is showing. */\n backgroundColor?: string;\n isPending: boolean;\n}\n\n/** The `web5://image/` prefix an author's token carries. */\nconst TOKEN_PREFIX = 'web5://image/';\n\nconst decodeToken = (raw: string): string => {\n const body = raw.startsWith(TOKEN_PREFIX)\n ? raw.slice(TOKEN_PREFIX.length)\n : raw;\n try {\n return decodeURIComponent(body).trim();\n } catch {\n return body.trim();\n }\n};\n\n/**\n * Content-derived, never an index and never a React key. A re-render\n * re-declares the same id so it does not re-request, and reordering items\n * cannot remap pictures onto different cards — which an index-based id would\n * do, visibly. Kept short and readable rather than cryptographic: it only has\n * to be stable, and unique within one request.\n */\nconst slotIdFor = (\n role: string,\n subjectKey: string,\n ratio: number,\n width: number,\n): string => {\n let h = 0;\n const material = `${role}|${subjectKey}|${ratio.toFixed(4)}|${width}`;\n for (let i = 0; i < material.length; i++) {\n h = (Math.imul(31, h) + material.charCodeAt(i)) | 0;\n }\n return `${role}-${(h >>> 0).toString(36)}`;\n};\n\n/** Module-level so `getSnapshot` returns a STABLE reference. Returning a fresh\n * `{ status: 'unavailable' }` on every call makes `useSyncExternalStore`\n * believe the store changed on every check, and it re-renders forever. */\nconst SETTLED_UNAVAILABLE: SlotState = { status: 'unavailable' };\n\nexport function useImageSlot(options: UseImageSlotOptions): ResolvedImage {\n const {\n role = 'background',\n ratio,\n width = 800,\n subject,\n fallbackUrl,\n enabled = true,\n } = options;\n\n // Optional for the same reason the provider's is: a section rendered without\n // dependencies configured should lose its picture, not throw.\n const deps = useOptionalComponentDependencies();\n const collector = useImageSlotCollector();\n const resolve = deps?.resolveImageSet;\n\n /** The declaration, derived purely from props. */\n const request = useMemo<ImageSlotRequest | null>(() => {\n if (!enabled) {\n return null;\n }\n\n if (subject.from === 'entity') {\n const entityId = subject.entityId?.trim();\n if (!entityId) {\n return null;\n }\n return {\n id: slotIdFor(role, `e:${entityId}`, ratio, width),\n kind: 'IMAGE_SLOT_KIND_ENTITY',\n ratio,\n renderWidthPx: width,\n entityId,\n };\n }\n\n const semantic =\n subject.from === 'token'\n ? decodeToken(subject.token ?? '')\n : composeSemantic({ title: subject.title, lead: subject.lead });\n\n // Too thin to retrieve on is not a query worth sending. `composeSemantic`\n // returns empty rather than guessing, and a token can be empty too\n // (`![alt](web5://image/)`).\n if (!semantic) {\n return null;\n }\n\n return {\n id: slotIdFor(role, `s:${semantic}`, ratio, width),\n kind: 'IMAGE_SLOT_KIND_EDITORIAL',\n ratio,\n renderWidthPx: width,\n semantic,\n };\n }, [enabled, role, ratio, width, subject]);\n\n // Used only when no collector is mounted above — a host that has not adopted\n // the section wrapper's provider. Same answer, no batching with siblings.\n const [soloState, setSoloState] = useState<SlotState>({ status: 'pending' });\n const soloSentRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!request) {\n return;\n }\n if (collector) {\n collector.declare(request);\n return;\n }\n if (soloSentRef.current === request.id) {\n return;\n }\n soloSentRef.current = request.id;\n\n if (!resolve) {\n setSoloState({ status: 'unavailable' });\n return;\n }\n\n let alive = true;\n resolve([request])\n .then((res) => {\n if (!alive) {\n return;\n }\n const got = res.slots.find((s) => s.slotId === request.id);\n setSoloState(\n got && got.imageUrl\n ? { status: 'resolved', slot: got }\n : { status: 'unavailable' },\n );\n })\n .catch(() => {\n if (alive) {\n setSoloState({ status: 'unavailable' });\n }\n });\n\n return () => {\n alive = false;\n };\n }, [collector, resolve, request]);\n\n // Subscribe to THIS slot only. `useSyncExternalStore` is the right shape\n // here: the collector holds slot state outside React so resolving one slot\n // does not re-render its siblings, and this is the supported way to read such\n // a store without tearing. Memoised on the slot id so a re-render does not\n // churn listeners.\n const slotId = request?.id;\n const subscribe = useMemo(\n () => (onChange: () => void) =>\n collector && slotId ? collector.subscribe(slotId, onChange) : () => {},\n [collector, slotId],\n );\n const getSnapshot = useMemo(\n () => () =>\n collector && slotId ? collector.read(slotId) : SETTLED_UNAVAILABLE,\n [collector, slotId],\n );\n const collectorState = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getSnapshot,\n );\n\n const state: SlotState = !request\n ? // Not asking (disabled, or nothing worth retrieving on) is settled, not\n // pending — a layout must not wait for an answer that will never come.\n SETTLED_UNAVAILABLE\n : collector\n ? collectorState\n : soloState;\n\n const resolvedUrl = state.status === 'resolved' ? state.slot.imageUrl : null;\n\n return {\n state,\n // Pending deliberately renders nothing rather than the fallback: swapping\n // fallback -> resolved a moment later is a visible flicker, and the layouts\n // that use this already have a ground colour for the pending case.\n url:\n state.status === 'pending' ? null : (resolvedUrl ?? fallbackUrl ?? null),\n backgroundColor:\n state.status === 'resolved'\n ? state.slot.visualMetadata?.backgroundColor || undefined\n : undefined,\n isPending: state.status === 'pending',\n };\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.composeSemantic = composeSemantic;
5
+ /**
6
+ * Turn a section's own words into a retrieval phrase.
7
+ *
8
+ * The section hands over its RAW props — a title, a lead paragraph — not a
9
+ * query string. Composing is this module's job, in one place, because the two
10
+ * call sites that do it today invent their own phrase, do not agree with each
11
+ * other, and neither is tested:
12
+ *
13
+ * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144
14
+ * `web5://image/${title}` — raw, so `**Nike** collections`
15
+ * is searched WITH its asterisks
16
+ * w5-client-circana/src/components/sections/ResearchSection.tsx:68
17
+ * `web5://image/${encodeURIComponent(title)}` — same idea, escaped
18
+ *
19
+ * Centralising it is most of what the text-derived case is worth.
20
+ *
21
+ * Lifted from the seed client package (ADR 0225): it started there because the
22
+ * CDN build externalises core, so a core change could not be seen through
23
+ * `?clientBundleUrl=` until it had been published. 0223 always assigned it
24
+ * here, and this is that move.
25
+ */
26
+
27
+ /** Longer than this and the tail stops helping retrieval and starts diluting it. */
28
+ const MAX_LENGTH = 120;
29
+
30
+ /** Below this there is no query worth sending — better to render no picture
31
+ * than to retrieve on a word like "More". */
32
+ const MIN_LENGTH = 3;
33
+ const stripMarkdown = raw => raw
34
+ // images before links: ![alt](src) would otherwise leave a stray `!`
35
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
36
+ // emphasis / strong / strike / inline code — keep the words, drop the marks
37
+ .replace(/(\*\*\*|\*\*|\*|___|__|_|~~|`)/g, '')
38
+ // leading heading hashes and blockquote marks
39
+ .replace(/^\s*#{1,6}\s*/gm, '').replace(/^\s*>\s?/gm, '')
40
+ // html tags an author or the parser may have left behind
41
+ .replace(/<[^>]+>/g, ' ');
42
+ const tidy = raw => stripMarkdown(raw).replace(/\s+/g, ' ').trim()
43
+ // trailing punctuation is noise in a retrieval phrase
44
+ .replace(/[\s.,;:!?—–-]+$/g, '').trim();
45
+
46
+ /**
47
+ * Clamp without cutting a word in half. Slicing at a fixed length leaves
48
+ * fragments like "the kinds of product" — observed in the first real run — and
49
+ * a dangling partial word is noise in a retrieval phrase, not a shorter
50
+ * version of it. Falls back to a hard slice only when the budget cannot fit
51
+ * even one word.
52
+ */
53
+ const clampToWord = (raw, max) => {
54
+ if (raw.length <= max) {
55
+ return raw;
56
+ }
57
+ // +1 so a boundary landing exactly on the budget still counts as a break.
58
+ const cut = raw.slice(0, max + 1);
59
+ const lastBreak = cut.lastIndexOf(' ');
60
+ const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);
61
+ return out.replace(/[\s.,;:!?—–-]+$/g, '').trim();
62
+ };
63
+
64
+ /** The first clause carries the subject; what follows is usually qualification. */
65
+ const firstClause = raw => {
66
+ const m = raw.match(/^[^.!?;]+/);
67
+ return (m ? m[0] : raw).trim();
68
+ };
69
+ /**
70
+ * Returns the phrase, or `''` when what survives is too thin to retrieve on —
71
+ * the caller then declares no slot at all rather than sending a bad query.
72
+ */
73
+ function composeSemantic({
74
+ title,
75
+ lead,
76
+ sectionSemantic
77
+ }) {
78
+ // A mission, when there is one, is the whole answer.
79
+ const mission = tidy(sectionSemantic ?? '');
80
+ if (mission.length >= MIN_LENGTH) {
81
+ return clampToWord(mission, MAX_LENGTH);
82
+ }
83
+ const head = tidy(title ?? '');
84
+ const body = firstClause(tidy(lead ?? ''));
85
+ const joined = [head, body].filter(Boolean).join('. ');
86
+ if (joined.length < MIN_LENGTH) return '';
87
+ if (joined.length <= MAX_LENGTH) return joined;
88
+
89
+ // Over budget: keep the title whole if it fits, since it is the stronger
90
+ // signal, and spend whatever is left on the lead.
91
+ if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);
92
+ return clampToWord(joined, MAX_LENGTH);
93
+ }
94
+ //# sourceMappingURL=composeSemantic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","tidy","trim","clampToWord","max","length","cut","slice","lastBreak","lastIndexOf","out","firstClause","m","match","composeSemantic","title","lead","sectionSemantic","mission","head","body","joined","filter","Boolean","join"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n */\n\n/** Longer than this and the tail stops helping retrieval and starts diluting it. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links: ![alt](src) would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst tidy = (raw: string): string =>\n stripMarkdown(raw)\n .replace(/\\s+/g, ' ')\n .trim()\n // trailing punctuation is noise in a retrieval phrase\n .replace(/[\\s.,;:!?—–-]+$/g, '')\n .trim();\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return out.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n};\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return clampToWord(mission, MAX_LENGTH);\n }\n\n const head = tidy(title ?? '');\n const body = firstClause(tidy(lead ?? ''));\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n if (joined.length <= MAX_LENGTH) return joined;\n\n // Over budget: keep the title whole if it fits, since it is the stronger\n // signal, and spend whatever is left on the lead.\n if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}