@rockhall/electron-offline-content 0.4.0

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 (196) hide show
  1. package/CHANGELOG.md +384 -0
  2. package/LICENSE +21 -0
  3. package/README.md +794 -0
  4. package/dist/internal/asset-file-name.cjs +13 -0
  5. package/dist/internal/asset-file-name.cjs.map +1 -0
  6. package/dist/internal/asset-file-name.d.cts +6 -0
  7. package/dist/internal/asset-file-name.d.cts.map +1 -0
  8. package/dist/internal/asset-file-name.d.ts +6 -0
  9. package/dist/internal/asset-file-name.d.ts.map +1 -0
  10. package/dist/internal/asset-file-name.js +12 -0
  11. package/dist/internal/asset-file-name.js.map +1 -0
  12. package/dist/internal/asset-key.cjs +30 -0
  13. package/dist/internal/asset-key.cjs.map +1 -0
  14. package/dist/internal/asset-key.d.cts +19 -0
  15. package/dist/internal/asset-key.d.cts.map +1 -0
  16. package/dist/internal/asset-key.d.ts +19 -0
  17. package/dist/internal/asset-key.d.ts.map +1 -0
  18. package/dist/internal/asset-key.js +27 -0
  19. package/dist/internal/asset-key.js.map +1 -0
  20. package/dist/internal/log-format.cjs +98 -0
  21. package/dist/internal/log-format.cjs.map +1 -0
  22. package/dist/internal/log-format.d.cts +10 -0
  23. package/dist/internal/log-format.d.cts.map +1 -0
  24. package/dist/internal/log-format.d.ts +10 -0
  25. package/dist/internal/log-format.d.ts.map +1 -0
  26. package/dist/internal/log-format.js +97 -0
  27. package/dist/internal/log-format.js.map +1 -0
  28. package/dist/internal/media-kind.cjs +46 -0
  29. package/dist/internal/media-kind.cjs.map +1 -0
  30. package/dist/internal/media-kind.d.cts +20 -0
  31. package/dist/internal/media-kind.d.cts.map +1 -0
  32. package/dist/internal/media-kind.d.ts +20 -0
  33. package/dist/internal/media-kind.d.ts.map +1 -0
  34. package/dist/internal/media-kind.js +45 -0
  35. package/dist/internal/media-kind.js.map +1 -0
  36. package/dist/internal/url-warn.cjs +14 -0
  37. package/dist/internal/url-warn.cjs.map +1 -0
  38. package/dist/internal/url-warn.d.cts +10 -0
  39. package/dist/internal/url-warn.d.cts.map +1 -0
  40. package/dist/internal/url-warn.d.ts +10 -0
  41. package/dist/internal/url-warn.d.ts.map +1 -0
  42. package/dist/internal/url-warn.js +13 -0
  43. package/dist/internal/url-warn.js.map +1 -0
  44. package/dist/internal/validation.cjs +222 -0
  45. package/dist/internal/validation.cjs.map +1 -0
  46. package/dist/internal/validation.d.cts +78 -0
  47. package/dist/internal/validation.d.cts.map +1 -0
  48. package/dist/internal/validation.d.ts +78 -0
  49. package/dist/internal/validation.d.ts.map +1 -0
  50. package/dist/internal/validation.js +196 -0
  51. package/dist/internal/validation.js.map +1 -0
  52. package/dist/main/asset-download.cjs +265 -0
  53. package/dist/main/asset-download.cjs.map +1 -0
  54. package/dist/main/asset-download.d.cts +12 -0
  55. package/dist/main/asset-download.d.cts.map +1 -0
  56. package/dist/main/asset-download.d.ts +12 -0
  57. package/dist/main/asset-download.d.ts.map +1 -0
  58. package/dist/main/asset-download.js +263 -0
  59. package/dist/main/asset-download.js.map +1 -0
  60. package/dist/main/database.cjs +473 -0
  61. package/dist/main/database.cjs.map +1 -0
  62. package/dist/main/database.d.cts +81 -0
  63. package/dist/main/database.d.cts.map +1 -0
  64. package/dist/main/database.d.ts +81 -0
  65. package/dist/main/database.d.ts.map +1 -0
  66. package/dist/main/database.js +472 -0
  67. package/dist/main/database.js.map +1 -0
  68. package/dist/main/index.cjs +22 -0
  69. package/dist/main/index.d.cts +7 -0
  70. package/dist/main/index.d.ts +7 -0
  71. package/dist/main/index.js +7 -0
  72. package/dist/main/media-cache.cjs +862 -0
  73. package/dist/main/media-cache.cjs.map +1 -0
  74. package/dist/main/media-cache.d.cts +134 -0
  75. package/dist/main/media-cache.d.cts.map +1 -0
  76. package/dist/main/media-cache.d.ts +134 -0
  77. package/dist/main/media-cache.d.ts.map +1 -0
  78. package/dist/main/media-cache.js +854 -0
  79. package/dist/main/media-cache.js.map +1 -0
  80. package/dist/main/storage-root-lock.cjs +124 -0
  81. package/dist/main/storage-root-lock.cjs.map +1 -0
  82. package/dist/main/storage-root-lock.d.cts +11 -0
  83. package/dist/main/storage-root-lock.d.cts.map +1 -0
  84. package/dist/main/storage-root-lock.d.ts +11 -0
  85. package/dist/main/storage-root-lock.d.ts.map +1 -0
  86. package/dist/main/storage-root-lock.js +120 -0
  87. package/dist/main/storage-root-lock.js.map +1 -0
  88. package/dist/main/store.cjs +197 -0
  89. package/dist/main/store.cjs.map +1 -0
  90. package/dist/main/store.d.cts +83 -0
  91. package/dist/main/store.d.cts.map +1 -0
  92. package/dist/main/store.d.ts +83 -0
  93. package/dist/main/store.d.ts.map +1 -0
  94. package/dist/main/store.js +195 -0
  95. package/dist/main/store.js.map +1 -0
  96. package/dist/preload/index.cjs +36 -0
  97. package/dist/preload/index.cjs.map +1 -0
  98. package/dist/preload/index.d.cts +14 -0
  99. package/dist/preload/index.d.cts.map +1 -0
  100. package/dist/preload/index.d.ts +14 -0
  101. package/dist/preload/index.d.ts.map +1 -0
  102. package/dist/preload/index.js +34 -0
  103. package/dist/preload/index.js.map +1 -0
  104. package/dist/react/index.cjs +199 -0
  105. package/dist/react/index.cjs.map +1 -0
  106. package/dist/react/index.d.cts +50 -0
  107. package/dist/react/index.d.cts.map +1 -0
  108. package/dist/react/index.d.ts +50 -0
  109. package/dist/react/index.d.ts.map +1 -0
  110. package/dist/react/index.js +191 -0
  111. package/dist/react/index.js.map +1 -0
  112. package/dist/renderer/helpers.cjs +36 -0
  113. package/dist/renderer/helpers.cjs.map +1 -0
  114. package/dist/renderer/helpers.d.cts +11 -0
  115. package/dist/renderer/helpers.d.cts.map +1 -0
  116. package/dist/renderer/helpers.d.ts +11 -0
  117. package/dist/renderer/helpers.d.ts.map +1 -0
  118. package/dist/renderer/helpers.js +35 -0
  119. package/dist/renderer/helpers.js.map +1 -0
  120. package/dist/renderer/index.cjs +20 -0
  121. package/dist/renderer/index.cjs.map +1 -0
  122. package/dist/renderer/index.d.cts +14 -0
  123. package/dist/renderer/index.d.cts.map +1 -0
  124. package/dist/renderer/index.d.ts +14 -0
  125. package/dist/renderer/index.d.ts.map +1 -0
  126. package/dist/renderer/index.js +14 -0
  127. package/dist/renderer/index.js.map +1 -0
  128. package/dist/renderer/runtime.cjs +278 -0
  129. package/dist/renderer/runtime.cjs.map +1 -0
  130. package/dist/renderer/runtime.d.cts +35 -0
  131. package/dist/renderer/runtime.d.cts.map +1 -0
  132. package/dist/renderer/runtime.d.ts +35 -0
  133. package/dist/renderer/runtime.d.ts.map +1 -0
  134. package/dist/renderer/runtime.js +273 -0
  135. package/dist/renderer/runtime.js.map +1 -0
  136. package/dist/renderer/window-globals.d.cts +9 -0
  137. package/dist/renderer/window-globals.d.cts.map +1 -0
  138. package/dist/renderer/window-globals.d.ts +9 -0
  139. package/dist/renderer/window-globals.d.ts.map +1 -0
  140. package/dist/shared/errors.cjs +102 -0
  141. package/dist/shared/errors.cjs.map +1 -0
  142. package/dist/shared/errors.d.cts +45 -0
  143. package/dist/shared/errors.d.cts.map +1 -0
  144. package/dist/shared/errors.d.ts +45 -0
  145. package/dist/shared/errors.d.ts.map +1 -0
  146. package/dist/shared/errors.js +93 -0
  147. package/dist/shared/errors.js.map +1 -0
  148. package/dist/shared/ipc.cjs +14 -0
  149. package/dist/shared/ipc.cjs.map +1 -0
  150. package/dist/shared/ipc.d.cts +12 -0
  151. package/dist/shared/ipc.d.cts.map +1 -0
  152. package/dist/shared/ipc.d.ts +12 -0
  153. package/dist/shared/ipc.d.ts.map +1 -0
  154. package/dist/shared/ipc.js +13 -0
  155. package/dist/shared/ipc.js.map +1 -0
  156. package/dist/shared/normalize.cjs +19 -0
  157. package/dist/shared/normalize.cjs.map +1 -0
  158. package/dist/shared/normalize.d.cts +11 -0
  159. package/dist/shared/normalize.d.cts.map +1 -0
  160. package/dist/shared/normalize.d.ts +11 -0
  161. package/dist/shared/normalize.d.ts.map +1 -0
  162. package/dist/shared/normalize.js +18 -0
  163. package/dist/shared/normalize.js.map +1 -0
  164. package/dist/shared/pagination.cjs +32 -0
  165. package/dist/shared/pagination.cjs.map +1 -0
  166. package/dist/shared/pagination.d.cts +14 -0
  167. package/dist/shared/pagination.d.cts.map +1 -0
  168. package/dist/shared/pagination.d.ts +14 -0
  169. package/dist/shared/pagination.d.ts.map +1 -0
  170. package/dist/shared/pagination.js +28 -0
  171. package/dist/shared/pagination.js.map +1 -0
  172. package/dist/shared/stem.cjs +16 -0
  173. package/dist/shared/stem.cjs.map +1 -0
  174. package/dist/shared/stem.d.cts +6 -0
  175. package/dist/shared/stem.d.cts.map +1 -0
  176. package/dist/shared/stem.d.ts +6 -0
  177. package/dist/shared/stem.d.ts.map +1 -0
  178. package/dist/shared/stem.js +14 -0
  179. package/dist/shared/stem.js.map +1 -0
  180. package/dist/shared/types.cjs +15 -0
  181. package/dist/shared/types.cjs.map +1 -0
  182. package/dist/shared/types.d.cts +234 -0
  183. package/dist/shared/types.d.cts.map +1 -0
  184. package/dist/shared/types.d.ts +234 -0
  185. package/dist/shared/types.d.ts.map +1 -0
  186. package/dist/shared/types.js +14 -0
  187. package/dist/shared/types.js.map +1 -0
  188. package/package.json +120 -0
  189. package/skills/authenticated-downloads/SKILL.md +203 -0
  190. package/skills/cache-configuration/SKILL.md +357 -0
  191. package/skills/cache-configuration/references/options.md +356 -0
  192. package/skills/getting-started/SKILL.md +407 -0
  193. package/skills/production-checklist/SKILL.md +397 -0
  194. package/skills/react-rendering/SKILL.md +424 -0
  195. package/skills/react-rendering/references/hooks.md +443 -0
  196. package/skills/store-authoring/SKILL.md +369 -0
@@ -0,0 +1,424 @@
1
+ ---
2
+ name: react-rendering
3
+ description: >
4
+ React bindings for @rockhall/electron-offline-content:
5
+ MediaCacheProvider context, useMediaAsset for single asset lookups,
6
+ useMediaByIndex for index-based queries, useMediaBridge and
7
+ useMediaCacheStatus for sync phase and progress, useFileStemMatch
8
+ for filename search, useMediaCacheReady for download gates, and
9
+ useMediaCacheErrors for aggregated error display. AsyncState shape,
10
+ refetchOnSyncComplete, rendering media:// URLs in video/img/audio/track
11
+ elements.
12
+ type: framework
13
+ library: electron-offline-content
14
+ framework: react
15
+ library_version: "0.4.0"
16
+ requires:
17
+ - getting-started
18
+ sources:
19
+ - "rockhallweb/electron-offline-content:src/react/index.tsx"
20
+ - "rockhallweb/electron-offline-content:src/shared/types.ts"
21
+ ---
22
+
23
+ This skill builds on getting-started. Read it first for full main → preload → renderer wiring.
24
+
25
+ ## Setup
26
+
27
+ Wrap your renderer entry with `MediaCacheProvider`. The bridge is auto-detected from `window.mediaCache` when omitted.
28
+
29
+ ```tsx
30
+ import { MediaCacheProvider } from "@rockhall/electron-offline-content/react";
31
+
32
+ function App() {
33
+ return (
34
+ <MediaCacheProvider>
35
+ <KioskShell />
36
+ </MediaCacheProvider>
37
+ );
38
+ }
39
+ ```
40
+
41
+ Gate content rendering on first sync completion, then render assets:
42
+
43
+ ```tsx
44
+ import { useMediaByIndex, useMediaCacheReady } from "@rockhall/electron-offline-content/react";
45
+
46
+ function KioskShell() {
47
+ const ready = useMediaCacheReady();
48
+ const videos = useMediaByIndex("category", "videos", { limit: 50 });
49
+
50
+ if (ready.loading || !ready.data?.ready) {
51
+ return <div>Preparing content…</div>;
52
+ }
53
+
54
+ if (videos.loading) return <div>Loading videos…</div>;
55
+
56
+ return (
57
+ <ul>
58
+ {videos.data?.items.map((asset) => (
59
+ <li key={asset.key}>
60
+ <video src={asset.url} controls />
61
+ <span>{asset.metadata.title as string}</span>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ );
66
+ }
67
+ ```
68
+
69
+ ## Hooks and Components
70
+
71
+ ### Loading gate with useMediaCacheReady
72
+
73
+ Returns `AsyncState<MediaCacheReadyState>` where `MediaCacheReadyState` has `{ ready, syncing, phase, activeGenerationId, syncError }`.
74
+
75
+ Use as a gate before rendering any content that depends on cached media.
76
+
77
+ ```tsx
78
+ import { useMediaCacheReady } from "@rockhall/electron-offline-content/react";
79
+
80
+ function LoadingGate({ children }: { children: React.ReactNode }) {
81
+ const { data, loading } = useMediaCacheReady();
82
+
83
+ if (loading || !data?.ready) {
84
+ return (
85
+ <div className="loading-screen">
86
+ <p>Preparing content…</p>
87
+ {data?.syncing && <p>Downloading assets…</p>}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ return <>{children}</>;
93
+ }
94
+ ```
95
+
96
+ ### Single asset lookup with useMediaAsset
97
+
98
+ Returns `AsyncState<ResolvedMediaAsset>` for a single asset by key.
99
+
100
+ ```tsx
101
+ import { useMediaAsset } from "@rockhall/electron-offline-content/react";
102
+
103
+ function WelcomeVideo() {
104
+ const { data: asset, loading } = useMediaAsset("video/welcome");
105
+
106
+ if (loading || !asset) return <p>Loading…</p>;
107
+
108
+ return <video src={asset.url} controls />;
109
+ }
110
+ ```
111
+
112
+ Use `useMediaAsset` when you know the exact asset key. Asset keys come from the `key` field passed to `store.add()` during `resolveStore`.
113
+
114
+ ### Index-based queries with useMediaByIndex
115
+
116
+ Returns `AsyncState<PaginationResult<ResolvedMediaAsset>>` for assets matching an index value.
117
+
118
+ ```tsx
119
+ import { useMediaByIndex } from "@rockhall/electron-offline-content/react";
120
+
121
+ function VideoList() {
122
+ const { data, loading, error, refresh } = useMediaByIndex("category", "videos", {
123
+ limit: 20,
124
+ refetchOnSyncComplete: true,
125
+ });
126
+
127
+ if (loading) return <p>Loading…</p>;
128
+ if (error) return <p>Error: {error.message}</p>;
129
+
130
+ return (
131
+ <ul>
132
+ {data?.items.map((asset) => (
133
+ <li key={asset.key}>{asset.metadata.title as string}</li>
134
+ ))}
135
+ </ul>
136
+ );
137
+ }
138
+ ```
139
+
140
+ Query by any index defined in `resolveStore`:
141
+
142
+ ```tsx
143
+ function FloorExhibits({ floor }: { floor: string }) {
144
+ const { data, loading } = useMediaByIndex("floor", floor, {
145
+ limit: 100,
146
+ refetchOnSyncComplete: true,
147
+ });
148
+
149
+ if (loading || !data) return null;
150
+
151
+ return (
152
+ <div>
153
+ {data.items.map((asset) => (
154
+ <figure key={asset.key}>
155
+ <img src={asset.url} alt={asset.metadata.title as string} />
156
+ <figcaption>{asset.metadata.title as string}</figcaption>
157
+ </figure>
158
+ ))}
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ### Sync progress with useMediaCacheStatus
165
+
166
+ Returns `AsyncState<MediaCacheStatus>` with `phase`, `progress`, `storageRoot`, `activeGenerationId`, `lastRun`, and `error`.
167
+
168
+ ```tsx
169
+ import { useMediaCacheStatus } from "@rockhall/electron-offline-content/react";
170
+
171
+ function SyncOverlay() {
172
+ const { data: status, loading } = useMediaCacheStatus();
173
+
174
+ if (loading || !status) return null;
175
+ if (status.phase !== "syncing") return null;
176
+
177
+ const { progress } = status;
178
+
179
+ return (
180
+ <div className="sync-overlay">
181
+ <p>Syncing…</p>
182
+ {progress && <progress value={progress.completedAssets} max={progress.totalAssets} />}
183
+ {progress && (
184
+ <p>
185
+ {progress.completedAssets}/{progress.totalAssets} assets (
186
+ {(progress.bytesDownloaded / 1_048_576).toFixed(1)} MB)
187
+ </p>
188
+ )}
189
+ </div>
190
+ );
191
+ }
192
+ ```
193
+
194
+ ### Imperative bridge access with useMediaBridge
195
+
196
+ Returns bridge methods together with shared `status` and aggregated `errors`.
197
+
198
+ ```tsx
199
+ import { useMediaBridge } from "@rockhall/electron-offline-content/react";
200
+
201
+ function DownloadButton() {
202
+ const { syncNow, status, errors } = useMediaBridge();
203
+
204
+ return (
205
+ <button
206
+ type="button"
207
+ disabled={status.data?.phase === "syncing"}
208
+ onClick={() => void syncNow()}
209
+ >
210
+ {errors.hasError ? `Retry sync (${errors.primaryError?.message})` : "Sync now"}
211
+ </button>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ### Error aggregation with useMediaCacheErrors
217
+
218
+ Returns `MediaCacheErrors` with `{ hasError, primaryError, syncError, statusError, queryErrors }`.
219
+
220
+ ```tsx
221
+ import { useMediaByIndex, useMediaCacheErrors } from "@rockhall/electron-offline-content/react";
222
+
223
+ function ExhibitPage() {
224
+ const videos = useMediaByIndex("category", "videos", { limit: 50 });
225
+ const images = useMediaByIndex("category", "images", { limit: 100 });
226
+ const errors = useMediaCacheErrors();
227
+
228
+ if (errors.hasError) {
229
+ return (
230
+ <div className="error-banner">
231
+ <p>Something went wrong: {errors.primaryError?.message}</p>
232
+ {errors.syncError && <p>Sync failed — content may be stale.</p>}
233
+ </div>
234
+ );
235
+ }
236
+
237
+ return <div>{/* render videos.data and images.data */}</div>;
238
+ }
239
+ ```
240
+
241
+ ### File stem matching with useFileStemMatch
242
+
243
+ Returns `AsyncState<PaginationResult<FileStemMatch>>`. Searches cached content by filename stem across all assets.
244
+
245
+ ```tsx
246
+ import { useFileStemMatch } from "@rockhall/electron-offline-content/react";
247
+
248
+ function AssetSearch({ query }: { query: string }) {
249
+ const { data, loading } = useFileStemMatch(query, {
250
+ limit: 25,
251
+ refetchOnSyncComplete: true,
252
+ });
253
+
254
+ if (loading || !data) return <p>Searching…</p>;
255
+
256
+ return (
257
+ <ul>
258
+ {data.items.map((match) => (
259
+ <li key={match.asset.key}>{match.asset.key}</li>
260
+ ))}
261
+ </ul>
262
+ );
263
+ }
264
+ ```
265
+
266
+ ### Rendering media:// URLs
267
+
268
+ URLs from hook results work directly in `src` attributes. In offline mode they resolve through the `media://` protocol handler registered in main. In `devPassthrough` mode they are remote HTTPS URLs. Never construct these URLs manually.
269
+
270
+ ```tsx
271
+ function MediaPlayer({ asset }: { asset: ResolvedMediaAsset }) {
272
+ return <video src={asset.url} controls autoPlay muted />;
273
+ }
274
+ ```
275
+
276
+ Audio assets work the same way:
277
+
278
+ ```tsx
279
+ function AudioPlayer({ asset }: { asset: ResolvedMediaAsset }) {
280
+ return <audio src={asset.url} controls />;
281
+ }
282
+ ```
283
+
284
+ For related assets (e.g. a video with a poster and subtitles), look up each asset by key:
285
+
286
+ ```tsx
287
+ function VideoWithExtras({ videoKey }: { videoKey: string }) {
288
+ const video = useMediaAsset(videoKey);
289
+ const poster = useMediaAsset(`${videoKey}/poster`);
290
+ const subs = useMediaAsset(`${videoKey}/subs-en`);
291
+
292
+ if (video.loading || !video.data) return <p>Loading…</p>;
293
+
294
+ return (
295
+ <video src={video.data.url} poster={poster.data?.url} controls>
296
+ {subs.data && <track src={subs.data.url} kind="subtitles" srcLang="en" default />}
297
+ </video>
298
+ );
299
+ }
300
+ ```
301
+
302
+ ## Common Mistakes
303
+
304
+ ### HIGH: Accessing data before loading completes
305
+
306
+ All hooks return `AsyncState<T>` where `data` is `null` until the first successful load. Accessing nested properties without a null check causes a `TypeError` at runtime.
307
+
308
+ ```tsx
309
+ // WRONG — TypeError when data is null
310
+ function Broken() {
311
+ const videos = useMediaByIndex("category", "videos");
312
+ return (
313
+ <ul>
314
+ {videos.data.items.map((asset) => (
315
+ <li key={asset.key}>{asset.metadata.title as string}</li>
316
+ ))}
317
+ </ul>
318
+ );
319
+ }
320
+
321
+ // CORRECT — guard on loading and null
322
+ function Fixed() {
323
+ const videos = useMediaByIndex("category", "videos");
324
+ if (videos.loading || !videos.data) return <p>Loading…</p>;
325
+ return (
326
+ <ul>
327
+ {videos.data.items.map((asset) => (
328
+ <li key={asset.key}>{asset.metadata.title as string}</li>
329
+ ))}
330
+ </ul>
331
+ );
332
+ }
333
+ ```
334
+
335
+ Source: `react/index.tsx` — `AsyncState` type definition
336
+
337
+ ### HIGH: Fetching remote URLs instead of rendering directly
338
+
339
+ Hook URLs are ready to render. Do not `fetch()` them in the renderer — it bypasses the protocol handler in offline mode and adds unnecessary complexity.
340
+
341
+ ```tsx
342
+ // WRONG — redundant fetch, breaks offline
343
+ function Broken({ asset }: { asset: ResolvedMediaAsset }) {
344
+ const [src, setSrc] = useState("");
345
+ useEffect(() => {
346
+ fetch(asset.url)
347
+ .then((r) => r.blob())
348
+ .then((b) => setSrc(URL.createObjectURL(b)));
349
+ }, [asset]);
350
+ return <video src={src} />;
351
+ }
352
+
353
+ // CORRECT — pass URL directly to src
354
+ function Fixed({ asset }: { asset: ResolvedMediaAsset }) {
355
+ return <video src={asset.url} controls />;
356
+ }
357
+ ```
358
+
359
+ Source: Maintainer interview
360
+
361
+ ### HIGH: Using removed useMedia hooks
362
+
363
+ `useMedia({ kind: "item", ... })` and `useMedia({ kind: "list", ... })` were removed in 0.4.0. Use `useMediaAsset` and `useMediaByIndex` instead.
364
+
365
+ ```tsx
366
+ // WRONG — removed API
367
+ const item = useMedia({ kind: "item", namespace: "videos", id: "welcome" });
368
+ const list = useMedia({ kind: "list", namespace: "videos", limit: 20 });
369
+
370
+ // CORRECT
371
+ const asset = useMediaAsset("video/welcome");
372
+ const videos = useMediaByIndex("category", "videos", { limit: 20 });
373
+ ```
374
+
375
+ Source: `CHANGELOG.md` 0.4.0; `react/index.tsx`
376
+
377
+ ### MEDIUM: Splitting imperative bridge state across multiple hooks
378
+
379
+ If a component needs `syncNow()`, status, and errors together, prefer `useMediaBridge()` over manually combining separate bridge, status, and error hooks. The combined hook matches the provider runtime and keeps imperative UI code simpler.
380
+
381
+ ```tsx
382
+ // WRONG — imperative bridge UI spread across separate hooks
383
+ function SyncButton() {
384
+ const status = useMediaCacheStatus();
385
+ const errors = useMediaCacheErrors();
386
+ // some other hook supplies syncNow()
387
+ }
388
+
389
+ // CORRECT — one hook for imperative bridge access
390
+ function SyncButton() {
391
+ const { syncNow, status, errors } = useMediaBridge();
392
+ return (
393
+ <button onClick={() => void syncNow()}>
394
+ {status.data?.phase ?? errors.primaryError?.message}
395
+ </button>
396
+ );
397
+ }
398
+ ```
399
+
400
+ Source: `react/index.tsx` — `useMediaCacheErrors` JSDoc
401
+
402
+ ### MEDIUM: Hardcoding media:// URLs instead of using hook data
403
+
404
+ URLs differ between offline mode (`media://`) and `devPassthrough` mode (remote HTTPS). Hardcoded URLs break in one of the two modes.
405
+
406
+ ```tsx
407
+ // WRONG — hardcoded protocol URL
408
+ <video src="media://asset/video%2Fwelcome" />;
409
+
410
+ // CORRECT — URL from hook result
411
+ const { data: asset } = useMediaAsset("video/welcome");
412
+ <video src={asset?.url} />;
413
+ ```
414
+
415
+ Source: README — devPassthrough documentation
416
+
417
+ ---
418
+
419
+ See also: getting-started/SKILL.md — Full main → preload → renderer wiring
420
+ See also: store-authoring/SKILL.md — Index definitions determine how hooks query content
421
+
422
+ ## References
423
+
424
+ - [Complete hooks API reference](references/hooks.md)