@jxsuite/studio 2.4.3 → 4.0.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 (208) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
  4. package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
  5. package/dist/chunks/content-source-t88vb2dr.js +49 -0
  6. package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
  9. package/dist/chunks/file-ops-rskmhxyq.js +48 -0
  10. package/dist/chunks/files-0cratsr0.js +92 -0
  11. package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
  14. package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
  15. package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
  16. package/dist/chunks/layers-q75tj2mf.js +45 -0
  17. package/dist/chunks/lit-html-j055mkh8.js +22 -0
  18. package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
  19. package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
  20. package/dist/chunks/notify-yjm79n4s.js +30 -0
  21. package/dist/chunks/parse-24d0gay3.js +19 -0
  22. package/dist/chunks/platform-ba1j1gbh.js +16 -0
  23. package/dist/chunks/publish-panel-54jvhskq.js +28 -0
  24. package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
  25. package/dist/chunks/references-3qx8fkyp.js +29 -0
  26. package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
  27. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
  28. package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
  29. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
  30. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
  31. package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
  32. package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
  33. package/dist/chunks/studio-2zkz8evn.js +3163 -0
  34. package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
  38. package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
  39. package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
  40. package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
  41. package/dist/chunks/studio-92ab2jqe.js +282 -0
  42. package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
  43. package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
  44. package/dist/chunks/studio-9n4mrmnb.js +184 -0
  45. package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
  46. package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
  51. package/dist/chunks/studio-eesa1myv.js +498 -0
  52. package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
  53. package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
  57. package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/studio-kyh9avvv.js +28 -0
  60. package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
  61. package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
  62. package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
  63. package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
  64. package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
  65. package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
  67. package/dist/chunks/studio-q43a5y9h.js +394 -0
  68. package/dist/chunks/studio-q7v10cg2.js +106 -0
  69. package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
  70. package/dist/chunks/studio-r1r7afxx.js +1066 -0
  71. package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
  72. package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
  73. package/dist/chunks/studio-sf7zjgv1.js +91 -0
  74. package/dist/chunks/studio-t7qb9db8.js +665 -0
  75. package/dist/chunks/studio-tmc5emrw.js +8 -0
  76. package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
  77. package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
  78. package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
  79. package/dist/chunks/studio-wcy8trfx.js +263 -0
  80. package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
  81. package/dist/chunks/studio-xrapxwwc.js +1539 -0
  82. package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
  83. package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
  84. package/dist/chunks/workspace-xpres40v.js +89 -0
  85. package/dist/iframe-entry.js +450 -48
  86. package/dist/studio.js +2455 -1761
  87. package/package.json +16 -15
  88. package/src/browse/library-layouts.ts +3 -3
  89. package/src/browse/library-pane.ts +59 -5
  90. package/src/browse/library-preview.ts +19 -3
  91. package/src/canvas/asset-refs.ts +119 -0
  92. package/src/canvas/asset-resolve.ts +256 -0
  93. package/src/canvas/canvas-live-render.ts +23 -15
  94. package/src/canvas/canvas-render.ts +23 -1
  95. package/src/canvas/iframe-entry.ts +1 -0
  96. package/src/canvas/iframe-host.ts +4 -0
  97. package/src/canvas/iframe-patch.ts +47 -21
  98. package/src/canvas/iframe-protocol.ts +9 -0
  99. package/src/canvas/iframe-render.ts +50 -16
  100. package/src/commands/app-commands.ts +4 -0
  101. package/src/commands/command-args.ts +29 -0
  102. package/src/commands/context.ts +15 -1
  103. package/src/commands/defaults.ts +29 -1
  104. package/src/commands/levels.ts +16 -0
  105. package/src/commands/live-context.ts +3 -0
  106. package/src/content/collection-match.ts +267 -0
  107. package/src/content/entry-commands.ts +38 -29
  108. package/src/content/entry-fields.ts +6 -0
  109. package/src/editor/file-drop-action.ts +2 -1
  110. package/src/editor/shortcuts.ts +5 -0
  111. package/src/files/file-ops.ts +10 -42
  112. package/src/files/files.ts +487 -61
  113. package/src/files/fs-events.ts +9 -0
  114. package/src/files/gitignore.ts +432 -0
  115. package/src/files/media-paths.ts +37 -14
  116. package/src/files/media-upload.ts +47 -6
  117. package/src/files/serialize-document.ts +60 -0
  118. package/src/format/convert-file.ts +511 -0
  119. package/src/format/format-choices.ts +199 -0
  120. package/src/grid/grid-idle.ts +45 -0
  121. package/src/grid/grid-view.ts +35 -0
  122. package/src/grid/schema-columns.ts +2 -1
  123. package/src/grid/sources/content-source.ts +24 -3
  124. package/src/media/media-open.ts +58 -0
  125. package/src/media/media-pane.ts +310 -0
  126. package/src/new-project/import-tab.ts +333 -160
  127. package/src/new-project/new-project-modal.ts +27 -34
  128. package/src/packages/ensure-deps.ts +14 -7
  129. package/src/packages/jxsuite-update.ts +11 -11
  130. package/src/page-params.ts +10 -46
  131. package/src/panels/activity-bar.ts +45 -16
  132. package/src/panels/ai-chat/attached-context.ts +6 -1
  133. package/src/panels/ai-chat/chat-view.ts +255 -13
  134. package/src/panels/ai-chat/composer.ts +21 -80
  135. package/src/panels/ai-panel.ts +116 -2
  136. package/src/panels/frontmatter-fields.ts +6 -2
  137. package/src/panels/navigator-panels.ts +7 -6
  138. package/src/panels/properties-panel.ts +8 -2
  139. package/src/panels/seo-modal.ts +5 -6
  140. package/src/panels/settings-menu.ts +799 -0
  141. package/src/panels/signals-panel.ts +2 -1
  142. package/src/panels/toolbar.ts +135 -15
  143. package/src/platforms/cloud.ts +34 -3
  144. package/src/platforms/devserver.ts +72 -34
  145. package/src/preview/preview-overlay.ts +226 -0
  146. package/src/services/ai-ask.ts +247 -0
  147. package/src/services/ai-import-tools.ts +494 -0
  148. package/src/services/ai-models.ts +101 -11
  149. package/src/services/ai-project-tools.ts +20 -26
  150. package/src/services/ai-settings.ts +69 -71
  151. package/src/services/ai-system-prompt.ts +88 -2
  152. package/src/services/ai-turn-signal.ts +70 -0
  153. package/src/services/cf-settings.ts +19 -37
  154. package/src/services/connection.ts +74 -0
  155. package/src/services/context-manager.ts +89 -0
  156. package/src/services/document-assistant.ts +49 -7
  157. package/src/services/idle.ts +6 -1
  158. package/src/services/import-client.ts +33 -6
  159. package/src/services/import-run.ts +158 -0
  160. package/src/services/import-seed.ts +120 -0
  161. package/src/services/jx-validate.ts +58 -6
  162. package/src/services/project-adoption.ts +71 -0
  163. package/src/services/references.ts +0 -0
  164. package/src/services/settings/definitions.ts +181 -0
  165. package/src/services/settings/kernel.ts +335 -0
  166. package/src/services/settings/write-queue.ts +116 -0
  167. package/src/services/tool-executor.ts +34 -2
  168. package/src/settings/contexts-section.ts +45 -3
  169. package/src/settings/dependencies-editor.ts +43 -23
  170. package/src/settings/general-settings.ts +30 -12
  171. package/src/settings/preferences-accounts.ts +6 -19
  172. package/src/settings/preferences-dialog.ts +18 -41
  173. package/src/settings/preferences-keymap.ts +8 -18
  174. package/src/settings/preferences-sections.ts +57 -0
  175. package/src/settings/schema-field-ui.ts +2 -2
  176. package/src/settings/settings-document.ts +65 -9
  177. package/src/shell.ts +6 -14
  178. package/src/studio.ts +65 -7
  179. package/src/tabs/transact.ts +42 -19
  180. package/src/types.ts +164 -8
  181. package/src/ui/ai-credentials-form.ts +52 -27
  182. package/src/ui/ai-model-picker.ts +162 -0
  183. package/src/ui/choice-field.ts +72 -0
  184. package/src/ui/layers.ts +83 -5
  185. package/src/ui/media-picker.ts +6 -11
  186. package/src/utils/studio-utils.ts +43 -31
  187. package/styles/forced-colors.css +7 -0
  188. package/styles/overlays.css +256 -0
  189. package/styles/panels.css +13 -1
  190. package/styles/shell.css +111 -0
  191. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  192. package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
  193. package/dist/chunks/content-source-k9724vcv.js +0 -39
  194. package/dist/chunks/files-yew27sn9.js +0 -69
  195. package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
  196. package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
  197. package/dist/chunks/quick-search-9avyc4pc.js +0 -54
  198. package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
  199. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  200. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  201. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  202. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  203. package/dist/chunks/studio-a7pzveq1.js +0 -110
  204. package/dist/chunks/studio-c31yb4t6.js +0 -55
  205. package/dist/chunks/studio-ra7ar50f.js +0 -6682
  206. package/src/canvas/content-assets.ts +0 -263
  207. package/src/canvas/site-style-css.ts +0 -121
  208. package/src/services/settings-store.ts +0 -99
@@ -0,0 +1,310 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The Media viewer — a pane that SHOWS a file the studio has no document model for.
4
+ *
5
+ * Clicking an image in the Files tree used to end in a toast: `openFileInTab` reads every file as
6
+ * text, finds no format class and no `.json`, and throws — so a PNG produced _"No format class
7
+ * imported for public/hero.jpg — add one to project.json imports"_, advice that means nothing about
8
+ * a binary asset. Video, audio, fonts and PDFs all failed the same way, and so did every tile in
9
+ * the Library, which routes to the same function. There was no way at all to look at an asset the
10
+ * project contains, which is exactly what somebody who has just imported a site needs to do.
11
+ *
12
+ * Almost nothing here is new. `files/media-paths.ts` already computes a src the parent realm can
13
+ * load, `files/media-meta.ts` already holds size, modified time and — once an `<img>` has loaded —
14
+ * pixel dimensions, and `files/media-usage.ts` already answers "which pages use this?" honestly.
15
+ * That last one had, in `site-architecture.md` §9.4's own words, _no reader but the delete
16
+ * confirmation_: you could only learn what an image was used for by trying to remove it. This is
17
+ * the surface that query was waiting for.
18
+ *
19
+ * Read-only, deliberately. Rename, delete and reveal are the file tree's, and a second set of
20
+ * buttons for them here would be a second place to keep them right.
21
+ *
22
+ * @docs studio/projects/media
23
+ */
24
+
25
+ import { html, nothing, render as litRender } from "lit-html";
26
+ import { effect, effectScope } from "../reactivity";
27
+ import { paneRegion } from "../ui/regions";
28
+ import { mediaSiteUrl, previewFileSrc } from "../files/media-paths";
29
+ import { formatBytes, loadMediaMeta, peekMediaMeta, recordImageSize } from "../files/media-meta";
30
+ import { extensionOf, mediaKind } from "../files/media-upload";
31
+ import { loadMediaUsages, mediaUsageHeadline, peekMediaUsages } from "../files/media-usage";
32
+ import type { MediaMeta } from "../files/media-meta";
33
+ import type { UsageState } from "../services/references";
34
+ import type { Tab } from "../tabs/tab";
35
+ import type { CanvasSurface } from "../canvas/canvas-surface";
36
+
37
+ /** The `canvasMode` the media viewer draws under. */
38
+ export const MEDIA_MODE = "media";
39
+
40
+ /** Extensions the viewer renders as a font specimen rather than as a file it cannot show. */
41
+ const FONT_EXTENSIONS = new Set([".woff", ".woff2", ".ttf", ".otf"]);
42
+
43
+ /** The pangram the font specimen is set in — short, and every letter of the alphabet once. */
44
+ const SPECIMEN = "The quick brown fox jumps over the lazy dog";
45
+
46
+ /** Specimen sizes, largest first: enough to judge a display face and a body face in one look. */
47
+ const SPECIMEN_SIZES = [48, 32, 24, 18, 14];
48
+
49
+ // ─── Mounting ────────────────────────────────────────────────────────────────
50
+
51
+ interface ActiveMediaPane {
52
+ paneId: string;
53
+ tabId: string;
54
+ wrap: HTMLElement;
55
+ scope: { stop: () => void; run: <T>(fn: () => T) => T | undefined };
56
+ }
57
+
58
+ /**
59
+ * The viewer mounted in each pane, keyed by pane id.
60
+ *
61
+ * Per pane rather than a singleton, for the reason `content/entry-editor.ts` documents: `media` is
62
+ * a kind the side pane may host, so two panes can hold one at once and a shared slot would let the
63
+ * second mount stop the first one's effect scope.
64
+ */
65
+ const _active = new Map<string, ActiveMediaPane>();
66
+
67
+ function activeIn(paneId: string): ActiveMediaPane | null {
68
+ return _active.get(paneId) ?? null;
69
+ }
70
+
71
+ /** Whether this tab's viewer is already mounted in this pane and still in the document. */
72
+ export function mediaPaneMounted(paneId: string, tab: Tab): boolean {
73
+ const panel = activeIn(paneId);
74
+ return panel !== null && panel.tabId === tab.id && panel.wrap.isConnected;
75
+ }
76
+
77
+ /** Tear one pane's viewer down (mode change, tab switch, project close). Idempotent. */
78
+ export function detachMediaPane(paneId: string): void {
79
+ const panel = _active.get(paneId);
80
+ if (!panel) {
81
+ return;
82
+ }
83
+ panel.scope.stop();
84
+ _active.delete(paneId);
85
+ }
86
+
87
+ // ─── Templates ───────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * The asset itself, in whatever element can show it.
91
+ *
92
+ * A file the browser cannot render is not a failure state — it is a font, or a zip, or a format
93
+ * this build has never heard of — so the fallback says what the file IS rather than apologising.
94
+ */
95
+ function assetTpl(path: string, src: string, redraw: () => void) {
96
+ const ext = extensionOf(path);
97
+ const kind = mediaKind({ name: path });
98
+
99
+ if (FONT_EXTENSIONS.has(ext)) {
100
+ /* A font is shown by being USED. The @font-face is scoped to a generated family name so two
101
+ specimens open side by side cannot claim the same one. */
102
+ const family = `jx-specimen-${path.replaceAll(/[^a-zA-Z0-9]/g, "-")}`;
103
+ return html`
104
+ <style>
105
+ @font-face {
106
+ font-family: "${family}";
107
+ src: url("${src}");
108
+ }
109
+ </style>
110
+ <div class="media-specimen" style="font-family: '${family}', system-ui, sans-serif">
111
+ ${SPECIMEN_SIZES.map(
112
+ (size) => html`<p class="media-specimen-line" style="font-size: ${size}px">
113
+ ${SPECIMEN}
114
+ </p>`,
115
+ )}
116
+ </div>
117
+ `;
118
+ }
119
+
120
+ switch (kind) {
121
+ case "image": {
122
+ return html`<img
123
+ class="media-image"
124
+ src=${src}
125
+ alt=""
126
+ @load=${(e: Event) => {
127
+ const img = e.target as HTMLImageElement;
128
+ /* The only honest source of pixel dimensions in a browser is an image that has loaded,
129
+ and this viewer is showing one at full size anyway. `recordImageSize` reports whether
130
+ the number was new, so a repaint happens once rather than on every load event. */
131
+ if (recordImageSize(path, img.naturalWidth, img.naturalHeight)) {
132
+ redraw();
133
+ }
134
+ }}
135
+ />`;
136
+ }
137
+ case "video": {
138
+ return html`<video class="media-video" src=${src} controls></video>`;
139
+ }
140
+ case "audio": {
141
+ return html`<audio class="media-audio" src=${src} controls></audio>`;
142
+ }
143
+ default: {
144
+ return ext === ".pdf"
145
+ ? html`<embed class="media-embed" src=${src} type="application/pdf" />`
146
+ : html`<div class="media-unviewable">
147
+ <p>Jx has no viewer for <code>${ext || "this file"}</code>.</p>
148
+ <p class="media-note">The file is in the project and builds normally.</p>
149
+ </div>`;
150
+ }
151
+ }
152
+ }
153
+
154
+ /** Kind, dimensions, size and modified time — each omitted rather than zeroed when unknown. */
155
+ function factsTpl(path: string, meta: MediaMeta | null) {
156
+ const facts: string[] = [mediaKind({ name: path })];
157
+ if (meta?.width != null && meta.height != null) {
158
+ facts.push(`${meta.width} × ${meta.height}`);
159
+ }
160
+ if (meta?.bytes != null) {
161
+ facts.push(formatBytes(meta.bytes));
162
+ }
163
+ if (meta?.modified) {
164
+ facts.push(`modified ${meta.modified.slice(0, 10)}`);
165
+ }
166
+ return html`<p class="media-facts">${facts.join(" · ")}</p>`;
167
+ }
168
+
169
+ /**
170
+ * The URL a document would reference this file by, with a copy button.
171
+ *
172
+ * The single most useful fact about an asset that is not the asset: `public/hero.jpg` is written
173
+ * `/hero.jpg`, a string that shares not one path segment with the file, and getting it wrong is the
174
+ * commonest way an image ends up missing from a page.
175
+ */
176
+ function refTpl(path: string) {
177
+ const ref = mediaSiteUrl(path);
178
+ return html`
179
+ <div class="media-ref">
180
+ <code class="media-ref-value">${ref}</code>
181
+ <sp-action-button
182
+ size="s"
183
+ quiet
184
+ title="Copy the reference"
185
+ @click=${() => {
186
+ void navigator.clipboard?.writeText(ref);
187
+ }}
188
+ >Copy</sp-action-button
189
+ >
190
+ </div>
191
+ `;
192
+ }
193
+
194
+ /** Which documents reference this file, or the honest reason there is no list. */
195
+ function usageTpl(usage: UsageState | null) {
196
+ const headline = mediaUsageHeadline(usage);
197
+ if (headline === null) {
198
+ // The host has no reference index. A zero here would be a number nobody can stand behind.
199
+ return nothing;
200
+ }
201
+ const files = usage?.status === "ready" ? usage.result.files : [];
202
+ return html`
203
+ <div class="media-usage">
204
+ <h4 class="media-usage-title">Used by</h4>
205
+ <p class="media-usage-headline">${headline}</p>
206
+ ${
207
+ files.length === 0
208
+ ? nothing
209
+ : html`<ul class="media-usage-list">
210
+ ${files.map(
211
+ (file) => html`<li>
212
+ <button
213
+ type="button"
214
+ class="media-usage-link"
215
+ @click=${() => {
216
+ void openDocument(file.path);
217
+ }}
218
+ >
219
+ ${file.path}
220
+ </button>
221
+ <span class="media-usage-count">${file.count}</span>
222
+ </li>`,
223
+ )}
224
+ </ul>`
225
+ }
226
+ </div>
227
+ `;
228
+ }
229
+
230
+ /**
231
+ * Open a document that references this file.
232
+ *
233
+ * Dynamic, for the reason `content/entry-editor.ts` gives: `files/files.ts` reaches the platform,
234
+ * the format registry and the packages layer, and a static edge would drag all of it in here.
235
+ */
236
+ async function openDocument(path: string): Promise<void> {
237
+ const { openFileInTab } = await import("../files/files");
238
+ await openFileInTab(path);
239
+ }
240
+
241
+ // ─── Render ──────────────────────────────────────────────────────────────────
242
+
243
+ /**
244
+ * Mount the media viewer into the pane.
245
+ *
246
+ * The same non-iframe-editor pattern as the grid, the Library, Project Settings and the Entry form:
247
+ * it owns its own effect scope from here, so learning an image's dimensions repaints this panel and
248
+ * nothing else.
249
+ *
250
+ * @param {CanvasSurface} surface
251
+ * @param {Tab} tab
252
+ */
253
+ export function renderMediaMode(surface: CanvasSurface, tab: Tab): void {
254
+ const { paneId, wrap: canvasWrap } = surface;
255
+ if (mediaPaneMounted(paneId, tab)) {
256
+ return;
257
+ }
258
+ detachMediaPane(paneId);
259
+
260
+ const scope = effectScope();
261
+ const panel: ActiveMediaPane = { paneId, scope, tabId: tab.id, wrap: canvasWrap };
262
+ _active.set(paneId, panel);
263
+
264
+ const path = tab.documentPath ?? "";
265
+
266
+ const redraw = () => {
267
+ if (activeIn(paneId) === panel) {
268
+ draw();
269
+ }
270
+ };
271
+
272
+ const draw = () => {
273
+ const meta = peekMediaMeta(path);
274
+ const usage = peekMediaUsages(path);
275
+ /* Asked on the first paint that finds it missing, never on every paint: both loaders cache and
276
+ de-duplicate in flight, and both repaint through `redraw` when they land. */
277
+ if (meta === null) {
278
+ void loadMediaMeta(path).then(redraw);
279
+ }
280
+ if (usage === null || usage.status === "pending") {
281
+ void loadMediaUsages(path).then(redraw);
282
+ }
283
+
284
+ litRender(
285
+ html`
286
+ <div class="media-viewer" data-jx-region=${paneRegion(paneId, "media")}>
287
+ <div class="media-stage">${assetTpl(path, previewFileSrc(path), redraw)}</div>
288
+ <div class="media-details">
289
+ <h3 class="media-name">${path.split("/").pop()}</h3>
290
+ ${factsTpl(path, meta)} ${refTpl(path)} ${usageTpl(usage)}
291
+ </div>
292
+ </div>
293
+ `,
294
+ canvasWrap,
295
+ );
296
+ };
297
+
298
+ scope.run(() => {
299
+ effect(() => {
300
+ if (activeIn(paneId) !== panel) {
301
+ return;
302
+ }
303
+ /* The tab's path is the whole of what this pane draws from — the file's bytes are the
304
+ browser's problem, and its metadata repaints through `redraw`. Reading it here is what
305
+ makes a rename repaint the viewer rather than leave it pointing at a name that is gone. */
306
+ void tab.documentPath;
307
+ draw();
308
+ });
309
+ });
310
+ }