@wix/web5-core 1.63.25 → 1.63.27
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/dist/cjs/components/ui/UserQuery.css +10 -7
- package/dist/cjs/components/ui/UserQuery.js +17 -33
- package/dist/cjs/components/ui/UserQuery.js.map +1 -1
- package/dist/cjs/context/ComponentDependenciesContext.js +15 -1
- package/dist/cjs/context/ComponentDependenciesContext.js.map +1 -1
- package/dist/cjs/context/ImageSlotContext.js +210 -0
- package/dist/cjs/context/ImageSlotContext.js.map +1 -0
- package/dist/cjs/hooks/useImageSlot.js +182 -0
- package/dist/cjs/hooks/useImageSlot.js.map +1 -0
- package/dist/cjs/image/composeSemantic.js +94 -0
- package/dist/cjs/image/composeSemantic.js.map +1 -0
- package/dist/cjs/image/imageSlotTypes.js +4 -0
- package/dist/cjs/image/imageSlotTypes.js.map +1 -0
- package/dist/cjs/index.js +11 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/dependencies.js.map +1 -1
- package/dist/esm/components/ui/UserQuery.css +10 -7
- package/dist/esm/components/ui/UserQuery.js +5 -9
- package/dist/esm/components/ui/UserQuery.js.map +1 -1
- package/dist/esm/context/ComponentDependenciesContext.js +13 -0
- package/dist/esm/context/ComponentDependenciesContext.js.map +1 -1
- package/dist/esm/context/ImageSlotContext.js +197 -0
- package/dist/esm/context/ImageSlotContext.js.map +1 -0
- package/dist/esm/hooks/useImageSlot.js +177 -0
- package/dist/esm/hooks/useImageSlot.js.map +1 -0
- package/dist/esm/image/composeSemantic.js +91 -0
- package/dist/esm/image/composeSemantic.js.map +1 -0
- package/dist/esm/image/imageSlotTypes.js +2 -0
- package/dist/esm/image/imageSlotTypes.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/dependencies.js.map +1 -1
- package/dist/types/components/ui/UserQuery.d.ts.map +1 -1
- package/dist/types/context/ComponentDependenciesContext.d.ts +10 -0
- package/dist/types/context/ComponentDependenciesContext.d.ts.map +1 -1
- package/dist/types/context/ImageSlotContext.d.ts +63 -0
- package/dist/types/context/ImageSlotContext.d.ts.map +1 -0
- package/dist/types/hooks/useImageSlot.d.ts +35 -0
- package/dist/types/hooks/useImageSlot.d.ts.map +1 -0
- package/dist/types/image/composeSemantic.d.ts +37 -0
- package/dist/types/image/composeSemantic.d.ts.map +1 -0
- package/dist/types/image/imageSlotTypes.d.ts +112 -0
- package/dist/types/image/imageSlotTypes.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types/dependencies.d.ts +11 -0
- package/dist/types/types/dependencies.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/ui/UserQuery.css +10 -7
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The section-scoped image slot collector.
|
|
3
|
+
*
|
|
4
|
+
* A section declares the holes in its layout; it never makes a request. This
|
|
5
|
+
* gathers every declaration made inside one section's subtree, fires ONE
|
|
6
|
+
* `ResolveImageSet` through the injected port, and hands each declaration its
|
|
7
|
+
* answer back by `slotId`. Sections stay ignorant of each other — no section
|
|
8
|
+
* imports another, no ordering rules, no exclusion lists threaded through
|
|
9
|
+
* props — and the network still sees the joint solve that distinctness depends
|
|
10
|
+
* on.
|
|
11
|
+
*
|
|
12
|
+
* WHY THE BOUNDARY IS THE SECTION
|
|
13
|
+
* -------------------------------
|
|
14
|
+
* The resolver solves a maximum-weight one-to-one assignment across everything
|
|
15
|
+
* in one request, so distinctness and palette cohesion are properties of that
|
|
16
|
+
* solve and stop at its edge. Drawing the boundary at the page would buy
|
|
17
|
+
* cross-section distinctness at the cost of making every section's pictures
|
|
18
|
+
* wait on the slowest declaration on the page; drawing it at the section keeps
|
|
19
|
+
* each section independent and accepts that two sections may land on the same
|
|
20
|
+
* photograph. The most visible case — four identical photos in one grid — is
|
|
21
|
+
* still prevented for free, because those slots share a request.
|
|
22
|
+
*
|
|
23
|
+
* WHY THERE IS NO READINESS POLICY
|
|
24
|
+
* --------------------------------
|
|
25
|
+
* There is nothing to wait for. A section component is never constructed from
|
|
26
|
+
* partial markdown: a declared-but-unstreamed section is a `PageSection` with
|
|
27
|
+
* `isSkeleton: true` and `component: null`, and the real element is built only
|
|
28
|
+
* once its body has finished streaming. So a section's props are final the
|
|
29
|
+
* moment it exists, and the flush is simply "after this section's first render
|
|
30
|
+
* commit" — one effect, not a scheduler.
|
|
31
|
+
*
|
|
32
|
+
* The same fact bounds the win honestly: because only a *mounted* section
|
|
33
|
+
* declares, no section can ask for a picture before its own body has streamed,
|
|
34
|
+
* including a background whose subject needed nothing from that body.
|
|
35
|
+
*/
|
|
36
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
37
|
+
import { useOptionalComponentDependencies } from './ComponentDependenciesContext.js';
|
|
38
|
+
import { DIAGNOSTIC_TYPES } from '../component/diagnosticTypes.js';
|
|
39
|
+
const ImageSlotContext = /*#__PURE__*/createContext(null);
|
|
40
|
+
/**
|
|
41
|
+
* Mounted once per section by the host's section wrapper, so every section in
|
|
42
|
+
* every client package gets a collector without opting in.
|
|
43
|
+
*/
|
|
44
|
+
export const ImageSlotProvider = _ref => {
|
|
45
|
+
let {
|
|
46
|
+
sectionId,
|
|
47
|
+
children
|
|
48
|
+
} = _ref;
|
|
49
|
+
// Optional on purpose: this is mounted by the host around EVERY section, so
|
|
50
|
+
// it sits above trees that may have no dependencies configured at all (a
|
|
51
|
+
// layout test, a story). Throwing there would make the image feature's
|
|
52
|
+
// provider a global requirement for rendering any section.
|
|
53
|
+
const deps = useOptionalComponentDependencies();
|
|
54
|
+
const resolve = deps == null ? void 0 : deps.resolveImageSet;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Slot state lives in a ref, NOT in React state, and the context value never
|
|
58
|
+
* changes. Holding it in state put it inside the provider's value, so every
|
|
59
|
+
* resolution produced a new value and re-rendered every consumer in the
|
|
60
|
+
* section — free at one slot, twelve re-renders per resolution in a
|
|
61
|
+
* twelve-card grid, which is exactly the shape this boundary exists to serve.
|
|
62
|
+
* Consumers now subscribe to their own id and nothing else.
|
|
63
|
+
*/
|
|
64
|
+
const statesRef = useRef({});
|
|
65
|
+
const listenersRef = useRef(new Map());
|
|
66
|
+
|
|
67
|
+
// Declarations arriving during this commit. A ref, not state: collecting a
|
|
68
|
+
// declaration must not itself cause a render, or every hook that declares
|
|
69
|
+
// would re-render every sibling before the batch is even sent.
|
|
70
|
+
const pendingRef = useRef(new Map());
|
|
71
|
+
// Ids already sent. A re-render re-declares the same content-derived id, and
|
|
72
|
+
// that must be a no-op rather than a second request.
|
|
73
|
+
const sentRef = useRef(new Set());
|
|
74
|
+
const flushScheduled = useRef(false);
|
|
75
|
+
const aliveRef = useRef(true);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
aliveRef.current = true;
|
|
78
|
+
return () => {
|
|
79
|
+
aliveRef.current = false;
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
const publish = useCallback(next => {
|
|
83
|
+
const changed = [];
|
|
84
|
+
for (const [id, state] of Object.entries(next)) {
|
|
85
|
+
if (statesRef.current[id] !== state) {
|
|
86
|
+
statesRef.current[id] = state;
|
|
87
|
+
changed.push(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const id of changed) {
|
|
91
|
+
var _listenersRef$current;
|
|
92
|
+
(_listenersRef$current = listenersRef.current.get(id)) == null || _listenersRef$current.forEach(fn => fn());
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
const markUnavailable = useCallback(batch => {
|
|
96
|
+
if (!aliveRef.current) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
publish(Object.fromEntries(batch.map(s => [s.id, {
|
|
100
|
+
status: 'unavailable'
|
|
101
|
+
}])));
|
|
102
|
+
}, [publish]);
|
|
103
|
+
const flush = useCallback(async () => {
|
|
104
|
+
flushScheduled.current = false;
|
|
105
|
+
const batch = [...pendingRef.current.values()].filter(s => !sentRef.current.has(s.id));
|
|
106
|
+
pendingRef.current.clear();
|
|
107
|
+
if (batch.length === 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
batch.forEach(s => sentRef.current.add(s.id));
|
|
111
|
+
|
|
112
|
+
// No port means the host has not injected a resolver. Fail the batch closed
|
|
113
|
+
// rather than silently leaving slots pending forever.
|
|
114
|
+
if (!resolve) {
|
|
115
|
+
markUnavailable(batch);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const res = await resolve(batch);
|
|
120
|
+
if (!aliveRef.current) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const bySlotId = new Map(res.slots.map(s => [s.slotId, s]));
|
|
124
|
+
const next = {};
|
|
125
|
+
for (const req of batch) {
|
|
126
|
+
const got = bySlotId.get(req.id);
|
|
127
|
+
// A slot the resolver could not fill comes back with a null url. That
|
|
128
|
+
// is `unavailable` to a layout, not a resolved picture.
|
|
129
|
+
next[req.id] = got && got.imageUrl ? {
|
|
130
|
+
status: 'resolved',
|
|
131
|
+
slot: got
|
|
132
|
+
} : {
|
|
133
|
+
status: 'unavailable'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
publish(next);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// Failure is contained to this section: one bad request costs this
|
|
139
|
+
// section its pictures and nothing else on the page.
|
|
140
|
+
deps == null || deps.reportDiagnostic == null || deps.reportDiagnostic(DIAGNOSTIC_TYPES.IMAGE_RESOLUTION_FAILED, sectionId, `ResolveImageSet failed for ${batch.length} slot(s): ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
markUnavailable(batch);
|
|
142
|
+
}
|
|
143
|
+
}, [resolve, deps, sectionId, publish, markUnavailable]);
|
|
144
|
+
const declare = useCallback(slot => {
|
|
145
|
+
if (sentRef.current.has(slot.id)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
pendingRef.current.set(slot.id, slot);
|
|
149
|
+
// Flush on a microtask, so every declaration made during this commit —
|
|
150
|
+
// the background, the cards, an inline image — lands in the same batch.
|
|
151
|
+
if (!flushScheduled.current) {
|
|
152
|
+
flushScheduled.current = true;
|
|
153
|
+
queueMicrotask(() => {
|
|
154
|
+
void flush();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}, [flush]);
|
|
158
|
+
const read = useCallback(slotId => statesRef.current[slotId] ?? {
|
|
159
|
+
status: 'pending'
|
|
160
|
+
}, []);
|
|
161
|
+
const subscribe = useCallback((slotId, onChange) => {
|
|
162
|
+
let set = listenersRef.current.get(slotId);
|
|
163
|
+
if (!set) {
|
|
164
|
+
set = new Set();
|
|
165
|
+
listenersRef.current.set(slotId, set);
|
|
166
|
+
}
|
|
167
|
+
set.add(onChange);
|
|
168
|
+
return () => {
|
|
169
|
+
var _set;
|
|
170
|
+
(_set = set) == null || _set.delete(onChange);
|
|
171
|
+
if (set && set.size === 0) {
|
|
172
|
+
listenersRef.current.delete(slotId);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
// Stable for the provider's lifetime: every member is a `useCallback` with no
|
|
178
|
+
// reactive deps, so mounting this around a section costs its consumers
|
|
179
|
+
// nothing after first render.
|
|
180
|
+
const api = useMemo(() => ({
|
|
181
|
+
declare,
|
|
182
|
+
read,
|
|
183
|
+
subscribe
|
|
184
|
+
}), [declare, read, subscribe]);
|
|
185
|
+
return /*#__PURE__*/React.createElement(ImageSlotContext.Provider, {
|
|
186
|
+
value: api
|
|
187
|
+
}, children);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Null when no provider is mounted above — a host that has not adopted the
|
|
192
|
+
* section wrapper's collector. `useImageSlot` handles that by resolving each
|
|
193
|
+
* slot on its own, so a section still gets its picture and only loses batching
|
|
194
|
+
* with its siblings.
|
|
195
|
+
*/
|
|
196
|
+
export const useImageSlotCollector = () => useContext(ImageSlotContext);
|
|
197
|
+
//# sourceMappingURL=ImageSlotContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","createContext","useCallback","useContext","useEffect","useMemo","useRef","useOptionalComponentDependencies","DIAGNOSTIC_TYPES","ImageSlotContext","ImageSlotProvider","_ref","sectionId","children","deps","resolve","resolveImageSet","statesRef","listenersRef","Map","pendingRef","sentRef","Set","flushScheduled","aliveRef","current","publish","next","changed","id","state","Object","entries","push","_listenersRef$current","get","forEach","fn","markUnavailable","batch","fromEntries","map","s","status","flush","values","filter","has","clear","length","add","res","bySlotId","slots","slotId","req","got","imageUrl","slot","err","reportDiagnostic","IMAGE_RESOLUTION_FAILED","Error","message","String","declare","set","queueMicrotask","read","subscribe","onChange","_set","delete","size","api","createElement","Provider","value","useImageSlotCollector"],"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\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 =>\n statesRef.current[slotId] ?? { status: '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":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAOA,KAAK,IACVC,aAAa,EACbC,WAAW,EACXC,UAAU,EACVC,SAAS,EACTC,OAAO,EACPC,MAAM,QACD,OAAO;AACd,SAASC,gCAAgC,QAAQ,gCAAgC;AACjF,SAASC,gBAAgB,QAAQ,8BAA8B;AAe/D,MAAMC,gBAAgB,gBAAGR,aAAa,CAA4B,IAAI,CAAC;AAQvE;AACA;AACA;AACA;AACA,OAAO,MAAMS,iBAAmD,GAAGC,IAAA,IAG7D;EAAA,IAH8D;IAClEC,SAAS;IACTC;EACF,CAAC,GAAAF,IAAA;EACC;EACA;EACA;EACA;EACA,MAAMG,IAAI,GAAGP,gCAAgC,CAAC,CAAC;EAC/C,MAAMQ,OAAO,GAAGD,IAAI,oBAAJA,IAAI,CAAEE,eAAe;;EAErC;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACE,MAAMC,SAAS,GAAGX,MAAM,CAA4B,CAAC,CAAC,CAAC;EACvD,MAAMY,YAAY,GAAGZ,MAAM,CAA+B,IAAIa,GAAG,CAAC,CAAC,CAAC;;EAEpE;EACA;EACA;EACA,MAAMC,UAAU,GAAGd,MAAM,CAAgC,IAAIa,GAAG,CAAC,CAAC,CAAC;EACnE;EACA;EACA,MAAME,OAAO,GAAGf,MAAM,CAAc,IAAIgB,GAAG,CAAC,CAAC,CAAC;EAC9C,MAAMC,cAAc,GAAGjB,MAAM,CAAC,KAAK,CAAC;EACpC,MAAMkB,QAAQ,GAAGlB,MAAM,CAAC,IAAI,CAAC;EAE7BF,SAAS,CAAC,MAAM;IACdoB,QAAQ,CAACC,OAAO,GAAG,IAAI;IACvB,OAAO,MAAM;MACXD,QAAQ,CAACC,OAAO,GAAG,KAAK;IAC1B,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMC,OAAO,GAAGxB,WAAW,CAAEyB,IAA+B,IAAK;IAC/D,MAAMC,OAAiB,GAAG,EAAE;IAC5B,KAAK,MAAM,CAACC,EAAE,EAAEC,KAAK,CAAC,IAAIC,MAAM,CAACC,OAAO,CAACL,IAAI,CAAC,EAAE;MAC9C,IAAIV,SAAS,CAACQ,OAAO,CAACI,EAAE,CAAC,KAAKC,KAAK,EAAE;QACnCb,SAAS,CAACQ,OAAO,CAACI,EAAE,CAAC,GAAGC,KAAK;QAC7BF,OAAO,CAACK,IAAI,CAACJ,EAAE,CAAC;MAClB;IACF;IACA,KAAK,MAAMA,EAAE,IAAID,OAAO,EAAE;MAAA,IAAAM,qBAAA;MACxB,CAAAA,qBAAA,GAAAhB,YAAY,CAACO,OAAO,CAACU,GAAG,CAACN,EAAE,CAAC,aAA5BK,qBAAA,CAA8BE,OAAO,CAAEC,EAAE,IAAKA,EAAE,CAAC,CAAC,CAAC;IACrD;EACF,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMC,eAAe,GAAGpC,WAAW,CAChCqC,KAAyB,IAAK;IAC7B,IAAI,CAACf,QAAQ,CAACC,OAAO,EAAE;MACrB;IACF;IACAC,OAAO,CACLK,MAAM,CAACS,WAAW,CAChBD,KAAK,CAACE,GAAG,CAAEC,CAAC,IAAK,CAACA,CAAC,CAACb,EAAE,EAAE;MAAEc,MAAM,EAAE;IAAc,CAAC,CAAc,CACjE,CACF,CAAC;EACH,CAAC,EACD,CAACjB,OAAO,CACV,CAAC;EAED,MAAMkB,KAAK,GAAG1C,WAAW,CAAC,YAAY;IACpCqB,cAAc,CAACE,OAAO,GAAG,KAAK;IAE9B,MAAMc,KAAK,GAAG,CAAC,GAAGnB,UAAU,CAACK,OAAO,CAACoB,MAAM,CAAC,CAAC,CAAC,CAACC,MAAM,CAClDJ,CAAC,IAAK,CAACrB,OAAO,CAACI,OAAO,CAACsB,GAAG,CAACL,CAAC,CAACb,EAAE,CAClC,CAAC;IACDT,UAAU,CAACK,OAAO,CAACuB,KAAK,CAAC,CAAC;IAC1B,IAAIT,KAAK,CAACU,MAAM,KAAK,CAAC,EAAE;MACtB;IACF;IACAV,KAAK,CAACH,OAAO,CAAEM,CAAC,IAAKrB,OAAO,CAACI,OAAO,CAACyB,GAAG,CAACR,CAAC,CAACb,EAAE,CAAC,CAAC;;IAE/C;IACA;IACA,IAAI,CAACd,OAAO,EAAE;MACZuB,eAAe,CAACC,KAAK,CAAC;MACtB;IACF;IAEA,IAAI;MACF,MAAMY,GAAG,GAAG,MAAMpC,OAAO,CAACwB,KAAK,CAAC;MAChC,IAAI,CAACf,QAAQ,CAACC,OAAO,EAAE;QACrB;MACF;MACA,MAAM2B,QAAQ,GAAG,IAAIjC,GAAG,CAACgC,GAAG,CAACE,KAAK,CAACZ,GAAG,CAAEC,CAAC,IAAK,CAACA,CAAC,CAACY,MAAM,EAAEZ,CAAC,CAAC,CAAC,CAAC;MAC7D,MAAMf,IAA+B,GAAG,CAAC,CAAC;MAC1C,KAAK,MAAM4B,GAAG,IAAIhB,KAAK,EAAE;QACvB,MAAMiB,GAAG,GAAGJ,QAAQ,CAACjB,GAAG,CAACoB,GAAG,CAAC1B,EAAE,CAAC;QAChC;QACA;QACAF,IAAI,CAAC4B,GAAG,CAAC1B,EAAE,CAAC,GACV2B,GAAG,IAAIA,GAAG,CAACC,QAAQ,GACf;UAAEd,MAAM,EAAE,UAAU;UAAEe,IAAI,EAAEF;QAAI,CAAC,GACjC;UAAEb,MAAM,EAAE;QAAc,CAAC;MACjC;MACAjB,OAAO,CAACC,IAAI,CAAC;IACf,CAAC,CAAC,OAAOgC,GAAG,EAAE;MACZ;MACA;MACA7C,IAAI,YAAJA,IAAI,CAAE8C,gBAAgB,YAAtB9C,IAAI,CAAE8C,gBAAgB,CACpBpD,gBAAgB,CAACqD,uBAAuB,EACxCjD,SAAS,EACT,8BAA8B2B,KAAK,CAACU,MAAM,aACxCU,GAAG,YAAYG,KAAK,GAAGH,GAAG,CAACI,OAAO,GAAGC,MAAM,CAACL,GAAG,CAAC,EAEpD,CAAC;MACDrB,eAAe,CAACC,KAAK,CAAC;IACxB;EACF,CAAC,EAAE,CAACxB,OAAO,EAAED,IAAI,EAAEF,SAAS,EAAEc,OAAO,EAAEY,eAAe,CAAC,CAAC;EAExD,MAAM2B,OAAO,GAAG/D,WAAW,CACxBwD,IAAsB,IAAK;IAC1B,IAAIrC,OAAO,CAACI,OAAO,CAACsB,GAAG,CAACW,IAAI,CAAC7B,EAAE,CAAC,EAAE;MAChC;IACF;IACAT,UAAU,CAACK,OAAO,CAACyC,GAAG,CAACR,IAAI,CAAC7B,EAAE,EAAE6B,IAAI,CAAC;IACrC;IACA;IACA,IAAI,CAACnC,cAAc,CAACE,OAAO,EAAE;MAC3BF,cAAc,CAACE,OAAO,GAAG,IAAI;MAC7B0C,cAAc,CAAC,MAAM;QACnB,KAAKvB,KAAK,CAAC,CAAC;MACd,CAAC,CAAC;IACJ;EACF,CAAC,EACD,CAACA,KAAK,CACR,CAAC;EAED,MAAMwB,IAAI,GAAGlE,WAAW,CACrBoD,MAAc,IACbrC,SAAS,CAACQ,OAAO,CAAC6B,MAAM,CAAC,IAAI;IAAEX,MAAM,EAAE;EAAU,CAAC,EACpD,EACF,CAAC;EAED,MAAM0B,SAAS,GAAGnE,WAAW,CAAC,CAACoD,MAAc,EAAEgB,QAAoB,KAAK;IACtE,IAAIJ,GAAG,GAAGhD,YAAY,CAACO,OAAO,CAACU,GAAG,CAACmB,MAAM,CAAC;IAC1C,IAAI,CAACY,GAAG,EAAE;MACRA,GAAG,GAAG,IAAI5C,GAAG,CAAC,CAAC;MACfJ,YAAY,CAACO,OAAO,CAACyC,GAAG,CAACZ,MAAM,EAAEY,GAAG,CAAC;IACvC;IACAA,GAAG,CAAChB,GAAG,CAACoB,QAAQ,CAAC;IACjB,OAAO,MAAM;MAAA,IAAAC,IAAA;MACX,CAAAA,IAAA,GAAAL,GAAG,aAAHK,IAAA,CAAKC,MAAM,CAACF,QAAQ,CAAC;MACrB,IAAIJ,GAAG,IAAIA,GAAG,CAACO,IAAI,KAAK,CAAC,EAAE;QACzBvD,YAAY,CAACO,OAAO,CAAC+C,MAAM,CAAClB,MAAM,CAAC;MACrC;IACF,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;;EAEN;EACA;EACA;EACA,MAAMoB,GAAG,GAAGrE,OAAO,CACjB,OAAO;IAAE4D,OAAO;IAAEG,IAAI;IAAEC;EAAU,CAAC,CAAC,EACpC,CAACJ,OAAO,EAAEG,IAAI,EAAEC,SAAS,CAC3B,CAAC;EAED,oBACErE,KAAA,CAAA2E,aAAA,CAAClE,gBAAgB,CAACmE,QAAQ;IAACC,KAAK,EAAEH;EAAI,GAAE7D,QAAoC,CAAC;AAEjF,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMiE,qBAAqB,GAAGA,CAAA,KACnC3E,UAAU,CAACM,gBAAgB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useImageSlot` — the whole section-facing surface for images.
|
|
3
|
+
*
|
|
4
|
+
* A section describes the hole in its layout and what the picture should be
|
|
5
|
+
* about. It does not name a request, a batch, or a set boundary; that omission
|
|
6
|
+
* is deliberate and load-bearing, because it is what lets the boundary widen
|
|
7
|
+
* from the section to the page later without touching a single component.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the per-image `useResolvedImageSources` path, whose endpoint was
|
|
10
|
+
* removed and, once restored, answers with an empty list.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
13
|
+
import { useOptionalComponentDependencies } from '../context/ComponentDependenciesContext.js';
|
|
14
|
+
import { useImageSlotCollector } from '../context/ImageSlotContext.js';
|
|
15
|
+
import { composeSemantic } from '../image/composeSemantic.js';
|
|
16
|
+
/** The `web5://image/` prefix an author's token carries. */
|
|
17
|
+
const TOKEN_PREFIX = 'web5://image/';
|
|
18
|
+
const decodeToken = raw => {
|
|
19
|
+
const body = raw.startsWith(TOKEN_PREFIX) ? raw.slice(TOKEN_PREFIX.length) : raw;
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(body).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return body.trim();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Content-derived, never an index and never a React key. A re-render
|
|
29
|
+
* re-declares the same id so it does not re-request, and reordering items
|
|
30
|
+
* cannot remap pictures onto different cards — which an index-based id would
|
|
31
|
+
* do, visibly. Kept short and readable rather than cryptographic: it only has
|
|
32
|
+
* to be stable, and unique within one request.
|
|
33
|
+
*/
|
|
34
|
+
const slotIdFor = (role, subjectKey, ratio, width) => {
|
|
35
|
+
let h = 0;
|
|
36
|
+
const material = `${role}|${subjectKey}|${ratio.toFixed(4)}|${width}`;
|
|
37
|
+
for (let i = 0; i < material.length; i++) {
|
|
38
|
+
h = Math.imul(31, h) + material.charCodeAt(i) | 0;
|
|
39
|
+
}
|
|
40
|
+
return `${role}-${(h >>> 0).toString(36)}`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Module-level so `getSnapshot` returns a STABLE reference. Returning a fresh
|
|
44
|
+
* `{ status: 'unavailable' }` on every call makes `useSyncExternalStore`
|
|
45
|
+
* believe the store changed on every check, and it re-renders forever. */
|
|
46
|
+
const SETTLED_UNAVAILABLE = {
|
|
47
|
+
status: 'unavailable'
|
|
48
|
+
};
|
|
49
|
+
export function useImageSlot(options) {
|
|
50
|
+
var _state$slot$visualMet;
|
|
51
|
+
const {
|
|
52
|
+
role = 'background',
|
|
53
|
+
ratio,
|
|
54
|
+
width = 800,
|
|
55
|
+
subject,
|
|
56
|
+
fallbackUrl,
|
|
57
|
+
enabled = true
|
|
58
|
+
} = options;
|
|
59
|
+
|
|
60
|
+
// Optional for the same reason the provider's is: a section rendered without
|
|
61
|
+
// dependencies configured should lose its picture, not throw.
|
|
62
|
+
const deps = useOptionalComponentDependencies();
|
|
63
|
+
const collector = useImageSlotCollector();
|
|
64
|
+
const resolve = deps == null ? void 0 : deps.resolveImageSet;
|
|
65
|
+
|
|
66
|
+
/** The declaration, derived purely from props. */
|
|
67
|
+
const request = useMemo(() => {
|
|
68
|
+
if (!enabled) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (subject.from === 'entity') {
|
|
72
|
+
var _subject$entityId;
|
|
73
|
+
const entityId = (_subject$entityId = subject.entityId) == null ? void 0 : _subject$entityId.trim();
|
|
74
|
+
if (!entityId) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: slotIdFor(role, `e:${entityId}`, ratio, width),
|
|
79
|
+
kind: 'IMAGE_SLOT_KIND_ENTITY',
|
|
80
|
+
ratio,
|
|
81
|
+
renderWidthPx: width,
|
|
82
|
+
entityId
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const semantic = subject.from === 'token' ? decodeToken(subject.token ?? '') : composeSemantic({
|
|
86
|
+
title: subject.title,
|
|
87
|
+
lead: subject.lead
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Too thin to retrieve on is not a query worth sending. `composeSemantic`
|
|
91
|
+
// returns empty rather than guessing, and a token can be empty too
|
|
92
|
+
// (``).
|
|
93
|
+
if (!semantic) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
id: slotIdFor(role, `s:${semantic}`, ratio, width),
|
|
98
|
+
kind: 'IMAGE_SLOT_KIND_EDITORIAL',
|
|
99
|
+
ratio,
|
|
100
|
+
renderWidthPx: width,
|
|
101
|
+
semantic
|
|
102
|
+
};
|
|
103
|
+
}, [enabled, role, ratio, width, subject]);
|
|
104
|
+
|
|
105
|
+
// Used only when no collector is mounted above — a host that has not adopted
|
|
106
|
+
// the section wrapper's provider. Same answer, no batching with siblings.
|
|
107
|
+
const [soloState, setSoloState] = useState({
|
|
108
|
+
status: 'pending'
|
|
109
|
+
});
|
|
110
|
+
const soloSentRef = useRef(null);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!request) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (collector) {
|
|
116
|
+
collector.declare(request);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (soloSentRef.current === request.id) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
soloSentRef.current = request.id;
|
|
123
|
+
if (!resolve) {
|
|
124
|
+
setSoloState({
|
|
125
|
+
status: 'unavailable'
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let alive = true;
|
|
130
|
+
resolve([request]).then(res => {
|
|
131
|
+
if (!alive) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const got = res.slots.find(s => s.slotId === request.id);
|
|
135
|
+
setSoloState(got && got.imageUrl ? {
|
|
136
|
+
status: 'resolved',
|
|
137
|
+
slot: got
|
|
138
|
+
} : {
|
|
139
|
+
status: 'unavailable'
|
|
140
|
+
});
|
|
141
|
+
}).catch(() => {
|
|
142
|
+
if (alive) {
|
|
143
|
+
setSoloState({
|
|
144
|
+
status: 'unavailable'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return () => {
|
|
149
|
+
alive = false;
|
|
150
|
+
};
|
|
151
|
+
}, [collector, resolve, request]);
|
|
152
|
+
|
|
153
|
+
// Subscribe to THIS slot only. `useSyncExternalStore` is the right shape
|
|
154
|
+
// here: the collector holds slot state outside React so resolving one slot
|
|
155
|
+
// does not re-render its siblings, and this is the supported way to read such
|
|
156
|
+
// a store without tearing. Memoised on the slot id so a re-render does not
|
|
157
|
+
// churn listeners.
|
|
158
|
+
const slotId = request == null ? void 0 : request.id;
|
|
159
|
+
const subscribe = useMemo(() => onChange => collector && slotId ? collector.subscribe(slotId, onChange) : () => {}, [collector, slotId]);
|
|
160
|
+
const getSnapshot = useMemo(() => () => collector && slotId ? collector.read(slotId) : SETTLED_UNAVAILABLE, [collector, slotId]);
|
|
161
|
+
const collectorState = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
162
|
+
const state = !request ?
|
|
163
|
+
// Not asking (disabled, or nothing worth retrieving on) is settled, not
|
|
164
|
+
// pending — a layout must not wait for an answer that will never come.
|
|
165
|
+
SETTLED_UNAVAILABLE : collector ? collectorState : soloState;
|
|
166
|
+
const resolvedUrl = state.status === 'resolved' ? state.slot.imageUrl : null;
|
|
167
|
+
return {
|
|
168
|
+
state,
|
|
169
|
+
// Pending deliberately renders nothing rather than the fallback: swapping
|
|
170
|
+
// fallback -> resolved a moment later is a visible flicker, and the layouts
|
|
171
|
+
// that use this already have a ground colour for the pending case.
|
|
172
|
+
url: state.status === 'pending' ? null : resolvedUrl ?? fallbackUrl ?? null,
|
|
173
|
+
backgroundColor: state.status === 'resolved' ? ((_state$slot$visualMet = state.slot.visualMetadata) == null ? void 0 : _state$slot$visualMet.backgroundColor) || undefined : undefined,
|
|
174
|
+
isPending: state.status === 'pending'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=useImageSlot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useEffect","useMemo","useRef","useState","useSyncExternalStore","useOptionalComponentDependencies","useImageSlotCollector","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","collector","resolve","resolveImageSet","request","from","_subject$entityId","entityId","id","kind","renderWidthPx","semantic","token","title","lead","soloState","setSoloState","soloSentRef","declare","current","alive","then","res","got","slots","find","s","slotId","imageUrl","slot","catch","subscribe","onChange","getSnapshot","read","collectorState","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 // (``).\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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a section's own words into a retrieval phrase.
|
|
3
|
+
*
|
|
4
|
+
* The section hands over its RAW props — a title, a lead paragraph — not a
|
|
5
|
+
* query string. Composing is this module's job, in one place, because the two
|
|
6
|
+
* call sites that do it today invent their own phrase, do not agree with each
|
|
7
|
+
* other, and neither is tested:
|
|
8
|
+
*
|
|
9
|
+
* w5-client-circana/src/components/sections/GenericEntitySection.tsx:144
|
|
10
|
+
* `web5://image/${title}` — raw, so `**Nike** collections`
|
|
11
|
+
* is searched WITH its asterisks
|
|
12
|
+
* w5-client-circana/src/components/sections/ResearchSection.tsx:68
|
|
13
|
+
* `web5://image/${encodeURIComponent(title)}` — same idea, escaped
|
|
14
|
+
*
|
|
15
|
+
* Centralising it is most of what the text-derived case is worth.
|
|
16
|
+
*
|
|
17
|
+
* Lifted from the seed client package (ADR 0225): it started there because the
|
|
18
|
+
* CDN build externalises core, so a core change could not be seen through
|
|
19
|
+
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
20
|
+
* here, and this is that move.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Longer than this and the tail stops helping retrieval and starts diluting it. */
|
|
24
|
+
const MAX_LENGTH = 120;
|
|
25
|
+
|
|
26
|
+
/** Below this there is no query worth sending — better to render no picture
|
|
27
|
+
* than to retrieve on a word like "More". */
|
|
28
|
+
const MIN_LENGTH = 3;
|
|
29
|
+
const stripMarkdown = raw => raw
|
|
30
|
+
// images before links:  would otherwise leave a stray `!`
|
|
31
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
32
|
+
// emphasis / strong / strike / inline code — keep the words, drop the marks
|
|
33
|
+
.replace(/(\*\*\*|\*\*|\*|___|__|_|~~|`)/g, '')
|
|
34
|
+
// leading heading hashes and blockquote marks
|
|
35
|
+
.replace(/^\s*#{1,6}\s*/gm, '').replace(/^\s*>\s?/gm, '')
|
|
36
|
+
// html tags an author or the parser may have left behind
|
|
37
|
+
.replace(/<[^>]+>/g, ' ');
|
|
38
|
+
const tidy = raw => stripMarkdown(raw).replace(/\s+/g, ' ').trim()
|
|
39
|
+
// trailing punctuation is noise in a retrieval phrase
|
|
40
|
+
.replace(/[\s.,;:!?—–-]+$/g, '').trim();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clamp without cutting a word in half. Slicing at a fixed length leaves
|
|
44
|
+
* fragments like "the kinds of product" — observed in the first real run — and
|
|
45
|
+
* a dangling partial word is noise in a retrieval phrase, not a shorter
|
|
46
|
+
* version of it. Falls back to a hard slice only when the budget cannot fit
|
|
47
|
+
* even one word.
|
|
48
|
+
*/
|
|
49
|
+
const clampToWord = (raw, max) => {
|
|
50
|
+
if (raw.length <= max) {
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
// +1 so a boundary landing exactly on the budget still counts as a break.
|
|
54
|
+
const cut = raw.slice(0, max + 1);
|
|
55
|
+
const lastBreak = cut.lastIndexOf(' ');
|
|
56
|
+
const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);
|
|
57
|
+
return out.replace(/[\s.,;:!?—–-]+$/g, '').trim();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** The first clause carries the subject; what follows is usually qualification. */
|
|
61
|
+
const firstClause = raw => {
|
|
62
|
+
const m = raw.match(/^[^.!?;]+/);
|
|
63
|
+
return (m ? m[0] : raw).trim();
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Returns the phrase, or `''` when what survives is too thin to retrieve on —
|
|
67
|
+
* the caller then declares no slot at all rather than sending a bad query.
|
|
68
|
+
*/
|
|
69
|
+
export function composeSemantic(_ref) {
|
|
70
|
+
let {
|
|
71
|
+
title,
|
|
72
|
+
lead,
|
|
73
|
+
sectionSemantic
|
|
74
|
+
} = _ref;
|
|
75
|
+
// A mission, when there is one, is the whole answer.
|
|
76
|
+
const mission = tidy(sectionSemantic ?? '');
|
|
77
|
+
if (mission.length >= MIN_LENGTH) {
|
|
78
|
+
return clampToWord(mission, MAX_LENGTH);
|
|
79
|
+
}
|
|
80
|
+
const head = tidy(title ?? '');
|
|
81
|
+
const body = firstClause(tidy(lead ?? ''));
|
|
82
|
+
const joined = [head, body].filter(Boolean).join('. ');
|
|
83
|
+
if (joined.length < MIN_LENGTH) return '';
|
|
84
|
+
if (joined.length <= MAX_LENGTH) return joined;
|
|
85
|
+
|
|
86
|
+
// Over budget: keep the title whole if it fits, since it is the stronger
|
|
87
|
+
// signal, and spend whatever is left on the lead.
|
|
88
|
+
if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);
|
|
89
|
+
return clampToWord(joined, MAX_LENGTH);
|
|
90
|
+
}
|
|
91
|
+
//# 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","_ref","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:  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":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["../../../src/image/imageSlotTypes.ts"],"sourcesContent":["/**\n * The image-slot vocabulary: what a section declares, and what it gets back.\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 * These mirror `wix.enterprise.web_five.v1.ImageService/ResolveImageSet`. The\n * wire is `preserving_proto_field_name` (`slot_id`, `image_url`,\n * `visual_metadata`, `background_color`) — confirmed against a live response,\n * not assumed — and normalising that is the transport's job, so nothing behind\n * the port ever sees snake_case.\n */\n\nexport type ImageSlotKind =\n | 'IMAGE_SLOT_KIND_ENTITY'\n | 'IMAGE_SLOT_KIND_EDITORIAL';\n\n/**\n * How well the resolver thinks it did. Read THIS rather than the presence of a\n * url: a `FALLBACK` is a real image the resolver is not proud of, so a card may\n * reasonably prefer its own payload picture over one, while a hero may prefer\n * no picture at all.\n */\nexport type ImageMatchQuality =\n | 'IMAGE_MATCH_QUALITY_EXACT'\n | 'IMAGE_MATCH_QUALITY_DEGRADED'\n | 'IMAGE_MATCH_QUALITY_FALLBACK';\n\nexport type ImageBackground =\n | 'IMAGE_BACKGROUND_TRANSPARENT'\n | 'IMAGE_BACKGROUND_SOLID'\n | 'IMAGE_BACKGROUND_MIXED';\n\n/** One hole in a layout, as sent. */\nexport interface ImageSlotRequest {\n /** Echoed back as `slotId` — the ONLY way to match a result to a slot. */\n id: string;\n kind: ImageSlotKind;\n /** width / height of the hole. Drives both the crop and the fit score. */\n ratio: number;\n /** Delivered pixel width. Server default is 800 when omitted. */\n renderWidthPx?: number;\n /** ENTITY slots only — scopes candidates to that entity's own pictures. */\n entityId?: string;\n /** EDITORIAL slots only — free text, semantically retrieved over labels. */\n semantic?: string;\n}\n\nexport interface ImagePalette {\n dominant: string;\n swatches: string[];\n}\n\n/** A square grid of per-cell values, `edge` on a side. */\nexport interface ImageStatGrid {\n edge: number;\n cells: number[];\n}\n\nexport interface ImageVisualMetadata {\n width: number;\n height: number;\n background?: ImageBackground;\n /** Paint the letterbox with this — a product shot on a seamless backdrop\n * needs that backdrop's colour, and only the response knows it. */\n backgroundColor: string;\n palette?: ImagePalette;\n /** Per-cell luma. Enough to compute a scrim that belongs to the photograph\n * rather than washing every hero with one blanket alpha. */\n luma?: ImageStatGrid;\n labels: string[];\n}\n\nexport interface ResolvedImageSlot {\n slotId: string;\n /** Already cropped and resized to the slot — there is no client-side\n * windowing to do. Null when the resolver could not fill the slot. */\n imageUrl: string | null;\n match?: ImageMatchQuality;\n score?: number;\n visualMetadata?: ImageVisualMetadata;\n}\n\nexport interface ResolveImageSetResponse {\n mode?: ImageBackground;\n slots: ResolvedImageSlot[];\n}\n\n/**\n * The port a host fills. Declared here, provided by `web50-server-ui` through\n * `AppDependenciesProvider` — the same split `resolveContextualImage*` already\n * uses, where core owns the hook and the host owns the adapter.\n */\nexport type ResolveImageSetPort = (\n images: ImageSlotRequest[],\n) => Promise<ResolveImageSetResponse>;\n\n/** What a declaration gets back. `pending` is the first paint, always. */\nexport type SlotState =\n | { status: 'pending' }\n | { status: 'resolved'; slot: ResolvedImageSlot }\n | { status: 'unavailable' };\n\n/**\n * Where a slot's subject comes from. Evaluated per slot, first hit wins:\n * an explicit token outranks an entity ref, which outranks the section's own\n * words. The precedence is what keeps the three cases free of each other — a\n * section written for one becomes another the day an author writes a token,\n * with no branch in section code.\n */\nexport type ImageSubject =\n /** A `web5://image/...` token an author wrote, or a bare phrase. */\n | { from: 'token'; token: string }\n /** A catalog entity — the resolver scopes candidates to its own pictures. */\n | { from: 'entity'; entityId: string }\n /** The section's own words. Hand over raw props; composing is core's job. */\n | { from: 'text'; title?: string; lead?: string };\n"],"mappings":"","ignoreList":[]}
|
package/dist/esm/index.js
CHANGED
|
@@ -67,6 +67,13 @@ export { useWeb5Link } from './hooks/useWeb5Link.js';
|
|
|
67
67
|
export { useConversation } from './hooks/useConversation.js';
|
|
68
68
|
export { useDebugImageContext } from './hooks/useDebugImageContext.js';
|
|
69
69
|
export { useResolvedImageSources } from './hooks/useResolvedImageSources.js';
|
|
70
|
+
|
|
71
|
+
// Image slots (ADR 0222/0223/0225): a section declares the holes in its layout
|
|
72
|
+
// and a per-section collector resolves them as one set. Replaces the per-image
|
|
73
|
+
// `useResolvedImageSources` path above, which is kept until its callers move.
|
|
74
|
+
export { useImageSlot } from './hooks/useImageSlot.js';
|
|
75
|
+
export { ImageSlotProvider, useImageSlotCollector } from './context/ImageSlotContext.js';
|
|
76
|
+
export { composeSemantic } from './image/composeSemantic.js';
|
|
70
77
|
export { useResolveGenericEntityData } from './hooks/useResolveGenericEntityData.js';
|
|
71
78
|
export { useEntityTransforms } from './hooks/useEntityTransforms.js';
|
|
72
79
|
export { useMarkdownUtils } from './hooks/useMarkdownUtils.js';
|