@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.
Files changed (49) hide show
  1. package/dist/cjs/components/ui/UserQuery.css +10 -7
  2. package/dist/cjs/components/ui/UserQuery.js +17 -33
  3. package/dist/cjs/components/ui/UserQuery.js.map +1 -1
  4. package/dist/cjs/context/ComponentDependenciesContext.js +15 -1
  5. package/dist/cjs/context/ComponentDependenciesContext.js.map +1 -1
  6. package/dist/cjs/context/ImageSlotContext.js +210 -0
  7. package/dist/cjs/context/ImageSlotContext.js.map +1 -0
  8. package/dist/cjs/hooks/useImageSlot.js +182 -0
  9. package/dist/cjs/hooks/useImageSlot.js.map +1 -0
  10. package/dist/cjs/image/composeSemantic.js +94 -0
  11. package/dist/cjs/image/composeSemantic.js.map +1 -0
  12. package/dist/cjs/image/imageSlotTypes.js +4 -0
  13. package/dist/cjs/image/imageSlotTypes.js.map +1 -0
  14. package/dist/cjs/index.js +11 -3
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/types/dependencies.js.map +1 -1
  17. package/dist/esm/components/ui/UserQuery.css +10 -7
  18. package/dist/esm/components/ui/UserQuery.js +5 -9
  19. package/dist/esm/components/ui/UserQuery.js.map +1 -1
  20. package/dist/esm/context/ComponentDependenciesContext.js +13 -0
  21. package/dist/esm/context/ComponentDependenciesContext.js.map +1 -1
  22. package/dist/esm/context/ImageSlotContext.js +197 -0
  23. package/dist/esm/context/ImageSlotContext.js.map +1 -0
  24. package/dist/esm/hooks/useImageSlot.js +177 -0
  25. package/dist/esm/hooks/useImageSlot.js.map +1 -0
  26. package/dist/esm/image/composeSemantic.js +91 -0
  27. package/dist/esm/image/composeSemantic.js.map +1 -0
  28. package/dist/esm/image/imageSlotTypes.js +2 -0
  29. package/dist/esm/image/imageSlotTypes.js.map +1 -0
  30. package/dist/esm/index.js +7 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/types/dependencies.js.map +1 -1
  33. package/dist/types/components/ui/UserQuery.d.ts.map +1 -1
  34. package/dist/types/context/ComponentDependenciesContext.d.ts +10 -0
  35. package/dist/types/context/ComponentDependenciesContext.d.ts.map +1 -1
  36. package/dist/types/context/ImageSlotContext.d.ts +63 -0
  37. package/dist/types/context/ImageSlotContext.d.ts.map +1 -0
  38. package/dist/types/hooks/useImageSlot.d.ts +35 -0
  39. package/dist/types/hooks/useImageSlot.d.ts.map +1 -0
  40. package/dist/types/image/composeSemantic.d.ts +37 -0
  41. package/dist/types/image/composeSemantic.d.ts.map +1 -0
  42. package/dist/types/image/imageSlotTypes.d.ts +112 -0
  43. package/dist/types/image/imageSlotTypes.d.ts.map +1 -0
  44. package/dist/types/index.d.ts +7 -0
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/types/types/dependencies.d.ts +11 -0
  47. package/dist/types/types/dependencies.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. 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":"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","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
+ // (`![alt](web5://image/)`).
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 // (`![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,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: ![alt](src) 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: ![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":[]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=imageSlotTypes.js.map
@@ -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';