@jxsuite/studio 4.1.0 → 5.1.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 +1 -1
- package/data/elements-meta.json +5 -15
- package/data/html-meta.json +51 -6
- package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
- package/dist/chunks/action-group-sp6tna8v.js +118 -0
- package/dist/chunks/adapter-nj7tnjfn.js +13 -0
- package/dist/chunks/cf-account-picker-qwkm9zrf.js +495 -0
- package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
- package/dist/chunks/color-area-v17df7b8.js +154 -0
- package/dist/chunks/color-field-anwybnkn.js +208 -0
- package/dist/chunks/color-slider-44ez96fc.js +79 -0
- package/dist/chunks/color-xgg3kfw7.js +35 -0
- package/dist/chunks/combobox-5e7jev23.js +236 -0
- package/dist/chunks/components-vj72gqtq.js +20 -0
- package/dist/chunks/content-source-5mex4x1h.js +46 -0
- package/dist/chunks/convert-to-repeater-n64h8znc.js +639 -0
- package/dist/chunks/dialog-s890b918.js +85 -0
- package/dist/chunks/editor-3qhbg51x.js +35 -0
- package/dist/chunks/entry-commands-234hjnqw.js +42 -0
- package/dist/chunks/extension-sections-3hpcambp.js +651 -0
- package/dist/chunks/file-ops-8yhd83xr.js +47 -0
- package/dist/chunks/files-hnxqxtsg.js +72 -0
- package/dist/chunks/format-host-779sfp2n.js +40 -0
- package/dist/chunks/fs-events-b88z7g4h.js +18 -0
- package/dist/chunks/git-panel-xwj8fpab.js +49 -0
- package/dist/chunks/icons-yy10wk9r.js +402 -0
- package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
- package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
- package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
- package/dist/chunks/iframe-host-syr0f9sb.js +115 -0
- package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
- package/dist/chunks/index-n4j7qxyr.js +109 -0
- package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
- package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
- package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
- package/dist/chunks/layers-ff81wthn.js +30 -0
- package/dist/chunks/layers-panel-pnnghw12.js +40 -0
- package/dist/chunks/listbox-6v2ac08j.js +44 -0
- package/dist/chunks/lit-html-m244w9nh.js +17 -0
- package/dist/chunks/media-picker-cqhhxhhe.js +33 -0
- package/dist/chunks/menu-9p2w46gj.js +15 -0
- package/dist/chunks/menu-q99sy9rf.js +285 -0
- package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
- package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
- package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
- package/dist/chunks/notify-628t0anh.js +21 -0
- package/dist/chunks/number-field-5cndyft8.js +72 -0
- package/dist/chunks/parse-0zx14y15.js +10 -0
- package/dist/chunks/platform-ts9cg1a4.js +15 -0
- package/dist/chunks/popover-47z3ytva.js +25 -0
- package/dist/chunks/publish-panel-5zt840sy.js +27 -0
- package/dist/chunks/quick-search-3748564r.js +45 -0
- package/dist/chunks/references-qvvxej7h.js +26 -0
- package/dist/chunks/right-panel-eccz92qt.js +45 -0
- package/dist/chunks/section-registry-5pq32t0q.js +25 -0
- package/dist/chunks/select-2r16ez41.js +49 -0
- package/dist/chunks/split-g7nm1rgb.js +191 -0
- package/dist/chunks/studio-0hqbpbd0.js +62 -0
- package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
- package/dist/chunks/{studio-b55y6ewx.js → studio-1knfqgy7.js} +58 -58
- package/dist/chunks/{studio-2b946z26.js → studio-1z1kcjv4.js} +3628 -6095
- package/dist/chunks/studio-3773086z.js +111 -0
- package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
- package/dist/chunks/{studio-x813zp48.js → studio-3rz5md06.js} +30 -30
- package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
- package/dist/chunks/studio-5te5tqng.js +81 -0
- package/dist/chunks/studio-5wt9nvhd.js +4763 -0
- package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
- package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
- package/dist/chunks/studio-6xa21ej9.js +28 -0
- package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
- package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
- package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
- package/dist/chunks/studio-7xnmgv66.js +13615 -0
- package/dist/chunks/studio-8scdew0e.js +258 -0
- package/dist/chunks/{studio-fbf92ckf.js → studio-93zafjcm.js} +275 -163
- package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
- package/dist/chunks/studio-a79k90n9.js +1162 -0
- package/dist/chunks/studio-aj37eer3.js +422 -0
- package/dist/chunks/studio-b0k5tqgp.js +2432 -0
- package/dist/chunks/studio-b4s9eykd.js +2811 -0
- package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
- package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
- package/dist/chunks/studio-e1pgga3k.js +839 -0
- package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
- package/dist/chunks/{studio-q43a5y9h.js → studio-g3jevyjt.js} +16 -16
- package/dist/chunks/studio-gb13095a.js +2806 -0
- package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
- package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
- package/dist/chunks/{studio-x4qkdat5.js → studio-js8nv53n.js} +1749 -2481
- package/dist/chunks/{studio-qjy2z9bv.js → studio-jxp12rnj.js} +32 -137
- package/dist/chunks/{studio-trxrabqf.js → studio-k75n7vhp.js} +527 -283
- package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
- package/dist/chunks/studio-kkqkcp3d.js +17668 -0
- package/dist/chunks/studio-mhhyamxf.js +763 -0
- package/dist/chunks/{studio-cft9r4r7.js → studio-nq5yf5mp.js} +22 -22
- package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
- package/dist/chunks/studio-nzzpvzx9.js +185 -0
- package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
- package/dist/chunks/{studio-3bqpr0zh.js → studio-pxbgypmg.js} +299 -110
- package/dist/chunks/studio-q0a3yk4n.js +33 -0
- package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
- package/dist/chunks/{studio-6yw4y3my.js → studio-qhhm6hbt.js} +8 -8
- package/dist/chunks/studio-qk4bfsq2.js +362 -0
- package/dist/chunks/{studio-msd847zn.js → studio-qk90fgqm.js} +361 -90
- package/dist/chunks/{studio-jkftxyvz.js → studio-r6nvacqs.js} +5861 -2154
- package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
- package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
- package/dist/chunks/studio-rs76k67r.js +12 -0
- package/dist/chunks/studio-sbjyc8bt.js +954 -0
- package/dist/chunks/{studio-1wagb550.js → studio-syg2tadj.js} +11 -11
- package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
- package/dist/chunks/studio-tqgryxwg.js +1171 -0
- package/dist/chunks/studio-try13h8c.js +8448 -0
- package/dist/chunks/studio-vfq8qpjt.js +1219 -0
- package/dist/chunks/studio-vhv5tgxm.js +51 -0
- package/dist/chunks/{studio-ahn3pdgx.js → studio-vw7mzp6z.js} +52 -38
- package/dist/chunks/studio-wqpngpsk.js +174 -0
- package/dist/chunks/studio-wqz6dggm.js +1356 -0
- package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
- package/dist/chunks/studio-yxa81f45.js +12 -0
- package/dist/chunks/studio-zdn2g6dt.js +148 -0
- package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
- package/dist/chunks/tabs-wbp1cdws.js +156 -0
- package/dist/chunks/textfield-abk6qyee.js +73 -0
- package/dist/chunks/toast-780pvfbv.js +27 -0
- package/dist/chunks/toast-host-3sqv46sn.js +116 -0
- package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
- package/dist/chunks/tooltip-eptb614w.js +369 -0
- package/dist/chunks/tree-a73p0pqm.js +367 -0
- package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
- package/dist/chunks/workspace-t55jdjbx.js +69 -0
- package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
- package/dist/iframe-entry.js +1873 -673
- package/dist/studio.js +52757 -36535
- package/dist/workers/editor.worker.js +1040 -1085
- package/dist/workers/json.worker.js +1946 -1991
- package/dist/workers/ts.worker.js +4960 -5005
- package/index.html +1 -0
- package/package.json +18 -52
- package/src/about/about-modal.ts +72 -123
- package/src/browse/library-commands.ts +20 -49
- package/src/browse/library-layouts.ts +87 -218
- package/src/browse/library-pane.ts +406 -438
- package/src/canvas/canvas-helpers.ts +0 -49
- package/src/canvas/canvas-live-render.ts +25 -2
- package/src/canvas/canvas-render.ts +666 -231
- package/src/canvas/canvas-surface.ts +0 -1
- package/src/canvas/canvas-utils.ts +333 -113
- package/src/canvas/dialog-path.ts +76 -0
- package/src/canvas/dialog-state.ts +67 -0
- package/src/canvas/diff-marks.ts +252 -0
- package/src/canvas/diff-toolbar.ts +349 -0
- package/src/canvas/diff-view.ts +123 -0
- package/src/canvas/edit-width-drag.ts +187 -0
- package/src/canvas/edit-width.ts +150 -0
- package/src/canvas/iframe-entry.ts +125 -8
- package/src/canvas/iframe-host.ts +271 -15
- package/src/canvas/iframe-interaction.ts +133 -13
- package/src/canvas/iframe-patch.ts +7 -1
- package/src/canvas/iframe-position.ts +5 -6
- package/src/canvas/iframe-protocol.ts +106 -5
- package/src/canvas/iframe-render.ts +373 -5
- package/src/canvas/iframe-slash.ts +2 -2
- package/src/canvas/iframe-subtree.ts +5 -8
- package/src/canvas/path-mapping.ts +25 -3
- package/src/canvas/popover-path.ts +106 -0
- package/src/canvas/popover-state.ts +155 -0
- package/src/canvas/surface-registry.ts +21 -10
- package/src/collab/presence-chips.ts +51 -73
- package/src/commands/app-commands.ts +10 -0
- package/src/commands/budget.ts +12 -0
- package/src/commands/command-args.ts +404 -6
- package/src/commands/defaults.ts +44 -14
- package/src/commands/live-context.ts +11 -5
- package/src/commands/reference.ts +12 -2
- package/src/commands/registry.ts +177 -10
- package/src/commands/run-reported.ts +120 -0
- package/src/content/entry-commands.ts +16 -23
- package/src/content/entry-editor.ts +144 -101
- package/src/editor/canvas-slash-bridge.ts +2 -2
- package/src/editor/context-menu.ts +49 -200
- package/src/editor/convert-to-component.ts +42 -82
- package/src/editor/convert-to-repeater.ts +132 -173
- package/src/editor/inline-edit.ts +11 -0
- package/src/editor/shortcuts.ts +76 -49
- package/src/editor/slash-menu.ts +150 -157
- package/src/files/components.ts +39 -0
- package/src/files/file-ops.ts +117 -11
- package/src/files/files.ts +944 -660
- package/src/files/json-layout.ts +474 -0
- package/src/files/serialize-document.ts +8 -1
- package/src/format/convert-file.ts +16 -10
- package/src/format/format-host.ts +50 -1
- package/src/github/github-auth.ts +71 -29
- package/src/github/github-publish.ts +57 -65
- package/src/grid/cell-editors.ts +46 -4
- package/src/grid/cell-popovers.ts +65 -62
- package/src/grid/grid-open.ts +86 -64
- package/src/grid/grid-panel.ts +390 -469
- package/src/grid/redirects-grid.ts +40 -44
- package/src/hosting/layout.ts +2 -0
- package/src/i18n/i18n-commands.ts +137 -62
- package/src/media/media-pane.ts +83 -133
- package/src/new-project/add-repo-modal.ts +125 -176
- package/src/new-project/import-tab.ts +181 -252
- package/src/new-project/location-fields.ts +93 -160
- package/src/new-project/new-project-modal.ts +376 -367
- package/src/packages/jxsuite-update.ts +76 -18
- package/src/packages/pull-package-sync.ts +5 -1
- package/src/panels/activity-panel.ts +137 -104
- package/src/panels/ai-chat/chat-markdown.ts +15 -13
- package/src/panels/ai-chat/chat-view.ts +307 -472
- package/src/panels/ai-chat/composer.ts +175 -163
- package/src/panels/ai-chat/sessions-view.ts +31 -62
- package/src/panels/ai-panel.ts +213 -185
- package/src/panels/block-action-bar.ts +444 -645
- package/src/panels/bottom-dock.ts +112 -47
- package/src/panels/chat-panel.ts +19 -14
- package/src/panels/data-explorer.ts +141 -102
- package/src/panels/data-grid.ts +183 -150
- package/src/panels/dnd.ts +75 -20
- package/src/panels/editors.ts +26 -19
- package/src/panels/elements-panel.ts +243 -186
- package/src/panels/empty-state.ts +26 -42
- package/src/panels/events-panel.ts +950 -729
- package/src/panels/formula-workspace.ts +434 -207
- package/src/panels/frontmatter-fields.ts +185 -153
- package/src/panels/frontmatter-panel.ts +411 -187
- package/src/panels/git-diff-open.ts +148 -0
- package/src/panels/git-panel.ts +679 -605
- package/src/panels/head-panel.ts +617 -435
- package/src/panels/i18n-panel.ts +148 -135
- package/src/panels/imports-panel.ts +388 -414
- package/src/panels/jump-bar.ts +175 -147
- package/src/panels/layers-panel.ts +785 -671
- package/src/panels/left-panel.ts +166 -94
- package/src/panels/navigator-panels.ts +1 -1
- package/src/panels/pane-context.ts +529 -677
- package/src/panels/pane-grid.ts +265 -241
- package/src/panels/panel-registry.ts +15 -4
- package/src/panels/problems-panel.ts +181 -99
- package/src/panels/properties-panel.ts +1525 -966
- package/src/panels/provenance.ts +31 -113
- package/src/panels/quick-search.ts +223 -184
- package/src/panels/right-panel.ts +135 -169
- package/src/panels/seo-modal.ts +346 -167
- package/src/panels/settings-menu.ts +114 -661
- package/src/panels/settings-pane.ts +102 -86
- package/src/panels/signals-panel.ts +1318 -1172
- package/src/panels/statement-editor.ts +556 -428
- package/src/panels/style-panel.ts +1367 -630
- package/src/panels/style-utils.ts +36 -32
- package/src/panels/stylebook-doc.ts +30 -73
- package/src/panels/stylebook-layers-panel.ts +202 -109
- package/src/panels/stylebook-panel.ts +124 -88
- package/src/panels/tab-strip.ts +386 -288
- package/src/platforms/cloud.ts +106 -16
- package/src/platforms/devserver.ts +13 -0
- package/src/publish/deploy-checklist.ts +124 -78
- package/src/publish/publish-commands.ts +4 -12
- package/src/publish/publish-panel.ts +134 -328
- package/src/services/a11y-report.ts +100 -43
- package/src/services/ai-command-tools.ts +538 -0
- package/src/services/ai-project-tools.ts +8 -2
- package/src/services/ai-session-store.ts +6 -0
- package/src/services/ai-system-prompt.ts +112 -24
- package/src/services/ai-tools.ts +36 -212
- package/src/services/ai-write-report.ts +174 -0
- package/src/services/document-assistant.ts +59 -8
- package/src/services/idle.ts +16 -14
- package/src/services/live-surfaces.ts +237 -0
- package/src/services/model-uri.ts +75 -0
- package/src/services/notify.ts +63 -6
- package/src/services/popover-report.ts +325 -0
- package/src/services/surface-registry.ts +56 -0
- package/src/services/tool-executor.ts +6 -0
- package/src/settings/contexts-section.ts +301 -299
- package/src/settings/contributed-section.ts +361 -222
- package/src/settings/css-vars-editor.ts +320 -325
- package/src/settings/defs-editor.ts +572 -535
- package/src/settings/dependencies-editor.ts +79 -107
- package/src/settings/extension-commands.ts +379 -0
- package/src/settings/extension-rows.ts +113 -0
- package/src/settings/extensions-section.ts +287 -0
- package/src/settings/general-settings.ts +130 -181
- package/src/settings/head-editor.ts +10 -305
- package/src/settings/locales-section.ts +146 -196
- package/src/settings/preferences-accounts.ts +1 -1
- package/src/settings/preferences-dialog.ts +350 -330
- package/src/settings/project-sections.ts +108 -181
- package/src/settings/schema-field-ui.ts +11 -399
- package/src/settings/section-registry.ts +8 -1
- package/src/settings/settings-document.ts +31 -29
- package/src/shell/tree.ts +42 -87
- package/src/shell.ts +48 -36
- package/src/studio.ts +104 -69
- package/src/style/token-ref.ts +0 -39
- package/src/surfaces/about.json +263 -0
- package/src/surfaces/about.ts +163 -0
- package/src/surfaces/add-repo.json +486 -0
- package/src/surfaces/add-repo.ts +236 -0
- package/src/surfaces/ai-chat.json +2185 -0
- package/src/surfaces/ai-chat.ts +549 -0
- package/src/surfaces/ai-credentials-form.json +332 -0
- package/src/surfaces/ai-credentials-form.ts +151 -0
- package/src/surfaces/ai-managed-connect.json +108 -0
- package/src/surfaces/ai-managed-connect.ts +123 -0
- package/src/surfaces/ai-model-picker.json +114 -0
- package/src/surfaces/ai-model-picker.ts +139 -0
- package/src/surfaces/block-action-bar.json +640 -0
- package/src/surfaces/block-action-bar.ts +368 -0
- package/src/surfaces/bottom-dock.json +154 -0
- package/src/surfaces/bottom-dock.ts +125 -0
- package/src/surfaces/canvas-stage.json +444 -0
- package/src/surfaces/canvas-stage.ts +253 -0
- package/src/surfaces/cf-account-picker.json +300 -0
- package/src/surfaces/cf-account-picker.ts +214 -0
- package/src/surfaces/commandbar.json +985 -0
- package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +417 -391
- package/src/surfaces/convert-repeater.json +309 -0
- package/src/surfaces/convert-repeater.ts +276 -0
- package/src/surfaces/data-actions.json +157 -0
- package/src/surfaces/data-actions.ts +115 -0
- package/src/surfaces/dialog.json +289 -0
- package/src/surfaces/dialog.ts +343 -0
- package/src/surfaces/diff-toolbar.json +266 -0
- package/src/surfaces/diff-toolbar.ts +139 -0
- package/src/surfaces/doc-header.json +739 -0
- package/src/surfaces/doc-header.ts +223 -0
- package/src/surfaces/empty-state.json +152 -0
- package/src/surfaces/empty-state.ts +173 -0
- package/src/surfaces/entry-editor.json +281 -0
- package/src/surfaces/entry-editor.ts +191 -0
- package/src/surfaces/expression-editor.json +977 -0
- package/src/surfaces/expression-editor.ts +266 -0
- package/src/surfaces/files-panel.json +592 -0
- package/src/surfaces/files-panel.ts +301 -0
- package/src/surfaces/formula-palette.json +364 -0
- package/src/surfaces/formula-palette.ts +340 -0
- package/src/surfaces/git-panel.json +1822 -0
- package/src/surfaces/git-panel.ts +272 -0
- package/src/surfaces/github-auth.json +112 -0
- package/src/surfaces/github-auth.ts +139 -0
- package/src/surfaces/github-publish.json +206 -0
- package/src/surfaces/github-publish.ts +169 -0
- package/src/surfaces/grid-cell.json +218 -0
- package/src/surfaces/grid-cell.ts +120 -0
- package/src/surfaces/grid-open.json +147 -0
- package/src/surfaces/grid-open.ts +87 -0
- package/src/surfaces/grid-panel.json +706 -0
- package/src/surfaces/grid-panel.ts +206 -0
- package/src/surfaces/grid-replace.json +179 -0
- package/src/surfaces/grid-replace.ts +89 -0
- package/src/surfaces/grid-views.json +446 -0
- package/src/surfaces/grid-views.ts +172 -0
- package/src/surfaces/inspector-dock.json +187 -0
- package/src/surfaces/inspector-dock.ts +161 -0
- package/src/surfaces/jump-bar.json +243 -0
- package/src/surfaces/jump-bar.ts +129 -0
- package/src/surfaces/jxsuite-update.json +146 -0
- package/src/surfaces/jxsuite-update.ts +149 -0
- package/src/surfaces/library-pane.json +1509 -0
- package/src/surfaces/library-pane.ts +363 -0
- package/src/surfaces/logic-panel.json +1949 -0
- package/src/surfaces/logic-panel.ts +369 -0
- package/src/surfaces/logic-workspace.json +685 -0
- package/src/surfaces/logic-workspace.ts +238 -0
- package/src/surfaces/media-browser.json +303 -0
- package/src/surfaces/media-browser.ts +145 -0
- package/src/surfaces/media-field.json +163 -0
- package/src/surfaces/media-field.ts +116 -0
- package/src/surfaces/media-pane.json +513 -0
- package/src/surfaces/media-pane.ts +197 -0
- package/src/surfaces/menu.json +232 -0
- package/src/surfaces/menu.ts +375 -0
- package/src/surfaces/navigator-dock.json +113 -0
- package/src/surfaces/navigator-dock.ts +130 -0
- package/src/surfaces/new-project.json +1654 -0
- package/src/surfaces/new-project.ts +411 -0
- package/src/surfaces/palette.json +535 -0
- package/src/surfaces/pane-context.json +1497 -0
- package/src/surfaces/pane-context.ts +513 -0
- package/src/surfaces/pane-grid.json +188 -0
- package/src/surfaces/pane-grid.ts +250 -0
- package/src/surfaces/panel-activity.json +563 -0
- package/src/surfaces/panel-activity.ts +187 -0
- package/src/surfaces/panel-data.json +141 -0
- package/src/surfaces/panel-data.ts +156 -0
- package/src/surfaces/panel-deploy-checklist.json +283 -0
- package/src/surfaces/panel-deploy-checklist.ts +175 -0
- package/src/surfaces/panel-elements.json +367 -0
- package/src/surfaces/panel-elements.ts +175 -0
- package/src/surfaces/panel-i18n.json +569 -0
- package/src/surfaces/panel-i18n.ts +212 -0
- package/src/surfaces/panel-imports.json +780 -0
- package/src/surfaces/panel-imports.ts +209 -0
- package/src/surfaces/panel-outline.json +734 -0
- package/src/surfaces/panel-outline.ts +326 -0
- package/src/surfaces/panel-page.json +1017 -0
- package/src/surfaces/panel-page.ts +240 -0
- package/src/surfaces/panel-problems.json +497 -0
- package/src/surfaces/panel-problems.ts +157 -0
- package/src/surfaces/panel-signals.json +1973 -0
- package/src/surfaces/panel-signals.ts +436 -0
- package/src/surfaces/panel-stylebook-layers.json +317 -0
- package/src/surfaces/panel-stylebook-layers.ts +155 -0
- package/src/surfaces/preferences.json +850 -0
- package/src/surfaces/preferences.ts +332 -0
- package/src/surfaces/progress-modal.json +110 -0
- package/src/surfaces/progress-modal.ts +150 -0
- package/src/surfaces/properties-panel.json +1578 -0
- package/src/surfaces/properties-panel.ts +337 -0
- package/src/surfaces/publish.json +945 -0
- package/src/surfaces/publish.ts +392 -0
- package/src/surfaces/push-plan.json +110 -0
- package/src/surfaces/push-plan.ts +87 -0
- package/src/surfaces/rail.json +255 -0
- package/src/surfaces/rail.ts +206 -0
- package/src/surfaces/reference-field.json +191 -0
- package/src/surfaces/reference-field.ts +168 -0
- package/src/surfaces/schema-builder.json +1011 -0
- package/src/surfaces/schema-builder.ts +181 -0
- package/src/surfaces/schema-form.json +1013 -0
- package/src/surfaces/schema-form.ts +287 -0
- package/src/surfaces/secret-field.json +60 -0
- package/src/surfaces/secret-field.ts +146 -0
- package/src/surfaces/seo.json +932 -0
- package/src/surfaces/seo.ts +334 -0
- package/src/surfaces/settings-contexts.json +538 -0
- package/src/surfaces/settings-contexts.ts +196 -0
- package/src/surfaces/settings-contributed.json +489 -0
- package/src/surfaces/settings-contributed.ts +199 -0
- package/src/surfaces/settings-css-vars.json +820 -0
- package/src/surfaces/settings-css-vars.ts +199 -0
- package/src/surfaces/settings-defs.json +1363 -0
- package/src/surfaces/settings-defs.ts +232 -0
- package/src/surfaces/settings-deploy.json +108 -0
- package/src/surfaces/settings-deploy.ts +135 -0
- package/src/surfaces/settings-extensions.json +486 -0
- package/src/surfaces/settings-extensions.ts +201 -0
- package/src/surfaces/settings-head.json +779 -0
- package/src/surfaces/settings-head.ts +397 -0
- package/src/surfaces/settings-locales.json +420 -0
- package/src/surfaces/settings-locales.ts +137 -0
- package/src/surfaces/settings-overview.json +423 -0
- package/src/surfaces/settings-overview.ts +218 -0
- package/src/surfaces/settings-packages.json +549 -0
- package/src/surfaces/settings-packages.ts +158 -0
- package/src/surfaces/settings-pane.json +147 -0
- package/src/surfaces/settings-pane.ts +150 -0
- package/src/surfaces/settings-rawjson.json +104 -0
- package/src/surfaces/settings-rawjson.ts +107 -0
- package/src/surfaces/shell.json +99 -0
- package/src/surfaces/shell.ts +30 -0
- package/src/surfaces/slash-menu.json +180 -0
- package/src/surfaces/slash-menu.ts +272 -0
- package/src/surfaces/statements.json +607 -0
- package/src/surfaces/statements.ts +238 -0
- package/src/surfaces/statusbar.json +94 -0
- package/src/{panels → surfaces}/statusbar.ts +164 -90
- package/src/surfaces/style-panel.json +1643 -0
- package/src/surfaces/style-panel.ts +338 -0
- package/src/surfaces/stylebook-chrome.json +122 -0
- package/src/surfaces/stylebook-chrome.ts +122 -0
- package/src/surfaces/tab-strip.json +448 -0
- package/src/surfaces/tab-strip.ts +200 -0
- package/src/surfaces/target-line.json +419 -0
- package/src/surfaces/target-line.ts +349 -0
- package/src/surfaces/toasts.json +189 -0
- package/src/surfaces/welcome.json +571 -0
- package/src/surfaces/welcome.ts +338 -0
- package/src/tabs/project-config.ts +111 -21
- package/src/tabs/tab.ts +52 -1
- package/src/types.ts +22 -0
- package/src/ui/ai-credentials-form.ts +104 -100
- package/src/ui/ai-managed-connect.ts +54 -36
- package/src/ui/ai-model-picker.ts +75 -47
- package/src/ui/cf-account-picker.ts +76 -141
- package/src/ui/color-selector.ts +80 -301
- package/src/ui/dynamic-slot.ts +43 -283
- package/src/ui/expression-editor.ts +797 -576
- package/src/ui/field-input.ts +8 -181
- package/src/ui/form-controls.ts +838 -366
- package/src/ui/formula-chips.ts +80 -85
- package/src/ui/kit.ts +33 -0
- package/src/ui/layers.ts +296 -593
- package/src/ui/media-picker.ts +203 -284
- package/src/ui/panel-resize.ts +251 -64
- package/src/ui/popover-surface.ts +153 -0
- package/src/ui/progress-modal.ts +22 -60
- package/src/ui/regions.ts +13 -7
- package/src/ui/schema-form.ts +906 -440
- package/src/ui/surface.ts +122 -0
- package/src/ui/unit-selector.ts +15 -131
- package/src/ui/value-source.ts +14 -0
- package/src/ui/virtual-window.ts +88 -9
- package/src/utils/canvas-media.ts +72 -0
- package/src/utils/edit-display.ts +7 -2
- package/src/utils/element-selectors.ts +69 -0
- package/src/utils/link-target.ts +2 -2
- package/src/utils/studio-utils.ts +0 -10
- package/src/view.ts +15 -0
- package/src/workspace/pane-derive.ts +36 -14
- package/src/workspace/workspace.ts +75 -6
- package/styles/canvas.css +72 -467
- package/styles/forced-colors.css +48 -82
- package/styles/forced-colors.json +41 -0
- package/styles/inspector.css +2 -1301
- package/styles/overlays.css +96 -2823
- package/styles/panels.css +6 -1423
- package/styles/shell-frame.css +272 -0
- package/styles/shell-frame.json +227 -0
- package/styles/shell.css +0 -1568
- package/styles/tokens.css +65 -136
- package/styles/tokens.json +84 -0
- package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
- package/dist/chunks/adapter-jkcyhgtr.js +0 -14
- package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
- package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
- package/dist/chunks/components-zmx10t62.js +0 -21
- package/dist/chunks/content-source-48xg86wj.js +0 -49
- package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
- package/dist/chunks/editor-cptq5jp1.js +0 -37
- package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
- package/dist/chunks/extension-sections-txv08xxg.js +0 -311
- package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
- package/dist/chunks/files-3kfkwhk4.js +0 -93
- package/dist/chunks/flow-qbfgrpeh.js +0 -634
- package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
- package/dist/chunks/focus-visible-6eg70z18.js +0 -167
- package/dist/chunks/format-host-w16q81tf.js +0 -43
- package/dist/chunks/fs-events-hrrg328g.js +0 -21
- package/dist/chunks/git-panel-532whr1y.js +0 -59
- package/dist/chunks/index-zqxmqxmh.js +0 -109
- package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
- package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
- package/dist/chunks/layers-q75tj2mf.js +0 -45
- package/dist/chunks/lit-html-j055mkh8.js +0 -22
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
- package/dist/chunks/notify-yjm79n4s.js +0 -30
- package/dist/chunks/parse-24d0gay3.js +0 -19
- package/dist/chunks/platform-ba1j1gbh.js +0 -16
- package/dist/chunks/publish-panel-v9st96ja.js +0 -29
- package/dist/chunks/quick-search-zccjp6gv.js +0 -66
- package/dist/chunks/references-3qx8fkyp.js +0 -29
- package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
- package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
- package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
- package/dist/chunks/studio-0a5qrh01.js +0 -3
- package/dist/chunks/studio-0bq6eh7v.js +0 -12
- package/dist/chunks/studio-18j3bmcz.js +0 -14
- package/dist/chunks/studio-1ngbejft.js +0 -181
- package/dist/chunks/studio-4b5z1x43.js +0 -424
- package/dist/chunks/studio-4n2b5gzb.js +0 -357
- package/dist/chunks/studio-4rzyrtjz.js +0 -426
- package/dist/chunks/studio-5fx3g9jg.js +0 -4325
- package/dist/chunks/studio-5myknm0a.js +0 -1903
- package/dist/chunks/studio-7afd21nw.js +0 -67
- package/dist/chunks/studio-7nte2hj4.js +0 -613
- package/dist/chunks/studio-8jpzqk5s.js +0 -128
- package/dist/chunks/studio-8q517dfr.js +0 -24
- package/dist/chunks/studio-950k0dx6.js +0 -6939
- package/dist/chunks/studio-9n4mrmnb.js +0 -184
- package/dist/chunks/studio-a0tswy0w.js +0 -58
- package/dist/chunks/studio-ahnzd9rg.js +0 -23
- package/dist/chunks/studio-bgxt93ga.js +0 -62
- package/dist/chunks/studio-cmb03bg5.js +0 -13890
- package/dist/chunks/studio-d1ks410h.js +0 -175
- package/dist/chunks/studio-ef49pnzx.js +0 -387
- package/dist/chunks/studio-eqq1w638.js +0 -113
- package/dist/chunks/studio-ew0djmhg.js +0 -341
- package/dist/chunks/studio-gfgb8pe6.js +0 -1652
- package/dist/chunks/studio-h4rxscfc.js +0 -10
- package/dist/chunks/studio-jc994dnr.js +0 -1066
- package/dist/chunks/studio-kyh9avvv.js +0 -28
- package/dist/chunks/studio-m3hwn7y1.js +0 -120
- package/dist/chunks/studio-mtfnaqs9.js +0 -659
- package/dist/chunks/studio-n6j9t10q.js +0 -318
- package/dist/chunks/studio-n8b9wqwy.js +0 -291
- package/dist/chunks/studio-qcy05dp8.js +0 -1743
- package/dist/chunks/studio-qq6jym04.js +0 -53
- package/dist/chunks/studio-rdx8jcxy.js +0 -83
- package/dist/chunks/studio-rn9ta2mz.js +0 -111
- package/dist/chunks/studio-t7qb9db8.js +0 -665
- package/dist/chunks/studio-tmc5emrw.js +0 -8
- package/dist/chunks/studio-z6nw2ek2.js +0 -18
- package/dist/chunks/studio-zczps70q.js +0 -378
- package/dist/chunks/studio-zqazsct2.js +0 -1823
- package/dist/chunks/workspace-fjx52ryv.js +0 -89
- package/src/panels/activity-bar.ts +0 -251
- package/src/panels/panel-scheduler.ts +0 -219
- package/src/panels/style-inputs.ts +0 -237
- package/src/panels/target-line.ts +0 -274
- package/src/panels/welcome-screen.ts +0 -346
- package/src/ui/button-group.ts +0 -102
- package/src/ui/choice-field.ts +0 -72
- package/src/ui/field-row.ts +0 -249
- package/src/ui/formula-palette.ts +0 -213
- package/src/ui/icons.ts +0 -52
- package/src/ui/jx-theme.ts +0 -130
- package/src/ui/spectrum.ts +0 -340
- package/src/ui/value-selector.ts +0 -174
- package/src/ui/widgets.ts +0 -172
- /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
- /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
- /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
- /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
|
@@ -13,13 +13,36 @@
|
|
|
13
13
|
* - **Media breakpoint DEFINITIONS → Project Settings › Contexts** (P4). Nothing remains here: the
|
|
14
14
|
* only reason adding a breakpoint used to cost you your element selection.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* **This module decides; `surfaces/properties-panel.json` draws.** Every question with an answer —
|
|
17
|
+
* which attributes apply to this tag, which cascade a value came from, which rungs of the value
|
|
18
|
+
* ladder the position permits, what a keystroke is worth, and what a commit writes — is answered
|
|
19
|
+
* here and handed over as sections whose every row is already decided. The document holds the
|
|
20
|
+
* markup, the ARIA and the style, and holds no idea what an attribute is.
|
|
21
|
+
*
|
|
22
|
+
* **It does not go through the panel scheduler, and that is the point.**
|
|
23
|
+
* `panels/panel-scheduler.ts` withholds a repaint while a text input inside the dock has focus,
|
|
24
|
+
* because a lit repaint rebuilds the field the reader is typing into and takes the caret with it. A
|
|
25
|
+
* document has one effect per bound property and skips a write equal to what is already there, so
|
|
26
|
+
* the Content tab is updated in place: the rows that changed change, the field being typed in is
|
|
27
|
+
* untouched, and there is nothing to guard against. What survives is the DRAFT layer
|
|
28
|
+
* (`ui/field-input.ts`), which is a different promise — it is what keeps a half-typed value from
|
|
29
|
+
* being replaced by the last committed one, and what keys that half-typed value to the NODE rather
|
|
30
|
+
* than to the field's name (§11.4).
|
|
31
|
+
*
|
|
32
|
+
* **Two leaves are drawn elsewhere**, and each is its own surface with readers outside this tab:
|
|
33
|
+
* the media picker and the expression editor. The document draws an empty `[part="control-host"]`
|
|
34
|
+
* for them and this module fills it, which is the seam `ui/schema-form.ts` established for an
|
|
35
|
+
* extension's registered control. The expression editor is lit and is RENDERED into the box; the
|
|
36
|
+
* media picker is a document and is MOUNTED into it, so a `control` takes the host instead of
|
|
37
|
+
* returning a template — a document cannot be handed back as a value, and it clears the host it is
|
|
38
|
+
* given. There were three: a `format: "color"` prop was the middle one, and it is a
|
|
39
|
+
* `jx-color-field` in the document now that `ui.md` §5.6 has landed.
|
|
40
|
+
*
|
|
41
|
+
* @docs studio/design/properties
|
|
17
42
|
*/
|
|
18
43
|
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import { live } from "lit-html/directives/live.js";
|
|
22
|
-
import { debouncedStyleCommit, getNodeAtPath, renderOnly } from "../store";
|
|
44
|
+
import { getNodeAtPath } from "../store";
|
|
45
|
+
import { effect, effectScope, reactive } from "../reactivity";
|
|
23
46
|
import { displayTagName, isRef, isTagExpression } from "@jxsuite/schema/guards";
|
|
24
47
|
import type { DirEntry, JsonValue } from "../types";
|
|
25
48
|
import {
|
|
@@ -44,11 +67,34 @@ import type { LayoutSelection } from "../shell";
|
|
|
44
67
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
45
68
|
import { activeRegistry } from "../commands/active-registry";
|
|
46
69
|
import { componentRegistry } from "../files/components";
|
|
47
|
-
import {
|
|
48
|
-
import { renderFieldRow, renderKvRow } from "../ui/field-row";
|
|
70
|
+
import { provenanceText, provenanceTitle } from "./provenance";
|
|
49
71
|
import type { FieldProvenance } from "./provenance";
|
|
50
|
-
import {
|
|
51
|
-
|
|
72
|
+
import {
|
|
73
|
+
effectiveSlotMode,
|
|
74
|
+
hasStashedSlotValue,
|
|
75
|
+
slotModeSeed,
|
|
76
|
+
stashSlotValue,
|
|
77
|
+
switchSlotMode,
|
|
78
|
+
} from "../ui/dynamic-slot";
|
|
79
|
+
import {
|
|
80
|
+
TAG_EXPRESSION_OPERATORS,
|
|
81
|
+
VALUE_SOURCE_LABELS,
|
|
82
|
+
slotCaps,
|
|
83
|
+
slotMode,
|
|
84
|
+
} from "../ui/value-source";
|
|
85
|
+
import type { SlotCapsSource, SlotMode } from "../ui/value-source";
|
|
86
|
+
import {
|
|
87
|
+
clearDraft,
|
|
88
|
+
commitField,
|
|
89
|
+
getFieldValue,
|
|
90
|
+
hasDraft,
|
|
91
|
+
scheduleDraftCommit,
|
|
92
|
+
setDraft,
|
|
93
|
+
} from "../ui/field-input";
|
|
94
|
+
import { INPUT_DEBOUNCE, LIVE_PREVIEW } from "../ui/timing";
|
|
95
|
+
import { cloneValue } from "../tabs/doc-op-apply";
|
|
96
|
+
import { openMenu } from "../surfaces/menu";
|
|
97
|
+
import type { MenuHandle } from "../surfaces/menu";
|
|
52
98
|
import {
|
|
53
99
|
attrLabel,
|
|
54
100
|
camelToLabel,
|
|
@@ -58,14 +104,10 @@ import {
|
|
|
58
104
|
} from "../utils/studio-utils";
|
|
59
105
|
import { classifyHref, composeHref } from "../utils/link-target";
|
|
60
106
|
import type { LinkKind } from "../utils/link-target";
|
|
61
|
-
import {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
staleSelectionMessage,
|
|
66
|
-
} from "./empty-state";
|
|
67
|
-
import { renderMediaPicker } from "../ui/media-picker";
|
|
68
|
-
import { renderColorSelector } from "../ui/color-selector";
|
|
107
|
+
import { clickAnythingTo, openPageAction, staleSelectionMessage } from "./empty-state";
|
|
108
|
+
import { mountMediaPicker, unmountMediaPicker } from "../ui/media-picker";
|
|
109
|
+
import { colorTokens, resolvedColor } from "../ui/color-selector";
|
|
110
|
+
import { mountExpressionEditor } from "../ui/expression-editor";
|
|
69
111
|
import {
|
|
70
112
|
loadUsages,
|
|
71
113
|
peekUsages,
|
|
@@ -75,9 +117,22 @@ import {
|
|
|
75
117
|
} from "../services/references";
|
|
76
118
|
import { getPlatform } from "../platform";
|
|
77
119
|
import htmlMeta from "../../data/html-meta.json";
|
|
120
|
+
import { popoverIdsIn } from "@jxsuite/schema/overlays";
|
|
121
|
+
import { POPOVER_TAGS } from "@jxsuite/ui";
|
|
122
|
+
import { showPromptDialog } from "../ui/layers";
|
|
123
|
+
import { emptyContentView, mountContentSurface } from "../surfaces/properties-panel";
|
|
78
124
|
|
|
125
|
+
import type {
|
|
126
|
+
ContentOption,
|
|
127
|
+
ContentPanelActions,
|
|
128
|
+
ContentPanelSurface,
|
|
129
|
+
ContentPanelView,
|
|
130
|
+
ContentRowView,
|
|
131
|
+
ContentSectionView,
|
|
132
|
+
} from "../surfaces/properties-panel";
|
|
79
133
|
import type {
|
|
80
134
|
JxAttributeValue,
|
|
135
|
+
JxElement,
|
|
81
136
|
JxMutableNode,
|
|
82
137
|
JxPrototypeDef,
|
|
83
138
|
JxStateDefinition,
|
|
@@ -85,6 +140,13 @@ import type {
|
|
|
85
140
|
} from "@jxsuite/schema/types";
|
|
86
141
|
import type { SignalOption } from "../ui/dynamic-slot";
|
|
87
142
|
import type { JxPath } from "../state";
|
|
143
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
144
|
+
|
|
145
|
+
/** The picker's escape hatch: a popover declared in another file, typed by hand. */
|
|
146
|
+
const POPOVER_TARGET_OTHER = "jx:other";
|
|
147
|
+
|
|
148
|
+
/** The empty row a picker offers so a value can be taken away with the same control that set it. */
|
|
149
|
+
const CLEAR_OPTION: ContentOption = { label: "—", value: "" };
|
|
88
150
|
|
|
89
151
|
interface HtmlMetaEntry {
|
|
90
152
|
$section: string;
|
|
@@ -159,8 +221,8 @@ function bindingDonor(value: unknown): string {
|
|
|
159
221
|
* The state entry a bound value reads, when the chip can name one — a `$ref` or a `${…}` template.
|
|
160
222
|
*
|
|
161
223
|
* §6.2's table says a bound chip "click opens the source", and on the Content tab it did not: both
|
|
162
|
-
* bound branches returned a `donor` and a `title` and no `onClick`, so
|
|
163
|
-
*
|
|
224
|
+
* bound branches returned a `donor` and a `title` and no `onClick`, so the chip was drawn as a
|
|
225
|
+
* handler-less span. The Style tab's chip has jumped to the Data panel since P5; the same promise
|
|
164
226
|
* on the other tab was a label. Same regex as `style-panel.ts`'s `templateSignalOf`, because both
|
|
165
227
|
* spellings of a template (`${x}` and `${state.x}`) name the same entry.
|
|
166
228
|
*/
|
|
@@ -208,172 +270,6 @@ function boundProvenance(value: unknown, title: string): FieldProvenance {
|
|
|
208
270
|
};
|
|
209
271
|
}
|
|
210
272
|
|
|
211
|
-
// ─── Sub-templates ──────────────────────────────────────────────────────────
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Component props, each row carrying §6.2's provenance chip — the SECOND cascade.
|
|
215
|
-
*
|
|
216
|
-
* A component instance's value for a prop either overrides the component's declared default or does
|
|
217
|
-
* not exist, and until now the two were the same blank field. The chip states which, and in the
|
|
218
|
-
* inherited case it names the donor AND jumps to it: the donor of a component default is the
|
|
219
|
-
* component definition, which this panel can already open.
|
|
220
|
-
*/
|
|
221
|
-
function renderComponentPropsFieldsTemplate(
|
|
222
|
-
node: JxMutableNode,
|
|
223
|
-
path: JxPath,
|
|
224
|
-
mapSignals: SignalOption[] | null,
|
|
225
|
-
navigateToComponent: (path: string) => void,
|
|
226
|
-
) {
|
|
227
|
-
const tab = activeTab.value;
|
|
228
|
-
const comp = componentRegistry.find((c) => c.tagName === displayTagName(node.tagName));
|
|
229
|
-
const definitionOfThis = isComponentDefinitionOpen(node);
|
|
230
|
-
if (!comp || !comp.props) {
|
|
231
|
-
return renderEmptyState({
|
|
232
|
-
compact: true,
|
|
233
|
-
message: "This component is not in the project's library, so it has no settings to show.",
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
const isNpm = comp.source === "npm";
|
|
237
|
-
/* In the definition the "value" of a setting is its DECLARED DEFAULT — the state entry's
|
|
238
|
-
`default` — not a per-instance override, so the row reads and writes there instead. */
|
|
239
|
-
const stateEntries = (tab?.doc.document.state ?? {}) as Record<string, { default?: JsonValue }>;
|
|
240
|
-
const currentVals: Record<string, unknown> = definitionOfThis
|
|
241
|
-
? Object.fromEntries(comp.props.map((prop) => [prop.name, stateEntries[prop.name]?.default]))
|
|
242
|
-
: isNpm
|
|
243
|
-
? node.attributes || {}
|
|
244
|
-
: node.$props || {};
|
|
245
|
-
const updateFn = definitionOfThis
|
|
246
|
-
? (name: string, v?: JsonValue) =>
|
|
247
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: v }))
|
|
248
|
-
: isNpm
|
|
249
|
-
? (name: string, v?: JsonValue) =>
|
|
250
|
-
transactDoc(activeTab.value, (t) =>
|
|
251
|
-
mutateUpdateAttribute(
|
|
252
|
-
t,
|
|
253
|
-
path,
|
|
254
|
-
name,
|
|
255
|
-
v === "" ? undefined : (v as JxAttributeValue | undefined),
|
|
256
|
-
),
|
|
257
|
-
)
|
|
258
|
-
: (name: string, v?: JsonValue) =>
|
|
259
|
-
transactDoc(activeTab.value, (t) => mutateUpdateProp(t, path, name, v));
|
|
260
|
-
|
|
261
|
-
const defs = tab!.doc.document.state || {};
|
|
262
|
-
const signalNames = bindableSignalNames(tab!.doc.document);
|
|
263
|
-
const extraSignals = mapSignals;
|
|
264
|
-
|
|
265
|
-
return html`
|
|
266
|
-
${comp.props.map((prop) => {
|
|
267
|
-
const rawValue = currentVals[prop.name];
|
|
268
|
-
const boundRef = isRef(rawValue) ? rawValue.$ref : null;
|
|
269
|
-
const hasVal = rawValue !== undefined && rawValue !== null;
|
|
270
|
-
const parsed = parseCemType(prop.type);
|
|
271
|
-
const onChange = (v?: JsonValue) => updateFn(prop.name, v);
|
|
272
|
-
const staticVal = slotMode(rawValue) === "literal" ? String(rawValue ?? "") : "";
|
|
273
|
-
|
|
274
|
-
// De-escalating to literal restores the bound signal's declared default (old unbind behavior).
|
|
275
|
-
const literalDefault = boundRef
|
|
276
|
-
? defaultAsString(defs[boundRef.startsWith("#/state/") ? boundRef.slice(8) : boundRef]) ||
|
|
277
|
-
undefined
|
|
278
|
-
: undefined;
|
|
279
|
-
|
|
280
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
281
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
282
|
-
let widgetTpl;
|
|
283
|
-
if (isMediaFormat(prop.format)) {
|
|
284
|
-
widgetTpl = renderMediaPicker(prop.name, staticVal, onChange);
|
|
285
|
-
} else if (prop.format === "color") {
|
|
286
|
-
widgetTpl = renderColorSelector(prop.name, staticVal, onChange);
|
|
287
|
-
} else if (prop.format === "date") {
|
|
288
|
-
widgetTpl = spTextField(
|
|
289
|
-
`cprop:${prop.name}`,
|
|
290
|
-
String(staticVal),
|
|
291
|
-
(v: string) => onChange(v),
|
|
292
|
-
{
|
|
293
|
-
placeholder: "YYYY-MM-DD",
|
|
294
|
-
},
|
|
295
|
-
);
|
|
296
|
-
} else if (parsed.kind === "boolean") {
|
|
297
|
-
widgetTpl = html`<sp-checkbox
|
|
298
|
-
size="s"
|
|
299
|
-
.checked=${live(Boolean(staticVal))}
|
|
300
|
-
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked || undefined)}
|
|
301
|
-
></sp-checkbox>`;
|
|
302
|
-
} else if (parsed.kind === "number") {
|
|
303
|
-
widgetTpl = html`<sp-number-field
|
|
304
|
-
size="s"
|
|
305
|
-
.value=${live(staticVal)}
|
|
306
|
-
@input=${(e: Event) => {
|
|
307
|
-
clearTimeout(debounce);
|
|
308
|
-
debounce = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
|
|
309
|
-
}}
|
|
310
|
-
></sp-number-field>`;
|
|
311
|
-
} else if (parsed.kind === "combobox") {
|
|
312
|
-
const options = parsed.options as string[];
|
|
313
|
-
widgetTpl = html`<jx-value-selector
|
|
314
|
-
.value=${String(staticVal)}
|
|
315
|
-
size="s"
|
|
316
|
-
placeholder="—"
|
|
317
|
-
.options=${options.map((o) => ({ label: camelToLabel(o), value: o }))}
|
|
318
|
-
@change=${(e: Event & { detail?: { value?: string } }) =>
|
|
319
|
-
onChange(e.detail?.value ?? (e.target as HTMLInputElement).value)}
|
|
320
|
-
></jx-value-selector>`;
|
|
321
|
-
} else {
|
|
322
|
-
widgetTpl = spTextField(`cprop:${prop.name}`, String(staticVal), (v: string) =>
|
|
323
|
-
onChange(v),
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const slot = renderDynamicSlot({
|
|
328
|
-
caps: "componentProp",
|
|
329
|
-
extraSignals,
|
|
330
|
-
fieldKey: `cprop|${path.join("/")}|${prop.name}`,
|
|
331
|
-
literalDefault,
|
|
332
|
-
onChange,
|
|
333
|
-
staticWidget: widgetTpl,
|
|
334
|
-
stateDefs: signalNames,
|
|
335
|
-
value: rawValue,
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
return renderFieldRow({
|
|
339
|
-
hasValue: hasVal,
|
|
340
|
-
label: camelToLabel(prop.name),
|
|
341
|
-
labelExtra: slot.modeButton,
|
|
342
|
-
prop: prop.name,
|
|
343
|
-
provenance: componentPropProvenance(prop, rawValue, hasVal, {
|
|
344
|
-
// No donor and no jump in the definition: the value IS the component's default, and
|
|
345
|
-
// "→ the component definition" would open the document already in front of you.
|
|
346
|
-
isDefinition: definitionOfThis,
|
|
347
|
-
onClear: () => updateFn(prop.name),
|
|
348
|
-
...(definitionOfThis || !comp.path
|
|
349
|
-
? {}
|
|
350
|
-
: { openDefinition: () => navigateToComponent(comp.path!) }),
|
|
351
|
-
}),
|
|
352
|
-
widget: slot.widget,
|
|
353
|
-
});
|
|
354
|
-
})}
|
|
355
|
-
${
|
|
356
|
-
comp.props.length === 0
|
|
357
|
-
? renderEmptyState({
|
|
358
|
-
compact: true,
|
|
359
|
-
message: definitionOfThis
|
|
360
|
-
? "This component declares no settings yet. Add a state entry with a default to " +
|
|
361
|
-
"give whoever places it something to fill in."
|
|
362
|
-
: "This component has no settings to fill in yet.",
|
|
363
|
-
})
|
|
364
|
-
: nothing
|
|
365
|
-
}
|
|
366
|
-
${
|
|
367
|
-
// A link to the definition, from inside the definition, is a link to here.
|
|
368
|
-
comp.path && !definitionOfThis
|
|
369
|
-
? html`<span class="kv-add" @click=${() => navigateToComponent(comp.path!)}
|
|
370
|
-
>→ Edit definition</span
|
|
371
|
-
>`
|
|
372
|
-
: nothing
|
|
373
|
-
}
|
|
374
|
-
`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
273
|
/**
|
|
378
274
|
* Is the selected node an INSTANCE of a component, or the definition of one?
|
|
379
275
|
*
|
|
@@ -426,51 +322,65 @@ function componentPropProvenance(
|
|
|
426
322
|
return { state: "default" };
|
|
427
323
|
}
|
|
428
324
|
|
|
429
|
-
/**
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
325
|
+
/** Which of §6.2's states an HTML-attribute row is in. Attributes have no third cascade layer. */
|
|
326
|
+
function attributeProvenance(
|
|
327
|
+
attr: string,
|
|
328
|
+
value: unknown,
|
|
329
|
+
hasVal: boolean,
|
|
330
|
+
onClear: () => void,
|
|
331
|
+
mixedCount = 0,
|
|
332
|
+
): FieldProvenance {
|
|
333
|
+
// Mixed first, for the same reason the Style tab decides it first: it is a fact about the
|
|
334
|
+
// SELECTION, and the primary's own value says nothing about whether the others agree. A count of
|
|
335
|
+
// 0 (a selection of one) can never reach this branch, so single-selection rows are untouched.
|
|
336
|
+
if (mixedCount > 0) {
|
|
337
|
+
return { donor: String(mixedCount), onClick: onClear, state: "mixed" };
|
|
338
|
+
}
|
|
339
|
+
if (hasVal && (isRef(value) || (typeof value === "string" && value.includes("${")))) {
|
|
340
|
+
return boundProvenance(value, `Bound — ${attr}`);
|
|
341
|
+
}
|
|
342
|
+
return hasVal ? { onClick: onClear, state: "set", title: `Clear ${attr}` } : { state: "default" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* How many selected elements disagree about `key` on `read` — 0 when they agree or there is one.
|
|
347
|
+
*
|
|
348
|
+
* The Content tab's two cascades (HTML attributes and component props) both need the same answer
|
|
349
|
+
* about the same set, so the question is asked once here and answered with `unifyValues`, the same
|
|
350
|
+
* comparison the Style tab uses.
|
|
351
|
+
*/
|
|
352
|
+
export function mixedAcrossSelection(
|
|
353
|
+
doc: JxMutableNode,
|
|
354
|
+
targets: readonly JxPath[],
|
|
355
|
+
read: (node: JxMutableNode | undefined) => unknown,
|
|
356
|
+
): number {
|
|
357
|
+
if (targets.length < 2) {
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
const values = targets.map((path) => read(getNodeAtPath(doc, path) as JxMutableNode | undefined));
|
|
361
|
+
return unifyValues(values).mixed ? targets.length : 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* True when an attribute value is a binding (a `$ref` object or a template string containing
|
|
366
|
+
* `${…}`), so the Link-target special-case must fall back to the raw widget to keep it editable.
|
|
367
|
+
*/
|
|
368
|
+
function isBoundAttrValue(value: unknown): boolean {
|
|
369
|
+
return isRef(value) || (typeof value === "string" && value.includes("${"));
|
|
460
370
|
}
|
|
461
371
|
|
|
462
372
|
// ─── Page-route enumeration (for the Link-target Internal picker) ─────────────
|
|
463
373
|
|
|
464
|
-
/**
|
|
374
|
+
/** Cached list of internal routes derived from the pages/ tree; `null` until one is asked for. */
|
|
465
375
|
let pageRouteEntries: string[] | null = null;
|
|
466
376
|
|
|
377
|
+
/** Whether a walk is already in flight, so a re-projection does not start a second one. */
|
|
378
|
+
let pageRoutesLoading = false;
|
|
379
|
+
|
|
467
380
|
/**
|
|
468
381
|
* Derive a site route from a page file path relative to `pages/`, following the file-based routing
|
|
469
382
|
* convention: `index.json` → the directory route, `[slug].json` → `:slug`, all others drop their
|
|
470
383
|
* extension. Directory routes get a trailing slash (`/about/`); the root is `/`.
|
|
471
|
-
*
|
|
472
|
-
* @param {string} relPath — path relative to `pages/`, forward-slashed (e.g. "blog/[slug].json").
|
|
473
|
-
* @returns {string}
|
|
474
384
|
*/
|
|
475
385
|
function routeForPagePath(relPath: string): string {
|
|
476
386
|
const withoutExt = relPath.replace(/\.[^./]+$/, "");
|
|
@@ -489,363 +399,541 @@ function routeForPagePath(relPath: string): string {
|
|
|
489
399
|
return isIndex || body.includes(":") ? `/${body}${isIndex ? "/" : ""}` : `/${body}/`;
|
|
490
400
|
}
|
|
491
401
|
|
|
492
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* Recursively walk the pages/ tree and populate {@link pageRouteEntries} with derived routes.
|
|
404
|
+
*
|
|
405
|
+
* The in-flight flag is released in a `finally` rather than after the walk: the projection starts
|
|
406
|
+
* this whenever the cache is empty, so a host with no platform registered — which throws on the
|
|
407
|
+
* first line — would otherwise leave the flag raised and the routes unenumerable for the life of
|
|
408
|
+
* the window.
|
|
409
|
+
*/
|
|
493
410
|
async function loadPageRouteEntries() {
|
|
494
|
-
|
|
411
|
+
pageRoutesLoading = true;
|
|
495
412
|
const routes: string[] = [];
|
|
496
413
|
const docExts = new Set([".json", ".md", ".html"]);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
listing
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
506
|
-
if (entry.type === "directory") {
|
|
507
|
-
await walk(entry.path ?? `${dir}/${entry.name}`, childRel);
|
|
508
|
-
} else if (docExts.has(entry.name.slice(entry.name.lastIndexOf(".")))) {
|
|
509
|
-
routes.push(routeForPagePath(childRel));
|
|
414
|
+
try {
|
|
415
|
+
const platform = getPlatform();
|
|
416
|
+
const walk = async (dir: string, rel: string): Promise<void> => {
|
|
417
|
+
let listing: DirEntry[];
|
|
418
|
+
try {
|
|
419
|
+
listing = await platform.listDirectory(dir);
|
|
420
|
+
} catch {
|
|
421
|
+
return;
|
|
510
422
|
}
|
|
511
|
-
|
|
423
|
+
for (const entry of listing) {
|
|
424
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
425
|
+
if (entry.type === "directory") {
|
|
426
|
+
await walk(entry.path ?? `${dir}/${entry.name}`, childRel);
|
|
427
|
+
} else if (docExts.has(entry.name.slice(entry.name.lastIndexOf(".")))) {
|
|
428
|
+
routes.push(routeForPagePath(childRel));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
await walk("pages", "");
|
|
433
|
+
pageRouteEntries = [...new Set(routes)].toSorted((a, b) => a.localeCompare(b));
|
|
434
|
+
} finally {
|
|
435
|
+
pageRoutesLoading = false;
|
|
512
436
|
}
|
|
513
|
-
|
|
514
|
-
pageRouteEntries = [...new Set(routes)].toSorted((a, b) => a.localeCompare(b));
|
|
515
|
-
renderOnly("rightPanel");
|
|
437
|
+
renderPropertiesPanel();
|
|
516
438
|
}
|
|
517
439
|
|
|
518
440
|
export function invalidatePageRouteCache() {
|
|
519
441
|
pageRouteEntries = null;
|
|
442
|
+
renderPropertiesPanel();
|
|
520
443
|
}
|
|
521
444
|
|
|
445
|
+
// ─── Section state ────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
522
447
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
448
|
+
* The Inspector's fixed section keys — the accordion rows the inspector always draws.
|
|
449
|
+
*
|
|
450
|
+
* Attribute-schema sections add their own keys at render time (a class's `$section`), so this is a
|
|
451
|
+
* floor, not a closed set: {@link inspectorSectionKeys} unions it with whatever the OPEN DOCUMENT
|
|
452
|
+
* declares, and that union is what `inspector.setSection` validates against. A fixed enum would
|
|
453
|
+
* refuse a section the user is looking at; no validation at all would accept the label the old
|
|
454
|
+
* `inspector.toggleSection` step passed ("Element", not `__element`) and silently do nothing.
|
|
526
455
|
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
456
|
+
* `__media` left with the breakpoint definitions, which are Project Settings › Contexts now (P4).
|
|
457
|
+
* `__observed`, `__cssprops` and `__cssparts` are drawn by the Logic tab; the key space is the
|
|
458
|
+
* INSPECTOR's, not one tab's, so `inspector.setSection` keeps addressing all of them.
|
|
529
459
|
*/
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
460
|
+
export const INSPECTOR_SECTION_KEYS = [
|
|
461
|
+
"__element",
|
|
462
|
+
"__observed",
|
|
463
|
+
"__usages",
|
|
464
|
+
"__custom",
|
|
465
|
+
"__cssprops",
|
|
466
|
+
"__cssparts",
|
|
467
|
+
] as const;
|
|
533
468
|
|
|
534
|
-
|
|
535
|
-
const composed = composeHref(nextKind, nextValue);
|
|
536
|
-
transactDoc(activeTab.value!, (t) =>
|
|
537
|
-
mutateUpdateAttribute(t, path, "href", composed || undefined),
|
|
538
|
-
);
|
|
539
|
-
};
|
|
469
|
+
export type InspectorSectionKey = (typeof INSPECTOR_SECTION_KEYS)[number];
|
|
540
470
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
{ label: "External URL", value: "external" },
|
|
544
|
-
{ label: "Anchor", value: "anchor" },
|
|
545
|
-
{ label: "Email", value: "mailto" },
|
|
546
|
-
{ label: "Phone", value: "tel" },
|
|
547
|
-
];
|
|
471
|
+
/** The Component Settings section's key — recorded rather than fixed, like an attribute section. */
|
|
472
|
+
const PROPS_SECTION = "__props";
|
|
548
473
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
@change=${(e: Event) => {
|
|
555
|
-
const nextKind = (e.target as HTMLInputElement).value as LinkKind;
|
|
556
|
-
// Switching kind reinterprets the current value under the new kind.
|
|
557
|
-
commit(nextKind, value);
|
|
558
|
-
}}
|
|
559
|
-
>
|
|
560
|
-
${kindOptions.map((o) => html`<sp-menu-item value=${o.value}>${o.label}</sp-menu-item>`)}
|
|
561
|
-
</sp-picker>
|
|
562
|
-
`;
|
|
563
|
-
|
|
564
|
-
let valueInput;
|
|
565
|
-
if (kind === "internal") {
|
|
566
|
-
if (pageRouteEntries === null) {
|
|
567
|
-
void loadPageRouteEntries();
|
|
568
|
-
}
|
|
569
|
-
const routes = pageRouteEntries ?? [];
|
|
570
|
-
const knownValue = value !== "" && !routes.includes(value);
|
|
571
|
-
valueInput = html`
|
|
572
|
-
<sp-picker
|
|
573
|
-
class="link-target-value"
|
|
574
|
-
size="s"
|
|
575
|
-
.value=${live(value)}
|
|
576
|
-
@change=${(e: Event) => commit("internal", (e.target as HTMLInputElement).value)}
|
|
577
|
-
>
|
|
578
|
-
${knownValue ? html`<sp-menu-item value=${value}>${value}</sp-menu-item>` : nothing}
|
|
579
|
-
${routes.map((r) => html`<sp-menu-item value=${r}>${r}</sp-menu-item>`)}
|
|
580
|
-
</sp-picker>
|
|
581
|
-
`;
|
|
582
|
-
} else {
|
|
583
|
-
const placeholder =
|
|
584
|
-
kind === "mailto"
|
|
585
|
-
? "name@example.com"
|
|
586
|
-
: kind === "tel"
|
|
587
|
-
? "+15551234567"
|
|
588
|
-
: kind === "anchor"
|
|
589
|
-
? "section-id"
|
|
590
|
-
: "https://example.com";
|
|
591
|
-
valueInput = html`
|
|
592
|
-
<sp-textfield
|
|
593
|
-
class="link-target-value"
|
|
594
|
-
size="s"
|
|
595
|
-
placeholder=${placeholder}
|
|
596
|
-
.value=${live(value)}
|
|
597
|
-
@input=${debouncedStyleCommit("link:href", 400, (e: Event) =>
|
|
598
|
-
commit(kind, (e.target as HTMLInputElement).value),
|
|
599
|
-
)}
|
|
600
|
-
></sp-textfield>
|
|
601
|
-
`;
|
|
602
|
-
}
|
|
474
|
+
/**
|
|
475
|
+
* The Usage section's key, in the same per-tab `inspectorSections` record every other section uses,
|
|
476
|
+
* so `inspector.setSection` addresses it exactly like the rest and nothing needs a second store.
|
|
477
|
+
*/
|
|
478
|
+
const USAGES_SECTION = "__usages";
|
|
603
479
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
prop: "href",
|
|
609
|
-
widget: html`<div class="link-target-field">${kindSelector}${valueInput}</div>`,
|
|
610
|
-
});
|
|
480
|
+
/** Section keys addressable right now: the fixed rows plus any already recorded for this tab. */
|
|
481
|
+
export function inspectorSectionKeys(): string[] {
|
|
482
|
+
const recorded = Object.keys(activeTab.value?.session.ui.inspectorSections ?? {});
|
|
483
|
+
return [...new Set<string>([...INSPECTOR_SECTION_KEYS, ...recorded])];
|
|
611
484
|
}
|
|
612
485
|
|
|
613
486
|
/**
|
|
614
|
-
*
|
|
615
|
-
* jx-value-selector so the four browsing-context keywords are offered as a dropdown.
|
|
487
|
+
* Whether one Inspector section is expanded, falling back to the caller's default.
|
|
616
488
|
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
* @param {HtmlMetaEntry} entry
|
|
489
|
+
* The fallback is the caller's because it is not a constant: an attribute section opens itself when
|
|
490
|
+
* one of its attributes is set, and Usage never does. Only the panel drawing the row knows.
|
|
620
491
|
*/
|
|
621
|
-
function
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
return renderFieldRow({
|
|
625
|
-
hasValue: current !== "",
|
|
626
|
-
label: attrLabel(entry, "target"),
|
|
627
|
-
onClear: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "target")),
|
|
628
|
-
prop: "target",
|
|
629
|
-
widget: html`
|
|
630
|
-
<sp-picker
|
|
631
|
-
class="link-target-window"
|
|
632
|
-
size="s"
|
|
633
|
-
.value=${live(current)}
|
|
634
|
-
@change=${(e: Event) =>
|
|
635
|
-
transactDoc(activeTab.value!, (t) =>
|
|
636
|
-
mutateUpdateAttribute(
|
|
637
|
-
t,
|
|
638
|
-
path,
|
|
639
|
-
"target",
|
|
640
|
-
(e.target as HTMLInputElement).value || undefined,
|
|
641
|
-
),
|
|
642
|
-
)}
|
|
643
|
-
>
|
|
644
|
-
${options.map((o) => html`<sp-menu-item value=${o}>${o}</sp-menu-item>`)}
|
|
645
|
-
</sp-picker>
|
|
646
|
-
`,
|
|
647
|
-
});
|
|
492
|
+
export function isInspectorSectionOpen(section: string, fallback: boolean): boolean {
|
|
493
|
+
const recorded = activeTab.value?.session.ui.inspectorSections?.[section];
|
|
494
|
+
return recorded === undefined ? fallback : recorded;
|
|
648
495
|
}
|
|
649
496
|
|
|
650
497
|
/**
|
|
651
|
-
*
|
|
652
|
-
*
|
|
498
|
+
* Open or close one Inspector section, per tab.
|
|
499
|
+
*
|
|
500
|
+
* The pair `{ section, open }` is the whole point: `inspector.toggleSection` named a DELTA against
|
|
501
|
+
* a section whose state depended on the selected node's own attributes (`autoOpen`), so the same
|
|
502
|
+
* three manifest steps opened a section on one document and closed it on the next. Writing a new
|
|
503
|
+
* object rather than mutating in place is what the reactive read in the projection depends on.
|
|
653
504
|
*/
|
|
654
|
-
function
|
|
655
|
-
|
|
505
|
+
export function setInspectorSection(section: string, open: boolean): void {
|
|
506
|
+
const tab = activeTab.value;
|
|
507
|
+
if (!tab) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
tab.session.ui.inspectorSections = { ...tab.session.ui.inspectorSections, [section]: open };
|
|
656
511
|
}
|
|
657
512
|
|
|
658
|
-
// ───
|
|
513
|
+
// ─── Row plans: how this module reads a gesture back ──────────────────────────
|
|
659
514
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
* the comment denied it.
|
|
681
|
-
*
|
|
682
|
-
* The companion resolves `shell.layoutSelection.layoutFile` in preference to the page's own
|
|
683
|
-
* `$layout` for the same reason the old code did: they are different answers for a nested chain,
|
|
684
|
-
* and the one the author clicked is the one they meant.
|
|
685
|
-
* 4. **`renderOnly("rightPanel")`.** `panels/right-panel.ts:106` tracks `shell.layoutSelection`
|
|
686
|
-
* itself and `:212` reads it, so the repaint was already subscribed — a second, imperative
|
|
687
|
-
* subscription that only looked necessary because the function above had just cleared the state
|
|
688
|
-
* the real one is keyed on.
|
|
689
|
-
*/
|
|
515
|
+
/** The ladder as one row holds it, so a rung change needs nothing re-derived. */
|
|
516
|
+
interface LadderPlan {
|
|
517
|
+
fieldKey: string;
|
|
518
|
+
mode: SlotMode;
|
|
519
|
+
offered: SlotMode[];
|
|
520
|
+
/** Every pointer the From data… rung offers, state entries and repeater signals alike. */
|
|
521
|
+
sources: SignalOption[];
|
|
522
|
+
/**
|
|
523
|
+
* The state entries alone, kept apart from {@link sources} because `slotModeSeed` reads them
|
|
524
|
+
* differently: a fresh Mixed text rung seeds `${state.<first>}` from a state entry and `${}` from
|
|
525
|
+
* anything else, so folding the two lists into one is how a bound prop dropped to an empty
|
|
526
|
+
* template instead of naming the signal it had just left.
|
|
527
|
+
*/
|
|
528
|
+
stateDefs: string[];
|
|
529
|
+
extraSignals: SignalOption[];
|
|
530
|
+
value: JsonValue | undefined;
|
|
531
|
+
onChange: (v?: JsonValue) => void;
|
|
532
|
+
literalDefault: JsonValue | undefined;
|
|
533
|
+
seedFor: ((mode: SlotMode) => JsonValue | undefined) | undefined;
|
|
534
|
+
}
|
|
690
535
|
|
|
691
536
|
/**
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* It spent a release cycle unreachable: `shell.layoutSelection` had this reader and no writer, so
|
|
695
|
-
* clicking the site name in the header — the first click a new user makes — did nothing at all. The
|
|
696
|
-
* canvas hit test writes it now (§8.2), and `panels/right-panel.ts` tracks it, so the panel
|
|
697
|
-
* renders.
|
|
537
|
+
* What a row's callbacks do. The document reports `commit("attr|href", "/about")` and nothing else;
|
|
538
|
+
* everything needed to turn that into a document write was decided when the row was built.
|
|
698
539
|
*/
|
|
540
|
+
interface RowPlan {
|
|
541
|
+
/** Turn a committed string into a document value. */
|
|
542
|
+
commit?: ((raw: string) => void) | undefined;
|
|
543
|
+
commitChecked?: ((checked: boolean) => void) | undefined;
|
|
544
|
+
commitKind?: ((kind: string) => void) | undefined;
|
|
545
|
+
/** The provenance chip's verb: clear it, jump to the donor, open the source. */
|
|
546
|
+
chipAction?: (() => void) | undefined;
|
|
547
|
+
/** An `action` row's verb, and a `file` row's path. */
|
|
548
|
+
action?: (() => void) | undefined;
|
|
549
|
+
openPath?: string | undefined;
|
|
550
|
+
/** The draft key for a text control; absent when the row commits with no draft. */
|
|
551
|
+
draft?: string | undefined;
|
|
552
|
+
/** How long after a keystroke {@link commit} runs; `0` means only on change. */
|
|
553
|
+
debounceMs: number;
|
|
554
|
+
/**
|
|
555
|
+
* A key/value pair's two draft keys, what each cell currently holds on disk, and the write that
|
|
556
|
+
* lands both at once. The committed pair is carried because either cell may be edited alone, and
|
|
557
|
+
* a missing draft has to fall back to the value that is actually there — reading `""` for the
|
|
558
|
+
* name is how a value-only edit renamed the attribute to nothing.
|
|
559
|
+
*/
|
|
560
|
+
kv?: {
|
|
561
|
+
nameKey: string;
|
|
562
|
+
valueKey: string;
|
|
563
|
+
name: string;
|
|
564
|
+
value: string;
|
|
565
|
+
write: (name: string, value: string) => void;
|
|
566
|
+
};
|
|
567
|
+
ladder?: LadderPlan | undefined;
|
|
568
|
+
/**
|
|
569
|
+
* A leaf the document does not draw, given the empty box it drew for it.
|
|
570
|
+
*
|
|
571
|
+
* It takes the host rather than returning a template because the two leaves do not agree about
|
|
572
|
+
* what drawing means: the expression editor is lit and renders into the box, the media picker is
|
|
573
|
+
* a document and is MOUNTED into it. Both are idempotent, so a repaint calls this again rather
|
|
574
|
+
* than deciding which of the two it is holding.
|
|
575
|
+
*/
|
|
576
|
+
control?: ((host: HTMLElement) => void) | undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** A row view with every field filled in, before the caller narrows it. */
|
|
580
|
+
function blankRow(key: string, prop: string, label: string): ContentRowView {
|
|
581
|
+
return {
|
|
582
|
+
checked: false,
|
|
583
|
+
chip: "",
|
|
584
|
+
chipState: "",
|
|
585
|
+
chipText: "",
|
|
586
|
+
chipTitle: "",
|
|
587
|
+
detail: "",
|
|
588
|
+
hasSource: false,
|
|
589
|
+
hasTokens: false,
|
|
590
|
+
key,
|
|
591
|
+
kind: "text",
|
|
592
|
+
kindOptions: [],
|
|
593
|
+
label,
|
|
594
|
+
linkKind: "",
|
|
595
|
+
linkValueKind: "text",
|
|
596
|
+
mono: false,
|
|
597
|
+
name: "",
|
|
598
|
+
options: [],
|
|
599
|
+
placeholder: "",
|
|
600
|
+
prop,
|
|
601
|
+
removeLabel: "",
|
|
602
|
+
resolved: "",
|
|
603
|
+
shape: "field",
|
|
604
|
+
source: "fixed",
|
|
605
|
+
sourceLabel: "",
|
|
606
|
+
sourceLocked: false,
|
|
607
|
+
sourceName: "",
|
|
608
|
+
text: "",
|
|
609
|
+
title: "",
|
|
610
|
+
tokens: [],
|
|
611
|
+
value: "",
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ─── The panel ───────────────────────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
/** Where a control is navigated to when a row asks to open a component definition. */
|
|
618
|
+
let navigateToComponent: (path: string) => void = () => {};
|
|
619
|
+
|
|
620
|
+
/** The mounted document, once the Inspector has handed over its container. */
|
|
621
|
+
let surface: ContentPanelSurface | null = null;
|
|
622
|
+
|
|
623
|
+
/** The effect that watches everything the projection reads. */
|
|
624
|
+
let panelScope: EffectScope | null = null;
|
|
625
|
+
|
|
626
|
+
/** Row key → what its callbacks do. Rebuilt by every projection. */
|
|
627
|
+
const plans = new Map<string, RowPlan>();
|
|
628
|
+
|
|
629
|
+
/** The lit-owned control hosts the document has announced, by row key. */
|
|
630
|
+
const controlHosts = new Map<string, HTMLElement>();
|
|
631
|
+
|
|
699
632
|
/**
|
|
700
|
-
*
|
|
633
|
+
* Every draft key this tab has minted.
|
|
701
634
|
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* a
|
|
705
|
-
*
|
|
706
|
-
* floating `void registry.run(…)` that swallows it. A chip that does nothing when you press it is
|
|
707
|
-
* worse than no chip: the sentence above it already says where the element comes from, and in that
|
|
708
|
-
* shell the layout is either already beside you or one Unsplit away.
|
|
635
|
+
* `ui/field-input.ts`'s draft map is module-global and a draft deliberately OUTLIVES the commit it
|
|
636
|
+
* provoked — that is what keeps a field controlled by the text in it until the reader blurs. So the
|
|
637
|
+
* one moment a draft of this tab's is certainly dead is when the tab is unbound, and nothing but
|
|
638
|
+
* this set knows which of them were ours.
|
|
709
639
|
*/
|
|
710
|
-
|
|
711
|
-
return html`<span
|
|
712
|
-
class="kv-add"
|
|
713
|
-
@click=${() => {
|
|
714
|
-
/* ONE command, addressed by id — the chip is a control, not a second definition site for
|
|
715
|
-
what "open the layout" means. It opens the layout BESIDE the page and leaves the keyboard
|
|
716
|
-
in the page, so the next click on layout chrome moves the side pane's selection instead of
|
|
717
|
-
teaching the author what a pane is. */
|
|
718
|
-
void activeRegistry()?.run("pane.derive", { preset: "layout" });
|
|
719
|
-
}}
|
|
720
|
-
>Open Layout →</span
|
|
721
|
-
>`;
|
|
722
|
-
}
|
|
640
|
+
const draftKeys = new Set<string>();
|
|
723
641
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const displayPath = selection.layoutFile || "layout";
|
|
729
|
-
|
|
730
|
-
return html`
|
|
731
|
-
<div class="style-sidebar">
|
|
732
|
-
<sp-accordion allow-multiple size="s">
|
|
733
|
-
<sp-accordion-item label="Layout Element" open>
|
|
734
|
-
<div class="style-section-body">
|
|
735
|
-
<div class="layout-origin">
|
|
736
|
-
<span class="layout-origin-badge">Layout</span>
|
|
737
|
-
<code class="layout-origin-tag"><${tagName}></code>
|
|
738
|
-
</div>
|
|
739
|
-
${
|
|
740
|
-
className
|
|
741
|
-
? renderFieldRow({
|
|
742
|
-
hasValue: false,
|
|
743
|
-
label: "Class",
|
|
744
|
-
prop: "className",
|
|
745
|
-
widget: html`<span class="layout-origin-class">${className}</span>`,
|
|
746
|
-
})
|
|
747
|
-
: nothing
|
|
748
|
-
}
|
|
749
|
-
<p class="layout-origin-note">
|
|
750
|
-
This element comes from ${displayPath}, which wraps every page that uses it. Open the
|
|
751
|
-
layout to edit it.
|
|
752
|
-
</p>
|
|
753
|
-
${deriveRefusal(workspace.activePaneId) === null ? openLayoutTpl() : nothing}
|
|
754
|
-
</div>
|
|
755
|
-
</sp-accordion-item>
|
|
756
|
-
</sp-accordion>
|
|
757
|
-
</div>
|
|
758
|
-
`;
|
|
642
|
+
/** Mint a draft key, remembering it for the unbind. */
|
|
643
|
+
function draft(key: string): string {
|
|
644
|
+
draftKeys.add(key);
|
|
645
|
+
return key;
|
|
759
646
|
}
|
|
760
647
|
|
|
761
|
-
|
|
648
|
+
/** The rung picker, while one is up. One at a time, and it goes down with the panel. */
|
|
649
|
+
let sourceMenu: MenuHandle | null = null;
|
|
762
650
|
|
|
763
651
|
/**
|
|
764
|
-
* The
|
|
765
|
-
*
|
|
652
|
+
* The repaint signal for state no reactive store carries — the component registry, the usage cache
|
|
653
|
+
* and the route walk, each of which lands asynchronously and used to repaint through
|
|
654
|
+
* `renderOnly("rightPanel")`.
|
|
766
655
|
*/
|
|
767
|
-
const
|
|
656
|
+
const pulse = reactive({ tick: 0 });
|
|
657
|
+
|
|
658
|
+
/** Whether a projection is already queued for the end of this tick. */
|
|
659
|
+
let projectionQueued = false;
|
|
768
660
|
|
|
769
661
|
/**
|
|
770
|
-
*
|
|
662
|
+
* Ask the Content tab to re-read the world.
|
|
771
663
|
*
|
|
772
|
-
* Three
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
|
|
777
|
-
|
|
664
|
+
* Three of the facts the projection reads are NOT reactive stores — the component registry, the
|
|
665
|
+
* usage cache and the enumerated page routes — so they announce themselves here. Every other
|
|
666
|
+
* repaint arrives through the watcher below, which is why this is a signal rather than a render
|
|
667
|
+
* call.
|
|
668
|
+
*/
|
|
669
|
+
export function renderPropertiesPanel(): void {
|
|
670
|
+
/* The pulse is the whole of it: the watcher reads it, so a bump reaches the same coalescer every
|
|
671
|
+
other fact does. A second queue here would be a second scheduler for one surface. */
|
|
672
|
+
pulse.tick += 1;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Hand the Content tab its container, and subscribe to the state it draws.
|
|
778
677
|
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
678
|
+
* `null` unbinds — the document goes down and the watcher stops — which is what the Inspector's own
|
|
679
|
+
* teardown needs. The same seam rather than a second export, for the reason `panels/ai-panel.ts`
|
|
680
|
+
* gives: a teardown nothing but a teardown reaches is one the reachability ledger has to carry.
|
|
782
681
|
*/
|
|
783
|
-
function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
682
|
+
export function bindContentHost(
|
|
683
|
+
el: HTMLElement | null,
|
|
684
|
+
ctx?: { navigateToComponent: (path: string) => void },
|
|
685
|
+
): void {
|
|
686
|
+
panelScope?.stop();
|
|
687
|
+
panelScope = null;
|
|
688
|
+
surface?.dispose();
|
|
689
|
+
surface = null;
|
|
690
|
+
plans.clear();
|
|
691
|
+
for (const host of controlHosts.values()) {
|
|
692
|
+
unmountMediaPicker(host);
|
|
791
693
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
694
|
+
controlHosts.clear();
|
|
695
|
+
for (const key of draftKeys) {
|
|
696
|
+
clearDraft(key);
|
|
795
697
|
}
|
|
698
|
+
draftKeys.clear();
|
|
699
|
+
sourceMenu?.close();
|
|
700
|
+
sourceMenu = null;
|
|
701
|
+
if (!el) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
navigateToComponent = ctx?.navigateToComponent ?? (() => {});
|
|
705
|
+
surface = mountContentSurface(el, emptyContentView(), ACTIONS, (key, host) => {
|
|
706
|
+
controlHosts.set(key, host);
|
|
707
|
+
paintControl(key, host);
|
|
708
|
+
});
|
|
709
|
+
panelScope = effectScope();
|
|
710
|
+
panelScope.run(() => {
|
|
711
|
+
effect(() => {
|
|
712
|
+
// Everything the projection reads that a store can announce. The tab's own fields are read
|
|
713
|
+
// One by one because a bare `void tab` tracks the holder and not the document inside it.
|
|
714
|
+
const tab = activeTab.value;
|
|
715
|
+
void shell.layoutSelection;
|
|
716
|
+
void workspace.activePaneId;
|
|
717
|
+
void pulse.tick;
|
|
718
|
+
if (tab) {
|
|
719
|
+
void tab.doc.document;
|
|
720
|
+
void tab.documentPath;
|
|
721
|
+
// The whole SET, joined — a bare property read would not re-trigger when the selection
|
|
722
|
+
// Changes WITHIN the array.
|
|
723
|
+
void tab.session.selection.map((path) => path.join("/")).join("|");
|
|
724
|
+
void tab.session.ui.inspectorSections;
|
|
725
|
+
}
|
|
726
|
+
queueProjection();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
}
|
|
796
730
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
size="s"
|
|
809
|
-
quiet
|
|
810
|
-
@click=${() => {
|
|
811
|
-
void retryUsages(query).then(() => renderOnly("rightPanel"));
|
|
812
|
-
}}
|
|
813
|
-
>Retry</sp-action-button
|
|
814
|
-
>
|
|
815
|
-
`;
|
|
816
|
-
} else {
|
|
817
|
-
const files = usageFiles(state.result);
|
|
818
|
-
heading = usageHeadline(state.result);
|
|
819
|
-
body =
|
|
820
|
-
files.length === 0
|
|
821
|
-
? html`<div class="usage-note">
|
|
822
|
-
Nothing else in this project places <code><${tagName}></code> yet.
|
|
823
|
-
</div>`
|
|
824
|
-
: html`${files.map(
|
|
825
|
-
(file) => html`
|
|
826
|
-
<button
|
|
827
|
-
class="usage-row"
|
|
828
|
-
type="button"
|
|
829
|
-
title=${file.refs.map((r) => `${r.refType} ${r.ref} ×${r.count}`).join(", ")}
|
|
830
|
-
@click=${() => openUsage(file.path)}
|
|
831
|
-
>
|
|
832
|
-
<span class="usage-row-path">${file.path}</span>
|
|
833
|
-
<span class="usage-row-count">${file.count}</span>
|
|
834
|
-
</button>
|
|
835
|
-
`,
|
|
836
|
-
)}`;
|
|
731
|
+
/**
|
|
732
|
+
* Project once at the end of this tick.
|
|
733
|
+
*
|
|
734
|
+
* Coalesced on a microtask because one gesture writes several reactive facts — a transaction moves
|
|
735
|
+
* the document, the selection and the history index — and the projection walks every applicable
|
|
736
|
+
* attribute of the selected tag. Running it once per write would build the whole accordion three
|
|
737
|
+
* times for one edit.
|
|
738
|
+
*/
|
|
739
|
+
function queueProjection(): void {
|
|
740
|
+
if (projectionQueued || !surface) {
|
|
741
|
+
return;
|
|
837
742
|
}
|
|
743
|
+
projectionQueued = true;
|
|
744
|
+
queueMicrotask(() => {
|
|
745
|
+
projectionQueued = false;
|
|
746
|
+
surface?.update(projectPanel());
|
|
747
|
+
paintControls();
|
|
748
|
+
});
|
|
749
|
+
}
|
|
838
750
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Draw one lit-owned leaf into the host the document announced for it.
|
|
753
|
+
*
|
|
754
|
+
* Connectedness is deliberately NOT consulted: a host is announced as it is CREATED, one reconcile
|
|
755
|
+
* step before it is in the page, so "is it connected" answers no for exactly the node that needs
|
|
756
|
+
* drawing.
|
|
757
|
+
*/
|
|
758
|
+
function paintControl(key: string, host: HTMLElement): void {
|
|
759
|
+
plans.get(key)?.control?.(host);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** Redraw every leaf this panel has been handed a host for, and forget the hosts that have gone. */
|
|
763
|
+
function paintControls(): void {
|
|
764
|
+
for (const [key, host] of controlHosts) {
|
|
765
|
+
if (plans.has(key)) {
|
|
766
|
+
paintControl(key, host);
|
|
767
|
+
} else {
|
|
768
|
+
controlHosts.delete(key);
|
|
769
|
+
/* A media picker in that box is a MOUNTED document, and the surface registry holds its host:
|
|
770
|
+
dropping our handle on it would leave the mount alive with nothing left to draw. */
|
|
771
|
+
unmountMediaPicker(host);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ─── What a control asks for ─────────────────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
const ACTIONS: ContentPanelActions = {
|
|
779
|
+
chipAction: (key) => {
|
|
780
|
+
plans.get(key)?.chipAction?.();
|
|
781
|
+
},
|
|
782
|
+
commit: (key, raw) => {
|
|
783
|
+
const plan = plans.get(key);
|
|
784
|
+
if (!plan?.commit) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (plan.draft) {
|
|
788
|
+
setDraft(plan.draft, raw);
|
|
789
|
+
commitField(plan.draft, plan.commit);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
plan.commit(raw);
|
|
793
|
+
},
|
|
794
|
+
commitChecked: (key, checked) => {
|
|
795
|
+
plans.get(key)?.commitChecked?.(checked);
|
|
796
|
+
},
|
|
797
|
+
commitKind: (key, kind) => {
|
|
798
|
+
plans.get(key)?.commitKind?.(kind);
|
|
799
|
+
},
|
|
800
|
+
edit: (key, raw) => {
|
|
801
|
+
const plan = plans.get(key);
|
|
802
|
+
if (!plan?.commit || !plan.draft || plan.debounceMs <= 0) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
setDraft(plan.draft, raw);
|
|
806
|
+
scheduleDraftCommit(plan.draft, plan.debounceMs, plan.commit);
|
|
807
|
+
},
|
|
808
|
+
editName: (key, raw) => {
|
|
809
|
+
editKvCell(key, "nameKey", raw);
|
|
810
|
+
},
|
|
811
|
+
editValue: (key, raw) => {
|
|
812
|
+
editKvCell(key, "valueKey", raw);
|
|
813
|
+
},
|
|
814
|
+
openFile: (key) => {
|
|
815
|
+
const path = plans.get(key)?.openPath;
|
|
816
|
+
if (path) {
|
|
817
|
+
openUsage(path);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
openLayout: () => {
|
|
821
|
+
/* ONE command, addressed by id — the chip is a control, not a second definition site for what
|
|
822
|
+
"open the layout" means. It opens the layout BESIDE the page and leaves the keyboard in the
|
|
823
|
+
page, so the next click on layout chrome moves the side pane's selection instead of teaching
|
|
824
|
+
the author what a pane is. */
|
|
825
|
+
void activeRegistry()?.run("pane.derive", { preset: "layout" });
|
|
826
|
+
},
|
|
827
|
+
pickSource: (key, anchor) => {
|
|
828
|
+
openSourceMenu(key, anchor);
|
|
829
|
+
},
|
|
830
|
+
removeRow: (key) => {
|
|
831
|
+
plans.get(key)?.action?.();
|
|
832
|
+
},
|
|
833
|
+
runAction: (key) => {
|
|
834
|
+
plans.get(key)?.action?.();
|
|
835
|
+
},
|
|
836
|
+
runEmptyAction: (id) => {
|
|
837
|
+
if (id === "open-page") {
|
|
838
|
+
// The one verb every "needs an open document" empty state offers, run from where it is
|
|
839
|
+
// Declared rather than re-implemented: `panels/empty-state.ts` owns both its words and its
|
|
840
|
+
// Lazy reach into Quick Access.
|
|
841
|
+
openPageAction().run();
|
|
842
|
+
} else if (id === "open-logic") {
|
|
843
|
+
showLogicTab();
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
setSection: (key, open) => {
|
|
847
|
+
// One writer: the accordion's own toggle and `inspector.setSection` land in the same function,
|
|
848
|
+
// So the command and the control cannot disagree about what "open" means.
|
|
849
|
+
setInspectorSection(key, open);
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
/** A keystroke in one cell of a key/value pair: draft it, and land both cells together. */
|
|
854
|
+
function editKvCell(key: string, cell: "nameKey" | "valueKey", raw: string): void {
|
|
855
|
+
const plan = plans.get(key);
|
|
856
|
+
if (!plan?.kv) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const cellDraft = plan.kv[cell];
|
|
860
|
+
setDraft(cellDraft, raw);
|
|
861
|
+
scheduleDraftCommit(cellDraft, plan.debounceMs, () => {
|
|
862
|
+
commitKvPair(key);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Land a key/value pair: whatever is in both cells right now, in one transaction. */
|
|
867
|
+
function commitKvPair(key: string): void {
|
|
868
|
+
const plan = plans.get(key);
|
|
869
|
+
if (!plan?.kv) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const { nameKey, valueKey, write } = plan.kv;
|
|
873
|
+
// Both cells schedule the same commit, so the second timer arrives with nothing left to say.
|
|
874
|
+
if (!hasDraft(nameKey) && !hasDraft(valueKey)) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const name = getFieldValue(nameKey, plan.kv.name);
|
|
878
|
+
const value = getFieldValue(valueKey, plan.kv.value);
|
|
879
|
+
clearDraft(nameKey);
|
|
880
|
+
clearDraft(valueKey);
|
|
881
|
+
write(name, value);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** The rung picker: every source this position permits, one action away (§6.3). */
|
|
885
|
+
function openSourceMenu(key: string, anchor: HTMLElement): void {
|
|
886
|
+
const ladder = plans.get(key)?.ladder;
|
|
887
|
+
if (!ladder) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const { fieldKey, mode, offered } = ladder;
|
|
891
|
+
sourceMenu?.close();
|
|
892
|
+
sourceMenu = openMenu({
|
|
893
|
+
label: "Value source",
|
|
894
|
+
onClosed: () => {
|
|
895
|
+
sourceMenu = null;
|
|
896
|
+
},
|
|
897
|
+
opener: anchor,
|
|
898
|
+
region: "value-source",
|
|
899
|
+
rows: offered.map((rung) => ({
|
|
900
|
+
checked: (rung === mode ? "true" : "false") as "true" | "false",
|
|
901
|
+
destructive: false,
|
|
902
|
+
disabled: false,
|
|
903
|
+
dividerAbove: false,
|
|
904
|
+
id: rung,
|
|
905
|
+
title: VALUE_SOURCE_LABELS[rung],
|
|
906
|
+
})),
|
|
907
|
+
run: (id) => {
|
|
908
|
+
const next = id as SlotMode;
|
|
909
|
+
if (next === mode) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
/* Leaving From data… seeds an empty Fixed value stash with the signal's declared default, so
|
|
913
|
+
unbind-restores-default survives a detour (ref → mixed text → fixed value). */
|
|
914
|
+
if (
|
|
915
|
+
mode === "ref" &&
|
|
916
|
+
ladder.literalDefault !== undefined &&
|
|
917
|
+
!hasStashedSlotValue(fieldKey, "literal")
|
|
918
|
+
) {
|
|
919
|
+
stashSlotValue(fieldKey, "literal", ladder.literalDefault);
|
|
920
|
+
}
|
|
921
|
+
ladder.onChange(
|
|
922
|
+
switchSlotMode(
|
|
923
|
+
fieldKey,
|
|
924
|
+
mode,
|
|
925
|
+
next,
|
|
926
|
+
cloneValue(ladder.value),
|
|
927
|
+
ladder.seedFor?.(next) ??
|
|
928
|
+
slotModeSeed(next, {
|
|
929
|
+
extraSignals: ladder.extraSignals,
|
|
930
|
+
literalDefault: ladder.literalDefault,
|
|
931
|
+
stateDefs: ladder.stateDefs,
|
|
932
|
+
}),
|
|
933
|
+
),
|
|
934
|
+
);
|
|
935
|
+
},
|
|
936
|
+
});
|
|
849
937
|
}
|
|
850
938
|
|
|
851
939
|
/** Open a referencing file in a tab — the "→" half of "Used on N pages →". */
|
|
@@ -860,172 +948,279 @@ function showLogicTab(): void {
|
|
|
860
948
|
void import("./right-panel").then((m) => m.setInspectorTab("events"));
|
|
861
949
|
}
|
|
862
950
|
|
|
863
|
-
// ───
|
|
951
|
+
// ─── Row builders ────────────────────────────────────────────────────────────
|
|
952
|
+
|
|
953
|
+
/** Record a row's plan and hand back the row, so a builder reads as one expression. */
|
|
954
|
+
function withPlan(row: ContentRowView, plan: RowPlan): ContentRowView {
|
|
955
|
+
plans.set(row.key, plan);
|
|
956
|
+
return row;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Fill in a row's provenance chip from the vocabulary `panels/provenance.ts` owns. */
|
|
960
|
+
function applyChip(
|
|
961
|
+
row: ContentRowView,
|
|
962
|
+
plan: RowPlan,
|
|
963
|
+
prop: string,
|
|
964
|
+
p: FieldProvenance | undefined,
|
|
965
|
+
): void {
|
|
966
|
+
if (!p || p.state === "default") {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
row.chipState = p.state;
|
|
970
|
+
row.chipText = provenanceText(p);
|
|
971
|
+
row.chipTitle = provenanceTitle(prop, p);
|
|
972
|
+
row.chip = p.state === "set" ? "dot" : p.onClick ? "press" : "text";
|
|
973
|
+
plan.chipAction = p.onClick;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Everything a bindable position needs to offer the value ladder. */
|
|
977
|
+
interface LadderOpts {
|
|
978
|
+
fieldKey: string;
|
|
979
|
+
caps: SlotCapsSource;
|
|
980
|
+
value: unknown;
|
|
981
|
+
onChange: (v?: JsonValue) => void;
|
|
982
|
+
stateDefs: string[];
|
|
983
|
+
extraSignals?: SignalOption[] | null;
|
|
984
|
+
literalDefault?: JsonValue | undefined;
|
|
985
|
+
seedFor?: ((mode: SlotMode) => JsonValue | undefined) | undefined;
|
|
986
|
+
/** The operators this position's grammar admits, when it admits fewer than all of them. */
|
|
987
|
+
operators?: readonly string[] | undefined;
|
|
988
|
+
}
|
|
864
989
|
|
|
865
990
|
/**
|
|
866
|
-
*
|
|
991
|
+
* Offer the value ladder on a row, and draw the rung it is actually at.
|
|
867
992
|
*
|
|
868
|
-
*
|
|
993
|
+
* The rungs are DERIVED from the schema at the named position, never listed by the caller, so a
|
|
994
|
+
* panel can no longer offer a rung the compiler would reject nor withhold one it would accept. The
|
|
995
|
+
* literal rung is whatever the caller already put on the row; every other rung replaces it.
|
|
869
996
|
*/
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
997
|
+
function applyLadder(row: ContentRowView, plan: RowPlan, opts: LadderOpts): void {
|
|
998
|
+
const caps = slotCaps(opts.caps);
|
|
999
|
+
const mode = effectiveSlotMode(opts.fieldKey, opts.value);
|
|
1000
|
+
const sources: SignalOption[] = [
|
|
1001
|
+
...opts.stateDefs.map((name) => ({ label: name, value: `#/state/${name}` })),
|
|
1002
|
+
...(opts.extraSignals ?? []),
|
|
1003
|
+
];
|
|
1004
|
+
// A ref rung with nothing to point at is a dead end — drop it rather than offer an empty picker.
|
|
1005
|
+
const offered = caps.filter((m) => m !== "ref" || sources.length > 0);
|
|
1006
|
+
const label = VALUE_SOURCE_LABELS[mode];
|
|
1007
|
+
/* Locked only when the rung this value already sits on is the only one there is. A value on a
|
|
1008
|
+
rung the position does NOT permit — a fixed string left in a `$switch`, say — is exactly the
|
|
1009
|
+
case that needs the chip most, and counting rungs alone used to grey it out. */
|
|
1010
|
+
const locked = !offered.some((m) => m !== mode);
|
|
1011
|
+
row.hasSource = true;
|
|
1012
|
+
row.sourceLabel = label;
|
|
1013
|
+
row.sourceLocked = locked;
|
|
1014
|
+
row.sourceName = locked
|
|
1015
|
+
? `Value source: ${label} (no other source available here)`
|
|
1016
|
+
: `Value source: ${label} — click to change`;
|
|
1017
|
+
row.source = mode === "literal" ? "fixed" : "bound";
|
|
1018
|
+
plan.ladder = {
|
|
1019
|
+
extraSignals: [...(opts.extraSignals ?? [])],
|
|
1020
|
+
fieldKey: opts.fieldKey,
|
|
1021
|
+
literalDefault: opts.literalDefault,
|
|
1022
|
+
mode,
|
|
1023
|
+
offered,
|
|
1024
|
+
onChange: opts.onChange,
|
|
1025
|
+
seedFor: opts.seedFor,
|
|
1026
|
+
sources,
|
|
1027
|
+
stateDefs: opts.stateDefs,
|
|
1028
|
+
value: cloneValue(opts.value as JsonValue | undefined),
|
|
1029
|
+
};
|
|
1030
|
+
if (mode === "ref") {
|
|
1031
|
+
row.kind = "select";
|
|
1032
|
+
row.value = isRef(opts.value) ? opts.value.$ref : "";
|
|
1033
|
+
row.options = [
|
|
1034
|
+
{ label: "— select signal —", value: "" },
|
|
1035
|
+
...sources.map((s) => ({ label: s.label, value: s.value })),
|
|
1036
|
+
];
|
|
1037
|
+
plan.commit = (raw) => opts.onChange(raw ? { $ref: raw } : undefined);
|
|
1038
|
+
plan.debounceMs = 0;
|
|
1039
|
+
plan.draft = undefined;
|
|
1040
|
+
plan.control = undefined;
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (mode === "template") {
|
|
1044
|
+
const templateDraft = draft(`${opts.fieldKey}:template`);
|
|
1045
|
+
row.kind = "text";
|
|
1046
|
+
row.mono = true;
|
|
1047
|
+
row.placeholder = "${state.…}";
|
|
1048
|
+
row.value = getFieldValue(templateDraft, typeof opts.value === "string" ? opts.value : "");
|
|
1049
|
+
plan.commit = (raw) => opts.onChange(raw);
|
|
1050
|
+
plan.debounceMs = LIVE_PREVIEW;
|
|
1051
|
+
plan.draft = templateDraft;
|
|
1052
|
+
plan.control = undefined;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (mode === "expression") {
|
|
1056
|
+
const node = (opts.value as { $expression?: unknown } | undefined)?.$expression;
|
|
1057
|
+
row.kind = "control";
|
|
1058
|
+
plan.commit = undefined;
|
|
1059
|
+
plan.draft = undefined;
|
|
1060
|
+
plan.control = (host) =>
|
|
1061
|
+
mountExpressionEditor(
|
|
1062
|
+
host,
|
|
1063
|
+
node,
|
|
1064
|
+
(next) => opts.onChange({ $expression: next } as JsonValue),
|
|
1065
|
+
{
|
|
1066
|
+
allowEventRef: false,
|
|
1067
|
+
...(opts.operators ? { operators: opts.operators } : {}),
|
|
1068
|
+
preview: null,
|
|
1069
|
+
stateDefs: opts.stateDefs,
|
|
1070
|
+
stateEntries: null,
|
|
1071
|
+
},
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** A text row bound to the draft layer: the one control every typed value in this tab goes through. */
|
|
1077
|
+
function textRow(opts: {
|
|
1078
|
+
key: string;
|
|
1079
|
+
prop: string;
|
|
1080
|
+
label: string;
|
|
1081
|
+
draft: string;
|
|
1082
|
+
value: string;
|
|
1083
|
+
placeholder?: string;
|
|
1084
|
+
commit: (raw: string) => void;
|
|
1085
|
+
}): { row: ContentRowView; plan: RowPlan } {
|
|
1086
|
+
const row = blankRow(opts.key, opts.prop, opts.label);
|
|
1087
|
+
row.kind = "text";
|
|
1088
|
+
row.value = getFieldValue(opts.draft, opts.value);
|
|
1089
|
+
row.placeholder = opts.placeholder ?? "";
|
|
1090
|
+
const plan: RowPlan = { commit: opts.commit, debounceMs: LIVE_PREVIEW, draft: draft(opts.draft) };
|
|
1091
|
+
return { plan, row };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// ─── The projection ──────────────────────────────────────────────────────────
|
|
1095
|
+
|
|
1096
|
+
/** An empty state, in the words §-6's copy rules ask for. */
|
|
1097
|
+
function emptyView(
|
|
1098
|
+
message: string,
|
|
1099
|
+
detail: string,
|
|
1100
|
+
actions: { id: string; label: string }[],
|
|
1101
|
+
): ContentPanelView {
|
|
1102
|
+
return {
|
|
1103
|
+
...emptyContentView(),
|
|
1104
|
+
emptyActions: actions,
|
|
1105
|
+
emptyDetail: detail,
|
|
1106
|
+
emptyMessage: message,
|
|
1107
|
+
hasEmptyActions: actions.length > 0,
|
|
1108
|
+
hasEmptyDetail: detail !== "",
|
|
1109
|
+
view: "empty",
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* The whole tab, as the document reads it.
|
|
1115
|
+
*
|
|
1116
|
+
* Every plan this panel can act on is rebuilt here, which is what makes a callback safe: a row key
|
|
1117
|
+
* that is no longer in the projection has no plan, so a click on a control the reconciler has since
|
|
1118
|
+
* taken away reaches nothing rather than the wrong node.
|
|
1119
|
+
*/
|
|
1120
|
+
function projectPanel(): ContentPanelView {
|
|
1121
|
+
plans.clear();
|
|
873
1122
|
const tab = activeTab.value;
|
|
874
1123
|
if (!tab) {
|
|
875
|
-
return
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
});
|
|
1124
|
+
return emptyView("Open a page to inspect and style what you click.", "", [
|
|
1125
|
+
{ id: "open-page", label: openPageAction().label },
|
|
1126
|
+
]);
|
|
879
1127
|
}
|
|
880
1128
|
|
|
881
|
-
// Layout
|
|
1129
|
+
// Layout chrome first: it is mutually exclusive with a document selection, and it is the one
|
|
1130
|
+
// Target whose name has to say where it came FROM.
|
|
882
1131
|
if (shell.layoutSelection) {
|
|
883
|
-
return
|
|
1132
|
+
return layoutView(shell.layoutSelection as LayoutSelection);
|
|
884
1133
|
}
|
|
885
1134
|
|
|
886
1135
|
const selected = primarySelection(tab.session.selection);
|
|
887
1136
|
if (!selected) {
|
|
888
|
-
return
|
|
1137
|
+
return emptyView(clickAnythingTo("edit its content"), "", []);
|
|
889
1138
|
}
|
|
890
1139
|
const path: JxPath = selected;
|
|
891
1140
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
892
1141
|
if (!node) {
|
|
893
|
-
return
|
|
1142
|
+
return emptyView(staleSelectionMessage(), "", []);
|
|
894
1143
|
}
|
|
895
1144
|
|
|
896
1145
|
// A repeating list has no content of its own — it has a source and a template, and both are
|
|
897
1146
|
// Wiring. Content says where the answer lives rather than drawing an empty accordion (§6.5).
|
|
898
1147
|
if (node.$prototype === "Array") {
|
|
899
|
-
return
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1148
|
+
return emptyView(
|
|
1149
|
+
"A repeating list has no content of its own.",
|
|
1150
|
+
"Its items, filter, sort and template are wiring, so they live in Logic.",
|
|
1151
|
+
[{ id: "open-logic", label: "Open Logic" }],
|
|
1152
|
+
);
|
|
904
1153
|
}
|
|
905
1154
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1155
|
+
return {
|
|
1156
|
+
...emptyContentView(),
|
|
1157
|
+
sections: sectionsFor(node, path),
|
|
1158
|
+
view: "content",
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** The Layout-element card — what the inspector says when you click page chrome. */
|
|
1163
|
+
function layoutView(selection: LayoutSelection): ContentPanelView {
|
|
1164
|
+
const displayPath = selection.layoutFile || "layout";
|
|
1165
|
+
return {
|
|
1166
|
+
...emptyContentView(),
|
|
1167
|
+
/* Drawn only where it can run. `canvas/iframe-host.ts` focuses the pane a layout click landed
|
|
1168
|
+
in — including a LENS, which draws layout chrome because it draws the same document — and
|
|
1169
|
+
from a derived pane `pane.derive` can only throw into a floating `void registry.run(…)` that
|
|
1170
|
+
swallows it. A chip that does nothing when you press it is worse than no chip. */
|
|
1171
|
+
layoutCanOpen: deriveRefusal(workspace.activePaneId) === null,
|
|
1172
|
+
layoutClass: selection.className,
|
|
1173
|
+
layoutHasClass: Boolean(selection.className),
|
|
1174
|
+
layoutNote:
|
|
1175
|
+
`This element comes from ${displayPath}, which wraps every page that uses it. ` +
|
|
1176
|
+
"Open the layout to edit it.",
|
|
1177
|
+
layoutTag: selection.tagName || "element",
|
|
1178
|
+
view: "layout",
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Every section the selected node draws, in reading order. */
|
|
1183
|
+
function sectionsFor(node: JxMutableNode, path: JxPath): ContentSectionView[] {
|
|
1184
|
+
const tab = activeTab.value!;
|
|
909
1185
|
const doc = tab.doc.document;
|
|
910
|
-
const
|
|
911
|
-
const chosenTag = isTagExpression(node.tagName) ? node.tagName.$expression : null;
|
|
1186
|
+
const targets: JxPath[] = tab.session.selection.length > 0 ? tab.session.selection : [path];
|
|
912
1187
|
const tagName = displayTagName(node.tagName) || "div";
|
|
1188
|
+
const isCustomInstance = displayTagName(node.tagName).includes("-");
|
|
913
1189
|
const attrs = node.attributes || {};
|
|
914
|
-
|
|
915
1190
|
const mapSignals = mapSignalsFor(path);
|
|
916
|
-
|
|
917
|
-
const bindableSignals = bindableSignalNames(tab.doc.document);
|
|
918
|
-
|
|
919
|
-
function renderAttrRow(attr: string, entry: HtmlMetaEntry, value: unknown) {
|
|
920
|
-
const type = inferInputType(entry);
|
|
921
|
-
const hasVal = value !== undefined && value !== "";
|
|
922
|
-
// One write per selected element, inside ONE transaction (§6.5). `targets` is `[path]` for a
|
|
923
|
-
// Single selection, so this is the same single mutation it has always been.
|
|
924
|
-
const commitAttr = (v?: JsonValue) =>
|
|
925
|
-
transactDoc(activeTab.value!, (t) => {
|
|
926
|
-
for (const target of targets) {
|
|
927
|
-
mutateUpdateAttribute(t, target, attr, v as JxAttributeValue | undefined);
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// Enhanced Link handling: only for anchors (a/area) with a plain (non-binding) value. Bindings
|
|
932
|
-
// ($ref objects or ${…} template strings) fall through to the raw widget to stay editable.
|
|
933
|
-
const isAnchor = tagName === "a" || tagName === "area";
|
|
934
|
-
if (isAnchor && !isBoundAttrValue(value)) {
|
|
935
|
-
if (attr === "href") {
|
|
936
|
-
return renderLinkTargetField(node, path);
|
|
937
|
-
}
|
|
938
|
-
if (attr === "target") {
|
|
939
|
-
return renderTargetField(node, path, entry);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
const attrSlot = (staticWidget: unknown) =>
|
|
944
|
-
renderDynamicSlot({
|
|
945
|
-
caps: "attribute",
|
|
946
|
-
extraSignals: mapSignals,
|
|
947
|
-
fieldKey: `attr|${path.join("/")}|${attr}`,
|
|
948
|
-
onChange: commitAttr,
|
|
949
|
-
staticWidget,
|
|
950
|
-
stateDefs: bindableSignals,
|
|
951
|
-
value,
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
const literalWidget =
|
|
955
|
-
entry.type === "boolean"
|
|
956
|
-
? html`
|
|
957
|
-
<sp-checkbox
|
|
958
|
-
size="s"
|
|
959
|
-
.checked=${live(Boolean(value))}
|
|
960
|
-
@change=${(e: Event) => commitAttr((e.target as HTMLInputElement).checked ? "" : undefined)}
|
|
961
|
-
>
|
|
962
|
-
</sp-checkbox>
|
|
963
|
-
`
|
|
964
|
-
: widgetForType(type, entry, attr, isRef(value) ? "" : String(value || ""), (v: string) =>
|
|
965
|
-
commitAttr(v || undefined),
|
|
966
|
-
);
|
|
967
|
-
const slot = attrSlot(literalWidget);
|
|
968
|
-
return renderFieldRow({
|
|
969
|
-
hasValue: hasVal,
|
|
970
|
-
label: attrLabel(entry, attr),
|
|
971
|
-
labelExtra: slot.modeButton,
|
|
972
|
-
prop: attr,
|
|
973
|
-
provenance: attributeProvenance(
|
|
974
|
-
attr,
|
|
975
|
-
value,
|
|
976
|
-
hasVal,
|
|
977
|
-
() =>
|
|
978
|
-
transactDoc(activeTab.value, (t) => {
|
|
979
|
-
for (const target of targets) {
|
|
980
|
-
mutateUpdateAttribute(t, target, attr);
|
|
981
|
-
}
|
|
982
|
-
}),
|
|
983
|
-
mixedAcrossSelection(doc, targets, (n) => (n?.attributes ?? {})[attr] ?? null),
|
|
984
|
-
),
|
|
985
|
-
widget: slot.widget,
|
|
986
|
-
});
|
|
987
|
-
}
|
|
1191
|
+
const bindableSignals = bindableSignalNames(doc);
|
|
988
1192
|
|
|
989
|
-
// ──
|
|
1193
|
+
// ── Which attributes this tag has, by section ──
|
|
990
1194
|
const applicableAttrs = {} as Record<string, HtmlMetaEntry>;
|
|
991
1195
|
for (const [attr, entry] of Object.entries(htmlMeta.$defs) as [string, HtmlMetaEntry][]) {
|
|
992
1196
|
if (!entry.$elements || entry.$elements.includes(displayTagName(tagName))) {
|
|
993
|
-
// The $attr field aliases a $defs key to a different attribute name
|
|
994
|
-
//
|
|
1197
|
+
// The $attr field aliases a $defs key to a different attribute name, so the same attribute
|
|
1198
|
+
// (e.g. "name") can carry per-element metadata.
|
|
995
1199
|
applicableAttrs[entry.$attr ?? attr] = entry;
|
|
996
1200
|
}
|
|
997
1201
|
}
|
|
998
|
-
|
|
999
1202
|
const attrSections: Record<string, { name: string; entry: HtmlMetaEntry }[]> = {};
|
|
1000
1203
|
for (const sec of htmlMeta.$sections) {
|
|
1001
1204
|
attrSections[sec.key] = [];
|
|
1002
1205
|
}
|
|
1003
1206
|
for (const [attr, entry] of Object.entries(applicableAttrs)) {
|
|
1004
|
-
|
|
1005
|
-
if (attrSections[secKey]) {
|
|
1006
|
-
attrSections[secKey].push({ entry, name: attr });
|
|
1007
|
-
}
|
|
1207
|
+
attrSections[entry.$section]?.push({ entry, name: attr });
|
|
1008
1208
|
}
|
|
1009
1209
|
for (const sec of htmlMeta.$sections) {
|
|
1010
|
-
attrSections[sec.key]!.sort(
|
|
1011
|
-
(a: { name: string; entry: HtmlMetaEntry }, b: { name: string; entry: HtmlMetaEntry }) =>
|
|
1012
|
-
a.entry.$order - b.entry.$order,
|
|
1013
|
-
);
|
|
1210
|
+
attrSections[sec.key]!.sort((a, b) => a.entry.$order - b.entry.$order);
|
|
1014
1211
|
}
|
|
1015
1212
|
|
|
1016
1213
|
const knownAttrNames = new Set(Object.keys(applicableAttrs));
|
|
1017
1214
|
if (isCustomInstance) {
|
|
1018
1215
|
const comp = componentRegistry.find((c) => c.tagName === node.tagName);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
knownAttrNames.add(p.name);
|
|
1022
|
-
}
|
|
1216
|
+
for (const p of comp?.props ?? []) {
|
|
1217
|
+
knownAttrNames.add(p.name);
|
|
1023
1218
|
}
|
|
1024
1219
|
}
|
|
1025
1220
|
const customAttrs = Object.entries(attrs).filter(([k]) => !knownAttrNames.has(k));
|
|
1026
1221
|
|
|
1027
|
-
const autoOpen = new Set();
|
|
1028
|
-
for (const
|
|
1222
|
+
const autoOpen = new Set<string>();
|
|
1223
|
+
for (const attr of Object.keys(attrs)) {
|
|
1029
1224
|
const entry = applicableAttrs[attr];
|
|
1030
1225
|
if (entry) {
|
|
1031
1226
|
autoOpen.add(entry.$section);
|
|
@@ -1035,48 +1230,107 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1035
1230
|
autoOpen.add("__custom");
|
|
1036
1231
|
}
|
|
1037
1232
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1233
|
+
const sections: ContentSectionView[] = [
|
|
1234
|
+
sectionView("__element", "Element", isInspectorSectionOpen("__element", false), [], 0),
|
|
1235
|
+
];
|
|
1236
|
+
sections[0]!.rows = elementRows(node, path, tagName, mapSignals, bindableSignals);
|
|
1237
|
+
|
|
1238
|
+
if (isCustomInstance) {
|
|
1239
|
+
const definitionOfThis = isComponentDefinitionOpen(node);
|
|
1240
|
+
sections.push(
|
|
1241
|
+
sectionView(
|
|
1242
|
+
PROPS_SECTION,
|
|
1243
|
+
definitionOfThis ? "Component Defaults" : "Component Settings",
|
|
1244
|
+
isInspectorSectionOpen(PROPS_SECTION, true),
|
|
1245
|
+
componentPropRows(node, path, mapSignals, definitionOfThis),
|
|
1246
|
+
0,
|
|
1247
|
+
),
|
|
1248
|
+
);
|
|
1249
|
+
// "Used on N pages →". Only for a component instance: an ordinary <div> is not a thing that can
|
|
1250
|
+
// Be reused, so the question does not arise.
|
|
1251
|
+
const usages = usagesSection(
|
|
1252
|
+
displayTagName(tagName),
|
|
1253
|
+
componentRegistry.find((c) => c.tagName === displayTagName(tagName))?.path ?? null,
|
|
1254
|
+
);
|
|
1255
|
+
if (usages) {
|
|
1256
|
+
sections.push(usages);
|
|
1257
|
+
}
|
|
1040
1258
|
}
|
|
1041
1259
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1260
|
+
for (const sec of htmlMeta.$sections) {
|
|
1261
|
+
const sectionAttrs = attrSections[sec.key]!;
|
|
1262
|
+
if (sectionAttrs.length === 0) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const setCount = sectionAttrs.filter((a) => attrs[a.name] !== undefined).length;
|
|
1266
|
+
const attrCtx: AttrCtx = {
|
|
1267
|
+
bindableSignals,
|
|
1268
|
+
doc,
|
|
1269
|
+
mapSignals,
|
|
1270
|
+
node,
|
|
1271
|
+
path,
|
|
1272
|
+
tagName,
|
|
1273
|
+
targets,
|
|
1274
|
+
};
|
|
1275
|
+
const rows = sectionAttrs.map((a) => attributeRow(a.name, a.entry, attrs[a.name], attrCtx));
|
|
1276
|
+
const open = isInspectorSectionOpen(sec.key, autoOpen.has(sec.key));
|
|
1277
|
+
sections.push(sectionView(sec.key, sec.label, open, rows, setCount));
|
|
1046
1278
|
}
|
|
1047
1279
|
|
|
1048
|
-
|
|
1280
|
+
if (customAttrs.length > 0 || Object.keys(attrs).length > 0) {
|
|
1281
|
+
const custom = customRows(path, customAttrs);
|
|
1282
|
+
const open = isInspectorSectionOpen("__custom", autoOpen.has("__custom"));
|
|
1283
|
+
sections.push(sectionView("__custom", "Custom", open, custom, customAttrs.length));
|
|
1284
|
+
}
|
|
1285
|
+
return sections;
|
|
1286
|
+
}
|
|
1049
1287
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
value:
|
|
1066
|
-
|
|
1288
|
+
/**
|
|
1289
|
+
* One accordion section, with §6.2's collapsed-header tally.
|
|
1290
|
+
*
|
|
1291
|
+
* The dot is an INDICATOR and not a control: it used to inherit `.set-dot`'s pointer cursor and its
|
|
1292
|
+
* danger hover, which together say "click me to clear this", and nothing happened because it never
|
|
1293
|
+
* had a handler. Clicking anywhere in the heading already toggles the section.
|
|
1294
|
+
*/
|
|
1295
|
+
function sectionView(
|
|
1296
|
+
key: string,
|
|
1297
|
+
label: string,
|
|
1298
|
+
open: boolean,
|
|
1299
|
+
rows: ContentRowView[],
|
|
1300
|
+
setCount: number,
|
|
1301
|
+
): ContentSectionView {
|
|
1302
|
+
return {
|
|
1303
|
+
dotTitle: `${setCount} value${setCount === 1 ? "" : "s"} set in this section`,
|
|
1304
|
+
hasDot: setCount > 0,
|
|
1305
|
+
key,
|
|
1306
|
+
label,
|
|
1307
|
+
open,
|
|
1308
|
+
rows,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1067
1311
|
|
|
1068
|
-
|
|
1069
|
-
* A draft key for one field ON THIS NODE.
|
|
1070
|
-
*
|
|
1071
|
-
* The three Element rows used constant keys — `"prop:tagName"`, `"prop:$id"`, `"prop:className"`
|
|
1072
|
-
* — and `ui/field-input.ts`'s draft map is module-global, so every element shared one draft slot
|
|
1073
|
-
* per field. Type a class name, click a sibling before blurring, and the sibling's Class row
|
|
1074
|
-
* showed the text you had typed for the first one; blur it there and the commit landed on the
|
|
1075
|
-
* WRONG element. Plan §11.4 asks for "drafts keyed by node path (today all elements share one
|
|
1076
|
-
* draft slot per field)".
|
|
1077
|
-
*/
|
|
1078
|
-
const fieldKey = (name: string) => `prop:${path.join("/")}:${name}`;
|
|
1312
|
+
// ─── The Element section ─────────────────────────────────────────────────────
|
|
1079
1313
|
|
|
1314
|
+
/**
|
|
1315
|
+
* A draft key for one field ON THIS NODE.
|
|
1316
|
+
*
|
|
1317
|
+
* The three Element rows used constant keys — `"prop:tagName"`, `"prop:$id"`, `"prop:className"` —
|
|
1318
|
+
* and `ui/field-input.ts`'s draft map is module-global, so every element shared one draft slot per
|
|
1319
|
+
* field. Type a class name, click a sibling before blurring, and the sibling's Class row showed the
|
|
1320
|
+
* text you had typed for the first one; blur it there and the commit landed on the WRONG element.
|
|
1321
|
+
*/
|
|
1322
|
+
function draftKey(path: JxPath, name: string): string {
|
|
1323
|
+
return `prop:${path.join("/")}:${name}`;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function elementRows(
|
|
1327
|
+
node: JxMutableNode,
|
|
1328
|
+
path: JxPath,
|
|
1329
|
+
tagName: string,
|
|
1330
|
+
mapSignals: SignalOption[] | null,
|
|
1331
|
+
bindableSignals: string[],
|
|
1332
|
+
): ContentRowView[] {
|
|
1333
|
+
const chosenTag = isTagExpression(node.tagName) ? node.tagName.$expression : null;
|
|
1080
1334
|
/**
|
|
1081
1335
|
* The set-here chip for a document property, with the tooltip in the label's own words.
|
|
1082
1336
|
*
|
|
@@ -1089,326 +1343,635 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1089
1343
|
state: "set",
|
|
1090
1344
|
title: `Clear ${human}`,
|
|
1091
1345
|
});
|
|
1346
|
+
const writeProperty = (name: string) => (raw: string) => {
|
|
1347
|
+
transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, name, raw || undefined));
|
|
1348
|
+
};
|
|
1092
1349
|
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
${renderFieldRow({
|
|
1184
|
-
hasValue: Boolean(node.hidden),
|
|
1185
|
-
label: "Hidden",
|
|
1186
|
-
prop: "hidden",
|
|
1187
|
-
...(node.hidden ? { provenance: propertyChip("hidden", "hidden") } : {}),
|
|
1188
|
-
widget: html`
|
|
1189
|
-
<sp-checkbox
|
|
1190
|
-
size="s"
|
|
1191
|
-
.checked=${live(Boolean(node.hidden))}
|
|
1192
|
-
@change=${(e: Event) =>
|
|
1193
|
-
transactDoc(activeTab.value, (t) =>
|
|
1194
|
-
mutateUpdateProperty(
|
|
1195
|
-
t,
|
|
1196
|
-
path,
|
|
1197
|
-
"hidden",
|
|
1198
|
-
(e.target as HTMLInputElement).checked || undefined,
|
|
1199
|
-
),
|
|
1200
|
-
)}
|
|
1201
|
-
>
|
|
1202
|
-
</sp-checkbox>
|
|
1203
|
-
`,
|
|
1204
|
-
})}
|
|
1205
|
-
</div>
|
|
1206
|
-
</sp-accordion-item>
|
|
1207
|
-
`;
|
|
1208
|
-
|
|
1209
|
-
const editingDefinition = isCustomInstance && isComponentDefinitionOpen(node);
|
|
1210
|
-
const compPropsT = isCustomInstance
|
|
1211
|
-
? html`
|
|
1212
|
-
<sp-accordion-item
|
|
1213
|
-
label=${editingDefinition ? "Component Defaults" : "Component Settings"}
|
|
1214
|
-
open
|
|
1215
|
-
>
|
|
1216
|
-
<div class="style-section-body">
|
|
1217
|
-
${renderComponentPropsFieldsTemplate(node, path, mapSignals, ctx.navigateToComponent)}
|
|
1218
|
-
</div>
|
|
1219
|
-
</sp-accordion-item>
|
|
1220
|
-
`
|
|
1221
|
-
: nothing;
|
|
1222
|
-
|
|
1223
|
-
// "Used on N pages →". Only for a component instance: an ordinary <div> is not a thing that can
|
|
1224
|
-
// Be reused, so the question does not arise.
|
|
1225
|
-
const usagesT = isCustomInstance
|
|
1226
|
-
? renderUsagesSection(
|
|
1227
|
-
displayTagName(tagName),
|
|
1228
|
-
componentRegistry.find((c) => c.tagName === displayTagName(tagName))?.path ?? null,
|
|
1229
|
-
)
|
|
1230
|
-
: nothing;
|
|
1231
|
-
|
|
1232
|
-
const attrSectionTemplates = htmlMeta.$sections
|
|
1233
|
-
.filter((sec) => attrSections[sec.key]!.length > 0)
|
|
1234
|
-
.map((sec) => {
|
|
1235
|
-
const sectionAttrs = attrSections[sec.key]!;
|
|
1236
|
-
const setCount = sectionAttrs.filter(
|
|
1237
|
-
(a: { name: string; entry: HtmlMetaEntry }) => attrs[a.name] !== undefined,
|
|
1238
|
-
).length;
|
|
1239
|
-
return html`
|
|
1240
|
-
<sp-accordion-item
|
|
1241
|
-
label=${sec.label}
|
|
1242
|
-
?open=${isSectionOpen(sec.key)}
|
|
1243
|
-
@sp-accordion-item-toggle=${() => toggleSection(sec.key)}
|
|
1244
|
-
>
|
|
1245
|
-
${sectionDot(setCount)}
|
|
1246
|
-
<div class="style-section-body">
|
|
1247
|
-
${sectionAttrs.map((a: { name: string; entry: HtmlMetaEntry }) =>
|
|
1248
|
-
renderAttrRow(a.name, a.entry, attrs[a.name]),
|
|
1249
|
-
)}
|
|
1250
|
-
</div>
|
|
1251
|
-
</sp-accordion-item>
|
|
1252
|
-
`;
|
|
1350
|
+
const rows: ContentRowView[] = [];
|
|
1351
|
+
|
|
1352
|
+
/* THE SHARED SLOT, not a hand-rolled control. `tagName` is a bindable position like any other — a
|
|
1353
|
+
literal name or a `TagExpression` — so it gets the same Value Source chip and the same
|
|
1354
|
+
expression editor as `href`, a style declaration or an event handler. The rungs are DERIVED
|
|
1355
|
+
from `SLOT_POSITION_SCHEMAS.elementTag`, which is why only Fixed value and Formula are offered:
|
|
1356
|
+
`TagName` carries a pattern, so a template rung is refused, and a `${…}` in tag position is
|
|
1357
|
+
precisely what that pattern exists to reject.
|
|
1358
|
+
|
|
1359
|
+
The seed is ours because the generic one is `{ operator: "??" }`, which a `TagExpression` does
|
|
1360
|
+
not admit — clicking the chip would otherwise write a document that fails its own validator.
|
|
1361
|
+
And the GRAMMAR is ours for exactly the same reason: seeding `?:` and then handing the node to
|
|
1362
|
+
an unrestricted editor left the very next click free to reoperate it to `capitalize`, or to
|
|
1363
|
+
pick `toUpperCase` out of the formula catalog. `TAG_EXPRESSION_OPERATORS` is read off the def,
|
|
1364
|
+
so the two branches the editor offers are the two branches the validator accepts. */
|
|
1365
|
+
const tag = textRow({
|
|
1366
|
+
commit: writeProperty("tagName"),
|
|
1367
|
+
draft: draftKey(path, "tagName"),
|
|
1368
|
+
key: "prop|tagName",
|
|
1369
|
+
label: "Tag",
|
|
1370
|
+
prop: "tagName",
|
|
1371
|
+
value: tagName,
|
|
1372
|
+
});
|
|
1373
|
+
applyLadder(tag.row, tag.plan, {
|
|
1374
|
+
caps: "elementTag",
|
|
1375
|
+
fieldKey: `${path.join(".")}:tagName`,
|
|
1376
|
+
operators: TAG_EXPRESSION_OPERATORS,
|
|
1377
|
+
onChange: (next) => {
|
|
1378
|
+
transactDoc(activeTab.value, (t) =>
|
|
1379
|
+
mutateUpdateProperty(t, path, "tagName", next ?? undefined),
|
|
1380
|
+
);
|
|
1381
|
+
},
|
|
1382
|
+
seedFor: (mode) =>
|
|
1383
|
+
mode === "expression"
|
|
1384
|
+
? { $expression: { initial: tagName, operator: "?:", target: null, value: tagName } }
|
|
1385
|
+
: undefined,
|
|
1386
|
+
stateDefs: bindableSignals,
|
|
1387
|
+
value: node.tagName as JsonValue | undefined,
|
|
1388
|
+
});
|
|
1389
|
+
void chosenTag;
|
|
1390
|
+
rows.push(withPlan(tag.row, tag.plan));
|
|
1391
|
+
|
|
1392
|
+
const id = textRow({
|
|
1393
|
+
commit: writeProperty("$id"),
|
|
1394
|
+
draft: draftKey(path, "$id"),
|
|
1395
|
+
key: "prop|$id",
|
|
1396
|
+
label: "ID",
|
|
1397
|
+
prop: "$id",
|
|
1398
|
+
value: String(node.$id || ""),
|
|
1399
|
+
});
|
|
1400
|
+
if (node.$id) {
|
|
1401
|
+
applyChip(id.row, id.plan, "$id", propertyChip("$id", "ID"));
|
|
1402
|
+
}
|
|
1403
|
+
rows.push(withPlan(id.row, id.plan));
|
|
1404
|
+
|
|
1405
|
+
const cls = textRow({
|
|
1406
|
+
commit: writeProperty("className"),
|
|
1407
|
+
draft: draftKey(path, "className"),
|
|
1408
|
+
key: "prop|className",
|
|
1409
|
+
label: "Class",
|
|
1410
|
+
prop: "className",
|
|
1411
|
+
value: String(node.className || ""),
|
|
1412
|
+
});
|
|
1413
|
+
if (node.className) {
|
|
1414
|
+
applyChip(cls.row, cls.plan, "className", propertyChip("className", "class"));
|
|
1415
|
+
}
|
|
1416
|
+
rows.push(withPlan(cls.row, cls.plan));
|
|
1417
|
+
|
|
1418
|
+
if (!Array.isArray(node.children) || node.children.length === 0) {
|
|
1419
|
+
const textDraft = draft(draftKey(path, "textContent"));
|
|
1420
|
+
const text = blankRow("prop|textContent", "textContent", "Text Content");
|
|
1421
|
+
text.kind = "textarea";
|
|
1422
|
+
text.value = getFieldValue(
|
|
1423
|
+
textDraft,
|
|
1424
|
+
typeof node.textContent === "string" ? node.textContent : "",
|
|
1425
|
+
);
|
|
1426
|
+
const plan: RowPlan = {
|
|
1427
|
+
commit: writeProperty("textContent"),
|
|
1428
|
+
debounceMs: LIVE_PREVIEW,
|
|
1429
|
+
draft: textDraft,
|
|
1430
|
+
};
|
|
1431
|
+
applyLadder(text, plan, {
|
|
1432
|
+
caps: "textProperty",
|
|
1433
|
+
extraSignals: mapSignals,
|
|
1434
|
+
fieldKey: `prop|${path.join("/")}|textContent`,
|
|
1435
|
+
onChange: (v) =>
|
|
1436
|
+
transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, "textContent", v)),
|
|
1437
|
+
stateDefs: bindableSignals,
|
|
1438
|
+
value: node.textContent,
|
|
1253
1439
|
});
|
|
1440
|
+
if (node.textContent !== undefined) {
|
|
1441
|
+
applyChip(text, plan, "textContent", propertyChip("textContent", "text"));
|
|
1442
|
+
}
|
|
1443
|
+
rows.push(withPlan(text, plan));
|
|
1444
|
+
}
|
|
1254
1445
|
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
return html`
|
|
1272
|
-
<div class="style-sidebar">
|
|
1273
|
-
<sp-accordion allow-multiple size="s">
|
|
1274
|
-
${elemT} ${compPropsT} ${usagesT} ${attrSectionTemplates} ${customSectionT}
|
|
1275
|
-
</sp-accordion>
|
|
1276
|
-
</div>
|
|
1277
|
-
`;
|
|
1446
|
+
const hidden = blankRow("prop|hidden", "hidden", "Hidden");
|
|
1447
|
+
hidden.kind = "checkbox";
|
|
1448
|
+
hidden.checked = Boolean(node.hidden);
|
|
1449
|
+
const hiddenPlan: RowPlan = {
|
|
1450
|
+
commitChecked: (checked) =>
|
|
1451
|
+
transactDoc(activeTab.value, (t) =>
|
|
1452
|
+
mutateUpdateProperty(t, path, "hidden", checked || undefined),
|
|
1453
|
+
),
|
|
1454
|
+
debounceMs: 0,
|
|
1455
|
+
};
|
|
1456
|
+
if (node.hidden) {
|
|
1457
|
+
applyChip(hidden, hiddenPlan, "hidden", propertyChip("hidden", "hidden"));
|
|
1458
|
+
}
|
|
1459
|
+
rows.push(withPlan(hidden, hiddenPlan));
|
|
1460
|
+
|
|
1461
|
+
return rows;
|
|
1278
1462
|
}
|
|
1279
1463
|
|
|
1464
|
+
// ─── Component settings ──────────────────────────────────────────────────────
|
|
1465
|
+
|
|
1280
1466
|
/**
|
|
1281
|
-
*
|
|
1467
|
+
* Component props, each row carrying §6.2's provenance chip — the SECOND cascade.
|
|
1282
1468
|
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
* already toggles the section, so a second, different meaning for the same click would be worse
|
|
1288
|
-
* than the silence. So it keeps the information and gives up the affordance: an inert, labelled
|
|
1289
|
-
* indicator that says how many values in the section are set.
|
|
1469
|
+
* A component instance's value for a prop either overrides the component's declared default or does
|
|
1470
|
+
* not exist, and until the chip landed the two were the same blank field. The chip states which,
|
|
1471
|
+
* and in the inherited case it names the donor AND jumps to it: the donor of a component default is
|
|
1472
|
+
* the component definition, which this panel can already open.
|
|
1290
1473
|
*/
|
|
1291
|
-
function
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1474
|
+
function componentPropRows(
|
|
1475
|
+
node: JxMutableNode,
|
|
1476
|
+
path: JxPath,
|
|
1477
|
+
mapSignals: SignalOption[] | null,
|
|
1478
|
+
definitionOfThis: boolean,
|
|
1479
|
+
): ContentRowView[] {
|
|
1480
|
+
const tab = activeTab.value!;
|
|
1481
|
+
const comp = componentRegistry.find((c) => c.tagName === displayTagName(node.tagName));
|
|
1482
|
+
if (!comp || !comp.props) {
|
|
1483
|
+
return [
|
|
1484
|
+
noteRow(
|
|
1485
|
+
"props|unknown",
|
|
1486
|
+
"This component is not in the project's library, so it has no settings to show.",
|
|
1487
|
+
),
|
|
1488
|
+
];
|
|
1489
|
+
}
|
|
1490
|
+
const isNpm = comp.source === "npm";
|
|
1491
|
+
/* In the definition the "value" of a setting is its DECLARED DEFAULT — the state entry's
|
|
1492
|
+
`default` — not a per-instance override, so the row reads and writes there instead. */
|
|
1493
|
+
const stateEntries = (tab.doc.document.state ?? {}) as Record<string, { default?: JsonValue }>;
|
|
1494
|
+
const currentVals: Record<string, unknown> = definitionOfThis
|
|
1495
|
+
? Object.fromEntries(comp.props.map((prop) => [prop.name, stateEntries[prop.name]?.default]))
|
|
1496
|
+
: isNpm
|
|
1497
|
+
? node.attributes || {}
|
|
1498
|
+
: node.$props || {};
|
|
1499
|
+
const updateFn = definitionOfThis
|
|
1500
|
+
? (name: string, v?: JsonValue) =>
|
|
1501
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: v }))
|
|
1502
|
+
: isNpm
|
|
1503
|
+
? (name: string, v?: JsonValue) =>
|
|
1504
|
+
transactDoc(activeTab.value, (t) =>
|
|
1505
|
+
mutateUpdateAttribute(
|
|
1506
|
+
t,
|
|
1507
|
+
path,
|
|
1508
|
+
name,
|
|
1509
|
+
v === "" ? undefined : (v as JxAttributeValue | undefined),
|
|
1510
|
+
),
|
|
1511
|
+
)
|
|
1512
|
+
: (name: string, v?: JsonValue) =>
|
|
1513
|
+
transactDoc(activeTab.value, (t) => mutateUpdateProp(t, path, name, v));
|
|
1514
|
+
|
|
1515
|
+
const defs = tab.doc.document.state || {};
|
|
1516
|
+
const signalNames = bindableSignalNames(tab.doc.document);
|
|
1517
|
+
const definitionPath = comp.path;
|
|
1518
|
+
/* One closure for the whole list rather than one per prop: it says the same thing every time,
|
|
1519
|
+
and building it inside the loop is what made every row carry its own copy of it. */
|
|
1520
|
+
const openDefinition =
|
|
1521
|
+
definitionPath && !definitionOfThis ? () => navigateToComponent(definitionPath) : undefined;
|
|
1522
|
+
const rows: ContentRowView[] = [];
|
|
1523
|
+
|
|
1524
|
+
for (const prop of comp.props) {
|
|
1525
|
+
const rawValue = currentVals[prop.name];
|
|
1526
|
+
const boundRef = isRef(rawValue) ? rawValue.$ref : null;
|
|
1527
|
+
const hasVal = rawValue !== undefined && rawValue !== null;
|
|
1528
|
+
const parsed = parseCemType(prop.type);
|
|
1529
|
+
const onChange = (v?: JsonValue) => updateFn(prop.name, v);
|
|
1530
|
+
const staticVal = slotMode(rawValue) === "literal" ? String(rawValue ?? "") : "";
|
|
1531
|
+
const key = `cprop|${prop.name}`;
|
|
1532
|
+
const propDraft = draft(`cprop:${path.join("/")}:${prop.name}`);
|
|
1533
|
+
const row = blankRow(key, prop.name, camelToLabel(prop.name));
|
|
1534
|
+
const plan: RowPlan = { debounceMs: LIVE_PREVIEW };
|
|
1535
|
+
|
|
1536
|
+
if (isMediaFormat(prop.format)) {
|
|
1537
|
+
row.kind = "control";
|
|
1538
|
+
plan.control = (host) =>
|
|
1539
|
+
mountMediaPicker(host, prop.name, staticVal, (v: string) => onChange(v));
|
|
1540
|
+
} else if (prop.format === "color") {
|
|
1541
|
+
row.kind = "color";
|
|
1542
|
+
row.tokens = colorTokens();
|
|
1543
|
+
row.hasTokens = row.tokens.length > 0;
|
|
1544
|
+
row.value = getFieldValue(propDraft, staticVal);
|
|
1545
|
+
row.resolved = resolvedColor(row.value);
|
|
1546
|
+
plan.commit = (raw) => onChange(raw);
|
|
1547
|
+
plan.draft = propDraft;
|
|
1548
|
+
} else if (prop.format === "date") {
|
|
1549
|
+
row.kind = "text";
|
|
1550
|
+
row.placeholder = "YYYY-MM-DD";
|
|
1551
|
+
row.value = getFieldValue(propDraft, staticVal);
|
|
1552
|
+
plan.commit = (raw) => onChange(raw);
|
|
1553
|
+
plan.draft = propDraft;
|
|
1554
|
+
} else if (parsed.kind === "boolean") {
|
|
1555
|
+
row.kind = "checkbox";
|
|
1556
|
+
row.checked = Boolean(staticVal);
|
|
1557
|
+
plan.commitChecked = (checked) => onChange(checked || undefined);
|
|
1558
|
+
} else if (parsed.kind === "number") {
|
|
1559
|
+
row.kind = "number";
|
|
1560
|
+
row.value = getFieldValue(propDraft, staticVal);
|
|
1561
|
+
plan.commit = (raw) => onChange(raw);
|
|
1562
|
+
plan.draft = propDraft;
|
|
1563
|
+
} else if (parsed.kind === "combobox") {
|
|
1564
|
+
row.kind = "select";
|
|
1565
|
+
row.value = staticVal;
|
|
1566
|
+
row.options = [
|
|
1567
|
+
CLEAR_OPTION,
|
|
1568
|
+
...(parsed.options as string[]).map((o) => ({ label: camelToLabel(o), value: o })),
|
|
1569
|
+
];
|
|
1570
|
+
plan.commit = (raw) => onChange(raw || undefined);
|
|
1571
|
+
plan.debounceMs = 0;
|
|
1572
|
+
} else {
|
|
1573
|
+
row.kind = "text";
|
|
1574
|
+
row.value = getFieldValue(propDraft, staticVal);
|
|
1575
|
+
plan.commit = (raw) => onChange(raw);
|
|
1576
|
+
plan.draft = propDraft;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
applyLadder(row, plan, {
|
|
1580
|
+
caps: "componentProp",
|
|
1581
|
+
extraSignals: mapSignals,
|
|
1582
|
+
fieldKey: `cprop|${path.join("/")}|${prop.name}`,
|
|
1583
|
+
// De-escalating to literal restores the bound signal's declared default (the old unbind).
|
|
1584
|
+
literalDefault: boundRef
|
|
1585
|
+
? defaultAsString(defs[boundRef.startsWith("#/state/") ? boundRef.slice(8) : boundRef]) ||
|
|
1586
|
+
undefined
|
|
1587
|
+
: undefined,
|
|
1588
|
+
onChange,
|
|
1589
|
+
stateDefs: signalNames,
|
|
1590
|
+
value: rawValue,
|
|
1591
|
+
});
|
|
1592
|
+
applyChip(
|
|
1593
|
+
row,
|
|
1594
|
+
plan,
|
|
1595
|
+
prop.name,
|
|
1596
|
+
componentPropProvenance(prop, rawValue, hasVal, {
|
|
1597
|
+
// No donor and no jump in the definition: the value IS the component's default, and
|
|
1598
|
+
// "→ the component definition" would open the document already in front of you.
|
|
1599
|
+
isDefinition: definitionOfThis,
|
|
1600
|
+
onClear: () => updateFn(prop.name),
|
|
1601
|
+
...(openDefinition ? { openDefinition } : {}),
|
|
1602
|
+
}),
|
|
1603
|
+
);
|
|
1604
|
+
rows.push(withPlan(row, plan));
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (comp.props.length === 0) {
|
|
1608
|
+
rows.push(
|
|
1609
|
+
noteRow(
|
|
1610
|
+
"props|empty",
|
|
1611
|
+
definitionOfThis
|
|
1612
|
+
? "This component declares no settings yet. Add a state entry with a default to give " +
|
|
1613
|
+
"whoever places it something to fill in."
|
|
1614
|
+
: "This component has no settings to fill in yet.",
|
|
1615
|
+
),
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
// A link to the definition, from inside the definition, is a link to here.
|
|
1619
|
+
if (openDefinition) {
|
|
1620
|
+
rows.push(
|
|
1621
|
+
actionRow("props|edit-definition", "editDefinition", "→ Edit definition", openDefinition),
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
return rows;
|
|
1300
1625
|
}
|
|
1301
1626
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1627
|
+
// ─── HTML attributes ─────────────────────────────────────────────────────────
|
|
1628
|
+
|
|
1629
|
+
interface AttrCtx {
|
|
1630
|
+
node: JxMutableNode;
|
|
1631
|
+
path: JxPath;
|
|
1632
|
+
doc: JxMutableNode;
|
|
1633
|
+
targets: JxPath[];
|
|
1634
|
+
tagName: string;
|
|
1635
|
+
mapSignals: SignalOption[] | null;
|
|
1636
|
+
bindableSignals: string[];
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function attributeRow(
|
|
1304
1640
|
attr: string,
|
|
1641
|
+
entry: HtmlMetaEntry,
|
|
1305
1642
|
value: unknown,
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
//
|
|
1313
|
-
|
|
1314
|
-
|
|
1643
|
+
ctx: AttrCtx,
|
|
1644
|
+
): ContentRowView {
|
|
1645
|
+
const { doc, node, path, tagName, targets } = ctx;
|
|
1646
|
+
const hasVal = value !== undefined && value !== "";
|
|
1647
|
+
const key = `attr|${attr}`;
|
|
1648
|
+
const label = attrLabel(entry, attr);
|
|
1649
|
+
// One write per selected element, inside ONE transaction (§6.5). `targets` is `[path]` for a
|
|
1650
|
+
// Single selection, so this is the same single mutation it has always been.
|
|
1651
|
+
const commitAttr = (v?: JsonValue) =>
|
|
1652
|
+
transactDoc(activeTab.value!, (t) => {
|
|
1653
|
+
for (const target of targets) {
|
|
1654
|
+
mutateUpdateAttribute(t, target, attr, v as JxAttributeValue | undefined);
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
const clearAttr = () =>
|
|
1658
|
+
transactDoc(activeTab.value, (t) => {
|
|
1659
|
+
for (const target of targets) {
|
|
1660
|
+
mutateUpdateAttribute(t, target, attr);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
const chip = () =>
|
|
1664
|
+
attributeProvenance(
|
|
1665
|
+
attr,
|
|
1666
|
+
value,
|
|
1667
|
+
hasVal,
|
|
1668
|
+
clearAttr,
|
|
1669
|
+
mixedAcrossSelection(doc, targets, (n) => (n?.attributes ?? {})[attr] ?? null),
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
// Enhanced Link handling: only for anchors (a/area) with a plain (non-binding) value. Bindings
|
|
1673
|
+
// ($ref objects or ${…} template strings) fall through to the raw widget to stay editable.
|
|
1674
|
+
const isAnchor = tagName === "a" || tagName === "area";
|
|
1675
|
+
if (isAnchor && !isBoundAttrValue(value)) {
|
|
1676
|
+
if (attr === "href") {
|
|
1677
|
+
return linkRow(key, label, node, path);
|
|
1678
|
+
}
|
|
1679
|
+
if (attr === "target") {
|
|
1680
|
+
const row = blankRow(key, attr, label);
|
|
1681
|
+
row.kind = "select";
|
|
1682
|
+
row.value = typeof node.attributes?.target === "string" ? node.attributes.target : "";
|
|
1683
|
+
row.options = [
|
|
1684
|
+
CLEAR_OPTION,
|
|
1685
|
+
...(Array.isArray(entry.enum) ? (entry.enum as string[]) : []).map((o) => ({
|
|
1686
|
+
label: o,
|
|
1687
|
+
value: o,
|
|
1688
|
+
})),
|
|
1689
|
+
];
|
|
1690
|
+
const plan: RowPlan = { commit: (raw) => commitAttr(raw || undefined), debounceMs: 0 };
|
|
1691
|
+
applyChip(row, plan, attr, chip());
|
|
1692
|
+
return withPlan(row, plan);
|
|
1693
|
+
}
|
|
1315
1694
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1695
|
+
|
|
1696
|
+
/* Same gate as the anchor fields above: a BOUND value falls through to the raw widget so it stays
|
|
1697
|
+
editable as an expression rather than being flattened into a picker. */
|
|
1698
|
+
if (entry.$input === "popover-target" && !isBoundAttrValue(value)) {
|
|
1699
|
+
const row = blankRow(key, attr, label);
|
|
1700
|
+
row.kind = "select";
|
|
1701
|
+
row.value = typeof value === "string" ? value : "";
|
|
1702
|
+
row.options = [
|
|
1703
|
+
CLEAR_OPTION,
|
|
1704
|
+
...popoverIdsIn((activeTab.value?.doc.document ?? {}) as JxElement, {
|
|
1705
|
+
popoverTags: POPOVER_TAGS,
|
|
1706
|
+
}).map((id) => ({ label: id, value: id })),
|
|
1707
|
+
{ label: "Type an id…", value: POPOVER_TARGET_OTHER },
|
|
1708
|
+
];
|
|
1709
|
+
const plan: RowPlan = {
|
|
1710
|
+
commit: (raw) => {
|
|
1711
|
+
if (raw === POPOVER_TARGET_OTHER) {
|
|
1712
|
+
/* It stays typeable, because the list can never be complete: a `popovertarget` resolves
|
|
1713
|
+
in the RENDERED DOM, and a page composes components whose internals this document
|
|
1714
|
+
cannot see. */
|
|
1715
|
+
void showPromptDialog("Popover id", {
|
|
1716
|
+
confirmLabel: "Use",
|
|
1717
|
+
message: "The id of a popover in another file.",
|
|
1718
|
+
placeholder: "site-mobile-menu",
|
|
1719
|
+
}).then((typed) => commitAttr(typed?.trim() || undefined));
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
commitAttr(raw || undefined);
|
|
1723
|
+
},
|
|
1724
|
+
debounceMs: 0,
|
|
1725
|
+
};
|
|
1726
|
+
applyChip(row, plan, attr, chip());
|
|
1727
|
+
return withPlan(row, plan);
|
|
1318
1728
|
}
|
|
1319
|
-
return hasVal ? { onClick: onClear, state: "set", title: `Clear ${attr}` } : { state: "default" };
|
|
1320
|
-
}
|
|
1321
1729
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1730
|
+
const row = blankRow(key, attr, label);
|
|
1731
|
+
const attrDraft = draft(`attr:${path.join("/")}:${attr}`);
|
|
1732
|
+
const plan: RowPlan = { debounceMs: INPUT_DEBOUNCE };
|
|
1733
|
+
const type = inferInputType(entry);
|
|
1734
|
+
if (entry.type === "boolean") {
|
|
1735
|
+
row.kind = "checkbox";
|
|
1736
|
+
row.checked = Boolean(value);
|
|
1737
|
+
plan.commitChecked = (checked) => commitAttr(checked ? "" : undefined);
|
|
1738
|
+
} else if (type === "media") {
|
|
1739
|
+
row.kind = "control";
|
|
1740
|
+
const staticVal = isRef(value) ? "" : String(value || "");
|
|
1741
|
+
plan.control = (host) =>
|
|
1742
|
+
mountMediaPicker(host, attr, staticVal, (v: string) => commitAttr(v || undefined));
|
|
1743
|
+
} else if (type === "select") {
|
|
1744
|
+
row.kind = "select";
|
|
1745
|
+
row.value = isRef(value) ? "" : String(value || "");
|
|
1746
|
+
row.options = [
|
|
1747
|
+
CLEAR_OPTION,
|
|
1748
|
+
...(Array.isArray(entry.enum) ? (entry.enum as string[]) : []).map((o) => ({
|
|
1749
|
+
label: o,
|
|
1750
|
+
value: o,
|
|
1751
|
+
})),
|
|
1752
|
+
];
|
|
1753
|
+
plan.commit = (raw) => commitAttr(raw || undefined);
|
|
1754
|
+
plan.debounceMs = 0;
|
|
1755
|
+
} else {
|
|
1756
|
+
row.kind = "text";
|
|
1757
|
+
row.value = getFieldValue(attrDraft, isRef(value) ? "" : String(value || ""));
|
|
1758
|
+
plan.commit = (raw) => commitAttr(raw || undefined);
|
|
1759
|
+
plan.draft = attrDraft;
|
|
1341
1760
|
}
|
|
1342
|
-
const values = targets.map((path) => read(getNodeAtPath(doc, path) as JxMutableNode | undefined));
|
|
1343
|
-
return unifyValues(values).mixed ? targets.length : 0;
|
|
1344
|
-
}
|
|
1345
1761
|
|
|
1346
|
-
|
|
1762
|
+
applyLadder(row, plan, {
|
|
1763
|
+
caps: "attribute",
|
|
1764
|
+
extraSignals: ctx.mapSignals,
|
|
1765
|
+
fieldKey: `attr|${path.join("/")}|${attr}`,
|
|
1766
|
+
onChange: commitAttr,
|
|
1767
|
+
stateDefs: ctx.bindableSignals,
|
|
1768
|
+
value,
|
|
1769
|
+
});
|
|
1770
|
+
applyChip(row, plan, attr, chip());
|
|
1771
|
+
return withPlan(row, plan);
|
|
1772
|
+
}
|
|
1347
1773
|
|
|
1348
1774
|
/**
|
|
1349
|
-
* The
|
|
1775
|
+
* The Link target: a kind and a value over one `href`.
|
|
1350
1776
|
*
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
* refuse a section the user is looking at; no validation at all would accept the label the old
|
|
1355
|
-
* `inspector.toggleSection` step passed ("Element", not `__element`) and silently do nothing.
|
|
1356
|
-
*
|
|
1357
|
-
* `__media` left with the breakpoint definitions, which are Project Settings › Contexts now (P4).
|
|
1358
|
-
* `__observed`, `__cssprops` and `__cssparts` are drawn by the Logic tab; the key space is the
|
|
1359
|
-
* INSPECTOR's, not one tab's, so `inspector.setSection` keeps addressing all of them.
|
|
1777
|
+
* `utils/link-target.ts` classifies the current value and composes the next one, so switching kind
|
|
1778
|
+
* REINTERPRETS what is there rather than throwing it away — `a@b.com` picked as Email becomes
|
|
1779
|
+
* `mailto:a@b.com`. Internal targets offer the routes enumerated from the `pages/` tree.
|
|
1360
1780
|
*/
|
|
1361
|
-
|
|
1362
|
-
"
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1781
|
+
function linkRow(key: string, label: string, node: JxMutableNode, path: JxPath): ContentRowView {
|
|
1782
|
+
const raw = typeof node.attributes?.href === "string" ? node.attributes.href : "";
|
|
1783
|
+
const { kind, value } = classifyHref(raw);
|
|
1784
|
+
const commit = (nextKind: LinkKind, nextValue: string) => {
|
|
1785
|
+
const composed = composeHref(nextKind, nextValue);
|
|
1786
|
+
transactDoc(activeTab.value!, (t) =>
|
|
1787
|
+
mutateUpdateAttribute(t, path, "href", composed || undefined),
|
|
1788
|
+
);
|
|
1789
|
+
};
|
|
1369
1790
|
|
|
1370
|
-
|
|
1791
|
+
const row = blankRow(key, "href", label);
|
|
1792
|
+
row.kind = "link";
|
|
1793
|
+
row.linkKind = kind;
|
|
1794
|
+
row.kindOptions = [
|
|
1795
|
+
{ label: "Internal Page", value: "internal" },
|
|
1796
|
+
{ label: "External URL", value: "external" },
|
|
1797
|
+
{ label: "Anchor", value: "anchor" },
|
|
1798
|
+
{ label: "Email", value: "mailto" },
|
|
1799
|
+
{ label: "Phone", value: "tel" },
|
|
1800
|
+
];
|
|
1801
|
+
const plan: RowPlan = {
|
|
1802
|
+
commit: (next) => commit(kind, next),
|
|
1803
|
+
commitKind: (nextKind) => commit(nextKind as LinkKind, value),
|
|
1804
|
+
debounceMs: INPUT_DEBOUNCE,
|
|
1805
|
+
};
|
|
1371
1806
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1807
|
+
if (kind === "internal") {
|
|
1808
|
+
if (pageRouteEntries === null && !pageRoutesLoading) {
|
|
1809
|
+
void loadPageRouteEntries();
|
|
1810
|
+
}
|
|
1811
|
+
row.linkValueKind = "route";
|
|
1812
|
+
row.value = value;
|
|
1813
|
+
// `jx-select` stands in a row for a value no option holds, so a route this document cannot
|
|
1814
|
+
// Enumerate — one in another file, or a listing that failed — stays selected and offered.
|
|
1815
|
+
row.options = (pageRouteEntries ?? []).map((r) => ({ label: r, value: r }));
|
|
1816
|
+
plan.debounceMs = 0;
|
|
1817
|
+
} else {
|
|
1818
|
+
const linkDraft = draft(`link:${path.join("/")}:href`);
|
|
1819
|
+
row.linkValueKind = "text";
|
|
1820
|
+
row.value = getFieldValue(linkDraft, value);
|
|
1821
|
+
row.placeholder =
|
|
1822
|
+
kind === "mailto"
|
|
1823
|
+
? "name@example.com"
|
|
1824
|
+
: kind === "tel"
|
|
1825
|
+
? "+15551234567"
|
|
1826
|
+
: kind === "anchor"
|
|
1827
|
+
? "section-id"
|
|
1828
|
+
: "https://example.com";
|
|
1829
|
+
plan.draft = linkDraft;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
applyChip(
|
|
1833
|
+
row,
|
|
1834
|
+
plan,
|
|
1835
|
+
"href",
|
|
1836
|
+
raw === ""
|
|
1837
|
+
? { state: "default" }
|
|
1838
|
+
: {
|
|
1839
|
+
onClick: () =>
|
|
1840
|
+
transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "href")),
|
|
1841
|
+
state: "set",
|
|
1842
|
+
title: "Clear href",
|
|
1843
|
+
},
|
|
1844
|
+
);
|
|
1845
|
+
return withPlan(row, plan);
|
|
1376
1846
|
}
|
|
1377
1847
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1848
|
+
// ─── Custom attributes ───────────────────────────────────────────────────────
|
|
1849
|
+
|
|
1850
|
+
function customRows(path: JxPath, customAttrs: [string, unknown][]): ContentRowView[] {
|
|
1851
|
+
const rows: ContentRowView[] = customAttrs.map(([attr, val]) => {
|
|
1852
|
+
const key = `kv|${attr}`;
|
|
1853
|
+
const nameKey = draft(`kvname:${path.join("/")}:${attr}`);
|
|
1854
|
+
const valueKey = draft(`kvval:${path.join("/")}:${attr}`);
|
|
1855
|
+
const row = blankRow(key, attr, `${attr} value`);
|
|
1856
|
+
row.shape = "kv";
|
|
1857
|
+
row.name = getFieldValue(nameKey, attr);
|
|
1858
|
+
row.value = getFieldValue(valueKey, String(val));
|
|
1859
|
+
row.removeLabel = `Remove ${attr}`;
|
|
1860
|
+
const plan: RowPlan = {
|
|
1861
|
+
action: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr)),
|
|
1862
|
+
debounceMs: INPUT_DEBOUNCE,
|
|
1863
|
+
kv: {
|
|
1864
|
+
name: attr,
|
|
1865
|
+
nameKey,
|
|
1866
|
+
value: String(val),
|
|
1867
|
+
valueKey,
|
|
1868
|
+
write: (nextAttr, nextVal) => {
|
|
1869
|
+
if (nextAttr !== attr) {
|
|
1870
|
+
transactDoc(activeTab.value, (t) => {
|
|
1871
|
+
mutateUpdateAttribute(t, path, attr);
|
|
1872
|
+
mutateUpdateAttribute(t, path, nextAttr, nextVal);
|
|
1873
|
+
});
|
|
1874
|
+
} else {
|
|
1875
|
+
transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, nextVal));
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
},
|
|
1879
|
+
};
|
|
1880
|
+
return withPlan(row, plan);
|
|
1881
|
+
});
|
|
1882
|
+
rows.push(
|
|
1883
|
+
actionRow("kv|add", "addAttribute", "+ Add attribute", () =>
|
|
1884
|
+
transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "data-", "")),
|
|
1885
|
+
),
|
|
1886
|
+
);
|
|
1887
|
+
return rows;
|
|
1387
1888
|
}
|
|
1388
1889
|
|
|
1890
|
+
// ─── Usage (§9.6) ────────────────────────────────────────────────────────────
|
|
1891
|
+
|
|
1389
1892
|
/**
|
|
1390
|
-
*
|
|
1893
|
+
* The component instance's usage line — "Used on 7 pages →", expanding to the files.
|
|
1391
1894
|
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1895
|
+
* Three things this deliberately does NOT do. It does not render when the host cannot answer
|
|
1896
|
+
* (`usagesSupported()` is `capability.findReferences`): a confident "0" for a component used
|
|
1897
|
+
* everywhere is worse than no line at all. It does not ask on every paint — `peekUsages` is
|
|
1898
|
+
* side-effect-free and only a cold target starts a request, which re-projects once when it lands.
|
|
1899
|
+
* And it does not swallow a failure: a failed count says so and offers Retry, because "we could not
|
|
1900
|
+
* check" and "nothing uses this" are the two answers a user must never confuse.
|
|
1396
1901
|
*/
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1902
|
+
function usagesSection(tagName: string, componentPath: string | null): ContentSectionView | null {
|
|
1903
|
+
const query = { tagName, ...(componentPath ? { path: componentPath } : {}) };
|
|
1904
|
+
const state = peekUsages(query);
|
|
1905
|
+
// ONE gate, and it is the peek itself: a host with no `findReferences` answers "unsupported"
|
|
1906
|
+
// Synchronously, and the section does not exist.
|
|
1907
|
+
if (state?.status === "unsupported") {
|
|
1908
|
+
return null;
|
|
1401
1909
|
}
|
|
1402
|
-
|
|
1910
|
+
if (state === null) {
|
|
1911
|
+
// Cold: ask once, and re-project when the answer arrives.
|
|
1912
|
+
void loadUsages(query).then(() => renderPropertiesPanel());
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const rows: ContentRowView[] = [];
|
|
1916
|
+
let heading = "Usage";
|
|
1917
|
+
if (state === null || state.status === "pending") {
|
|
1918
|
+
rows.push(noteRow("usage|pending", "Counting references…"));
|
|
1919
|
+
} else if (state.status === "failed") {
|
|
1920
|
+
heading = "Usage · unknown";
|
|
1921
|
+
rows.push(
|
|
1922
|
+
noteRow(
|
|
1923
|
+
"usage|failed",
|
|
1924
|
+
`References could not be counted: ${state.message}. This is not the same as “unused”.`,
|
|
1925
|
+
),
|
|
1926
|
+
actionRow("usage|retry", "retryUsages", "Retry", () => {
|
|
1927
|
+
void retryUsages(query).then(() => renderPropertiesPanel());
|
|
1928
|
+
}),
|
|
1929
|
+
);
|
|
1930
|
+
} else {
|
|
1931
|
+
const files = usageFiles(state.result);
|
|
1932
|
+
heading = usageHeadline(state.result);
|
|
1933
|
+
if (files.length === 0) {
|
|
1934
|
+
rows.push(noteRow("usage|none", `Nothing else in this project places <${tagName}> yet.`));
|
|
1935
|
+
} else {
|
|
1936
|
+
for (const file of files) {
|
|
1937
|
+
const row = blankRow(`usage|${file.path}`, file.path, file.path);
|
|
1938
|
+
row.shape = "file";
|
|
1939
|
+
row.text = file.path;
|
|
1940
|
+
row.detail = String(file.count);
|
|
1941
|
+
row.title = file.refs.map((r) => `${r.refType} ${r.ref} ×${r.count}`).join(", ");
|
|
1942
|
+
rows.push(withPlan(row, { debounceMs: 0, openPath: file.path }));
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return sectionView(
|
|
1947
|
+
USAGES_SECTION,
|
|
1948
|
+
heading,
|
|
1949
|
+
isInspectorSectionOpen(USAGES_SECTION, false),
|
|
1950
|
+
rows,
|
|
1951
|
+
0,
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// ─── The two rows that are not fields ────────────────────────────────────────
|
|
1956
|
+
|
|
1957
|
+
function noteRow(key: string, text: string): ContentRowView {
|
|
1958
|
+
const row = blankRow(key, key, "");
|
|
1959
|
+
row.shape = "note";
|
|
1960
|
+
row.text = text;
|
|
1961
|
+
plans.set(key, { debounceMs: 0 });
|
|
1962
|
+
return row;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function actionRow(key: string, prop: string, text: string, run: () => void): ContentRowView {
|
|
1966
|
+
const row = blankRow(key, prop, text);
|
|
1967
|
+
row.shape = "action";
|
|
1968
|
+
row.text = text;
|
|
1969
|
+
return withPlan(row, { action: run, debounceMs: 0 });
|
|
1403
1970
|
}
|
|
1404
1971
|
|
|
1405
1972
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
1406
1973
|
|
|
1407
|
-
/**
|
|
1408
|
-
* The Inspector's section verb. **This is the setter that retires `inspector.toggleSection`.**
|
|
1409
|
-
*
|
|
1410
|
-
* @returns {AnyCommand[]}
|
|
1411
|
-
*/
|
|
1974
|
+
/** The Inspector's section verb. **This is the setter that retires `inspector.toggleSection`.** */
|
|
1412
1975
|
export function inspectorCommands(): AnyCommand[] {
|
|
1413
1976
|
return [
|
|
1414
1977
|
{
|
|
@@ -1467,7 +2030,7 @@ export function inspectorCommands(): AnyCommand[] {
|
|
|
1467
2030
|
const componentPath = componentRegistry.find((c) => c.tagName === tagName)?.path ?? null;
|
|
1468
2031
|
setInspectorSection(USAGES_SECTION, true);
|
|
1469
2032
|
void loadUsages({ tagName, ...(componentPath ? { path: componentPath } : {}) }).then(() =>
|
|
1470
|
-
|
|
2033
|
+
renderPropertiesPanel(),
|
|
1471
2034
|
);
|
|
1472
2035
|
},
|
|
1473
2036
|
title: "Find Usages",
|
|
@@ -1494,11 +2057,7 @@ function componentSelectionTag(): string | null {
|
|
|
1494
2057
|
return tagName.includes("-") ? tagName : null;
|
|
1495
2058
|
}
|
|
1496
2059
|
|
|
1497
|
-
/**
|
|
1498
|
-
* Register the Inspector's section verb.
|
|
1499
|
-
*
|
|
1500
|
-
* @param {CommandRegistry} registry
|
|
1501
|
-
*/
|
|
2060
|
+
/** Register the Inspector's section verb. */
|
|
1502
2061
|
export function registerInspectorCommands(registry: CommandRegistry): void {
|
|
1503
2062
|
registry.registerAll(inspectorCommands());
|
|
1504
2063
|
}
|