@jxsuite/studio 6.1.1 → 8.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/README.md +5 -5
- package/dist/chunks/2020-5wtzce5g.js +4 -0
- package/dist/chunks/{action-group-2mqby73c.js → action-group-4aan7g6g.js} +3 -3
- package/dist/chunks/adapter-5tphg7xe.js +4 -0
- package/dist/chunks/add-new-flow-psym14sn.js +4 -0
- package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
- package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
- package/dist/chunks/color-area-6rbra5dy.js +4 -0
- package/dist/chunks/color-field-gy56c94g.js +4 -0
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/color-slider-sqqdyf6c.js +4 -0
- package/dist/chunks/combobox-72h32ejk.js +4 -0
- package/dist/chunks/components-r22xca09.js +4 -0
- package/dist/chunks/content-source-mmppmcyg.js +4 -0
- package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
- package/dist/chunks/dialog-c4x05vyx.js +4 -0
- package/dist/chunks/editor-ccd9jwsa.js +4 -0
- package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
- package/dist/chunks/extension-sections-0egb88dm.js +4 -0
- package/dist/chunks/file-ops-qnab36ek.js +4 -0
- package/dist/chunks/files-rcn56qvm.js +4 -0
- package/dist/chunks/format-host-yxz0kjjs.js +4 -0
- package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
- package/dist/chunks/grid-open-4z1g062t.js +4 -0
- package/dist/chunks/grid-view-qg1b5eqx.js +51 -0
- package/dist/chunks/icons-63bjtbs7.js +4 -0
- package/dist/chunks/iframe-entry-2zggzajv.js +5 -0
- package/dist/chunks/iframe-entry-76j8vwwx.js +5 -0
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/iframe-entry-pkgmcgp0.js +5 -0
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-2veqmnhr.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-8nbvs12s.js → javascript-pjp734q4.js} +3 -3
- package/dist/chunks/jsonMode-njkvsa02.js +13 -0
- package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
- package/dist/chunks/layers-bgyr11tr.js +4 -0
- package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
- package/dist/chunks/listbox-c421cf8c.js +4 -0
- package/dist/chunks/lit-html-v9gh687x.js +4 -0
- package/dist/chunks/media-library-sq468wts.js +4 -0
- package/dist/chunks/media-picker-shfe59pt.js +4 -0
- package/dist/chunks/menu-bdpzbfvs.js +4 -0
- package/dist/chunks/monaco-binding-vq1hgnn0.js +4 -0
- package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
- package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
- package/dist/chunks/notify-kzrjr3fv.js +4 -0
- package/dist/chunks/number-field-y62xq0ty.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-7v4rtd7m.js +4 -0
- package/dist/chunks/popover-b89sfej7.js +4 -0
- package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
- package/dist/chunks/publish-quick-qx2455cj.js +4 -0
- package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
- package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
- package/dist/chunks/references-9547rg5s.js +4 -0
- package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
- package/dist/chunks/select-rzds841e.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/split-c9zfdgxf.js +4 -0
- package/dist/chunks/studio-03ct6yfv.js +9 -0
- package/dist/chunks/studio-07rcfje3.js +5 -0
- package/dist/chunks/studio-3e54qh39.js +7 -0
- package/dist/chunks/studio-3exxhz5g.js +5 -0
- package/dist/chunks/studio-3q40e3rj.js +5 -0
- package/dist/chunks/studio-3qeh9t0h.js +6 -0
- package/dist/chunks/studio-3qqjeep5.js +6 -0
- package/dist/chunks/studio-43sd2mam.js +4 -0
- package/dist/chunks/studio-4hp8pkyt.js +5 -0
- package/dist/chunks/studio-58xg6j2q.js +9 -0
- package/dist/chunks/studio-5g0rprdt.js +7 -0
- package/dist/chunks/studio-6m380wp7.js +5 -0
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-kggey1jt.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/studio-8tr6cfrc.js +5 -0
- package/dist/chunks/studio-8vcvfs7r.js +6 -0
- package/dist/chunks/studio-9tmebf0s.js +5 -0
- package/dist/chunks/studio-a2bvaptz.js +5 -0
- package/dist/chunks/studio-b7y5mr30.js +5 -0
- package/dist/chunks/studio-bavb31r0.js +5 -0
- package/dist/chunks/studio-bbe4c90a.js +5 -0
- package/dist/chunks/studio-c05wdnt3.js +5 -0
- package/dist/chunks/studio-ccv2svc6.js +5 -0
- package/dist/chunks/studio-d5grtb9e.js +16 -0
- package/dist/chunks/studio-df0baea9.js +5 -0
- package/dist/chunks/studio-dgk4dqtx.js +7 -0
- package/dist/chunks/studio-e1g64q43.js +5 -0
- package/dist/chunks/studio-ed1hp6zf.js +6 -0
- package/dist/chunks/studio-ed3xx40h.js +7 -0
- package/dist/chunks/studio-f14e1zdj.js +5 -0
- package/dist/chunks/studio-fbhxxc5f.js +5 -0
- package/dist/chunks/studio-fyh0jc96.js +5 -0
- package/dist/chunks/studio-gcxk20n7.js +5 -0
- package/dist/chunks/studio-jaaz8ce9.js +5 -0
- package/dist/chunks/{studio-8grwxtqk.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-1q1sh2ax.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/studio-k7evxqh4.js +5 -0
- package/dist/chunks/studio-kt3pjkwd.js +5 -0
- package/dist/chunks/studio-m8g16bgz.js +5 -0
- package/dist/chunks/studio-mn0476ag.js +5 -0
- package/dist/chunks/studio-mx1smyq0.js +5 -0
- package/dist/chunks/studio-n0c5h6mf.js +5 -0
- package/dist/chunks/{studio-rv11kfwa.js → studio-nh6d342n.js} +7 -7
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/studio-p4nrkvqr.js +5 -0
- package/dist/chunks/{studio-h46vtzk8.js → studio-pbxa5fpc.js} +4 -4
- package/dist/chunks/studio-pgmvdv7z.js +7 -0
- package/dist/chunks/studio-ph84avkf.js +5 -0
- package/dist/chunks/{studio-wszjyh0j.js → studio-q0mvbaq6.js} +4 -4
- package/dist/chunks/studio-q0tccd43.js +5 -0
- package/dist/chunks/studio-q7rtx6zq.js +14 -0
- package/dist/chunks/{studio-pre0skq6.js → studio-qgh13jtz.js} +4 -4
- package/dist/chunks/studio-qmn1xc24.js +5 -0
- package/dist/chunks/studio-qwdqwkj0.js +5 -0
- package/dist/chunks/{studio-t6b6qe31.js → studio-r5qy30vx.js} +4 -4
- package/dist/chunks/studio-rj9eamf8.js +5 -0
- package/dist/chunks/studio-rptgsk96.js +5 -0
- package/dist/chunks/studio-rthyg42z.js +928 -0
- package/dist/chunks/studio-s19tp1xr.js +5 -0
- package/dist/chunks/studio-s32578pn.js +5 -0
- package/dist/chunks/studio-svb7yyve.js +5 -0
- package/dist/chunks/studio-syreb644.js +5 -0
- package/dist/chunks/studio-tf9jryb1.js +10 -0
- package/dist/chunks/studio-tfkznv39.js +5 -0
- package/dist/chunks/studio-tft6r105.js +5 -0
- package/dist/chunks/studio-txf2kysx.js +5 -0
- package/dist/chunks/studio-v8867ddv.js +12 -0
- package/dist/chunks/studio-vxq7fcvv.js +5 -0
- package/dist/chunks/studio-w4rz9hdk.js +5 -0
- package/dist/chunks/studio-wbcjbhy1.js +9 -0
- package/dist/chunks/studio-wc649wss.js +5 -0
- package/dist/chunks/studio-wys23mxz.js +5 -0
- package/dist/chunks/studio-x1x167jh.js +5 -0
- package/dist/chunks/studio-x9hsze61.js +5 -0
- package/dist/chunks/studio-y0n7yxea.js +9 -0
- package/dist/chunks/studio-y3ahhdpx.js +5 -0
- package/dist/chunks/studio-z97c48fk.js +5 -0
- package/dist/chunks/studio-zat62m0c.js +5 -0
- package/dist/chunks/studio-zcd2e7yz.js +5 -0
- package/dist/chunks/studio-zz9qe3rw.js +64 -0
- package/dist/chunks/swatch-group-ewh24n1f.js +4 -0
- package/dist/chunks/tabs-f5cw3tgp.js +4 -0
- package/dist/chunks/textfield-wewc8rq2.js +4 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/toast-host-1yg5ar8d.js +4 -0
- package/dist/chunks/{toolbar-2nnzqhrj.js → toolbar-44cm5cta.js} +3 -3
- package/dist/chunks/tooltip-yypz28gq.js +4 -0
- package/dist/chunks/tree-9xa4hm7n.js +4 -0
- package/dist/chunks/tsMode-epyfz1hv.js +14 -0
- package/dist/chunks/workspace-dz7nxfhv.js +4 -0
- package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
- package/dist/chunks/ws-client-bv552sfx.js +4 -0
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +106 -107
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +17 -16
- package/src/about/about-modal.ts +4 -3
- package/src/account/action-flow.ts +45 -38
- package/src/account-status.ts +7 -2
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +270 -53
- package/src/browse/library-model.ts +194 -134
- package/src/browse/library-pane.ts +748 -440
- package/src/browse/library-preview.ts +161 -26
- package/src/browse/library-source.ts +308 -41
- package/src/browse/preview-root.ts +78 -0
- package/src/browse/preview-style.ts +155 -0
- package/src/canvas/asset-refs.ts +40 -10
- package/src/canvas/asset-resolve.ts +19 -23
- package/src/canvas/canvas-helpers.ts +7 -6
- package/src/canvas/canvas-live-render.ts +260 -77
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-patcher.ts +30 -30
- package/src/canvas/canvas-perf.ts +11 -5
- package/src/canvas/canvas-render.ts +175 -148
- package/src/canvas/canvas-surface.ts +5 -3
- package/src/canvas/canvas-utils.ts +147 -61
- package/src/canvas/dialog-path.ts +9 -4
- package/src/canvas/dialog-state.ts +1 -2
- package/src/canvas/diff-marks.ts +9 -16
- package/src/canvas/diff-toolbar.ts +8 -7
- package/src/canvas/edit-width-drag.ts +4 -1
- package/src/canvas/edit-width.ts +1 -3
- package/src/canvas/iframe-channel.ts +27 -13
- package/src/canvas/iframe-drop.ts +120 -12
- package/src/canvas/iframe-editable-root.ts +29 -16
- package/src/canvas/iframe-entry.ts +104 -75
- package/src/canvas/iframe-eval.ts +73 -49
- package/src/canvas/iframe-host.ts +269 -158
- package/src/canvas/iframe-inline-edit.ts +49 -30
- package/src/canvas/iframe-insert.ts +5 -4
- package/src/canvas/iframe-interaction.ts +30 -25
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-overlay.ts +1 -1
- package/src/canvas/iframe-patch.ts +25 -21
- package/src/canvas/iframe-position.ts +12 -6
- package/src/canvas/iframe-protocol.ts +36 -21
- package/src/canvas/iframe-render.ts +84 -35
- package/src/canvas/iframe-slash.ts +24 -11
- package/src/canvas/iframe-subtree.ts +10 -3
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/path-mapping.ts +19 -12
- package/src/canvas/popover-path.ts +7 -3
- package/src/canvas/popover-state.ts +1 -2
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/serialize-scope.ts +13 -5
- package/src/canvas/stage-scrollbar.ts +7 -2
- package/src/canvas/surface-registry.ts +27 -3
- package/src/collab/collab-commands.ts +7 -2
- package/src/collab/collab-session.ts +84 -45
- package/src/collab/collab-state.ts +4 -3
- package/src/collab/monaco-binding.ts +16 -13
- package/src/collab/monaco-cursors.ts +5 -2
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +13 -11
- package/src/commands/command-args.ts +103 -71
- package/src/commands/context.ts +55 -28
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +56 -43
- package/src/commands/levels.ts +14 -8
- package/src/commands/live-context.ts +9 -7
- package/src/commands/reference.ts +2 -1
- package/src/commands/registry.ts +22 -11
- package/src/commands/run-reported.ts +15 -10
- package/src/component-props.ts +46 -30
- package/src/content/collection-match.ts +11 -9
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/draft-state.ts +4 -3
- package/src/content/entry-commands.ts +54 -62
- package/src/content/entry-editor.ts +137 -32
- package/src/content/entry-fields.ts +25 -8
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +38 -15
- package/src/content/entry-open.ts +80 -0
- package/src/content/entry-summaries.ts +440 -0
- package/src/content/file-record-ids.ts +80 -0
- package/src/content/file-records.ts +125 -0
- package/src/content/page-summaries.ts +129 -0
- package/src/content/record-entry.ts +201 -0
- package/src/content/writing-banner.ts +177 -0
- package/src/create/add-new-commands.ts +185 -0
- package/src/create/add-new-flow.ts +1116 -0
- package/src/create/add-new.ts +1014 -0
- package/src/create/element-name.ts +121 -0
- package/src/editor/canvas-context-menu.ts +1 -2
- package/src/editor/canvas-slash-bridge.ts +3 -6
- package/src/editor/content-slice.ts +16 -14
- package/src/editor/context-menu.ts +35 -22
- package/src/editor/convert-targets.ts +30 -35
- package/src/editor/convert-to-component.ts +78 -48
- package/src/editor/convert-to-repeater.ts +34 -22
- package/src/editor/editable-tags.ts +17 -2
- package/src/editor/file-drop-action.ts +29 -22
- package/src/editor/inline-edit-apply.ts +79 -17
- package/src/editor/inline-edit.ts +76 -50
- package/src/editor/inline-format.ts +59 -56
- package/src/editor/inline-link.ts +40 -15
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +14 -5
- package/src/editor/merge-tags.ts +58 -19
- package/src/editor/repeater-scope.ts +39 -28
- package/src/editor/shortcuts.ts +76 -42
- package/src/editor/slash-menu.ts +52 -14
- package/src/files/asset-versions.ts +4 -4
- package/src/files/components.ts +89 -20
- package/src/files/elements.ts +5 -6
- package/src/files/file-ops.ts +92 -41
- package/src/files/files.ts +248 -239
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +97 -2
- package/src/files/media-paths.ts +3 -0
- package/src/files/media-types.ts +55 -0
- package/src/files/media-upload.ts +209 -52
- package/src/files/new-file-name.ts +156 -0
- package/src/files/serialize-document.ts +15 -5
- package/src/format/constraints.ts +1 -1
- package/src/format/convert-file.ts +27 -13
- package/src/format/format-choices.ts +2 -1
- package/src/format/format-host.ts +91 -50
- package/src/github/github-auth-status.ts +27 -3
- package/src/github/github-auth.ts +21 -11
- package/src/github/github-publish.ts +28 -23
- package/src/grid/cell-editors.ts +46 -31
- package/src/grid/cell-popovers.ts +5 -3
- package/src/grid/edit-buffer.ts +10 -9
- package/src/grid/grid-controller.ts +25 -23
- package/src/grid/grid-layout.ts +21 -5
- package/src/grid/grid-open.ts +3 -33
- package/src/grid/grid-panel.ts +65 -6
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +9 -31
- package/src/grid/grid-view.ts +45 -23
- package/src/grid/redirects-grid.ts +14 -5
- package/src/grid/redirects.ts +16 -14
- package/src/grid/schema-columns.ts +25 -19
- package/src/grid/sources/connector-source.ts +47 -20
- package/src/grid/sources/content-source.ts +100 -31
- package/src/grid/sources/csv-file-source.ts +2 -2
- package/src/grid/tabulator-tables.d.ts +61 -34
- package/src/hosting/document.ts +2 -2
- package/src/hosting/stage.ts +7 -7
- package/src/i18n/i18n-commands.ts +3 -3
- package/src/media/media-commands.ts +181 -0
- package/src/media/media-library-model.ts +232 -0
- package/src/media/media-library.ts +1283 -0
- package/src/media/media-pane.ts +51 -20
- package/src/new-project/add-repo-modal.ts +20 -19
- package/src/new-project/import-tab.ts +10 -14
- package/src/new-project/location-fields.ts +2 -1
- package/src/new-project/new-project-modal.ts +12 -9
- package/src/packages/jxsuite-update.ts +4 -3
- package/src/packages/pull-package-sync.ts +26 -25
- package/src/page-params.ts +97 -68
- package/src/panels/activity-panel.ts +38 -14
- package/src/panels/ai-chat/attached-context.ts +13 -7
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/ai-chat/chat-view.ts +80 -73
- package/src/panels/ai-chat/composer.ts +30 -33
- package/src/panels/ai-chat/sessions-view.ts +1 -2
- package/src/panels/ai-panel.ts +23 -20
- package/src/panels/block-action-bar.ts +57 -46
- package/src/panels/bottom-dock.ts +5 -3
- package/src/panels/canvas-dnd-bridge.ts +36 -26
- package/src/panels/chat-panel.ts +6 -6
- package/src/panels/collections-panel.ts +306 -0
- package/src/panels/component-preview.ts +35 -13
- package/src/panels/data-explorer.ts +63 -26
- package/src/panels/data-grid.ts +18 -11
- package/src/panels/dnd.ts +115 -138
- package/src/panels/editors.ts +52 -21
- package/src/panels/elements-panel.ts +19 -5
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/events-panel.ts +84 -69
- package/src/panels/formula-workspace.ts +78 -54
- package/src/panels/frontmatter-fields.ts +33 -28
- package/src/panels/git-diff-open.ts +10 -6
- package/src/panels/head-panel.ts +488 -202
- package/src/panels/i18n-panel.ts +20 -12
- package/src/panels/imports-panel.ts +25 -47
- package/src/panels/jump-bar.ts +75 -31
- package/src/panels/layers-panel.ts +130 -101
- package/src/panels/left-panel.ts +25 -11
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +140 -49
- package/src/panels/pane-grid.ts +55 -26
- package/src/panels/panel-registry.ts +36 -46
- package/src/panels/properties-panel.ts +182 -124
- package/src/panels/quick-search.ts +53 -31
- package/src/panels/right-panel.ts +201 -45
- package/src/panels/seo-modal.ts +54 -125
- package/src/panels/settings-menu.ts +63 -61
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +156 -135
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/statement-editor.ts +230 -140
- package/src/panels/style-panel.ts +253 -206
- package/src/panels/style-utils.ts +114 -51
- package/src/panels/stylebook-doc.ts +195 -60
- package/src/panels/stylebook-layers-panel.ts +48 -89
- package/src/panels/stylebook-panel.ts +60 -27
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-drop.ts +27 -11
- package/src/panels/tab-strip.ts +143 -49
- package/src/platform-errors.ts +57 -43
- package/src/platform.ts +59 -31
- package/src/platforms/cloud.ts +373 -176
- package/src/platforms/default-platform.ts +14 -8
- package/src/platforms/devserver.ts +180 -142
- package/src/platforms/read-batcher.ts +16 -8
- package/src/project-list.ts +2 -1
- package/src/publish/deploy-checklist.ts +20 -9
- package/src/publish/pages-service.ts +66 -27
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +88 -22
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-panel.ts +10 -6
- package/src/publish/publish-quick.ts +148 -0
- package/src/publish/publish-workspace.ts +926 -0
- package/src/publish/source-control.ts +907 -0
- package/src/recent-projects.ts +15 -8
- package/src/resize-edges.ts +26 -36
- package/src/services/a11y-report.ts +26 -19
- package/src/services/agent-seed.ts +23 -11
- package/src/services/ai-ask.ts +9 -12
- package/src/services/ai-command-tools.ts +68 -62
- package/src/services/ai-import-tools.ts +80 -67
- package/src/services/ai-models.ts +17 -13
- package/src/services/ai-project-tools.ts +68 -32
- package/src/services/ai-session-store.ts +19 -10
- package/src/services/ai-system-prompt.ts +46 -20
- package/src/services/ai-tools.ts +71 -27
- package/src/services/ai-write-report.ts +20 -12
- package/src/services/ai-writes.ts +4 -4
- package/src/services/automation.ts +74 -51
- package/src/services/cem-export.ts +149 -58
- package/src/services/cf-settings.ts +4 -4
- package/src/services/clock.ts +6 -1
- package/src/services/code-services.ts +37 -31
- package/src/services/connection.ts +2 -2
- package/src/services/context-manager.ts +14 -10
- package/src/services/context-resolver.ts +20 -8
- package/src/services/data-service.ts +19 -15
- package/src/services/directory-picker.ts +20 -14
- package/src/services/document-assistant.ts +39 -19
- package/src/services/gated-registry.ts +10 -6
- package/src/services/harness/chat-reducer.ts +23 -1
- package/src/services/import-client.ts +91 -38
- package/src/services/import-seed.ts +8 -2
- package/src/services/jx-validate.ts +28 -31
- package/src/services/live-preview.ts +0 -0
- package/src/services/live-surfaces.ts +6 -3
- package/src/services/model-uri.ts +10 -2
- package/src/services/module-preloads.ts +12 -8
- package/src/services/monaco-buffer.ts +43 -24
- package/src/services/monaco-lazy.ts +7 -8
- package/src/services/monaco-setup.ts +23 -16
- package/src/services/notify.ts +44 -17
- package/src/services/popover-report.ts +25 -10
- package/src/services/preview-eval.ts +20 -8
- package/src/services/profile.ts +13 -6
- package/src/services/project-path.ts +13 -10
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +29 -9
- package/src/services/settings/definitions.ts +25 -14
- package/src/services/settings/kernel.ts +13 -4
- package/src/services/settings/write-queue.ts +9 -9
- package/src/services/token-lint.ts +12 -10
- package/src/services/tool-executor.ts +17 -5
- package/src/services/tool-outcomes.ts +44 -24
- package/src/services/trusted-types.ts +24 -7
- package/src/settings/contexts-section.ts +11 -8
- package/src/settings/contributed-section.ts +128 -70
- package/src/settings/css-vars-editor.ts +119 -32
- package/src/settings/defs-editor.ts +38 -35
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-rows.ts +16 -8
- package/src/settings/extension-sections.ts +42 -14
- package/src/settings/extensions-section.ts +4 -1
- package/src/settings/general-settings.ts +101 -28
- package/src/settings/locales-section.ts +20 -12
- package/src/settings/preferences-dialog.ts +17 -7
- package/src/settings/preferences-keymap.ts +7 -5
- package/src/settings/preferences-sections.ts +11 -5
- package/src/settings/project-sections.ts +28 -11
- package/src/settings/schema-field-ui.ts +12 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +186 -166
- package/src/shell/tree.ts +6 -5
- package/src/shell.ts +536 -477
- package/src/site-context.ts +101 -32
- package/src/state.ts +68 -57
- package/src/store.ts +54 -34
- package/src/studio.ts +219 -116
- package/src/style/live-preview.ts +1 -1
- package/src/style/project-styles.ts +40 -7
- package/src/style/stylebook-specimens.ts +89 -0
- package/src/style/stylebook-target.ts +26 -0
- package/src/style/tag-inheritance.ts +105 -0
- package/src/style/token-ref.ts +5 -4
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/about.ts +18 -19
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/add-repo.ts +17 -19
- package/src/surfaces/ai-chat.ts +33 -38
- package/src/surfaces/ai-credentials-form.ts +6 -16
- package/src/surfaces/ai-managed-connect.ts +3 -4
- package/src/surfaces/ai-model-picker.ts +3 -5
- package/src/surfaces/block-action-bar.ts +22 -11
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/boot-failure.ts +4 -5
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/bottom-dock.ts +9 -18
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +17 -16
- package/src/surfaces/cf-account-picker.ts +17 -18
- package/src/surfaces/collections-panel.json +615 -0
- package/src/surfaces/collections-panel.ts +168 -0
- package/src/surfaces/commandbar.json +241 -96
- package/src/surfaces/commandbar.ts +145 -105
- package/src/surfaces/convert-repeater.ts +17 -19
- package/src/surfaces/data-actions.ts +9 -7
- package/src/surfaces/dialog.ts +26 -28
- package/src/surfaces/diff-toolbar.ts +9 -7
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/empty-state.ts +7 -10
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +35 -11
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/expression-editor.ts +11 -25
- package/src/surfaces/files-panel.ts +27 -14
- package/src/surfaces/formula-palette.ts +4 -5
- package/src/surfaces/github-auth.ts +16 -20
- package/src/surfaces/github-publish.ts +17 -22
- package/src/surfaces/grid-cell.ts +30 -31
- package/src/surfaces/grid-open.ts +10 -14
- package/src/surfaces/grid-panel.ts +13 -9
- package/src/surfaces/grid-replace.ts +13 -13
- package/src/surfaces/grid-views.ts +15 -18
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +59 -31
- package/src/surfaces/jump-bar.ts +5 -6
- package/src/surfaces/jxsuite-update.ts +16 -22
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +111 -77
- package/src/surfaces/logic-panel.ts +17 -30
- package/src/surfaces/logic-workspace.ts +13 -15
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +26 -11
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-field.ts +4 -5
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pane.ts +8 -7
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +47 -23
- package/src/surfaces/navigator-dock.ts +11 -12
- package/src/surfaces/new-project.ts +27 -26
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +44 -25
- package/src/surfaces/pane-grid.ts +15 -10
- package/src/surfaces/panel-activity.ts +5 -6
- package/src/surfaces/panel-data.ts +3 -4
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +8 -7
- package/src/surfaces/panel-elements.ts +5 -7
- package/src/surfaces/panel-i18n.ts +5 -7
- package/src/surfaces/panel-imports.ts +6 -13
- package/src/surfaces/panel-outline.ts +25 -19
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +49 -25
- package/src/surfaces/panel-problems.ts +5 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +25 -39
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +5 -29
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +29 -11
- package/src/surfaces/progress-modal.ts +15 -9
- package/src/surfaces/properties-panel.json +1 -1
- package/src/surfaces/properties-panel.ts +20 -42
- package/src/surfaces/publish-quick.json +428 -0
- package/src/surfaces/publish-quick.ts +165 -0
- package/src/surfaces/publish-workspace.json +2892 -0
- package/src/surfaces/publish-workspace.ts +391 -0
- package/src/surfaces/publish.ts +18 -18
- package/src/surfaces/push-plan.ts +7 -8
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +22 -79
- package/src/surfaces/reference-field.ts +11 -15
- package/src/surfaces/schema-builder.ts +16 -22
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +17 -25
- package/src/surfaces/secret-field.ts +9 -6
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/seo.ts +21 -15
- package/src/surfaces/settings-contexts.ts +9 -7
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +32 -12
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +82 -16
- package/src/surfaces/settings-defs.json +2 -2
- package/src/surfaces/settings-defs.ts +17 -21
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +9 -16
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-extensions.ts +5 -6
- package/src/surfaces/settings-head.ts +33 -39
- package/src/surfaces/settings-locales.ts +8 -20
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +14 -8
- package/src/surfaces/settings-packages.ts +8 -7
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +88 -36
- package/src/surfaces/settings-rawjson.ts +5 -6
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/shell.ts +1 -2
- package/src/surfaces/site-panel.json +1029 -0
- package/src/surfaces/site-panel.ts +233 -0
- package/src/surfaces/slash-menu.json +63 -21
- package/src/surfaces/slash-menu.ts +42 -9
- package/src/surfaces/statements.ts +13 -18
- package/src/surfaces/statusbar.ts +56 -29
- package/src/surfaces/style-panel.ts +18 -31
- package/src/surfaces/stylebook-chrome.ts +3 -5
- package/src/surfaces/styles-panel.json +735 -0
- package/src/surfaces/styles-panel.ts +195 -0
- package/src/surfaces/styles-variables.json +149 -0
- package/src/surfaces/styles-variables.ts +145 -0
- package/src/surfaces/tab-strip.json +4 -4
- package/src/surfaces/tab-strip.ts +14 -13
- package/src/surfaces/target-line.ts +6 -7
- package/src/surfaces/welcome.ts +6 -8
- package/src/surfaces/writing-banner.json +94 -0
- package/src/surfaces/writing-banner.ts +80 -0
- package/src/tabs/bare-repeaters.ts +101 -0
- package/src/tabs/patch-ops.ts +4 -3
- package/src/tabs/project-config.ts +32 -21
- package/src/tabs/selection.ts +36 -8
- package/src/tabs/tab.ts +99 -48
- package/src/tabs/transact.ts +98 -53
- package/src/types.ts +53 -43
- package/src/ui/ai-credentials-form.ts +10 -13
- package/src/ui/ai-model-picker.ts +3 -3
- package/src/ui/cf-account-picker.ts +7 -9
- package/src/ui/color-selector.ts +9 -5
- package/src/ui/dynamic-slot.ts +7 -8
- package/src/ui/expression-editor.ts +112 -105
- package/src/ui/form-controls.ts +104 -56
- package/src/ui/formula-catalog.ts +239 -230
- package/src/ui/formula-chips.ts +33 -18
- package/src/ui/layers.ts +57 -31
- package/src/ui/media-picker.ts +300 -47
- package/src/ui/popover-surface.ts +20 -12
- package/src/ui/progress-modal.ts +13 -8
- package/src/ui/regions.ts +14 -12
- package/src/ui/schema-form.ts +372 -97
- package/src/ui/surface.ts +71 -8
- package/src/ui/timing.ts +9 -0
- package/src/ui/value-source.ts +44 -31
- package/src/ui/virtual-window.ts +29 -3
- package/src/utils/base64.ts +6 -4
- package/src/utils/edit-display.ts +366 -111
- package/src/utils/element-selectors.ts +3 -2
- package/src/utils/google-fonts.ts +15 -14
- package/src/utils/inherited-style.ts +18 -20
- package/src/utils/link-target.ts +1 -1
- package/src/utils/local-day.ts +18 -0
- package/src/utils/preview-format.ts +25 -3
- package/src/utils/strip-events.ts +49 -22
- package/src/utils/studio-utils.ts +29 -14
- package/src/view.ts +2 -3
- package/src/workspace/pane-derive.ts +14 -16
- package/src/workspace/session.ts +81 -50
- package/src/workspace/tab-workspace.ts +137 -0
- package/src/workspace/workspace-back.ts +189 -0
- package/src/workspace/workspace-home.ts +133 -0
- package/src/workspace/workspace-ids.ts +129 -0
- package/src/workspace/workspace-switch.ts +372 -0
- package/src/workspace/workspace.ts +165 -47
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-va2wwra4.js +0 -4
- package/dist/chunks/adapter-f93pafd7.js +0 -4
- package/dist/chunks/cf-account-picker-af1h381v.js +0 -4
- package/dist/chunks/cf-settings-125tdm0w.js +0 -4
- package/dist/chunks/color-area-jqhna8zy.js +0 -4
- package/dist/chunks/color-field-embq5ctg.js +0 -4
- package/dist/chunks/color-fpezvnkt.js +0 -4
- package/dist/chunks/color-slider-1qmw7wqy.js +0 -4
- package/dist/chunks/combobox-773hdzyj.js +0 -4
- package/dist/chunks/components-ax2f1fnn.js +0 -4
- package/dist/chunks/content-source-108aqtk1.js +0 -4
- package/dist/chunks/convert-to-repeater-ry5rmppr.js +0 -4
- package/dist/chunks/dialog-mtm9fq5t.js +0 -4
- package/dist/chunks/editor-pfgdmkb0.js +0 -4
- package/dist/chunks/entry-commands-t0k1q0kx.js +0 -4
- package/dist/chunks/extension-sections-6mwh2hf8.js +0 -4
- package/dist/chunks/file-ops-meej5mqf.js +0 -4
- package/dist/chunks/files-44vp8a2s.js +0 -4
- package/dist/chunks/format-host-7q1petb9.js +0 -4
- package/dist/chunks/fs-events-h2mwnxaz.js +0 -4
- package/dist/chunks/git-panel-zngbymvd.js +0 -4
- package/dist/chunks/grid-view-sx9vynsp.js +0 -51
- package/dist/chunks/icons-p44xhjf0.js +0 -4
- package/dist/chunks/iframe-entry-f7mqjwpm.js +0 -5
- package/dist/chunks/iframe-entry-haw7jkvz.js +0 -5
- package/dist/chunks/iframe-entry-z6qrytv3.js +0 -5
- package/dist/chunks/iframe-host-qk3ycq87.js +0 -4
- package/dist/chunks/index-wkyfjeqz.js +0 -4
- package/dist/chunks/jsonMode-s8kt9dsj.js +0 -13
- package/dist/chunks/jx-validate-psgd7vsx.js +0 -4
- package/dist/chunks/layers-panel-1kq2jyfk.js +0 -4
- package/dist/chunks/layers-xd92fg8d.js +0 -4
- package/dist/chunks/listbox-s8fkmnq8.js +0 -4
- package/dist/chunks/lit-html-ac4cwwr4.js +0 -4
- package/dist/chunks/media-picker-0svzkdyb.js +0 -4
- package/dist/chunks/menu-37d07z9m.js +0 -4
- package/dist/chunks/menu-5c55ea3d.js +0 -4
- package/dist/chunks/monaco-binding-z6r70pdt.js +0 -4
- package/dist/chunks/monaco-buffer-fz6gq5y7.js +0 -4
- package/dist/chunks/monaco-setup-q4axkgj7.js +0 -4
- package/dist/chunks/notify-0xgtn8q1.js +0 -4
- package/dist/chunks/number-field-gw7sq8rn.js +0 -4
- package/dist/chunks/parse-897fvqah.js +0 -4
- package/dist/chunks/platform-v1qctbe6.js +0 -4
- package/dist/chunks/popover-8htra4er.js +0 -4
- package/dist/chunks/publish-panel-ym1fmgq5.js +0 -4
- package/dist/chunks/quick-search-a54e6t5n.js +0 -4
- package/dist/chunks/references-gmqp82fs.js +0 -4
- package/dist/chunks/right-panel-v7t9awbk.js +0 -4
- package/dist/chunks/section-registry-vhyf3zsr.js +0 -4
- package/dist/chunks/select-n3v7gwas.js +0 -4
- package/dist/chunks/split-pff8f3ze.js +0 -4
- package/dist/chunks/studio-0bhx5hz6.js +0 -5
- package/dist/chunks/studio-0k1ddnyy.js +0 -6
- package/dist/chunks/studio-1b04pd0j.js +0 -64
- package/dist/chunks/studio-4dwbj6gb.js +0 -5
- package/dist/chunks/studio-4tanh828.js +0 -5
- package/dist/chunks/studio-55h4mstj.js +0 -4
- package/dist/chunks/studio-5tnvjkz4.js +0 -5
- package/dist/chunks/studio-60dqgb0c.js +0 -5
- package/dist/chunks/studio-61dp09qg.js +0 -9
- package/dist/chunks/studio-63epcrj2.js +0 -5
- package/dist/chunks/studio-74k29dy9.js +0 -5
- package/dist/chunks/studio-7vg6jcd3.js +0 -5
- package/dist/chunks/studio-7zx31knw.js +0 -9
- package/dist/chunks/studio-8bxk97km.js +0 -5
- package/dist/chunks/studio-933e30n4.js +0 -5
- package/dist/chunks/studio-98wnwhbg.js +0 -5
- package/dist/chunks/studio-9mccjwjh.js +0 -14
- package/dist/chunks/studio-a2ke1e4t.js +0 -5
- package/dist/chunks/studio-e3a2a6v3.js +0 -4
- package/dist/chunks/studio-e81q1vb9.js +0 -5
- package/dist/chunks/studio-epgrmqqm.js +0 -5
- package/dist/chunks/studio-ery42sqr.js +0 -4
- package/dist/chunks/studio-g1shz58g.js +0 -5
- package/dist/chunks/studio-gf0fz03c.js +0 -5
- package/dist/chunks/studio-gjtpe62q.js +0 -6
- package/dist/chunks/studio-gkskhg7m.js +0 -5
- package/dist/chunks/studio-gx4dnjzd.js +0 -5
- package/dist/chunks/studio-j03zga3y.js +0 -9
- package/dist/chunks/studio-jhpxeckz.js +0 -5
- package/dist/chunks/studio-k01r7ykp.js +0 -5
- package/dist/chunks/studio-kk7h30ne.js +0 -7
- package/dist/chunks/studio-kt1zqney.js +0 -5
- package/dist/chunks/studio-m3cwhvye.js +0 -5
- package/dist/chunks/studio-mcayt6hz.js +0 -5
- package/dist/chunks/studio-myjcghv3.js +0 -5
- package/dist/chunks/studio-n8bxwc3y.js +0 -4
- package/dist/chunks/studio-ngm8gvpb.js +0 -5
- package/dist/chunks/studio-nj5a01jy.js +0 -928
- package/dist/chunks/studio-pcgz8heb.js +0 -5
- package/dist/chunks/studio-qcsq0t15.js +0 -5
- package/dist/chunks/studio-qkstvve9.js +0 -5
- package/dist/chunks/studio-qv1pwd3c.js +0 -5
- package/dist/chunks/studio-rc6dvxjb.js +0 -7
- package/dist/chunks/studio-s8dmmz66.js +0 -5
- package/dist/chunks/studio-sp5xzbj4.js +0 -6
- package/dist/chunks/studio-szsjbj0g.js +0 -5
- package/dist/chunks/studio-t1xzawhr.js +0 -16
- package/dist/chunks/studio-tdcprbqb.js +0 -5
- package/dist/chunks/studio-vrmcjf9g.js +0 -7
- package/dist/chunks/studio-vyc0b4he.js +0 -10
- package/dist/chunks/studio-w0cgf6jg.js +0 -5
- package/dist/chunks/studio-w3hskzqk.js +0 -5
- package/dist/chunks/studio-xgrgcdyh.js +0 -9
- package/dist/chunks/studio-xz2yxfcb.js +0 -5
- package/dist/chunks/studio-y6n42tgs.js +0 -5
- package/dist/chunks/studio-yb560vxr.js +0 -5
- package/dist/chunks/studio-yn9f8vtx.js +0 -5
- package/dist/chunks/studio-yw05w8f0.js +0 -7
- package/dist/chunks/studio-z16sgavc.js +0 -5
- package/dist/chunks/studio-zey7zsdw.js +0 -12
- package/dist/chunks/studio-zyqn40w4.js +0 -5
- package/dist/chunks/swatch-group-8qh9a3bm.js +0 -4
- package/dist/chunks/tabs-jwthg40w.js +0 -4
- package/dist/chunks/textfield-spy3hc77.js +0 -4
- package/dist/chunks/toast-4d35zd64.js +0 -4
- package/dist/chunks/toast-host-8sjmfed3.js +0 -4
- package/dist/chunks/tooltip-fd6g9xy3.js +0 -4
- package/dist/chunks/tree-h19v1jfz.js +0 -4
- package/dist/chunks/tsMode-2frah6h1.js +0 -14
- package/dist/chunks/workspace-zt81g1ms.js +0 -4
- package/dist/chunks/ws-client-n2bh4hkf.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -701
- package/src/panels/git-panel.ts +0 -1120
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -231
- package/src/surfaces/git-panel.json +0 -1880
- package/src/surfaces/git-panel.ts +0 -272
package/src/panels/head-panel.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
* its body: its frontmatter, the layout it is poured into, its title,
|
|
5
|
-
* icon, its OpenGraph card, and the raw `$head` tags no structured
|
|
3
|
+
* The Page panel — the Inspector's first tab (§6) — everything the open document says about itself
|
|
4
|
+
* before anybody reads its body: its frontmatter, the layout it is poured into, its title,
|
|
5
|
+
* description, viewport and icon, its OpenGraph card, and the raw `$head` tags no structured
|
|
6
|
+
* control owns.
|
|
6
7
|
*
|
|
7
8
|
* This is the FLOW. The markup is `surfaces/panel-page.json` and the scope that feeds it is
|
|
8
9
|
* `surfaces/panel-page.ts`; what stays here is every decision — what a `$head` entry means, which
|
|
@@ -13,27 +14,27 @@
|
|
|
13
14
|
* read at submit time, which is a spelling a document does not have: a binding writes only when the
|
|
14
15
|
* SCOPE moves, so every field's setter states what the control now holds before anything is decided
|
|
15
16
|
* about it, and clearing one after a successful add is then a real change the runtime carries back.
|
|
16
|
-
* `sync()` — not
|
|
17
|
-
*
|
|
18
|
-
* field.
|
|
17
|
+
* `sync()` — not a dock repaint — is what an echo calls: it assigns into the standing scope in the
|
|
18
|
+
* same turn, where a dock repaint is a frame away and would rebuild far more than one field.
|
|
19
19
|
*
|
|
20
|
-
* **What a frontmatter row looks like is `panels/frontmatter-fields.ts`'s answer.** The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* **What a frontmatter row looks like is `panels/frontmatter-fields.ts`'s answer.** The Page tab is
|
|
21
|
+
* the one surface that edits a page's frontmatter (the Document Header card that once drew the same
|
|
22
|
+
* field set above the canvas is gone, studio.md §4.2), and keeping the row vocabulary in its own
|
|
23
|
+
* module is what stopped two surfaces disagreeing about `title`.
|
|
24
24
|
*
|
|
25
25
|
* The other half of this module has no surface at all: the merged-`$head` preview model that
|
|
26
|
-
* `panels/seo-modal.ts`
|
|
26
|
+
* `panels/seo-modal.ts` reads. It is pure, and pinned against
|
|
27
27
|
* `packages/compiler/src/site/head-merger.ts` by `tests/head-panel.test.ts`.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
30
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
31
|
+
import { withPageDescription } from "@jxsuite/site/head-merger";
|
|
32
|
+
import { projectState, renderOnly, updateSession } from "../store";
|
|
32
33
|
import type { DirEntry, JsonValue } from "../types";
|
|
33
34
|
import { activeTab } from "../workspace/workspace";
|
|
34
35
|
import { activeRegistry } from "../commands/active-registry";
|
|
35
36
|
import { clearProblems, notify } from "../services/notify";
|
|
36
|
-
import {
|
|
37
|
+
import type { NotifyOptions } from "../services/notify";
|
|
37
38
|
import { mutateUpdateFrontmatter, transact, transactDoc } from "../tabs/transact";
|
|
38
39
|
import { collectFmFields, projectFmField } from "./frontmatter-fields";
|
|
39
40
|
import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts";
|
|
@@ -43,19 +44,28 @@ import { pageRoute } from "./tab-strip";
|
|
|
43
44
|
import { LIVE_PREVIEW } from "../ui/timing";
|
|
44
45
|
import { previewAssetSrc } from "../canvas/asset-refs";
|
|
45
46
|
import { IMAGE_EXTENSIONS, extensionOf } from "../files/media-upload";
|
|
46
|
-
import { renderPagePanelSurface } from "../surfaces/panel-page";
|
|
47
|
+
import { renderPagePanelSurface, unmountPagePanelSurface } from "../surfaces/panel-page";
|
|
48
|
+
import { emptyState } from "../surfaces/empty-state";
|
|
49
|
+
import type { EmptyStateSpec } from "./empty-state";
|
|
50
|
+
import { collectionForFile } from "../content/collection-match";
|
|
51
|
+
import {
|
|
52
|
+
isEntryTab,
|
|
53
|
+
renderEntryInspector,
|
|
54
|
+
unmountEntryInspector,
|
|
55
|
+
} from "../content/entry-inspector";
|
|
47
56
|
|
|
48
57
|
import type { JxHeadEntry, JxMutableNode } from "@jxsuite/schema/types";
|
|
49
58
|
import type { Tab } from "../tabs/tab";
|
|
50
59
|
import type * as MediaPickerModule from "../ui/media-picker";
|
|
51
60
|
import type {
|
|
52
|
-
PageChoice,
|
|
53
61
|
PagePanelActions,
|
|
54
62
|
PagePanelView,
|
|
55
63
|
PageRow,
|
|
56
64
|
PageSection,
|
|
57
65
|
PageTagRow,
|
|
58
66
|
} from "../surfaces/panel-page";
|
|
67
|
+
import type { SurfaceOption } from "../ui/surface";
|
|
68
|
+
import type { SeoWarningView } from "../surfaces/seo";
|
|
59
69
|
|
|
60
70
|
interface MetaField {
|
|
61
71
|
label: string;
|
|
@@ -70,7 +80,14 @@ interface MetaField {
|
|
|
70
80
|
/** @type {{ name: string; path: string }[] | null} */
|
|
71
81
|
let layoutEntries: { name: string; path: string }[] | null = null;
|
|
72
82
|
|
|
73
|
-
|
|
83
|
+
/**
|
|
84
|
+
* List `layouts/` now and keep the answer as the picker's cache.
|
|
85
|
+
*
|
|
86
|
+
* The one reading of which files are layouts and what each is called: the Page panel's picker and
|
|
87
|
+
* Add new (`create/add-new-flow.ts`) both read it, so they cannot name different templates. JSON
|
|
88
|
+
* only, because a layout is resolved as JSON.
|
|
89
|
+
*/
|
|
90
|
+
export async function listLayoutEntries(): Promise<{ name: string; path: string }[]> {
|
|
74
91
|
try {
|
|
75
92
|
const platform = getPlatform();
|
|
76
93
|
const listing = await platform.listDirectory("layouts");
|
|
@@ -86,8 +103,12 @@ async function loadLayoutEntries() {
|
|
|
86
103
|
} catch {
|
|
87
104
|
layoutEntries = [];
|
|
88
105
|
}
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
return layoutEntries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadLayoutEntries() {
|
|
110
|
+
await listLayoutEntries();
|
|
111
|
+
renderOnly("rightPanel", "seoModal");
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
/**
|
|
@@ -103,11 +124,10 @@ export function invalidateLayoutPickerCache() {
|
|
|
103
124
|
/**
|
|
104
125
|
* The layouts the picker offers, or `null` while the directory is still being listed.
|
|
105
126
|
*
|
|
106
|
-
* The listing
|
|
107
|
-
* (`src/surfaces/panel-page.json`) and
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* only one of them. Asking starts the read; it repaints both surfaces when it lands.
|
|
127
|
+
* The listing the Page tab's Layout picker is drawn from, handed over as DATA: the Page panel
|
|
128
|
+
* (`src/surfaces/panel-page.json`) is a document and reads it as a projection. One listing is one
|
|
129
|
+
* cache with one lifetime, which {@link invalidateLayoutPickerCache} forgets. Asking starts the
|
|
130
|
+
* read; it repaints the Inspector when it lands.
|
|
111
131
|
*
|
|
112
132
|
* @returns {{ name: string; path: string }[] | null}
|
|
113
133
|
*/
|
|
@@ -141,19 +161,75 @@ export const OG_FIELDS: MetaField[] = [
|
|
|
141
161
|
const MANAGED_META_KEYS = new Set([...PAGE_FIELDS, ...OG_FIELDS].map((f) => f.key));
|
|
142
162
|
|
|
143
163
|
/**
|
|
144
|
-
* Frontmatter keys the
|
|
145
|
-
*
|
|
164
|
+
* Frontmatter keys the Page tab owns with a dedicated control, so the generic field list must not
|
|
165
|
+
* print them a second time.
|
|
146
166
|
*
|
|
147
|
-
* **The one policy.**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* two field sets was only safe once one of the two policies won, and it is this one: a document has
|
|
151
|
-
* ONE title, and the surface that gives it a named row is the one that owns the key.
|
|
167
|
+
* **The one policy.** A document has ONE title, and the surface that gives it a named row is the
|
|
168
|
+
* one that owns the key: listed as a generic field as well, the same key rendered twice with two
|
|
169
|
+
* different commit paths.
|
|
152
170
|
*/
|
|
153
171
|
export const RESERVED_FM_KEYS = new Set(["title"]);
|
|
154
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The frontmatter keys a document's head controls own, so its generic field list leaves them out:
|
|
175
|
+
* {@link RESERVED_FM_KEYS}, plus `description` on a PAGE, whose description the build writes as its
|
|
176
|
+
* description meta (`withPageDescription` in `@jxsuite/site/head-merger`). Listed as a frontmatter
|
|
177
|
+
* field as well, a Markdown page showed two Description rows with two values (the frontmatter's and
|
|
178
|
+
* the head's empty one), and only one of them reaches the browser. A layout's or component's
|
|
179
|
+
* `description` is data the build does not read, so it stays an ordinary field.
|
|
180
|
+
*
|
|
181
|
+
* @param tab The document whose fields are listed.
|
|
182
|
+
*/
|
|
183
|
+
export function reservedFmKeys(tab: Tab | null): ReadonlySet<string> {
|
|
184
|
+
return isPageDocument(tab) ? new Set([...RESERVED_FM_KEYS, "description"]) : RESERVED_FM_KEYS;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A page's description, from wherever the build reads it: a `<meta name="description">` in its own
|
|
189
|
+
* `$head` when it has one (stated last, it wins), else its `description` property.
|
|
190
|
+
*
|
|
191
|
+
* @param doc The head-bearing view of the document (`buildHeadDoc` for a Markdown page).
|
|
192
|
+
*/
|
|
193
|
+
export function pageDescription(doc: JxMutableNode): string {
|
|
194
|
+
const meta = findMetaEntry(doc.$head ?? [], "name", "description");
|
|
195
|
+
if (meta) {
|
|
196
|
+
return String(meta.attributes?.content ?? "");
|
|
197
|
+
}
|
|
198
|
+
return isString(doc.description) ? doc.description : "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Write a page's description where {@link pageDescription} reads it, so the one field never edits a
|
|
203
|
+
* value the build then overrules: the `description` property when that is where the page keeps it
|
|
204
|
+
* and its `$head` states none, else the head's meta (a page with neither gets the meta, as before).
|
|
205
|
+
* An empty value removes it.
|
|
206
|
+
*
|
|
207
|
+
* @param doc The head-bearing view of the document.
|
|
208
|
+
* @param value The new description, already trimmed.
|
|
209
|
+
*/
|
|
210
|
+
export function setPageDescription(doc: JxMutableNode, value: string): void {
|
|
211
|
+
if (!findMetaEntry(doc.$head ?? [], "name", "description") && isString(doc.description)) {
|
|
212
|
+
if (value) {
|
|
213
|
+
doc.description = value;
|
|
214
|
+
} else {
|
|
215
|
+
delete doc.description;
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
upsertMeta(doc, "name", "description", value);
|
|
220
|
+
}
|
|
221
|
+
|
|
155
222
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
156
223
|
|
|
224
|
+
/**
|
|
225
|
+
* One slot of a `$head` array as it is read off a document.
|
|
226
|
+
*
|
|
227
|
+
* Wider than `JxHeadEntry` on purpose: a Markdown page's `$head` is its frontmatter, which reaches
|
|
228
|
+
* these helpers through {@link buildHeadDoc} without being validated, so an empty YAML list item
|
|
229
|
+
* arrives as `null`. Every reader here steps over such a slot rather than throwing on it.
|
|
230
|
+
*/
|
|
231
|
+
type HeadSlot = JxHeadEntry | null | undefined;
|
|
232
|
+
|
|
157
233
|
/**
|
|
158
234
|
* Find a `$head` meta entry by attribute match.
|
|
159
235
|
*
|
|
@@ -162,11 +238,8 @@ export const RESERVED_FM_KEYS = new Set(["title"]);
|
|
|
162
238
|
* @param {string} key
|
|
163
239
|
* @returns {JxHeadEntry | undefined}
|
|
164
240
|
*/
|
|
165
|
-
export function findMetaEntry(head:
|
|
166
|
-
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
return head.find((e: JxHeadEntry) => e?.tagName === "meta" && e?.attributes?.[attr] === key);
|
|
241
|
+
export function findMetaEntry(head: readonly HeadSlot[], attr: "name" | "property", key: string) {
|
|
242
|
+
return head.find((e): e is JxHeadEntry => e?.tagName === "meta" && e.attributes?.[attr] === key);
|
|
170
243
|
}
|
|
171
244
|
|
|
172
245
|
/**
|
|
@@ -176,11 +249,8 @@ export function findMetaEntry(head: JxHeadEntry[], attr: "name" | "property", ke
|
|
|
176
249
|
* @param {string} rel
|
|
177
250
|
* @returns {JxHeadEntry | undefined}
|
|
178
251
|
*/
|
|
179
|
-
export function findLinkEntry(head:
|
|
180
|
-
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
return head.find((e: JxHeadEntry) => e?.tagName === "link" && e?.attributes?.rel === rel);
|
|
252
|
+
export function findLinkEntry(head: readonly HeadSlot[], rel: string) {
|
|
253
|
+
return head.find((e): e is JxHeadEntry => e?.tagName === "link" && e.attributes?.rel === rel);
|
|
184
254
|
}
|
|
185
255
|
|
|
186
256
|
/**
|
|
@@ -189,20 +259,20 @@ export function findLinkEntry(head: JxHeadEntry[], rel: string) {
|
|
|
189
259
|
* @param {JxHeadEntry} entry
|
|
190
260
|
* @returns {boolean}
|
|
191
261
|
*/
|
|
192
|
-
export function isManagedEntry(entry:
|
|
262
|
+
export function isManagedEntry(entry: HeadSlot) {
|
|
193
263
|
if (!entry?.tagName) {
|
|
194
264
|
return false;
|
|
195
265
|
}
|
|
196
266
|
// Managed meta tags
|
|
197
267
|
if (entry.tagName === "meta") {
|
|
198
|
-
const name = String(entry
|
|
199
|
-
const prop = String(entry
|
|
268
|
+
const name = String(entry.attributes?.name ?? "");
|
|
269
|
+
const prop = String(entry.attributes?.property ?? "");
|
|
200
270
|
return (
|
|
201
271
|
Boolean(name && MANAGED_META_KEYS.has(name)) || Boolean(prop && MANAGED_META_KEYS.has(prop))
|
|
202
272
|
);
|
|
203
273
|
}
|
|
204
274
|
// Managed link: favicon
|
|
205
|
-
if (entry.tagName === "link" && entry
|
|
275
|
+
if (entry.tagName === "link" && entry.attributes?.rel === "icon") {
|
|
206
276
|
return true;
|
|
207
277
|
}
|
|
208
278
|
return false;
|
|
@@ -226,7 +296,7 @@ export function upsertMeta(
|
|
|
226
296
|
doc.$head = [];
|
|
227
297
|
}
|
|
228
298
|
const idx = doc.$head.findIndex(
|
|
229
|
-
(e:
|
|
299
|
+
(e: HeadSlot) => e?.tagName === "meta" && e.attributes?.[attr] === key,
|
|
230
300
|
);
|
|
231
301
|
if (content) {
|
|
232
302
|
const entry = { attributes: { [attr]: key, content }, tagName: "meta" };
|
|
@@ -252,7 +322,7 @@ export function upsertLink(doc: JxMutableNode, rel: string, href: string) {
|
|
|
252
322
|
doc.$head = [];
|
|
253
323
|
}
|
|
254
324
|
const idx = doc.$head.findIndex(
|
|
255
|
-
(e:
|
|
325
|
+
(e: HeadSlot) => e?.tagName === "link" && e.attributes?.rel === rel,
|
|
256
326
|
);
|
|
257
327
|
if (href) {
|
|
258
328
|
const entry = { attributes: { href, rel }, tagName: "link" };
|
|
@@ -272,7 +342,7 @@ export function upsertLink(doc: JxMutableNode, rel: string, href: string) {
|
|
|
272
342
|
* @param {JxHeadEntry} entry
|
|
273
343
|
* @returns {string}
|
|
274
344
|
*/
|
|
275
|
-
export function entryLabel(entry:
|
|
345
|
+
export function entryLabel(entry: HeadSlot) {
|
|
276
346
|
if (!entry?.tagName) {
|
|
277
347
|
return "unknown";
|
|
278
348
|
}
|
|
@@ -301,7 +371,7 @@ export function entryLabel(entry: JxHeadEntry) {
|
|
|
301
371
|
* @param {JxHeadEntry} entry
|
|
302
372
|
* @returns {string}
|
|
303
373
|
*/
|
|
304
|
-
export function entryValue(entry:
|
|
374
|
+
export function entryValue(entry: HeadSlot) {
|
|
305
375
|
const a = entry?.attributes ?? {};
|
|
306
376
|
return String(a.content ?? a.href ?? a.src ?? entry?.textContent ?? "");
|
|
307
377
|
}
|
|
@@ -358,11 +428,15 @@ const NOT_SUPPLIED: ResolvedHeadField = { donor: null, source: "none", value: ""
|
|
|
358
428
|
* Last rather than first, because the merger folds a layer into a keyed map in array order: a layer
|
|
359
429
|
* that lists `description` twice emits the second one.
|
|
360
430
|
*/
|
|
361
|
-
function metaContentIn(
|
|
431
|
+
function metaContentIn(
|
|
432
|
+
head: readonly HeadSlot[],
|
|
433
|
+
attr: "name" | "property",
|
|
434
|
+
key: string,
|
|
435
|
+
): string | null {
|
|
362
436
|
let found: string | null = null;
|
|
363
437
|
for (const entry of head) {
|
|
364
|
-
if (entry?.tagName === "meta" && entry
|
|
365
|
-
found = String(entry.attributes
|
|
438
|
+
if (entry?.tagName === "meta" && entry.attributes?.[attr] === key) {
|
|
439
|
+
found = String(entry.attributes.content ?? "");
|
|
366
440
|
}
|
|
367
441
|
}
|
|
368
442
|
return found;
|
|
@@ -483,12 +557,12 @@ export interface SeoField extends ResolvedHeadField {
|
|
|
483
557
|
* a verdict, and the verdict is what gets optimised (§16.6). A count beside a limit says the same
|
|
484
558
|
* thing without pretending to rank it.
|
|
485
559
|
*/
|
|
486
|
-
export const SEO_LIMITS:
|
|
487
|
-
description
|
|
488
|
-
"og:description"
|
|
489
|
-
"og:title"
|
|
490
|
-
title
|
|
491
|
-
|
|
560
|
+
export const SEO_LIMITS: ReadonlyMap<string, number> = new Map([
|
|
561
|
+
["description", 160],
|
|
562
|
+
["og:description", 200],
|
|
563
|
+
["og:title", 60],
|
|
564
|
+
["title", 60],
|
|
565
|
+
]);
|
|
492
566
|
|
|
493
567
|
/** Counts what a reader sees as one character. See {@link visibleLength}. */
|
|
494
568
|
const GRAPHEME_SEGMENTER = new Intl.Segmenter("en-US", { granularity: "grapheme" });
|
|
@@ -518,15 +592,6 @@ export function visibleLength(value: string): number {
|
|
|
518
592
|
return count;
|
|
519
593
|
}
|
|
520
594
|
|
|
521
|
-
/** A named thing that is wrong, or absent. Never summed — the list IS the report. */
|
|
522
|
-
export interface SeoWarning {
|
|
523
|
-
/** Stable id, so a test and a shot name the same warning. */
|
|
524
|
-
id: string;
|
|
525
|
-
/** The head key the warning is about, so the field row can carry it. */
|
|
526
|
-
field: string;
|
|
527
|
-
message: string;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
595
|
/** The Problems `source` the SEO warnings carry, so a re-run replaces rather than stacks. */
|
|
531
596
|
export const SEO_PROBLEM_SOURCE = "Search appearance";
|
|
532
597
|
|
|
@@ -548,13 +613,16 @@ export const SEO_PROBLEM_SOURCE = "Search appearance";
|
|
|
548
613
|
export function reportSeoProblems(preview: SeoPreview, path?: string): number {
|
|
549
614
|
clearProblems((record) => record.source === SEO_PROBLEM_SOURCE);
|
|
550
615
|
for (const warning of preview.warnings) {
|
|
551
|
-
|
|
616
|
+
const opts: NotifyOptions = {
|
|
552
617
|
action: "document.openSeo",
|
|
553
618
|
key: `seo.${warning.id}`,
|
|
554
619
|
source: SEO_PROBLEM_SOURCE,
|
|
555
620
|
tier: "problem",
|
|
556
|
-
|
|
557
|
-
|
|
621
|
+
};
|
|
622
|
+
if (path !== undefined) {
|
|
623
|
+
opts.path = path;
|
|
624
|
+
}
|
|
625
|
+
notify.warn(warning.message, opts);
|
|
558
626
|
}
|
|
559
627
|
return preview.warnings.length;
|
|
560
628
|
}
|
|
@@ -564,7 +632,7 @@ export interface SeoPreview {
|
|
|
564
632
|
url: SeoUrl;
|
|
565
633
|
/** In render order: `title`, `description`, then the four OpenGraph keys. */
|
|
566
634
|
fields: SeoField[];
|
|
567
|
-
warnings:
|
|
635
|
+
warnings: SeoWarningView[];
|
|
568
636
|
}
|
|
569
637
|
|
|
570
638
|
/** Look one field up by key — the previews read four of the six by name. */
|
|
@@ -574,14 +642,16 @@ export function seoField(preview: SeoPreview, key: string): SeoField {
|
|
|
574
642
|
...NOT_SUPPLIED,
|
|
575
643
|
key,
|
|
576
644
|
label: key,
|
|
577
|
-
limit: SEO_LIMITS
|
|
645
|
+
limit: SEO_LIMITS.get(key) ?? null,
|
|
578
646
|
}
|
|
579
647
|
);
|
|
580
648
|
}
|
|
581
649
|
|
|
582
650
|
/** Whether any layer declares a `<title>` element, which the merger discards. */
|
|
583
651
|
function hasTitleEntry(layers: HeadLayers): boolean {
|
|
584
|
-
return [...layers.site, ...layers.layout, ...layers.page].some(
|
|
652
|
+
return [...layers.site, ...layers.layout, ...layers.page].some(
|
|
653
|
+
(e: HeadSlot) => e?.tagName === "title",
|
|
654
|
+
);
|
|
585
655
|
}
|
|
586
656
|
|
|
587
657
|
/**
|
|
@@ -593,13 +663,13 @@ function hasTitleEntry(layers: HeadLayers): boolean {
|
|
|
593
663
|
*
|
|
594
664
|
* @param {SeoPreview} preview — `warnings` is ignored; this computes it.
|
|
595
665
|
* @param {HeadLayers} layers
|
|
596
|
-
* @returns {
|
|
666
|
+
* @returns {SeoWarningView[]}
|
|
597
667
|
*/
|
|
598
668
|
export function seoWarnings(
|
|
599
669
|
preview: Omit<SeoPreview, "warnings">,
|
|
600
670
|
layers: HeadLayers,
|
|
601
|
-
):
|
|
602
|
-
const warnings:
|
|
671
|
+
): SeoWarningView[] {
|
|
672
|
+
const warnings: SeoWarningView[] = [];
|
|
603
673
|
const get = (key: string) => preview.fields.find((f) => f.key === key);
|
|
604
674
|
|
|
605
675
|
const title = get("title");
|
|
@@ -671,6 +741,14 @@ export function seoWarnings(
|
|
|
671
741
|
return warnings;
|
|
672
742
|
}
|
|
673
743
|
|
|
744
|
+
/** What {@link buildSeoPreview} needs besides the layers: the page's own title, route and site. */
|
|
745
|
+
export interface SeoPreviewContext {
|
|
746
|
+
pageTitle: string;
|
|
747
|
+
route: string | null;
|
|
748
|
+
siteName?: string;
|
|
749
|
+
siteUrl?: string;
|
|
750
|
+
}
|
|
751
|
+
|
|
674
752
|
/**
|
|
675
753
|
* Build the whole preview from the three layers, the page title, the route and the site config.
|
|
676
754
|
*
|
|
@@ -678,18 +756,15 @@ export function seoWarnings(
|
|
|
678
756
|
* (resolving the layout document) happens in {@link layoutHeadEntries} before this is called.
|
|
679
757
|
*
|
|
680
758
|
* @param {HeadLayers} layers
|
|
681
|
-
* @param {
|
|
759
|
+
* @param {SeoPreviewContext} ctx
|
|
682
760
|
* @returns {SeoPreview}
|
|
683
761
|
*/
|
|
684
|
-
export function buildSeoPreview(
|
|
685
|
-
layers: HeadLayers,
|
|
686
|
-
ctx: { pageTitle: string; route: string | null; siteName?: string; siteUrl?: string },
|
|
687
|
-
): SeoPreview {
|
|
762
|
+
export function buildSeoPreview(layers: HeadLayers, ctx: SeoPreviewContext): SeoPreview {
|
|
688
763
|
const field = (key: string, label: string, resolved: ResolvedHeadField): SeoField => ({
|
|
689
764
|
...resolved,
|
|
690
765
|
key,
|
|
691
766
|
label,
|
|
692
|
-
limit: SEO_LIMITS
|
|
767
|
+
limit: SEO_LIMITS.get(key) ?? null,
|
|
693
768
|
});
|
|
694
769
|
|
|
695
770
|
const fields: SeoField[] = [
|
|
@@ -741,11 +816,10 @@ async function loadLayoutHead(path: string): Promise<void> {
|
|
|
741
816
|
_layoutHeadPending = null;
|
|
742
817
|
_layoutHeadPath = path;
|
|
743
818
|
_layoutHead = doc?.$head ?? [];
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
// Off this list renders the page's own head with the layout layer missing — which reads as
|
|
819
|
+
// Search appearance shows the merged head, and the layout layer arrives asynchronously. A modal
|
|
820
|
+
// Left off this list renders the page's own head with the layout layer missing — which reads as
|
|
747
821
|
// "you have no description" on a page that inherits one.
|
|
748
|
-
renderOnly("
|
|
822
|
+
renderOnly("seoModal");
|
|
749
823
|
}
|
|
750
824
|
|
|
751
825
|
/**
|
|
@@ -765,6 +839,12 @@ export function layoutDisplayName(path: string): string {
|
|
|
765
839
|
);
|
|
766
840
|
}
|
|
767
841
|
|
|
842
|
+
/** A layout's `$head` entries, and the name to print for the layout. */
|
|
843
|
+
export interface LayoutHead {
|
|
844
|
+
entries: JxHeadEntry[];
|
|
845
|
+
name: string | null;
|
|
846
|
+
}
|
|
847
|
+
|
|
768
848
|
/**
|
|
769
849
|
* The effective layout's `$head`, and the name to print for it.
|
|
770
850
|
*
|
|
@@ -774,15 +854,9 @@ export function layoutDisplayName(path: string): string {
|
|
|
774
854
|
*
|
|
775
855
|
* @param {Tab | null} tab — the tab whose document the entries are being resolved FOR.
|
|
776
856
|
* @param {string | false} [docLayout]
|
|
777
|
-
* @returns {
|
|
857
|
+
* @returns {LayoutHead}
|
|
778
858
|
*/
|
|
779
|
-
export function layoutHeadEntries(
|
|
780
|
-
tab: Tab | null,
|
|
781
|
-
docLayout?: string | false,
|
|
782
|
-
): {
|
|
783
|
-
entries: JxHeadEntry[];
|
|
784
|
-
name: string | null;
|
|
785
|
-
} {
|
|
859
|
+
export function layoutHeadEntries(tab: Tab | null, docLayout?: string | false): LayoutHead {
|
|
786
860
|
const path = isPageDocument(tab) ? getEffectiveLayoutPath(docLayout) : null;
|
|
787
861
|
if (path === null) {
|
|
788
862
|
return { entries: [], name: null };
|
|
@@ -807,19 +881,26 @@ export function seoPreviewFor(tab: Tab | null, doc: JxMutableNode): SeoPreview {
|
|
|
807
881
|
const config = projectState?.projectConfig;
|
|
808
882
|
const layout = layoutHeadEntries(tab, doc.$layout);
|
|
809
883
|
const path = tab?.documentPath;
|
|
884
|
+
// A content document's head doc may carry a non-string title (see buildHeadDoc).
|
|
885
|
+
const ctx: SeoPreviewContext = {
|
|
886
|
+
pageTitle: isString(doc.title) ? doc.title : "",
|
|
887
|
+
route: path ? pageRoute(path) : null,
|
|
888
|
+
};
|
|
889
|
+
if (config?.name !== undefined) {
|
|
890
|
+
ctx.siteName = config.name;
|
|
891
|
+
}
|
|
892
|
+
if (config?.url !== undefined) {
|
|
893
|
+
ctx.siteUrl = config.url;
|
|
894
|
+
}
|
|
810
895
|
return buildSeoPreview(
|
|
811
896
|
{
|
|
812
897
|
layout: layout.entries,
|
|
813
898
|
layoutName: layout.name,
|
|
814
|
-
page:
|
|
899
|
+
// The page level as the build merges it: its `description` property leads its own head.
|
|
900
|
+
page: withPageDescription(doc.$head ?? [], doc.description),
|
|
815
901
|
site: config?.$head ?? [],
|
|
816
902
|
},
|
|
817
|
-
|
|
818
|
-
pageTitle: typeof doc.title === "string" ? doc.title : "",
|
|
819
|
-
route: path ? pageRoute(path) : null,
|
|
820
|
-
...(config?.name === undefined ? {} : { siteName: config.name }),
|
|
821
|
-
...(config?.url === undefined ? {} : { siteUrl: config.url }),
|
|
822
|
-
},
|
|
903
|
+
ctx,
|
|
823
904
|
);
|
|
824
905
|
}
|
|
825
906
|
|
|
@@ -837,7 +918,7 @@ export function seoPreviewFor(tab: Tab | null, doc: JxMutableNode): SeoPreview {
|
|
|
837
918
|
* component GAINED a header card because the focused tab was a page — and the Layout picker
|
|
838
919
|
* appeared or vanished in the pane you were editing according to the document in the other one.
|
|
839
920
|
*
|
|
840
|
-
* A caller whose subject genuinely IS the focused document (the
|
|
921
|
+
* A caller whose subject genuinely IS the focused document (the Inspector's Page tab) passes
|
|
841
922
|
* `activeTab.value`, where a reviewer can see it. The same bargain `setCanvasMode` made.
|
|
842
923
|
*
|
|
843
924
|
* @param {Tab | null} tab
|
|
@@ -853,14 +934,32 @@ export function isPageDocument(tab: Tab | null): boolean {
|
|
|
853
934
|
}
|
|
854
935
|
|
|
855
936
|
/** Overlay content-mode frontmatter title/`$head` onto the document the panel edits. */
|
|
856
|
-
export function buildHeadDoc(
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
937
|
+
export function buildHeadDoc(
|
|
938
|
+
doc: JxMutableNode,
|
|
939
|
+
fm: Tab["doc"]["content"]["frontmatter"],
|
|
940
|
+
): JxMutableNode {
|
|
941
|
+
const headDoc: JxMutableNode = { ...doc };
|
|
942
|
+
if (fm.title !== undefined) {
|
|
943
|
+
/* SAFETY: deliberately not narrowed — a YAML title can be a number, and it is overlaid as
|
|
944
|
+
written. Every reader of a head doc's `title` (titleRow here, seoPreviewFor) tests it with
|
|
945
|
+
isString before using it as text. */
|
|
946
|
+
headDoc.title = fm.title as string;
|
|
947
|
+
}
|
|
948
|
+
if (isString(fm.description)) {
|
|
949
|
+
// The page's description property, which the build writes as its description meta.
|
|
950
|
+
headDoc.description = fm.description;
|
|
951
|
+
}
|
|
952
|
+
if (isString(fm.$layout) || fm.$layout === false) {
|
|
953
|
+
// A Markdown page's layout override is its frontmatter `$layout`, which the build reads.
|
|
954
|
+
headDoc.$layout = fm.$layout;
|
|
955
|
+
}
|
|
956
|
+
if (fm.$head !== undefined) {
|
|
957
|
+
/* SAFETY: Studio's one writer of the frontmatter `$head` key is applyContentMutation below,
|
|
958
|
+
which stores a JxHeadEntry[] or deletes the key. A hand-written one is trusted exactly as
|
|
959
|
+
far as a JSON page's `doc.$head` is, and the readers step over null slots (HeadSlot). */
|
|
960
|
+
headDoc.$head = fm.$head as JxHeadEntry[];
|
|
961
|
+
}
|
|
962
|
+
return headDoc;
|
|
864
963
|
}
|
|
865
964
|
|
|
866
965
|
/**
|
|
@@ -870,13 +969,10 @@ export function buildHeadDoc(doc: JxMutableNode, fm: Record<string, unknown>): J
|
|
|
870
969
|
* adapts one to the other in the module that owns both, instead of in the Navigator orchestrator
|
|
871
970
|
* that owns neither.
|
|
872
971
|
*
|
|
873
|
-
* **`tab` is a parameter for the same reason every other frontmatter helper's is.**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* on a markdown page the card in one pane retitled the document in the other. The comment beside
|
|
878
|
-
* the card's JSON branch has claimed since the pane grid landed that this was fixed; the fix
|
|
879
|
-
* reached the JSON branch only, and this is the half that was left.
|
|
972
|
+
* **`tab` is a parameter for the same reason every other frontmatter helper's is.** Search
|
|
973
|
+
* appearance commits through it for the tab it was opened over, which need not be the focused
|
|
974
|
+
* pane's; resolving `activeTab.value` here once let an edit in one pane retitle the document in the
|
|
975
|
+
* other.
|
|
880
976
|
*
|
|
881
977
|
* @param {Tab | null} tab The document to commit into.
|
|
882
978
|
* @param {() => void} rerender
|
|
@@ -890,19 +986,61 @@ export function applyContentMutation(
|
|
|
890
986
|
if (!tab) {
|
|
891
987
|
return;
|
|
892
988
|
}
|
|
893
|
-
const fmNow =
|
|
894
|
-
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (
|
|
901
|
-
|
|
989
|
+
const fmNow = tab.doc.content.frontmatter;
|
|
990
|
+
// The frontmatter `$head`, read through the one place that types it.
|
|
991
|
+
const fmHead = buildHeadDoc({}, fmNow).$head;
|
|
992
|
+
const tmp: JxMutableNode = {};
|
|
993
|
+
if (isString(fmNow.title)) {
|
|
994
|
+
tmp.title = fmNow.title;
|
|
995
|
+
}
|
|
996
|
+
if (isString(fmNow.description)) {
|
|
997
|
+
tmp.description = fmNow.description;
|
|
998
|
+
}
|
|
999
|
+
if (fmHead) {
|
|
1000
|
+
tmp.$head = [...fmHead];
|
|
1001
|
+
}
|
|
1002
|
+
if (isString(fmNow.$layout) || fmNow.$layout === false) {
|
|
1003
|
+
tmp.$layout = fmNow.$layout;
|
|
902
1004
|
}
|
|
1005
|
+
fn(tmp);
|
|
903
1006
|
const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
|
|
904
|
-
|
|
905
|
-
|
|
1007
|
+
const layoutChanged = tmp.$layout !== fmNow.$layout;
|
|
1008
|
+
const titleChanged = tmp.title !== fmNow.title;
|
|
1009
|
+
const descriptionChanged = isString(fmNow.description) && tmp.description !== fmNow.description;
|
|
1010
|
+
const headChanged = JSON.stringify(newHead) !== JSON.stringify(fmHead);
|
|
1011
|
+
/* The writes go through the transaction log, as every other frontmatter commit does. That is what
|
|
1012
|
+
records them for undo and swaps the document's root reference, which is the signal the canvas
|
|
1013
|
+
redraws on: a Layout pick on a Markdown page that only mutated the frontmatter in place was
|
|
1014
|
+
stored, and the page went on showing its old layout until something unrelated redrew it. A
|
|
1015
|
+
mutation that changed nothing records nothing, since an empty transaction is still an undo step. */
|
|
1016
|
+
if (layoutChanged || titleChanged || descriptionChanged || headChanged) {
|
|
1017
|
+
transactDoc(tab, (t) => {
|
|
1018
|
+
// The Layout picker writes `$layout`, and on a Markdown page that is a frontmatter key.
|
|
1019
|
+
if (layoutChanged) {
|
|
1020
|
+
mutateUpdateFrontmatter(
|
|
1021
|
+
t,
|
|
1022
|
+
"$layout",
|
|
1023
|
+
isString(tmp.$layout) || tmp.$layout === false ? tmp.$layout : undefined,
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
if (titleChanged) {
|
|
1027
|
+
mutateUpdateFrontmatter(t, "title", tmp.title);
|
|
1028
|
+
}
|
|
1029
|
+
if (descriptionChanged) {
|
|
1030
|
+
mutateUpdateFrontmatter(
|
|
1031
|
+
t,
|
|
1032
|
+
"description",
|
|
1033
|
+
isString(tmp.description) ? tmp.description : undefined,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
if (headChanged) {
|
|
1037
|
+
/* SAFETY: every entry here either came from the frontmatter (JSON, from the format's split)
|
|
1038
|
+
or was built by a head editor out of string attributes; JxHeadEntry is wider than
|
|
1039
|
+
JsonValue only through its open index signature, which no head editor writes. */
|
|
1040
|
+
mutateUpdateFrontmatter(t, "$head", newHead as JsonValue | undefined);
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
906
1044
|
rerender();
|
|
907
1045
|
}
|
|
908
1046
|
|
|
@@ -914,15 +1052,22 @@ interface RowCommit {
|
|
|
914
1052
|
clear: () => void;
|
|
915
1053
|
/** Commit what the control settled on — a string from every kind but the checkbox. */
|
|
916
1054
|
commit: (raw: string | boolean) => void;
|
|
1055
|
+
/**
|
|
1056
|
+
* Whether this row's Browse lists only pictures (studio.md §9.3): the social image and the icon
|
|
1057
|
+
* always do, a frontmatter media field only when its schema says `format: "image"` — a video or a
|
|
1058
|
+
* PDF field browsing a picture-only list could never be filled from it.
|
|
1059
|
+
*/
|
|
1060
|
+
images: boolean;
|
|
917
1061
|
}
|
|
918
1062
|
|
|
919
|
-
/** What the panel is drawn against
|
|
1063
|
+
/** What the panel is drawn against: the focused document, reduced to what this reads. */
|
|
920
1064
|
export interface PagePanelContext {
|
|
921
1065
|
/** The head-bearing view of the open document — `buildHeadDoc` for a content page. */
|
|
922
1066
|
document: JxMutableNode;
|
|
923
1067
|
/** How a change to that view is written back into whichever realm it belongs to. */
|
|
924
1068
|
applyMutation: (fn: (doc: JxMutableNode) => void) => void;
|
|
925
|
-
|
|
1069
|
+
/** Repaint the host the panel is drawn in — the Inspector's Page tab. */
|
|
1070
|
+
rerender: () => void;
|
|
926
1071
|
}
|
|
927
1072
|
|
|
928
1073
|
/** Where the surface is mounted, and the context it was last drawn against. */
|
|
@@ -943,6 +1088,22 @@ let _addValue = "";
|
|
|
943
1088
|
/** This projection's rows, by key. Rebuilt with each projection. */
|
|
944
1089
|
const _commits = new Map<string, RowCommit>();
|
|
945
1090
|
|
|
1091
|
+
/** This projection's row buttons ({@link PageRow.action}), by row key. Rebuilt with each projection. */
|
|
1092
|
+
const _rowActions = new Map<string, () => void>();
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* The page's URL, derived from where its file sits under `pages/` (`pages/about.md` → `/about`), as
|
|
1096
|
+
* a readout: changing a URL is moving the file, which the file tree owns. `null` for anything that
|
|
1097
|
+
* is not a page.
|
|
1098
|
+
*/
|
|
1099
|
+
function urlRow(tab: Tab | null): PageRow | null {
|
|
1100
|
+
const route = tab?.documentPath ? pageRoute(tab.documentPath) : null;
|
|
1101
|
+
if (route === null) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
return { ...blankRow("__url", "url", "URL"), kind: "readout", value: route };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
946
1107
|
/**
|
|
947
1108
|
* This projection's custom `$head` entries, by the key they were drawn with.
|
|
948
1109
|
*
|
|
@@ -957,7 +1118,7 @@ const _entries = new Map<string, JxHeadEntry>();
|
|
|
957
1118
|
const _pending = new Map<string, { commit: () => void; timer: ReturnType<typeof setTimeout> }>();
|
|
958
1119
|
|
|
959
1120
|
/** The elements the add form offers, in the order a reader meets them. */
|
|
960
|
-
const TAG_OPTIONS:
|
|
1121
|
+
const TAG_OPTIONS: SurfaceOption[] = [
|
|
961
1122
|
{ label: "meta", value: "meta" },
|
|
962
1123
|
{ label: "link", value: "link" },
|
|
963
1124
|
{ label: "script", value: "script" },
|
|
@@ -966,8 +1127,10 @@ const TAG_OPTIONS: PageChoice[] = [
|
|
|
966
1127
|
/** A blank row, so every kind carries every field the document's bindings read. */
|
|
967
1128
|
function blankRow(key: string, prop: string, label: string): PageRow {
|
|
968
1129
|
return {
|
|
1130
|
+
action: "",
|
|
969
1131
|
checked: false,
|
|
970
1132
|
clearLabel: `Clear ${prop}`,
|
|
1133
|
+
hasAction: false,
|
|
971
1134
|
hasNote: false,
|
|
972
1135
|
hasThumb: false,
|
|
973
1136
|
isSet: false,
|
|
@@ -991,8 +1154,9 @@ function thumbFor(row: PageRow, value: string): void {
|
|
|
991
1154
|
|
|
992
1155
|
/** The one head value you type while writing, in both realms. */
|
|
993
1156
|
function titleRow(doc: JxMutableNode, apply: PagePanelContext["applyMutation"]): PageRow {
|
|
994
|
-
const title =
|
|
1157
|
+
const title = isString(doc.title) ? doc.title : "";
|
|
995
1158
|
_commits.set("title", {
|
|
1159
|
+
images: false,
|
|
996
1160
|
clear: () =>
|
|
997
1161
|
apply((d) => {
|
|
998
1162
|
delete d.title;
|
|
@@ -1025,6 +1189,7 @@ function metaRow(
|
|
|
1025
1189
|
const value = String(findMetaEntry(head, field.attr, field.key)?.attributes?.content ?? "");
|
|
1026
1190
|
const key = `meta:${field.attr}:${field.key}`;
|
|
1027
1191
|
_commits.set(key, {
|
|
1192
|
+
images: field.media === true,
|
|
1028
1193
|
clear: () => apply((d) => upsertMeta(d, field.attr, field.key, "")),
|
|
1029
1194
|
/* A media field is not trimmed: a path is committed exactly as it was chosen, and the browser
|
|
1030
1195
|
and the upload both hand one back already clean. */
|
|
@@ -1047,10 +1212,31 @@ function metaRow(
|
|
|
1047
1212
|
return row;
|
|
1048
1213
|
}
|
|
1049
1214
|
|
|
1215
|
+
/**
|
|
1216
|
+
* The page's one Description: the value the build writes as its description meta, read and written
|
|
1217
|
+
* where it lives ({@link pageDescription}, {@link setPageDescription}).
|
|
1218
|
+
*/
|
|
1219
|
+
function descriptionRow(doc: JxMutableNode, apply: PagePanelContext["applyMutation"]): PageRow {
|
|
1220
|
+
const value = pageDescription(doc);
|
|
1221
|
+
const key = "meta:name:description";
|
|
1222
|
+
_commits.set(key, {
|
|
1223
|
+
images: false,
|
|
1224
|
+
clear: () => apply((d) => setPageDescription(d, "")),
|
|
1225
|
+
commit: (raw) => apply((d) => setPageDescription(d, String(raw).trim())),
|
|
1226
|
+
});
|
|
1227
|
+
const row = blankRow(key, "description", "Description");
|
|
1228
|
+
row.isSet = Boolean(value);
|
|
1229
|
+
row.value = value;
|
|
1230
|
+
row.kind = "text";
|
|
1231
|
+
row.placeholder = "Description…";
|
|
1232
|
+
return row;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1050
1235
|
/** The favicon, which is a `<link rel="icon">` rather than a `<meta>`. */
|
|
1051
1236
|
function iconRow(head: JxHeadEntry[], apply: PagePanelContext["applyMutation"]): PageRow {
|
|
1052
1237
|
const value = String(findLinkEntry(head, "icon")?.attributes?.href ?? "");
|
|
1053
1238
|
_commits.set("link:icon", {
|
|
1239
|
+
images: true,
|
|
1054
1240
|
clear: () => apply((d) => upsertLink(d, "icon", "")),
|
|
1055
1241
|
commit: (raw) => apply((d) => upsertLink(d, "icon", String(raw))),
|
|
1056
1242
|
});
|
|
@@ -1066,8 +1252,7 @@ function iconRow(head: JxHeadEntry[], apply: PagePanelContext["applyMutation"]):
|
|
|
1066
1252
|
* The Layout picker, or `null` when this document takes none — a component has no layout, and a
|
|
1067
1253
|
* page whose layouts directory is still being listed has nothing to offer yet.
|
|
1068
1254
|
*
|
|
1069
|
-
* The listing is {@link layoutPickerEntries}',
|
|
1070
|
-
* layout invalidates one cache rather than two.
|
|
1255
|
+
* The listing is {@link layoutPickerEntries}', one cache that creating a layout invalidates.
|
|
1071
1256
|
*/
|
|
1072
1257
|
function layoutSection(
|
|
1073
1258
|
tab: Tab | null,
|
|
@@ -1085,6 +1270,7 @@ function layoutSection(
|
|
|
1085
1270
|
const defaultPath = projectState?.projectConfig?.defaults?.layout;
|
|
1086
1271
|
const defaultLabel = defaultPath ? layoutDisplayName(defaultPath) : "";
|
|
1087
1272
|
_commits.set("__layout", {
|
|
1273
|
+
images: false,
|
|
1088
1274
|
clear: () =>
|
|
1089
1275
|
apply((d) => {
|
|
1090
1276
|
delete d.$layout;
|
|
@@ -1112,6 +1298,17 @@ function layoutSection(
|
|
|
1112
1298
|
...entries.map((l) => ({ label: l.name, value: l.path })),
|
|
1113
1299
|
];
|
|
1114
1300
|
row.value = current === false ? "__none__" : current || "__default__";
|
|
1301
|
+
// The template the page is drawn in, opened in Design: its own `$layout`, else the default.
|
|
1302
|
+
const effective = getEffectiveLayoutPath(current);
|
|
1303
|
+
if (effective) {
|
|
1304
|
+
row.action = "Edit template";
|
|
1305
|
+
row.hasAction = true;
|
|
1306
|
+
_rowActions.set("__layout", () => {
|
|
1307
|
+
void import("../files/files").then(({ openFileInTab }) =>
|
|
1308
|
+
openFileInTab(effective.replace(/^\.\//, "")),
|
|
1309
|
+
);
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1115
1312
|
return { hasSeo: false, key: "layout", rows: [row], title: "Layout" };
|
|
1116
1313
|
}
|
|
1117
1314
|
|
|
@@ -1128,7 +1325,7 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
|
|
|
1128
1325
|
const { collection, fields, hasSchema, requiredFields } = collectFmFields(
|
|
1129
1326
|
tab,
|
|
1130
1327
|
projectState?.projectConfig,
|
|
1131
|
-
|
|
1328
|
+
reservedFmKeys(tab),
|
|
1132
1329
|
);
|
|
1133
1330
|
if (fields.length === 0 && !hasSchema) {
|
|
1134
1331
|
return null;
|
|
@@ -1139,9 +1336,9 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
|
|
|
1139
1336
|
rerender: sync,
|
|
1140
1337
|
});
|
|
1141
1338
|
_commits.set(key, {
|
|
1339
|
+
images: row.kind === "media" && f.entry.format === "image",
|
|
1142
1340
|
clear: () => transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field)),
|
|
1143
|
-
commit: (raw) =>
|
|
1144
|
-
transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field, parse(raw) as JsonValue)),
|
|
1341
|
+
commit: (raw) => transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field, parse(raw))),
|
|
1145
1342
|
});
|
|
1146
1343
|
return Object.assign(blankRow(key, f.field, row.label), row, {
|
|
1147
1344
|
clearLabel: `Clear ${f.field}`,
|
|
@@ -1158,27 +1355,30 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
|
|
|
1158
1355
|
/** The `$head` entries no structured control owns, and the keys a Remove hands back. */
|
|
1159
1356
|
function customEntries(head: JxHeadEntry[]): PageTagRow[] {
|
|
1160
1357
|
_entries.clear();
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
});
|
|
1358
|
+
const rows: PageTagRow[] = [];
|
|
1359
|
+
for (const entry of head) {
|
|
1360
|
+
if (isManagedEntry(entry) || isGoogleFontEntry(entry) || isGoogleFontPreconnect(entry)) {
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
// The key counts the LISTED entries, as the index of the filtered list it replaces did.
|
|
1364
|
+
const key = `tag:${rows.length}`;
|
|
1365
|
+
_entries.set(key, entry);
|
|
1366
|
+
const label = entryLabel(entry);
|
|
1367
|
+
rows.push({ key, label, removeLabel: `Remove ${label}`, value: entryValue(entry) });
|
|
1368
|
+
}
|
|
1369
|
+
return rows;
|
|
1171
1370
|
}
|
|
1172
1371
|
|
|
1173
1372
|
/**
|
|
1174
1373
|
* Everything the panel says about one document, and the commit table behind it.
|
|
1175
1374
|
*
|
|
1176
|
-
* `activeTab.value` is read HERE and nowhere below it: the
|
|
1375
|
+
* `activeTab.value` is read HERE and nowhere below it: the Inspector's Page tab is an app-level
|
|
1177
1376
|
* surface that shows the focused document by definition, and spelling that out once at the top is
|
|
1178
1377
|
* what lets every helper under it take a tab instead of asking for one.
|
|
1179
1378
|
*/
|
|
1180
1379
|
function view(ctx: PagePanelContext): PagePanelView {
|
|
1181
1380
|
_commits.clear();
|
|
1381
|
+
_rowActions.clear();
|
|
1182
1382
|
const tab = activeTab.value;
|
|
1183
1383
|
const doc = ctx.document;
|
|
1184
1384
|
const head = doc.$head ?? [];
|
|
@@ -1192,13 +1392,19 @@ function view(ctx: PagePanelContext): PagePanelView {
|
|
|
1192
1392
|
if (layout) {
|
|
1193
1393
|
sections.push(layout);
|
|
1194
1394
|
}
|
|
1395
|
+
const url = urlRow(tab);
|
|
1195
1396
|
sections.push(
|
|
1196
1397
|
{
|
|
1197
1398
|
hasSeo: true,
|
|
1198
1399
|
key: "page",
|
|
1199
1400
|
rows: [
|
|
1200
1401
|
titleRow(doc, ctx.applyMutation),
|
|
1201
|
-
...
|
|
1402
|
+
...(url ? [url] : []),
|
|
1403
|
+
...PAGE_FIELDS.map((field) =>
|
|
1404
|
+
field.key === "description"
|
|
1405
|
+
? descriptionRow(doc, ctx.applyMutation)
|
|
1406
|
+
: metaRow(field, head, ctx.applyMutation),
|
|
1407
|
+
),
|
|
1202
1408
|
iconRow(head, ctx.applyMutation),
|
|
1203
1409
|
],
|
|
1204
1410
|
title: "Page",
|
|
@@ -1290,7 +1496,7 @@ function addEntry(): void {
|
|
|
1290
1496
|
}
|
|
1291
1497
|
d.$head.push(entry);
|
|
1292
1498
|
});
|
|
1293
|
-
ctx.
|
|
1499
|
+
ctx.rerender();
|
|
1294
1500
|
}
|
|
1295
1501
|
|
|
1296
1502
|
/** Take one custom `$head` entry away, by the entry the key was drawn for. */
|
|
@@ -1309,7 +1515,7 @@ function removeEntry(key: string): void {
|
|
|
1309
1515
|
d.$head.splice(idx, 1);
|
|
1310
1516
|
}
|
|
1311
1517
|
});
|
|
1312
|
-
ctx.
|
|
1518
|
+
ctx.rerender();
|
|
1313
1519
|
}
|
|
1314
1520
|
|
|
1315
1521
|
/**
|
|
@@ -1342,9 +1548,13 @@ const ACTIONS: PagePanelActions = {
|
|
|
1342
1548
|
flushPending(key);
|
|
1343
1549
|
void mediaPicker().then((m) => {
|
|
1344
1550
|
if (anchor instanceof HTMLElement) {
|
|
1345
|
-
m.showMediaPickerPopover(
|
|
1346
|
-
|
|
1347
|
-
|
|
1551
|
+
m.showMediaPickerPopover(
|
|
1552
|
+
anchor,
|
|
1553
|
+
(val: string) => {
|
|
1554
|
+
_commits.get(key)?.commit(val);
|
|
1555
|
+
},
|
|
1556
|
+
{ images: _commits.get(key)?.images ?? true },
|
|
1557
|
+
);
|
|
1348
1558
|
}
|
|
1349
1559
|
});
|
|
1350
1560
|
},
|
|
@@ -1377,11 +1587,14 @@ const ACTIONS: PagePanelActions = {
|
|
|
1377
1587
|
sync();
|
|
1378
1588
|
},
|
|
1379
1589
|
openSeo: () => {
|
|
1380
|
-
/* The COMMAND, not a local open()
|
|
1381
|
-
|
|
1590
|
+
/* The COMMAND, not a local open(): no surface owns the capability, so the palette has it by
|
|
1591
|
+
name. */
|
|
1382
1592
|
void activeRegistry()?.run("document.openSeo");
|
|
1383
1593
|
},
|
|
1384
1594
|
removeEntry,
|
|
1595
|
+
runRowAction: (key) => {
|
|
1596
|
+
_rowActions.get(key)?.();
|
|
1597
|
+
},
|
|
1385
1598
|
setBoolean: (key, checked) => commitNow(key, checked),
|
|
1386
1599
|
setChoice: (key, value) => commitNow(key, value),
|
|
1387
1600
|
setNumber: (key, value) => commitNow(key, value),
|
|
@@ -1424,64 +1637,137 @@ export function renderPagePanel(host: HTMLElement, ctx: PagePanelContext): void
|
|
|
1424
1637
|
sync();
|
|
1425
1638
|
}
|
|
1426
1639
|
|
|
1640
|
+
// ─── The Inspector's Page tab ────────────────────────────────────────────
|
|
1641
|
+
|
|
1427
1642
|
/**
|
|
1428
|
-
*
|
|
1643
|
+
* What the Page tab is called for a document (§6): Page under `pages/`, Template under `layouts/`,
|
|
1644
|
+
* Component under `components/`, Entry for a file a collection owns, Document for anything else.
|
|
1429
1645
|
*
|
|
1430
|
-
*
|
|
1431
|
-
* is
|
|
1432
|
-
* clearing it is how switching to another panel takes this surface down. A body drawn by the
|
|
1433
|
-
* fallback path has no content area; mounting into the body itself is then still correct.
|
|
1646
|
+
* Path-based, like the rest of the site model: a file's role in a project is where it lives, and
|
|
1647
|
+
* `collectionForFile` is the one matcher that answers "is this an entry" (§22.1 uses it too).
|
|
1434
1648
|
*/
|
|
1435
|
-
function
|
|
1436
|
-
|
|
1649
|
+
export function pageTabTitle(path: string | null | undefined): string {
|
|
1650
|
+
if (!path) {
|
|
1651
|
+
return "Document";
|
|
1652
|
+
}
|
|
1653
|
+
if (path.startsWith("pages/")) {
|
|
1654
|
+
return "Page";
|
|
1655
|
+
}
|
|
1656
|
+
if (path.startsWith("layouts/")) {
|
|
1657
|
+
return "Template";
|
|
1658
|
+
}
|
|
1659
|
+
if (path.startsWith("components/")) {
|
|
1660
|
+
return "Component";
|
|
1661
|
+
}
|
|
1662
|
+
const collection = collectionForFile(path);
|
|
1663
|
+
return collection && !collection.fileBacked ? "Entry" : "Document";
|
|
1437
1664
|
}
|
|
1438
1665
|
|
|
1666
|
+
/** What the Component tab says: why there is no page form, and the one verb that reaches its own. */
|
|
1667
|
+
export const COMPONENT_TAB_MESSAGE =
|
|
1668
|
+
"A component has no page settings: its title, search and social details come from the page that uses it.";
|
|
1669
|
+
|
|
1670
|
+
/** The Component tab's empty state, for a component document's tab. */
|
|
1671
|
+
export function componentTabSpec(tab: Tab): EmptyStateSpec {
|
|
1672
|
+
return {
|
|
1673
|
+
actions: [
|
|
1674
|
+
{
|
|
1675
|
+
label: "Edit tag and props",
|
|
1676
|
+
run: () => {
|
|
1677
|
+
// The root is the component's definition; selecting it opens Element › Content on its
|
|
1678
|
+
// Tag name and "Component Defaults" (`panels/properties-panel.ts`).
|
|
1679
|
+
updateSession(tab, { selection: [[]], ui: { rightTab: "properties" } });
|
|
1680
|
+
},
|
|
1681
|
+
},
|
|
1682
|
+
],
|
|
1683
|
+
detail: "Its tag name and the props it takes are set on its root element.",
|
|
1684
|
+
message: COMPONENT_TAB_MESSAGE,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/** The sentence the Page tab shows with no document open. */
|
|
1689
|
+
export const PAGE_TAB_EMPTY = "Open a page to edit its title, description and social preview.";
|
|
1690
|
+
|
|
1691
|
+
/** The Inspector's Page body host, handed over once by the dock. */
|
|
1692
|
+
let _tabHost: HTMLElement | null = null;
|
|
1693
|
+
|
|
1439
1694
|
/**
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
* The record below returns `nothing` and mounts its document in `afterRender`, so this is a stub:
|
|
1443
|
-
* it survives only because `NavigatorPanelDeps` still declares the injection and `studio.ts` still
|
|
1444
|
-
* passes it. Both go in the change that deletes this.
|
|
1695
|
+
* Take the Inspector's Page body host, or let it go (`null`, when the dock is taken down).
|
|
1445
1696
|
*
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1697
|
+
* The Page panel moved here from the Navigator (§6): the same surface, re-hosted unchanged, drawn
|
|
1698
|
+
* against the focused document. `panels/right-panel.ts` hands the host over and calls
|
|
1699
|
+
* {@link renderPageTab} whenever the dock repaints.
|
|
1448
1700
|
*/
|
|
1449
|
-
export function
|
|
1450
|
-
|
|
1701
|
+
export function bindPageHost(host: HTMLElement | null): void {
|
|
1702
|
+
if (_tabHost && _tabHost !== host) {
|
|
1703
|
+
unmountEntryInspector(_tabHost);
|
|
1704
|
+
unmountPagePanelSurface(_tabHost);
|
|
1705
|
+
if (_host === _tabHost) {
|
|
1706
|
+
_host = null;
|
|
1707
|
+
_ctx = null;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
_tabHost = host;
|
|
1711
|
+
renderPageTab();
|
|
1451
1712
|
}
|
|
1452
1713
|
|
|
1453
1714
|
/**
|
|
1454
|
-
*
|
|
1715
|
+
* Draw the Page tab against the focused document, or empty it when nothing is open.
|
|
1455
1716
|
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* the surface Page. The record is what keeps the surface addressable under one name.
|
|
1717
|
+
* Idempotent and cheap: a standing surface is only assigned to (`renderPagePanelSurface`), so the
|
|
1718
|
+
* dock can call this on every repaint without taking the caret out of a field.
|
|
1459
1719
|
*/
|
|
1460
|
-
export function
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1720
|
+
export function renderPageTab(): void {
|
|
1721
|
+
const host = _tabHost;
|
|
1722
|
+
if (!host) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const tab = activeTab.value;
|
|
1726
|
+
if (!tab || isEntryTab(tab)) {
|
|
1727
|
+
unmountPagePanelSurface(host);
|
|
1728
|
+
if (_host === host) {
|
|
1729
|
+
_host = null;
|
|
1730
|
+
_ctx = null;
|
|
1731
|
+
}
|
|
1732
|
+
/* A collection entry's tab is titled Entry and draws the entry's own settings — status,
|
|
1733
|
+
fields, page URL, file (§22.5) — rather than a page's title, head and SEO preview: the
|
|
1734
|
+
entry's page is its collection's template, and its head is that page's. */
|
|
1735
|
+
if (tab) {
|
|
1736
|
+
renderEntryInspector(host, tab);
|
|
1737
|
+
} else {
|
|
1738
|
+
unmountEntryInspector(host);
|
|
1739
|
+
}
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
unmountEntryInspector(host);
|
|
1743
|
+
/* A component is not a page: it has no title, no head and no social card of its own — the pages
|
|
1744
|
+
that use it do. Its own settings (its tag, the props it takes and their defaults) are its ROOT
|
|
1745
|
+
element's, edited in Element › Content, so the tab says that and goes there. */
|
|
1746
|
+
if (pageTabTitle(tab.documentPath) === "Component") {
|
|
1747
|
+
unmountPagePanelSurface(host);
|
|
1748
|
+
if (_host === host) {
|
|
1749
|
+
_host = null;
|
|
1750
|
+
_ctx = null;
|
|
1751
|
+
}
|
|
1752
|
+
// Assigned on every repaint; placed only when it is not already the tab's one child.
|
|
1753
|
+
const state = emptyState(host, componentTabSpec(tab));
|
|
1754
|
+
if (host.firstChild !== state || host.childNodes.length !== 1) {
|
|
1755
|
+
host.replaceChildren(state);
|
|
1756
|
+
}
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const isContent = tab.doc.mode === "content";
|
|
1760
|
+
renderPagePanel(host, {
|
|
1761
|
+
/* The Page tab IS an app-level surface: it shows the focused document by definition, so it
|
|
1762
|
+
spells that out at the call site instead of leaving it to a helper two other surfaces share. */
|
|
1763
|
+
applyMutation: isContent
|
|
1764
|
+
? (fn) => applyContentMutation(activeTab.value, renderPageTab, fn)
|
|
1765
|
+
: (fn) => {
|
|
1766
|
+
transact(activeTab.value, fn);
|
|
1767
|
+
},
|
|
1768
|
+
document: isContent
|
|
1769
|
+
? buildHeadDoc(tab.doc.document, tab.doc.content.frontmatter)
|
|
1770
|
+
: tab.doc.document,
|
|
1771
|
+
rerender: renderPageTab,
|
|
1486
1772
|
});
|
|
1487
1773
|
}
|