@jxsuite/studio 8.0.1 → 9.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.
- package/dist/chunks/{2020-5wtzce5g.js → 2020-89fz3pfy.js} +3 -3
- package/dist/chunks/adapter-ay13gema.js +4 -0
- package/dist/chunks/add-new-flow-nmkqpjym.js +4 -0
- package/dist/chunks/cf-account-picker-2e06qxey.js +4 -0
- package/dist/chunks/cf-settings-5y04810c.js +4 -0
- package/dist/chunks/color-04361ttk.js +4 -0
- package/dist/chunks/{color-area-6rbra5dy.js → color-area-h8dbasp1.js} +2 -2
- package/dist/chunks/{color-field-gy56c94g.js → color-field-qv5r2b16.js} +3 -3
- package/dist/chunks/{color-slider-sqqdyf6c.js → color-slider-nz2mrzky.js} +2 -2
- package/dist/chunks/{combobox-72h32ejk.js → combobox-6hs00996.js} +2 -2
- package/dist/chunks/components-3vf75n83.js +4 -0
- package/dist/chunks/content-source-4f2raf4a.js +4 -0
- package/dist/chunks/{convert-to-repeater-he70fc2b.js → convert-to-repeater-j9hfcj4f.js} +3 -3
- package/dist/chunks/editor-645krvtz.js +4 -0
- package/dist/chunks/entry-commands-3qmv8wkr.js +4 -0
- package/dist/chunks/{extension-sections-0egb88dm.js → extension-sections-thfke054.js} +3 -3
- package/dist/chunks/file-ops-kkhp9ma7.js +4 -0
- package/dist/chunks/files-yswdhxrf.js +4 -0
- package/dist/chunks/format-host-4g8ex6bz.js +4 -0
- package/dist/chunks/fs-events-z9gpvc6t.js +4 -0
- package/dist/chunks/grid-open-fm4j183c.js +4 -0
- package/dist/chunks/{grid-view-qg1b5eqx.js → grid-view-7d2nx83y.js} +16 -15
- package/dist/chunks/{icons-63bjtbs7.js → icons-853n6qt3.js} +2 -2
- package/dist/chunks/{iframe-entry-pkgmcgp0.js → iframe-entry-2x9bn5ng.js} +4 -4
- package/dist/chunks/{iframe-entry-99nr5zfb.js → iframe-entry-42hpmwz5.js} +4 -4
- package/dist/chunks/{iframe-entry-76j8vwwx.js → iframe-entry-b5qgggyp.js} +2 -2
- package/dist/chunks/{iframe-entry-2zggzajv.js → iframe-entry-btck0e9s.js} +4 -4
- package/dist/chunks/iframe-host-q7fedsmr.js +4 -0
- package/dist/chunks/{index-32eqzv1k.js → index-jxmt7vh8.js} +3 -3
- package/dist/chunks/index-pt4wh98x.js +4 -0
- package/dist/chunks/{javascript-pjp734q4.js → javascript-vmqp56ke.js} +2 -2
- package/dist/chunks/{jsonMode-njkvsa02.js → jsonMode-n3wmpant.js} +5 -5
- package/dist/chunks/jx-validate-19y62knt.js +4 -0
- package/dist/chunks/layers-panel-1qvc83y9.js +4 -0
- package/dist/chunks/layers-qf0bc7ze.js +4 -0
- package/dist/chunks/{listbox-c421cf8c.js → listbox-7gka1h86.js} +2 -2
- package/dist/chunks/lit-html-wyqw04y2.js +4 -0
- package/dist/chunks/media-library-f9d5qcjb.js +4 -0
- package/dist/chunks/media-picker-rsb1fmk2.js +4 -0
- package/dist/chunks/{menu-bdpzbfvs.js → menu-d2jxpgma.js} +2 -2
- package/dist/chunks/{monaco-binding-vq1hgnn0.js → monaco-binding-wfhmz89d.js} +3 -3
- package/dist/chunks/monaco-buffer-ak0raadw.js +4 -0
- package/dist/chunks/{monaco-setup-xrd2cbkp.js → monaco-setup-fty7a8vg.js} +3 -3
- package/dist/chunks/notify-4qj70q0x.js +4 -0
- package/dist/chunks/parse-n8dfxnqf.js +4 -0
- package/dist/chunks/platform-ehg6t9vd.js +4 -0
- package/dist/chunks/{popover-b89sfej7.js → popover-9tf4cjrk.js} +2 -2
- package/dist/chunks/publish-panel-yhmda3cd.js +4 -0
- package/dist/chunks/{publish-quick-qx2455cj.js → publish-quick-12hh8926.js} +3 -3
- package/dist/chunks/publish-workspace-hx8qdv9p.js +4 -0
- package/dist/chunks/quick-search-r955m3q1.js +4 -0
- package/dist/chunks/references-8gxv0x7t.js +4 -0
- package/dist/chunks/right-panel-jarwqjpy.js +4 -0
- package/dist/chunks/source-control-84qt25m9.js +4 -0
- package/dist/chunks/{split-c9zfdgxf.js → split-m5rwn31z.js} +2 -2
- package/dist/chunks/studio-0nefxhar.js +7 -0
- package/dist/chunks/studio-0rg30pvs.js +5 -0
- package/dist/chunks/{studio-s32578pn.js → studio-14hh60j8.js} +4 -4
- package/dist/chunks/studio-1cfyd1ys.js +5 -0
- package/dist/chunks/studio-2bq7hheq.js +5 -0
- package/dist/chunks/{studio-zz9qe3rw.js → studio-32d8wyxq.js} +37 -37
- package/dist/chunks/studio-3hpcawh8.js +4 -0
- package/dist/chunks/{studio-svb7yyve.js → studio-4j35hgsw.js} +4 -4
- package/dist/chunks/{studio-x1x167jh.js → studio-4qg61g9j.js} +4 -4
- package/dist/chunks/studio-4rgem12q.js +5 -0
- package/dist/chunks/studio-6k9djnxw.js +5 -0
- package/dist/chunks/studio-6rbaedw2.js +9 -0
- package/dist/chunks/studio-732mw0g8.js +5 -0
- package/dist/chunks/studio-79r8msnr.js +7 -0
- package/dist/chunks/studio-7q6jdd4h.js +5 -0
- package/dist/chunks/{studio-r5qy30vx.js → studio-8hf8qcm1.js} +4 -4
- package/dist/chunks/{studio-8tr6cfrc.js → studio-8pne0rx4.js} +4 -4
- package/dist/chunks/{studio-y0n7yxea.js → studio-9179bsm7.js} +7 -7
- package/dist/chunks/{studio-pbxa5fpc.js → studio-9ff0vqff.js} +3 -3
- package/dist/chunks/studio-9gzvypw4.js +5 -0
- package/dist/chunks/studio-9ndgd55w.js +5 -0
- package/dist/chunks/studio-9ra9e7m4.js +5 -0
- package/dist/chunks/studio-a6e5fzve.js +5 -0
- package/dist/chunks/studio-a9k86nq6.js +5 -0
- package/dist/chunks/studio-ae7a69jc.js +6 -0
- package/dist/chunks/studio-aghsg0ds.js +7 -0
- package/dist/chunks/{studio-k3xxbagr.js → studio-ajw4k8hp.js} +3 -3
- package/dist/chunks/{studio-qmn1xc24.js → studio-apapy17g.js} +4 -4
- package/dist/chunks/{studio-8m0y2qe7.js → studio-by8x0jvc.js} +4 -4
- package/dist/chunks/{studio-wbcjbhy1.js → studio-c88gq88j.js} +5 -5
- package/dist/chunks/{studio-n0c5h6mf.js → studio-cg54vtet.js} +4 -4
- package/dist/chunks/studio-dvhxxxbd.js +5 -0
- package/dist/chunks/{studio-3q40e3rj.js → studio-e4rq6x0q.js} +4 -4
- package/dist/chunks/{studio-fbhxxc5f.js → studio-ekzbgnjp.js} +4 -4
- package/dist/chunks/studio-fg4fj9ec.js +10 -0
- package/dist/chunks/studio-fj6jzgxw.js +5 -0
- package/dist/chunks/{studio-nh6d342n.js → studio-g45r5vm8.js} +7 -7
- package/dist/chunks/{studio-a2bvaptz.js → studio-gczve8zf.js} +4 -4
- package/dist/chunks/studio-ge08zwpv.js +5 -0
- package/dist/chunks/studio-gk9yk8dj.js +12 -0
- package/dist/chunks/{studio-bbe4c90a.js → studio-gskkgfe3.js} +4 -4
- package/dist/chunks/{studio-83mk4bgg.js → studio-h4jbgne2.js} +8 -8
- package/dist/chunks/{studio-b7y5mr30.js → studio-hcy9zeac.js} +4 -4
- package/dist/chunks/studio-ht81xjkr.js +5 -0
- package/dist/chunks/{studio-jaaz8ce9.js → studio-jr30g4ae.js} +4 -4
- package/dist/chunks/{studio-qgh13jtz.js → studio-k5n4es74.js} +4 -4
- package/dist/chunks/{studio-ccv2svc6.js → studio-k6hbx2fc.js} +4 -4
- package/dist/chunks/{studio-9tmebf0s.js → studio-kqh7fxn6.js} +4 -4
- package/dist/chunks/{studio-q0mvbaq6.js → studio-kyw47pet.js} +4 -4
- package/dist/chunks/studio-mkw17ngg.js +5 -0
- package/dist/chunks/{studio-x9hsze61.js → studio-myb4mf0h.js} +4 -4
- package/dist/chunks/studio-n4qq1n57.js +5 -0
- package/dist/chunks/studio-nfmpmkqb.js +5 -0
- package/dist/chunks/studio-nfwacw31.js +5 -0
- package/dist/chunks/{studio-rthyg42z.js → studio-nm86874b.js} +172 -172
- package/dist/chunks/studio-nqexty8x.js +16 -0
- package/dist/chunks/studio-nr0kcqc6.js +5 -0
- package/dist/chunks/{studio-jsgf2q5k.js → studio-nvfa137z.js} +5 -5
- package/dist/chunks/studio-psh8mwnw.js +4 -0
- package/dist/chunks/studio-qd0cycm4.js +9 -0
- package/dist/chunks/{studio-tfkznv39.js → studio-qevf5z9d.js} +4 -4
- package/dist/chunks/studio-qs5fmhtj.js +14 -0
- package/dist/chunks/studio-ry4789ng.js +7 -0
- package/dist/chunks/studio-s4fq9jht.js +5 -0
- package/dist/chunks/studio-sc35ttdk.js +5 -0
- package/dist/chunks/studio-sm2f0sc6.js +6 -0
- package/dist/chunks/{studio-3qeh9t0h.js → studio-spza69tb.js} +5 -5
- package/dist/chunks/studio-t6dqnnft.js +5 -0
- package/dist/chunks/{studio-43sd2mam.js → studio-tfmhnwyr.js} +3 -3
- package/dist/chunks/studio-tn7v8gey.js +6 -0
- package/dist/chunks/{studio-tft6r105.js → studio-trcfer2b.js} +4 -4
- package/dist/chunks/studio-tyw7tk0x.js +5 -0
- package/dist/chunks/{studio-jcnsdaep.js → studio-tyzgte21.js} +4 -4
- package/dist/chunks/studio-vtey9w98.js +5 -0
- package/dist/chunks/studio-ws2r1d7e.js +5 -0
- package/dist/chunks/studio-x2trkhk5.js +5 -0
- package/dist/chunks/{studio-mn0476ag.js → studio-xzgp0cp7.js} +4 -4
- package/dist/chunks/studio-y47090vq.js +5 -0
- package/dist/chunks/{studio-5g0rprdt.js → studio-yjh9gddd.js} +5 -5
- package/dist/chunks/studio-ywf9y1e2.js +5 -0
- package/dist/chunks/{studio-k7evxqh4.js → studio-z1p91z5a.js} +4 -4
- package/dist/chunks/studio-ztppbam0.js +5 -0
- package/dist/chunks/{toast-host-1yg5ar8d.js → toast-host-g3r2s0kn.js} +2 -2
- package/dist/chunks/toast-w11qpkyz.js +4 -0
- package/dist/chunks/{tooltip-yypz28gq.js → tooltip-dj1pvgz7.js} +2 -2
- package/dist/chunks/{tree-9xa4hm7n.js → tree-j0ebwr5s.js} +2 -2
- package/dist/chunks/{tsMode-epyfz1hv.js → tsMode-byje4a01.js} +5 -5
- package/dist/chunks/workspace-7q4arnt3.js +4 -0
- package/dist/chunks/workspace-home-ge027kr6.js +4 -0
- package/dist/chunks/{ws-client-bv552sfx.js → ws-client-h09h6sqg.js} +3 -3
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +76 -76
- package/dist/studio.js +88 -88
- package/package.json +15 -16
- package/src/browse/component-examples.ts +15 -224
- package/src/browse/library-preview.ts +6 -13
- package/src/browse/preview-root.ts +7 -4
- package/src/canvas/canvas-utils.ts +18 -4
- package/src/canvas/iframe-editable-root.ts +13 -4
- package/src/canvas/iframe-entry.ts +3 -1
- package/src/canvas/iframe-interaction.ts +11 -2
- package/src/canvas/iframe-render.ts +25 -3
- package/src/canvas/preview-placeholder.ts +120 -0
- package/src/collab/collab-session.ts +6 -2
- package/src/commands/context.ts +4 -0
- package/src/commands/defaults.ts +2 -0
- package/src/commands/live-context.ts +2 -0
- package/src/editor/shortcuts.ts +3 -0
- package/src/files/files.ts +6 -3
- package/src/new-project/new-project-modal.ts +1 -4
- package/src/panels/ai-panel.ts +27 -34
- package/src/panels/component-preview.ts +5 -0
- package/src/panels/pane-context.ts +43 -18
- package/src/panels/site-model.ts +7 -19
- package/src/panels/site-panel.ts +2 -25
- package/src/panels/stylebook-doc.ts +2 -53
- package/src/platform.ts +20 -0
- package/src/platforms/devserver.ts +2 -120
- package/src/services/ai-ask.ts +29 -171
- package/src/services/ai-command-tools.ts +40 -50
- package/src/services/ai-import-tools.ts +4 -5
- package/src/services/ai-project-tools.ts +13 -3
- package/src/services/ai-tools.ts +26 -564
- package/src/services/ai-writes.ts +84 -2
- package/src/services/document-assistant.ts +29 -58
- package/src/services/harness/assistant-tools.ts +68 -0
- package/src/services/harness/interaction-store.ts +118 -0
- package/src/services/harness/studio-documents.ts +105 -0
- package/src/services/harness/turn-hooks.ts +1 -6
- package/src/services/project-adoption.ts +8 -16
- package/src/services/references.ts +0 -0
- package/src/services/tool-executor.ts +25 -22
- package/src/state.ts +3 -13
- package/src/style/stylebook-specimens.ts +11 -15
- package/src/surfaces/files-panel.json +12 -0
- package/src/surfaces/files-panel.ts +5 -2
- package/src/surfaces/pane-context.json +286 -265
- package/src/surfaces/pane-context.ts +17 -8
- package/src/surfaces/welcome.ts +4 -1
- package/src/tabs/selection.ts +14 -109
- package/src/tabs/tab.ts +15 -7
- package/src/tabs/transact.ts +316 -124
- package/src/types.ts +13 -8
- package/src/utils/geometry.ts +49 -2
- package/dist/chunks/adapter-5tphg7xe.js +0 -4
- package/dist/chunks/add-new-flow-psym14sn.js +0 -4
- package/dist/chunks/cf-account-picker-haqazq5r.js +0 -4
- package/dist/chunks/cf-settings-h2wrh6hq.js +0 -4
- package/dist/chunks/color-m7r1vk54.js +0 -4
- package/dist/chunks/components-r22xca09.js +0 -4
- package/dist/chunks/content-source-mmppmcyg.js +0 -4
- package/dist/chunks/editor-ccd9jwsa.js +0 -4
- package/dist/chunks/entry-commands-6qxz9e8p.js +0 -4
- package/dist/chunks/file-ops-qnab36ek.js +0 -4
- package/dist/chunks/files-rcn56qvm.js +0 -4
- package/dist/chunks/format-host-yxz0kjjs.js +0 -4
- package/dist/chunks/fs-events-d1qxq4z4.js +0 -4
- package/dist/chunks/grid-open-4z1g062t.js +0 -4
- package/dist/chunks/iframe-host-ssv7kyj0.js +0 -4
- package/dist/chunks/index-f0jn7nnp.js +0 -4
- package/dist/chunks/jx-validate-ttasrbh7.js +0 -4
- package/dist/chunks/layers-bgyr11tr.js +0 -4
- package/dist/chunks/layers-panel-7ez66zeq.js +0 -4
- package/dist/chunks/lit-html-v9gh687x.js +0 -4
- package/dist/chunks/media-library-sq468wts.js +0 -4
- package/dist/chunks/media-picker-shfe59pt.js +0 -4
- package/dist/chunks/monaco-buffer-3afgwvbp.js +0 -4
- package/dist/chunks/notify-kzrjr3fv.js +0 -4
- package/dist/chunks/parse-v77xtwv7.js +0 -4
- package/dist/chunks/platform-7v4rtd7m.js +0 -4
- package/dist/chunks/publish-panel-2z9gw8ee.js +0 -4
- package/dist/chunks/publish-workspace-wrc1dgwx.js +0 -4
- package/dist/chunks/quick-search-n0t0eqxb.js +0 -4
- package/dist/chunks/references-9547rg5s.js +0 -4
- package/dist/chunks/right-panel-wsgnr9pn.js +0 -4
- package/dist/chunks/source-control-kwktr9y6.js +0 -4
- package/dist/chunks/studio-03ct6yfv.js +0 -9
- package/dist/chunks/studio-07rcfje3.js +0 -5
- package/dist/chunks/studio-3e54qh39.js +0 -7
- package/dist/chunks/studio-3exxhz5g.js +0 -5
- package/dist/chunks/studio-3qqjeep5.js +0 -6
- package/dist/chunks/studio-4hp8pkyt.js +0 -5
- package/dist/chunks/studio-58xg6j2q.js +0 -9
- package/dist/chunks/studio-6m380wp7.js +0 -5
- package/dist/chunks/studio-80jyh1xh.js +0 -4
- package/dist/chunks/studio-8vcvfs7r.js +0 -6
- package/dist/chunks/studio-bavb31r0.js +0 -5
- package/dist/chunks/studio-c05wdnt3.js +0 -5
- package/dist/chunks/studio-d5grtb9e.js +0 -16
- package/dist/chunks/studio-df0baea9.js +0 -5
- package/dist/chunks/studio-dgk4dqtx.js +0 -7
- package/dist/chunks/studio-e1g64q43.js +0 -5
- package/dist/chunks/studio-ed1hp6zf.js +0 -6
- package/dist/chunks/studio-ed3xx40h.js +0 -7
- package/dist/chunks/studio-f14e1zdj.js +0 -5
- package/dist/chunks/studio-fyh0jc96.js +0 -5
- package/dist/chunks/studio-gcxk20n7.js +0 -5
- package/dist/chunks/studio-kt3pjkwd.js +0 -5
- package/dist/chunks/studio-m8g16bgz.js +0 -5
- package/dist/chunks/studio-mx1smyq0.js +0 -5
- package/dist/chunks/studio-nqxqf8p2.js +0 -4
- package/dist/chunks/studio-p4nrkvqr.js +0 -5
- package/dist/chunks/studio-pgmvdv7z.js +0 -7
- package/dist/chunks/studio-ph84avkf.js +0 -5
- package/dist/chunks/studio-q0tccd43.js +0 -5
- package/dist/chunks/studio-q7rtx6zq.js +0 -14
- package/dist/chunks/studio-qwdqwkj0.js +0 -5
- package/dist/chunks/studio-rj9eamf8.js +0 -5
- package/dist/chunks/studio-rptgsk96.js +0 -5
- package/dist/chunks/studio-s19tp1xr.js +0 -5
- package/dist/chunks/studio-syreb644.js +0 -5
- package/dist/chunks/studio-tf9jryb1.js +0 -10
- package/dist/chunks/studio-txf2kysx.js +0 -5
- package/dist/chunks/studio-v8867ddv.js +0 -12
- package/dist/chunks/studio-vxq7fcvv.js +0 -5
- package/dist/chunks/studio-w4rz9hdk.js +0 -5
- package/dist/chunks/studio-wc649wss.js +0 -5
- package/dist/chunks/studio-wys23mxz.js +0 -5
- package/dist/chunks/studio-y3ahhdpx.js +0 -5
- package/dist/chunks/studio-z97c48fk.js +0 -5
- package/dist/chunks/studio-zat62m0c.js +0 -5
- package/dist/chunks/studio-zcd2e7yz.js +0 -5
- package/dist/chunks/toast-1gv0h53h.js +0 -4
- package/dist/chunks/workspace-dz7nxfhv.js +0 -4
- package/dist/chunks/workspace-home-xsd7hkjw.js +0 -4
- package/src/services/directory-picker.ts +0 -187
- package/src/services/gated-registry.ts +0 -77
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* A stand-in picture for an image a component preview has no source for.
|
|
4
|
+
*
|
|
5
|
+
* Component previews draw a definition with only its own declared `examples` (`browse/
|
|
6
|
+
* component-examples.ts`), never with data borrowed from the project. A card component whose image
|
|
7
|
+
* prop defaults to `""` therefore drew a broken-image glyph — or, worse, an `<img src="">`, which
|
|
8
|
+
* resolves to the document's own url and fails as an image. Previews show the component's SHAPE, so
|
|
9
|
+
* an empty image is drawn as a neutral placeholder of a typical photo's proportions instead.
|
|
10
|
+
*
|
|
11
|
+
* Used by the two surfaces that draw components on their own: the parent-realm previews
|
|
12
|
+
* (`panels/component-preview.ts`: the Library, the Site panel's component cards, the palette) and
|
|
13
|
+
* the Stylebook's specimen render in the canvas frame (`canvas/iframe-render.ts`). Never the page
|
|
14
|
+
* canvas: an empty image on a page is something its author should see.
|
|
15
|
+
*
|
|
16
|
+
* Plain DOM with no realm-specific imports, so the frame and the editor share one definition.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A neutral landscape glyph: the canvas's one structural gray (`#808080`, as `STYLEBOOK_CSS` uses
|
|
21
|
+
* it) at low opacity, so it reads as a quiet tint on a light card and a dark one alike. A `data:`
|
|
22
|
+
* image is a document of its own, which no theme token or custom property can reach into.
|
|
23
|
+
*/
|
|
24
|
+
export const PREVIEW_IMAGE_PLACEHOLDER = `data:image/svg+xml,${encodeURIComponent(
|
|
25
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" viewBox="0 0 320 180">' +
|
|
26
|
+
'<rect width="320" height="180" fill="#808080" fill-opacity="0.16"/>' +
|
|
27
|
+
'<g fill="#808080" fill-opacity="0.38">' +
|
|
28
|
+
'<circle cx="118" cy="64" r="14"/>' +
|
|
29
|
+
'<path d="M60 140l52-50 34 30 38-46 76 66z"/>' +
|
|
30
|
+
"</g></svg>",
|
|
31
|
+
)}`;
|
|
32
|
+
|
|
33
|
+
/** Marks an `<img>` whose source is the placeholder, so a later real source is told apart. */
|
|
34
|
+
export const PLACEHOLDER_ATTR = "data-jx-image-placeholder";
|
|
35
|
+
|
|
36
|
+
/** Whether an image names no source at all: a blank `src` (or none) and a blank `srcset`. */
|
|
37
|
+
export function hasNoImageSource(img: HTMLImageElement): boolean {
|
|
38
|
+
const src = img.getAttribute("src")?.trim() ?? "";
|
|
39
|
+
const srcset = img.getAttribute("srcset")?.trim() ?? "";
|
|
40
|
+
if (src !== "" || srcset !== "") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// A <picture>'s <source srcset> picks the image even when the <img> itself names none.
|
|
44
|
+
const picture = img.parentElement?.localName === "picture" ? img.parentElement : null;
|
|
45
|
+
return !picture?.querySelector("source[srcset]:not([srcset=''])");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Give one image the placeholder when it has no source; clear the mark once it has a real one. */
|
|
49
|
+
function fill(img: HTMLImageElement): void {
|
|
50
|
+
if (hasNoImageSource(img)) {
|
|
51
|
+
img.setAttribute(PLACEHOLDER_ATTR, "");
|
|
52
|
+
img.setAttribute("src", PREVIEW_IMAGE_PLACEHOLDER);
|
|
53
|
+
} else if (
|
|
54
|
+
img.hasAttribute(PLACEHOLDER_ATTR) &&
|
|
55
|
+
img.getAttribute("src") !== PREVIEW_IMAGE_PLACEHOLDER
|
|
56
|
+
) {
|
|
57
|
+
img.removeAttribute(PLACEHOLDER_ATTR);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether an element is an `<img>`. By tag name rather than `instanceof`: the Stylebook's frame is
|
|
63
|
+
* another realm, whose `HTMLImageElement` is not this module's.
|
|
64
|
+
*/
|
|
65
|
+
function isImage(element: Element): element is HTMLImageElement {
|
|
66
|
+
return element.localName === "img";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every `<img>` at or under `node`. */
|
|
70
|
+
function imagesIn(node: Node): HTMLImageElement[] {
|
|
71
|
+
if (node.nodeType !== node.ELEMENT_NODE || !("querySelectorAll" in node)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
// SAFETY: an ELEMENT_NODE with `querySelectorAll` is an Element, in whichever realm it lives.
|
|
75
|
+
const element = node as Element;
|
|
76
|
+
const found = [...element.querySelectorAll("img")];
|
|
77
|
+
return isImage(element) ? [element, ...found] : found;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fill every source-less image under `root` with {@link PREVIEW_IMAGE_PLACEHOLDER}, now and as the
|
|
82
|
+
* tree changes: a component draws its own children in its `connectedCallback`, after the preview
|
|
83
|
+
* element is created, and a binding can clear an image's source later. A real source always wins —
|
|
84
|
+
* the placeholder only ever replaces nothing. Returns a function that stops watching.
|
|
85
|
+
*/
|
|
86
|
+
export function fillPreviewImages(root: Element): () => void {
|
|
87
|
+
for (const img of imagesIn(root)) {
|
|
88
|
+
fill(img);
|
|
89
|
+
}
|
|
90
|
+
/* The root's own realm's observer where it has one (the Stylebook's frame); this realm's for a
|
|
91
|
+
document with no window, such as a parsed one — an observer watches any same-origin node. */
|
|
92
|
+
const Observer = root.ownerDocument.defaultView?.MutationObserver ?? MutationObserver;
|
|
93
|
+
const observer = new Observer((records) => {
|
|
94
|
+
for (const record of records) {
|
|
95
|
+
if (record.type === "attributes") {
|
|
96
|
+
const { target } = record;
|
|
97
|
+
if (target.nodeType === target.ELEMENT_NODE && "localName" in target) {
|
|
98
|
+
// SAFETY: an ELEMENT_NODE target with `localName` is an Element (attributes only land on elements).
|
|
99
|
+
const element = target as Element;
|
|
100
|
+
if (isImage(element)) {
|
|
101
|
+
fill(element);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const added of record.addedNodes) {
|
|
107
|
+
for (const img of imagesIn(added)) {
|
|
108
|
+
fill(img);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
observer.observe(root, {
|
|
114
|
+
attributeFilter: ["src", "srcset"],
|
|
115
|
+
attributes: true,
|
|
116
|
+
childList: true,
|
|
117
|
+
subtree: true,
|
|
118
|
+
});
|
|
119
|
+
return () => observer.disconnect();
|
|
120
|
+
}
|
|
@@ -26,7 +26,7 @@ import { jsonClone } from "../utils/studio-utils";
|
|
|
26
26
|
import { cloneSelection } from "../tabs/selection";
|
|
27
27
|
import {
|
|
28
28
|
applyExternalDocOps,
|
|
29
|
-
|
|
29
|
+
isBatched,
|
|
30
30
|
setBatchEndNotifier,
|
|
31
31
|
setHistoryDelegate,
|
|
32
32
|
setTransactGate,
|
|
@@ -318,7 +318,11 @@ function onBatchEnd(tab: Tab): void {
|
|
|
318
318
|
|
|
319
319
|
function publishRecord(session: ActiveSession, record: TransactionRecord): void {
|
|
320
320
|
const forward = record.docOps.map((pair) => pair.forward);
|
|
321
|
-
|
|
321
|
+
/* Buffered only while THIS tab has a lane segment open, which means the transaction just
|
|
322
|
+
observed was the lane owner's: anyone else's write closes the segment before it runs, and the
|
|
323
|
+
close flushes the buffer, so the Y document receives the owner's ops before the other writer's
|
|
324
|
+
and in local order. */
|
|
325
|
+
if (isBatched(session.tab)) {
|
|
322
326
|
session.batchOps ??= [];
|
|
323
327
|
if (forward.length === 0) {
|
|
324
328
|
session.batchNeedsDiff = true;
|
package/src/commands/context.ts
CHANGED
|
@@ -31,6 +31,9 @@ export const CAPABILITIES = [
|
|
|
31
31
|
"gitClone",
|
|
32
32
|
"importSite",
|
|
33
33
|
"openProjectInNewWindow",
|
|
34
|
+
/* Open Project's gate: a native dialog or the cloud's repository picker. A dev-server window opens
|
|
35
|
+
its project from `?project=`, so the command is not offered there. */
|
|
36
|
+
"openProject",
|
|
34
37
|
"dataRows",
|
|
35
38
|
"windowControls",
|
|
36
39
|
"findReferences",
|
|
@@ -339,6 +342,7 @@ export function emptyContext(): CommandContext {
|
|
|
339
342
|
gitClone: false,
|
|
340
343
|
importSite: false,
|
|
341
344
|
openProjectInNewWindow: false,
|
|
345
|
+
openProject: false,
|
|
342
346
|
dataRows: false,
|
|
343
347
|
windowControls: false,
|
|
344
348
|
findReferences: false,
|
package/src/commands/defaults.ts
CHANGED
|
@@ -563,6 +563,8 @@ export function defaultCommands(deps: CommandDeps): AnyCommand[] {
|
|
|
563
563
|
keybinding: "mod+o",
|
|
564
564
|
menus: ["commandbar/overflow", "statusbar/project", "palette"],
|
|
565
565
|
group: "1_file",
|
|
566
|
+
when: (ctx: CommandContext) => ctx.capability.openProject,
|
|
567
|
+
requires: "a host that can open a project from a dialog",
|
|
566
568
|
run: () => deps.openProject(),
|
|
567
569
|
},
|
|
568
570
|
{
|
|
@@ -38,6 +38,7 @@ import { collabState } from "../collab/collab-state";
|
|
|
38
38
|
|
|
39
39
|
import { canvasViewForMode, editorKindForMode, emptyContext } from "./context";
|
|
40
40
|
import type { CommandContext } from "./context";
|
|
41
|
+
import { canOpenProject } from "../platform";
|
|
41
42
|
import type { StudioPlatform } from "../types";
|
|
42
43
|
|
|
43
44
|
/** Elements that own the keyboard while focused. Matches the old listener's target test exactly. */
|
|
@@ -95,6 +96,7 @@ function capabilities(platform: StudioPlatform | null): CommandContext["capabili
|
|
|
95
96
|
findReferences: has("findReferences"),
|
|
96
97
|
gitClone: has("gitClone"),
|
|
97
98
|
importSite: has("importSite"),
|
|
99
|
+
openProject: canOpenProject(platform),
|
|
98
100
|
openProjectInNewWindow: has("openProjectInNewWindow"),
|
|
99
101
|
readFileBytes: has("readFileBytes"),
|
|
100
102
|
publish: has("cfApi"),
|
package/src/editor/shortcuts.ts
CHANGED
|
@@ -1010,6 +1010,9 @@ const SELF_SCROLLING_MODES: ReadonlySet<string> = new Set([
|
|
|
1010
1010
|
"media-home",
|
|
1011
1011
|
"preview",
|
|
1012
1012
|
"publish-home",
|
|
1013
|
+
/* The Styles workspace's Variables view — `style/variables-view.ts`. An `overflow-y: auto`
|
|
1014
|
+
column over the stage; without it here the wheel panned an artboard that does not exist. */
|
|
1015
|
+
"tokens",
|
|
1013
1016
|
]);
|
|
1014
1017
|
|
|
1015
1018
|
/**
|
package/src/files/files.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { errorMessage } from "@jxsuite/schema/parse";
|
|
|
39
39
|
import { localeLabel, localeOfPath, resolveI18n } from "@jxsuite/schema/locale";
|
|
40
40
|
import { showPromptDialog } from "../ui/layers";
|
|
41
41
|
import { projectState, requireProjectState, setProjectState } from "../store";
|
|
42
|
-
import { getPlatform } from "../platform";
|
|
42
|
+
import { canOpenProject, getPlatform } from "../platform";
|
|
43
43
|
import { disarmPreviewOverlay } from "../preview/preview-overlay";
|
|
44
44
|
import { notify } from "../services/notify";
|
|
45
45
|
import { loadComponentRegistry } from "./components";
|
|
@@ -309,7 +309,8 @@ export async function openProject({
|
|
|
309
309
|
}): Promise<boolean> {
|
|
310
310
|
try {
|
|
311
311
|
const platform = getPlatform();
|
|
312
|
-
|
|
312
|
+
// The command is gated on the capability, so a host without the member never gets here.
|
|
313
|
+
const result = await platform.openProject?.();
|
|
313
314
|
if (!result) {
|
|
314
315
|
return false;
|
|
315
316
|
} // User cancelled
|
|
@@ -864,7 +865,9 @@ function filesPanelValues(): FilesPanelValues {
|
|
|
864
865
|
}
|
|
865
866
|
if (!projectState.isSiteProject && projectState.projectRoot === ".") {
|
|
866
867
|
_fileRows = [];
|
|
867
|
-
|
|
868
|
+
// A host with no Open Project (the dev server) is told how its windows DO open one instead.
|
|
869
|
+
const view = canOpenProject(getPlatform()) ? "welcome" : "address";
|
|
870
|
+
const values: FilesPanelValues = { ...emptyFilesPanelValues(), view };
|
|
868
871
|
_lastValues = values;
|
|
869
872
|
return values;
|
|
870
873
|
}
|
|
@@ -346,10 +346,7 @@ function finish(result: { root: string; config: ProjectConfig } | null) {
|
|
|
346
346
|
* runs this same sequence itself.
|
|
347
347
|
*/
|
|
348
348
|
async function finishCreated(result: { root: string; config: ProjectConfig }) {
|
|
349
|
-
const { adopted, error, openedElsewhere } = await adoptCreatedProject(result.root
|
|
350
|
-
// The wizard runs outside the agent loop — there is never a batch to flush and re-anchor.
|
|
351
|
-
getTab: () => null,
|
|
352
|
-
});
|
|
349
|
+
const { adopted, error, openedElsewhere } = await adoptCreatedProject(result.root);
|
|
353
350
|
if (!adopted && !openedElsewhere) {
|
|
354
351
|
notify.warn(
|
|
355
352
|
"Created the project, but could not open it here.",
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
|
|
29
29
|
import { effect, effectScope } from "../reactivity";
|
|
30
30
|
import { createDocumentAssistant } from "../services/document-assistant";
|
|
31
|
-
import {
|
|
31
|
+
import { restoreTurn } from "../services/ai-writes";
|
|
32
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
32
33
|
import { notify } from "../services/notify";
|
|
33
|
-
import {
|
|
34
|
+
import type { NotifyOptions } from "../services/notify";
|
|
34
35
|
import { activeTab } from "../workspace/workspace";
|
|
35
36
|
import { setOpenAiKey } from "../services/ai-settings";
|
|
36
37
|
import { hasAiCredentials } from "../services/ai-models";
|
|
37
|
-
import { answerAsk, isAwaitingAnswer, pendingAsk, skipAsk } from "../services/ai-ask";
|
|
38
38
|
import { activeImportRun, importRun } from "../services/import-run";
|
|
39
39
|
import type { ImportBrief } from "../services/import-seed";
|
|
40
40
|
import type { ImportBreakpointPolicy } from "../types";
|
|
@@ -124,7 +124,7 @@ export function bindAiPanelHost(el: HTMLElement | null) {
|
|
|
124
124
|
run: runCommand,
|
|
125
125
|
send: () => composer.send(),
|
|
126
126
|
skip: () => {
|
|
127
|
-
|
|
127
|
+
assistant.interactions.skip();
|
|
128
128
|
renderAiPanel();
|
|
129
129
|
},
|
|
130
130
|
stop,
|
|
@@ -208,10 +208,10 @@ function watchAssistant() {
|
|
|
208
208
|
// The registry is composed AFTER the bootstrap mounts this, and it is a reactive holder —
|
|
209
209
|
// Reading it here is what turns the header's skeleton into its real buttons.
|
|
210
210
|
void activeRegistry();
|
|
211
|
-
// The question is not chat state — it lives in
|
|
212
|
-
//
|
|
213
|
-
// (and its ANSWER would never repaint the card at all).
|
|
214
|
-
void
|
|
211
|
+
// The question is not chat state — it lives in the assistant's interaction store — but it is
|
|
212
|
+
// Drawn into this panel, so the same effect has to track it or a question would appear a turn
|
|
213
|
+
// Late (and its ANSWER would never repaint the card at all).
|
|
214
|
+
void assistant.interactions.pending;
|
|
215
215
|
/* The run record, for the same reason: an import reports a line at a time from a store that
|
|
216
216
|
is not chat state, and its chip has to follow every one of them. Reading the ACTIVE run
|
|
217
217
|
tracks the whole record, so any field moving re-projects. */
|
|
@@ -268,7 +268,7 @@ async function handleAssistantSend(text: string) {
|
|
|
268
268
|
}
|
|
269
269
|
/* Answering comes first: a question is put by a turn that is still running, so a send while one
|
|
270
270
|
waits is its answer, never a second turn. */
|
|
271
|
-
if (
|
|
271
|
+
if (assistant.interactions.answer(text.trim())) {
|
|
272
272
|
surface?.pin();
|
|
273
273
|
renderAiPanel();
|
|
274
274
|
return;
|
|
@@ -319,38 +319,31 @@ async function handleRetry(): Promise<void> {
|
|
|
319
319
|
/**
|
|
320
320
|
* Undo everything one assistant turn changed — ai.md §3.2's "Restore to here".
|
|
321
321
|
*
|
|
322
|
-
* The
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
322
|
+
* The turn's batch lane left one step per document it wrote, and {@link restoreTurn} takes each
|
|
323
|
+
* back while it is still the top of its tab's history, or refuses the whole turn with a sentence.
|
|
324
|
+
* The button is offered by the projection only when every recorded change was transactional; the
|
|
325
|
+
* refusals are the second half of the same promise, because a ledger can be trimmed (MAX_TURNS) and
|
|
326
|
+
* a document edited between the projection and the click.
|
|
327
327
|
*
|
|
328
328
|
* @param {string} messageId
|
|
329
329
|
*/
|
|
330
330
|
/* Exported so the guard can be exercised directly: the button is not DRAWN for a turn that touched
|
|
331
331
|
disk, which would otherwise make the refusal path unreachable from the panel. */
|
|
332
332
|
export function handleRestore(messageId: string): void {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
notify.warn(
|
|
333
|
+
const restored = restoreTurn(messageId);
|
|
334
|
+
if (isString(restored)) {
|
|
335
|
+
notify.warn(restored, { source: "Assistant" });
|
|
336
336
|
return;
|
|
337
337
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const tab = activeTab.value;
|
|
348
|
-
if (!tab) {
|
|
349
|
-
notify.warn("Open the document that turn edited to restore it.", { source: "Assistant" });
|
|
350
|
-
return;
|
|
338
|
+
/* Redo reaches the active tab only, so it is offered only when that is the one tab restored;
|
|
339
|
+
across documents it would bring back part of the turn. */
|
|
340
|
+
const [only, ...rest] = restored;
|
|
341
|
+
const redoable = rest.length === 0 && only === activeTab.value;
|
|
342
|
+
const options: NotifyOptions = { source: "Assistant" };
|
|
343
|
+
if (redoable) {
|
|
344
|
+
options.action = "edit.redo";
|
|
351
345
|
}
|
|
352
|
-
|
|
353
|
-
notify.success("Restored to before that turn.", { action: "edit.redo", source: "Assistant" });
|
|
346
|
+
notify.success("Restored to before that turn.", options);
|
|
354
347
|
renderAiPanel();
|
|
355
348
|
}
|
|
356
349
|
|
|
@@ -509,7 +502,7 @@ export function revealAssistant(): void {
|
|
|
509
502
|
* turn waiting forever on a question nobody wants to answer is exactly what Stop is for.
|
|
510
503
|
*/
|
|
511
504
|
export function isAssistantWaiting(): boolean {
|
|
512
|
-
return
|
|
505
|
+
return assistant.interactions.pending !== null;
|
|
513
506
|
}
|
|
514
507
|
|
|
515
508
|
/**
|
|
@@ -617,7 +610,7 @@ function projectPanel(): AiChatView {
|
|
|
617
610
|
const advice = error ? formatErrorAdvice(error) : "";
|
|
618
611
|
const newChatCommand = projectCommand("assistant.newChat", { icon: "plus" });
|
|
619
612
|
const rows = projectRows({
|
|
620
|
-
ask: { importRun, pendingId:
|
|
613
|
+
ask: { importRun, pendingId: assistant.interactions.pending?.callId ?? null },
|
|
621
614
|
messages: cs.messages,
|
|
622
615
|
status: cs.status,
|
|
623
616
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
10
10
|
import { projectState } from "../store";
|
|
11
|
+
import { fillPreviewImages } from "../canvas/preview-placeholder";
|
|
11
12
|
import type { ComponentEntry } from "../files/components";
|
|
12
13
|
import type { JsonValue } from "@jxsuite/schema/types";
|
|
13
14
|
|
|
@@ -43,6 +44,10 @@ export async function renderComponentPreview(
|
|
|
43
44
|
for (const [name, value] of props) {
|
|
44
45
|
properties[name] = value;
|
|
45
46
|
}
|
|
47
|
+
/* Watched from now on: the instance draws its children when it connects, after this returns.
|
|
48
|
+
A preview shows the component's shape, so an image it has no source for is a placeholder,
|
|
49
|
+
not a broken glyph (`canvas/preview-placeholder.ts`). */
|
|
50
|
+
fillPreviewImages(el);
|
|
46
51
|
return el;
|
|
47
52
|
} catch (error) {
|
|
48
53
|
console.warn("Component preview failed:", comp.tagName, error);
|
|
@@ -105,6 +105,7 @@ import { componentPropEntries, isComponentDoc } from "../component-props";
|
|
|
105
105
|
import { mediaDisplayName } from "./shared";
|
|
106
106
|
import type { CanvasView, FitMode } from "../canvas/canvas-utils";
|
|
107
107
|
import { EDITOR_KIND_LABELS } from "../commands/context";
|
|
108
|
+
import type { EditorKind } from "../commands/context";
|
|
108
109
|
import type { ParamValues } from "../page-params";
|
|
109
110
|
import type { CommandArgValues } from "../commands/command-args";
|
|
110
111
|
import type { Tab } from "../tabs/tab";
|
|
@@ -781,22 +782,40 @@ function lensSummary(paneId: string): string {
|
|
|
781
782
|
|
|
782
783
|
// ─── Axis 1 · Editor kind ────────────────────────────────────────────────────
|
|
783
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Editor kinds that are entered by a gesture elsewhere and never offered as a choice.
|
|
787
|
+
*
|
|
788
|
+
* A comparison (`diff`) is opened from Publish's changed files or its version history, which says
|
|
789
|
+
* what it is compared against; a Diff segment on every document would compare whatever is open
|
|
790
|
+
* against whatever HEAD is, and was a third way into a view two browsers already own. While a tab
|
|
791
|
+
* is IN one, the axis names it and offers nothing: the way out is Back, beside it.
|
|
792
|
+
*/
|
|
793
|
+
const IMPLICIT_EDITOR_KINDS: ReadonlySet<EditorKind> = new Set(["diff"]);
|
|
794
|
+
|
|
784
795
|
function editorAxis(tab: Tab): Partial<PaneContextView> {
|
|
785
796
|
/* What the DOCUMENT declares, and nothing else. This read `hostableKindsOf` — the declared kinds
|
|
786
797
|
narrowed by what the tab's pane was allowed to host — so a page in the side pane was offered
|
|
787
798
|
Code and not Design. The pane cap is gone; a control that cannot go anywhere is still the
|
|
788
799
|
defect this axis exists to remove, and one declared kind still renders as text. */
|
|
789
|
-
const kinds = editorKindsOf(tab);
|
|
790
800
|
const current = editorKindOf(tab);
|
|
801
|
+
const kinds = IMPLICIT_EDITOR_KINDS.has(current)
|
|
802
|
+
? [current]
|
|
803
|
+
: editorKindsOf(tab).filter((kind) => !IMPLICIT_EDITOR_KINDS.has(kind));
|
|
791
804
|
if (kinds.length < 2) {
|
|
792
|
-
// One kind is not a choice. Rendering it as a
|
|
805
|
+
// One kind is not a choice. Rendering it as a segmented group would be a control that cannot move —
|
|
793
806
|
// Which is the defect this axis exists to remove, in miniature.
|
|
794
|
-
return { editorLabel: EDITOR_KIND_LABELS[current], editorMode: "static" };
|
|
807
|
+
return { editorLabel: EDITOR_KIND_LABELS[current], editorMode: "static", editorState: "shown" };
|
|
795
808
|
}
|
|
796
809
|
return {
|
|
797
810
|
editorMode: "picker",
|
|
798
|
-
|
|
799
|
-
|
|
811
|
+
editorState: "shown",
|
|
812
|
+
editors: kinds.map((kind) => ({
|
|
813
|
+
checked: kind === current ? "true" : "false",
|
|
814
|
+
hint: `Open this document in ${EDITOR_KIND_LABELS[kind]}`,
|
|
815
|
+
key: kind,
|
|
816
|
+
label: EDITOR_KIND_LABELS[kind],
|
|
817
|
+
value: kind,
|
|
818
|
+
})),
|
|
800
819
|
};
|
|
801
820
|
}
|
|
802
821
|
|
|
@@ -895,25 +914,26 @@ function contextAxis(tab: Tab, paneId: string, ctx: PaneContextCtx): Partial<Pan
|
|
|
895
914
|
const plainQueries = featureQueries.filter(({ query }) => !isSchemeQuery(query));
|
|
896
915
|
const scheme = ui.previewColorScheme;
|
|
897
916
|
const { activeMedia } = ui;
|
|
898
|
-
const sizeLabel = activeMedia ? mediaDisplayName(activeMedia) : "Base";
|
|
899
|
-
const schemeLabel = SCHEMES.find(([value]) => value === scheme)?.[1] ?? "Auto";
|
|
900
917
|
const locale = localeOf(tab);
|
|
901
|
-
/* The language joins the trigger ONLY when the pane is not drawing the document's own — a French
|
|
902
|
-
page open in a French pane is not a rendering context worth three more characters, and a bar
|
|
903
|
-
that grew a third term in every multilingual project would stop reading as a state. */
|
|
904
|
-
const contextSummary = [
|
|
905
|
-
sizeLabel,
|
|
906
|
-
schemeQueries.length > 0 ? schemeLabel : null,
|
|
907
|
-
locale?.overridden ? localeLabel(locale.effective) : null,
|
|
908
|
-
]
|
|
909
|
-
.filter((part) => part !== null)
|
|
910
|
-
.join(" · ");
|
|
911
918
|
/* An entry written on the canvas is wrapped in its template's layout like a page (§22.5), so it
|
|
912
919
|
offers the same "show layout" choice. Whether a layout resolves for it is read asynchronously
|
|
913
920
|
by the canvas; the project default is the answer this bar can give synchronously. */
|
|
914
921
|
const hasLayout =
|
|
915
922
|
(isPageDoc(tab) && Boolean(getEffectiveLayoutPath(tab.doc.document.$layout))) ||
|
|
916
923
|
(Boolean(projectState?.isSiteProject) && writesEntryOnCanvas(tab.documentPath));
|
|
924
|
+
/* The trigger names only what its popover holds and is NOT at rest. Size and scheme are on the bar
|
|
925
|
+
as groups now, so repeating them here would print the same state twice; and a trigger that read
|
|
926
|
+
"Base · Auto" over a popover that no longer offers either would be describing another control.
|
|
927
|
+
The language joins ONLY when the pane is not drawing the document's own — a French page open in
|
|
928
|
+
a French pane is not a rendering context worth three more characters. */
|
|
929
|
+
const featuresOn = plainQueries.filter(({ name }) => ui.featureToggles[name]).length;
|
|
930
|
+
const contextSummary = [
|
|
931
|
+
locale?.overridden ? localeLabel(locale.effective) : null,
|
|
932
|
+
featuresOn > 0 ? `${featuresOn} ${featuresOn === 1 ? "query" : "queries"}` : null,
|
|
933
|
+
hasLayout && ui.showLayout === false ? "No layout" : null,
|
|
934
|
+
]
|
|
935
|
+
.filter((part) => part !== null)
|
|
936
|
+
.join(" · ");
|
|
917
937
|
const page = isPageDoc(tab);
|
|
918
938
|
const params = page ? paramRows(tab) : [];
|
|
919
939
|
const props = page ? [] : propRows(tab, paneId);
|
|
@@ -952,6 +972,8 @@ function contextAxis(tab: Tab, paneId: string, ctx: PaneContextCtx): Partial<Pan
|
|
|
952
972
|
})) satisfies PaneContextChoice[],
|
|
953
973
|
schemeState: schemeQueries.length > 0 ? "shown" : "hidden",
|
|
954
974
|
sizes: sizeRows(sizeBreakpoints, activeMedia),
|
|
975
|
+
// Base alone is not a choice: a one-segment group would be a control that cannot move.
|
|
976
|
+
sizesState: sizeBreakpoints.length > 0 ? "shown" : "hidden",
|
|
955
977
|
};
|
|
956
978
|
}
|
|
957
979
|
|
|
@@ -1134,7 +1156,10 @@ function actionsFor(paneId: string, ctx: PaneContextCtx): PaneContextActions {
|
|
|
1134
1156
|
if (!tab) {
|
|
1135
1157
|
return;
|
|
1136
1158
|
}
|
|
1137
|
-
|
|
1159
|
+
// An implicit kind is refused like an undeclared one: the axis never offers it.
|
|
1160
|
+
const editorKind = editorKindsOf(tab).find(
|
|
1161
|
+
(candidate) => candidate === kind && !IMPLICIT_EDITOR_KINDS.has(candidate),
|
|
1162
|
+
);
|
|
1138
1163
|
const mode = editorKind && modeForEditorKind(tab, editorKind);
|
|
1139
1164
|
if (!mode) {
|
|
1140
1165
|
return;
|
package/src/panels/site-model.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The panel lists the project as an author thinks of it: pages by address, then the templates,
|
|
5
5
|
* components and media pages are made of. Everything here turns facts the flow has already gathered
|
|
6
|
-
* (paths, `$paths.contentType`, entry counts
|
|
7
|
-
*
|
|
6
|
+
* (paths, `$paths.contentType`, entry counts) into the rows the surface draws, so the ordering, the
|
|
7
|
+
* nesting, the labels and the filter are testable without a project.
|
|
8
8
|
*
|
|
9
9
|
* **Routes come from the router.** Rows are ordered and labelled with `fileToRoute` and
|
|
10
10
|
* `compareRoutes` from `@jxsuite/site/routes` — the functions the build's page discovery sorts with
|
|
@@ -64,7 +64,7 @@ export interface SiteTemplateRow {
|
|
|
64
64
|
key: string;
|
|
65
65
|
path: string;
|
|
66
66
|
label: string;
|
|
67
|
-
/** "
|
|
67
|
+
/** "default layout" for the project's `defaults.layout`, else empty. */
|
|
68
68
|
detail: string;
|
|
69
69
|
selected: SiteSelected;
|
|
70
70
|
}
|
|
@@ -320,28 +320,16 @@ export function filterPageRows(rows: readonly SitePageRow[], needle: string): Si
|
|
|
320
320
|
return kept;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
/** "used by 3 pages", "used by 1 page", "not used yet", or "" when the count is unknown. */
|
|
324
|
-
export function usedByLabel(pages: number | null): string {
|
|
325
|
-
if (pages === null) {
|
|
326
|
-
return "";
|
|
327
|
-
}
|
|
328
|
-
if (pages === 0) {
|
|
329
|
-
return "not used yet";
|
|
330
|
-
}
|
|
331
|
-
return `used by ${pages} ${pages === 1 ? "page" : "pages"}`;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
323
|
/**
|
|
335
324
|
* The Templates section's rows: every JSON layout under `layouts/`, by path. Layouts are JSON only
|
|
336
325
|
* (the layout resolver parses nothing else), so a non-JSON file there is not a template.
|
|
337
326
|
*
|
|
338
|
-
* The project's default layout (`defaults.layout`) says so
|
|
339
|
-
*
|
|
340
|
-
*
|
|
327
|
+
* The project's default layout (`defaults.layout`) says so. No row says how many pages use it:
|
|
328
|
+
* counting would ask the reference index once per template on every paint, which is a project-wide
|
|
329
|
+
* sweep per row (`specs/studio.md` §5.7).
|
|
341
330
|
*/
|
|
342
331
|
export function templateRows(
|
|
343
332
|
paths: readonly string[],
|
|
344
|
-
usedBy: (path: string) => number | null,
|
|
345
333
|
activePath: string | null,
|
|
346
334
|
needle: string,
|
|
347
335
|
defaultPath: string | null = null,
|
|
@@ -356,7 +344,7 @@ export function templateRows(
|
|
|
356
344
|
}
|
|
357
345
|
return [
|
|
358
346
|
{
|
|
359
|
-
detail: path === defaultPath ? "default layout" :
|
|
347
|
+
detail: path === defaultPath ? "default layout" : "",
|
|
360
348
|
key: path,
|
|
361
349
|
label,
|
|
362
350
|
path,
|
package/src/panels/site-panel.ts
CHANGED
|
@@ -48,7 +48,6 @@ import { previewFileSrc } from "../files/media-paths";
|
|
|
48
48
|
import { elementForAsset } from "../editor/file-drop-action";
|
|
49
49
|
import { readPageFacts } from "../content/collection-pages";
|
|
50
50
|
import { listCollectionEntryIds } from "../grid/sources/content-source";
|
|
51
|
-
import { loadUsages, onUsagesInvalidated, peekUsages, splitUsages } from "../services/references";
|
|
52
51
|
import { projectMediaFiles } from "../ui/media-picker";
|
|
53
52
|
import { runActiveReported } from "../commands/run-reported";
|
|
54
53
|
import { emptySitePanelValues, mountSitePanelSurface } from "../surfaces/site-panel";
|
|
@@ -312,16 +311,6 @@ function defaultLayoutPath(): string | null {
|
|
|
312
311
|
return isString(layout) ? layout.replace(/^\.\//, "") : null;
|
|
313
312
|
}
|
|
314
313
|
|
|
315
|
-
/** How many pages use a template, from the reference index — `null` where it cannot say yet. */
|
|
316
|
-
function templateUsage(path: string): number | null {
|
|
317
|
-
const state = peekUsages({ path });
|
|
318
|
-
if (state === null) {
|
|
319
|
-
void loadUsages({ path }).then(repaint);
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
return state.status === "ready" ? splitUsages(state.result).pages : null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
314
|
// ─── The view ────────────────────────────────────────────────────────────────
|
|
326
315
|
|
|
327
316
|
/** The filter, as typed. One per window: the panel is project-level and there is one project. */
|
|
@@ -366,12 +355,8 @@ export function sitePanelValues(): SitePanelValues {
|
|
|
366
355
|
: [];
|
|
367
356
|
const pages = filterPageRows(allPages, needle);
|
|
368
357
|
const defaultLayout = defaultLayoutPath();
|
|
369
|
-
const allTemplates = index
|
|
370
|
-
|
|
371
|
-
: [];
|
|
372
|
-
const templates = index
|
|
373
|
-
? templateRows(index.layouts, templateUsage, activePath, needle, defaultLayout)
|
|
374
|
-
: [];
|
|
358
|
+
const allTemplates = index ? templateRows(index.layouts, activePath, "", defaultLayout) : [];
|
|
359
|
+
const templates = index ? templateRows(index.layouts, activePath, needle, defaultLayout) : [];
|
|
375
360
|
const registryLoading = !_componentRegistryLoaded;
|
|
376
361
|
const allComponents = componentRows("", activePath);
|
|
377
362
|
const components = componentRows(needle, activePath);
|
|
@@ -537,9 +522,6 @@ export function mediaDragData(path: string): BlockDragData | null {
|
|
|
537
522
|
|
|
538
523
|
/** Mount the surface into `host`, or bring the one already there up to date. */
|
|
539
524
|
function mountInto(host: HTMLElement): void {
|
|
540
|
-
/* A page created, saved or deleted changes how many pages use a template, and the usage cache is
|
|
541
|
-
dropped for it: the Templates rows ask again on the repaint this asks for. */
|
|
542
|
-
_usagesUnlisten ??= onUsagesInvalidated(repaint);
|
|
543
525
|
if (_standing && (_standing.host !== host || _standing.handle?.connected() === false)) {
|
|
544
526
|
dispose(_standing);
|
|
545
527
|
_standing = null;
|
|
@@ -585,9 +567,6 @@ function mountInto(host: HTMLElement): void {
|
|
|
585
567
|
});
|
|
586
568
|
}
|
|
587
569
|
|
|
588
|
-
/** The usage-invalidation subscription, taken once the panel first mounts. */
|
|
589
|
-
let _usagesUnlisten: (() => void) | null = null;
|
|
590
|
-
|
|
591
570
|
/** Repaint the Navigator, which repaints this panel when it is the one showing. */
|
|
592
571
|
function repaint(): void {
|
|
593
572
|
renderOnly("leftPanel");
|
|
@@ -625,8 +604,6 @@ export function resetSitePanel(): void {
|
|
|
625
604
|
dispose(_standing);
|
|
626
605
|
}
|
|
627
606
|
_standing = null;
|
|
628
|
-
_usagesUnlisten?.();
|
|
629
|
-
_usagesUnlisten = null;
|
|
630
607
|
_filter = "";
|
|
631
608
|
_epoch += 1;
|
|
632
609
|
_index = null;
|