@jxsuite/studio 2.0.1 → 2.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/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -1,43 +1,72 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The Content tab — everything about WHAT this element is and says.
|
|
4
|
+
*
|
|
5
|
+
* Plan §6.5 re-split the inspector by task. Content is the element itself (tag, id, class, text),
|
|
6
|
+
* its HTML attributes, its link target, its custom attributes, its component props and its media.
|
|
7
|
+
* Three things left, and each left for a place that already had a better claim on it:
|
|
8
|
+
*
|
|
9
|
+
* - **Repeater · Switch · Observed Attributes · CSS Properties · CSS Parts → the Logic tab.** Wiring
|
|
10
|
+
* a `$switch` and wiring a click handler are the same task (`panels/events-panel.ts`).
|
|
11
|
+
* - **The Page section → the Document Header card.** `panels/head-panel.ts` owns the one layout
|
|
12
|
+
* picker; this panel drew a second one that could disagree with it.
|
|
13
|
+
* - **Media breakpoint DEFINITIONS → Project Settings › Contexts** (P4). Nothing remains here: the
|
|
14
|
+
* only reason adding a breakpoint used to cost you your element selection.
|
|
15
|
+
*
|
|
16
|
+
* What is left is one accordion of rows, all of which go through `ui/field-row.ts`.
|
|
17
|
+
*/
|
|
3
18
|
|
|
4
19
|
import { html, nothing } from "lit-html";
|
|
20
|
+
import type { TemplateResult } from "lit-html";
|
|
5
21
|
import { live } from "lit-html/directives/live.js";
|
|
6
|
-
import { debouncedStyleCommit, getNodeAtPath,
|
|
7
|
-
import { isRef } from "@jxsuite/schema/guards";
|
|
22
|
+
import { debouncedStyleCommit, getNodeAtPath, renderOnly } from "../store";
|
|
23
|
+
import { displayTagName, isRef, isTagExpression } from "@jxsuite/schema/guards";
|
|
8
24
|
import type { DirEntry, JsonValue } from "../types";
|
|
9
25
|
import {
|
|
10
|
-
mutateAddSwitchCase,
|
|
11
|
-
mutateRemoveSwitchCase,
|
|
12
|
-
mutateRenameSwitchCase,
|
|
13
26
|
mutateUpdateAttribute,
|
|
14
|
-
|
|
27
|
+
mutateUpdateDef,
|
|
15
28
|
mutateUpdateProp,
|
|
16
29
|
mutateUpdateProperty,
|
|
17
30
|
transactDoc,
|
|
18
31
|
} from "../tabs/transact";
|
|
19
|
-
import { activeTab } from "../workspace/workspace";
|
|
20
|
-
import {
|
|
32
|
+
import { activeTab, workspace } from "../workspace/workspace";
|
|
33
|
+
import { deriveRefusal } from "../workspace/pane-derive";
|
|
34
|
+
import { primarySelection, unifyValues } from "../tabs/selection";
|
|
35
|
+
import { shell } from "../shell";
|
|
36
|
+
import {
|
|
37
|
+
argsSchema,
|
|
38
|
+
booleanArg,
|
|
39
|
+
booleanProperty,
|
|
40
|
+
stringArg,
|
|
41
|
+
stringProperty,
|
|
42
|
+
} from "../commands/command-args";
|
|
43
|
+
import type { LayoutSelection } from "../shell";
|
|
44
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
45
|
+
import { activeRegistry } from "../commands/active-registry";
|
|
21
46
|
import { componentRegistry } from "../files/components";
|
|
22
47
|
import { widgetForType } from "./style-inputs";
|
|
23
|
-
import { renderFieldRow } from "../ui/field-row";
|
|
48
|
+
import { renderFieldRow, renderKvRow } from "../ui/field-row";
|
|
49
|
+
import type { FieldProvenance } from "./provenance";
|
|
24
50
|
import { renderDynamicSlot, slotMode } from "../ui/dynamic-slot";
|
|
25
51
|
import { spTextArea, spTextField } from "../ui/field-input";
|
|
26
|
-
import {
|
|
27
|
-
attrLabel,
|
|
28
|
-
camelToLabel,
|
|
29
|
-
friendlyNameToVar,
|
|
30
|
-
inferInputType,
|
|
31
|
-
parseCemType,
|
|
32
|
-
} from "../utils/studio-utils";
|
|
52
|
+
import { attrLabel, camelToLabel, inferInputType, parseCemType } from "../utils/studio-utils";
|
|
33
53
|
import { classifyHref, composeHref } from "../utils/link-target";
|
|
34
54
|
import type { LinkKind } from "../utils/link-target";
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
import {
|
|
56
|
+
clickAnythingTo,
|
|
57
|
+
openPageAction,
|
|
58
|
+
renderEmptyState,
|
|
59
|
+
staleSelectionMessage,
|
|
60
|
+
} from "./empty-state";
|
|
38
61
|
import { renderMediaPicker } from "../ui/media-picker";
|
|
39
62
|
import { renderColorSelector } from "../ui/color-selector";
|
|
40
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
loadUsages,
|
|
65
|
+
peekUsages,
|
|
66
|
+
retryUsages,
|
|
67
|
+
usageFiles,
|
|
68
|
+
usageHeadline,
|
|
69
|
+
} from "../services/references";
|
|
41
70
|
import { getPlatform } from "../platform";
|
|
42
71
|
import htmlMeta from "../../data/html-meta.json";
|
|
43
72
|
|
|
@@ -48,14 +77,8 @@ import type {
|
|
|
48
77
|
JxStateDefinition,
|
|
49
78
|
JxStateObject,
|
|
50
79
|
} from "@jxsuite/schema/types";
|
|
51
|
-
import type {
|
|
80
|
+
import type { SignalOption } from "../ui/dynamic-slot";
|
|
52
81
|
import type { JxPath } from "../state";
|
|
53
|
-
import type { SignalDef } from "./signals-panel.js";
|
|
54
|
-
|
|
55
|
-
interface SignalOption {
|
|
56
|
-
value: string;
|
|
57
|
-
label: string;
|
|
58
|
-
}
|
|
59
82
|
|
|
60
83
|
interface HtmlMetaEntry {
|
|
61
84
|
$section: string;
|
|
@@ -69,23 +92,35 @@ interface HtmlMetaEntry {
|
|
|
69
92
|
[key: string]: unknown;
|
|
70
93
|
}
|
|
71
94
|
|
|
95
|
+
// ─── The binding vocabulary, shared with the Logic tab ───────────────────────
|
|
96
|
+
|
|
72
97
|
/**
|
|
73
|
-
*
|
|
98
|
+
* The two extra signals a position inside a repeater template can bind to, or `null` outside one.
|
|
74
99
|
*
|
|
75
|
-
*
|
|
100
|
+
* A "map" segment addresses a repeater template (`[…, "children", i, "map", …]`, or the legacy `[…,
|
|
101
|
+
* "children", "map", …]`), so anything at or below it is inside one.
|
|
102
|
+
*
|
|
103
|
+
* Exported because Content and Logic both offer them and there is one right answer: a `$switch`
|
|
104
|
+
* inside a `$map` can read `$map/item`, and so can the `alt` attribute two nodes below it.
|
|
76
105
|
*/
|
|
77
|
-
function
|
|
78
|
-
return
|
|
106
|
+
export function mapSignalsFor(path: JxPath): SignalOption[] | null {
|
|
107
|
+
return path.includes("map")
|
|
108
|
+
? [
|
|
109
|
+
{ label: "$map/item", value: "$map/item" },
|
|
110
|
+
{ label: "$map/index", value: "$map/index" },
|
|
111
|
+
]
|
|
112
|
+
: null;
|
|
79
113
|
}
|
|
80
114
|
|
|
81
|
-
/**
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
/** State keys a value position may bind to — handlers and Functions are not values. */
|
|
116
|
+
export function bindableSignalNames(doc: JxMutableNode): string[] {
|
|
117
|
+
return Object.entries(doc.state || {})
|
|
118
|
+
.filter(
|
|
119
|
+
([, d]) =>
|
|
120
|
+
!(d as Record<string, unknown>)?.$handler &&
|
|
121
|
+
(d as JxPrototypeDef)?.$prototype !== "Function",
|
|
122
|
+
)
|
|
123
|
+
.map(([defName]) => defName);
|
|
89
124
|
}
|
|
90
125
|
|
|
91
126
|
/**
|
|
@@ -94,7 +129,7 @@ function isInsideMapTemplate(path: JxPath | null) {
|
|
|
94
129
|
* @param {import("@jxsuite/schema/types").JxStateDefinition | undefined} def
|
|
95
130
|
* @returns {string}
|
|
96
131
|
*/
|
|
97
|
-
function defaultAsString(def: JxStateDefinition | undefined) {
|
|
132
|
+
export function defaultAsString(def: JxStateDefinition | undefined) {
|
|
98
133
|
if (!def || typeof def !== "object" || Array.isArray(def)) {
|
|
99
134
|
return "";
|
|
100
135
|
}
|
|
@@ -105,305 +140,78 @@ function defaultAsString(def: JxStateDefinition | undefined) {
|
|
|
105
140
|
return typeof dv === "object" ? JSON.stringify(dv) : String(dv);
|
|
106
141
|
}
|
|
107
142
|
|
|
143
|
+
/** Who a bound value comes from, in the fewest words that are true — the chip's donor text. */
|
|
144
|
+
function bindingDonor(value: unknown): string {
|
|
145
|
+
if (isRef(value)) {
|
|
146
|
+
const ref = value.$ref;
|
|
147
|
+
return ref.startsWith("#/state/") ? ref.slice(8) : ref || "nothing yet";
|
|
148
|
+
}
|
|
149
|
+
return typeof value === "string" ? "a template" : "a formula";
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
153
|
+
* The state entry a bound value reads, when the chip can name one — a `$ref` or a `${…}` template.
|
|
154
|
+
*
|
|
155
|
+
* §6.2's table says a bound chip "click opens the source", and on the Content tab it did not: both
|
|
156
|
+
* bound branches returned a `donor` and a `title` and no `onClick`, so `renderProvenanceChip` drew
|
|
157
|
+
* a handler-less span. The Style tab's chip has jumped to the Data panel since P5; the same promise
|
|
158
|
+
* on the other tab was a label. Same regex as `style-panel.ts`'s `templateSignalOf`, because both
|
|
159
|
+
* spellings of a template (`${x}` and `${state.x}`) name the same entry.
|
|
111
160
|
*/
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const tab = activeTab.value;
|
|
123
|
-
const defs = tab!.doc.document.state || {};
|
|
124
|
-
const boundRef = isRef(rawValue) ? rawValue.$ref : null;
|
|
125
|
-
const isDynamic = slotMode(rawValue) !== "literal";
|
|
161
|
+
function boundSignalOf(value: unknown): string | null {
|
|
162
|
+
if (isRef(value)) {
|
|
163
|
+
const ref = value.$ref;
|
|
164
|
+
return ref.startsWith("#/state/") ? (ref.slice(8).split("/")[0] ?? null) : null;
|
|
165
|
+
}
|
|
166
|
+
if (typeof value !== "string") {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return /\$\{\s*(?:state\.)?([A-Za-z_$][\w$]*)/.exec(value)?.[1] ?? null;
|
|
170
|
+
}
|
|
126
171
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
type === "textarea"
|
|
138
|
-
? spTextArea(draftKey, String(staticVal), (v: string) => onChange(v))
|
|
139
|
-
: type === "checkbox"
|
|
140
|
-
? html`<sp-checkbox
|
|
141
|
-
?checked=${Boolean(staticVal)}
|
|
142
|
-
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
|
|
143
|
-
></sp-checkbox>`
|
|
144
|
-
: spTextField(draftKey, String(staticVal), (v: string) => onChange(v));
|
|
145
|
-
|
|
146
|
-
// De-escalating to literal restores the bound signal's declared default (old unbind behavior).
|
|
147
|
-
const literalDefault = boundRef
|
|
148
|
-
? defaultAsString(defs[boundRef.startsWith("#/state/") ? boundRef.slice(8) : boundRef]) ||
|
|
149
|
-
undefined
|
|
150
|
-
: undefined;
|
|
151
|
-
|
|
152
|
-
const slot = renderDynamicSlot({
|
|
153
|
-
caps,
|
|
154
|
-
extraSignals,
|
|
155
|
-
fieldKey,
|
|
156
|
-
literalDefault,
|
|
157
|
-
onChange,
|
|
158
|
-
staticWidget: staticTpl,
|
|
159
|
-
stateDefs: signalDefs.map(([defName]) => defName),
|
|
160
|
-
value: rawValue,
|
|
161
|
-
});
|
|
162
|
-
return html`
|
|
163
|
-
<div class="field-row">
|
|
164
|
-
<sp-field-label size="s">${label}</sp-field-label>
|
|
165
|
-
${slot.modeButton} ${slot.widget}
|
|
166
|
-
</div>
|
|
167
|
-
`;
|
|
172
|
+
/**
|
|
173
|
+
* Reveal the state entry a bound field reads: the Data panel, and that entry's row.
|
|
174
|
+
*
|
|
175
|
+
* Both verbs, because the rail tab alone leaves you looking at a collapsed list — the same pair the
|
|
176
|
+
* Style tab's chip runs.
|
|
177
|
+
*/
|
|
178
|
+
function revealSignal(name: string): void {
|
|
179
|
+
const registry = activeRegistry();
|
|
180
|
+
void registry?.run("view.setActivity", { tab: "data" });
|
|
181
|
+
void registry?.run("data.expandRow", { name });
|
|
168
182
|
}
|
|
169
183
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The bound chip, naming its donor and — when the donor is a state entry — opening it.
|
|
186
|
+
*
|
|
187
|
+
* The entry list is read here rather than threaded through both callers' signatures: the two
|
|
188
|
+
* provenance functions differ in everything else, and the question "does this document define
|
|
189
|
+
* `title`?" has one answer that neither of them should have to be handed.
|
|
190
|
+
*/
|
|
191
|
+
function boundProvenance(value: unknown, title: string): FieldProvenance {
|
|
192
|
+
const signal = boundSignalOf(value);
|
|
193
|
+
const document = activeTab.value?.doc.document;
|
|
194
|
+
const known = signal && document ? bindableSignalNames(document).includes(signal) : false;
|
|
195
|
+
return {
|
|
196
|
+
donor: bindingDonor(value),
|
|
197
|
+
state: "bound",
|
|
198
|
+
title,
|
|
199
|
+
// Only when the document actually defines it: a chip that jumps to a row that is not there
|
|
200
|
+
// Would be a worse answer than a chip that does not jump.
|
|
201
|
+
...(known && signal ? { onClick: () => revealSignal(signal) } : {}),
|
|
185
202
|
};
|
|
186
|
-
const placeholder = datalistId === "css-props" ? getCssInitialMap().get(key) || "" : "";
|
|
187
|
-
return html`
|
|
188
|
-
<div class="kv-row">
|
|
189
|
-
<sp-textfield
|
|
190
|
-
size="s"
|
|
191
|
-
class="kv-key"
|
|
192
|
-
.value=${live(key)}
|
|
193
|
-
@input=${(e: Event) => {
|
|
194
|
-
currentKey = (e.target as HTMLInputElement).value;
|
|
195
|
-
commit();
|
|
196
|
-
}}
|
|
197
|
-
@change=${
|
|
198
|
-
datalistId === "css-props"
|
|
199
|
-
? (e: Event) => {
|
|
200
|
-
const el = (e.target as HTMLInputElement)
|
|
201
|
-
.closest(".kv-row")
|
|
202
|
-
?.querySelector(".kv-val");
|
|
203
|
-
if (el) {
|
|
204
|
-
el.setAttribute(
|
|
205
|
-
"placeholder",
|
|
206
|
-
getCssInitialMap().get((e.target as HTMLInputElement).value) || "",
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
: nothing
|
|
211
|
-
}
|
|
212
|
-
></sp-textfield>
|
|
213
|
-
<sp-textfield
|
|
214
|
-
size="s"
|
|
215
|
-
class="kv-val"
|
|
216
|
-
.value=${live(value)}
|
|
217
|
-
placeholder=${placeholder}
|
|
218
|
-
@input=${(e: Event) => {
|
|
219
|
-
currentVal = (e.target as HTMLInputElement).value;
|
|
220
|
-
commit();
|
|
221
|
-
}}
|
|
222
|
-
></sp-textfield>
|
|
223
|
-
<sp-action-button size="xs" quiet @click=${onDelete}>
|
|
224
|
-
<sp-icon-close slot="icon"></sp-icon-close>
|
|
225
|
-
</sp-action-button>
|
|
226
|
-
</div>
|
|
227
|
-
`;
|
|
228
203
|
}
|
|
229
204
|
|
|
230
205
|
// ─── Sub-templates ──────────────────────────────────────────────────────────
|
|
231
206
|
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"Items",
|
|
241
|
-
"text",
|
|
242
|
-
node.items,
|
|
243
|
-
(v: JsonValue) =>
|
|
244
|
-
transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, "items", v)),
|
|
245
|
-
null,
|
|
246
|
-
null,
|
|
247
|
-
`prop|${path.join("/")}|items`,
|
|
248
|
-
)}
|
|
249
|
-
${
|
|
250
|
-
node.filter
|
|
251
|
-
? bindableFieldRow(
|
|
252
|
-
"Filter",
|
|
253
|
-
"text",
|
|
254
|
-
node.filter,
|
|
255
|
-
(v: JsonValue) =>
|
|
256
|
-
transactDoc(activeTab.value, (t) =>
|
|
257
|
-
mutateUpdateProperty(t, path, "filter", v || undefined),
|
|
258
|
-
),
|
|
259
|
-
null,
|
|
260
|
-
null,
|
|
261
|
-
`prop|${path.join("/")}|filter`,
|
|
262
|
-
)
|
|
263
|
-
: nothing
|
|
264
|
-
}
|
|
265
|
-
${
|
|
266
|
-
node.sort
|
|
267
|
-
? bindableFieldRow(
|
|
268
|
-
"Sort",
|
|
269
|
-
"text",
|
|
270
|
-
node.sort,
|
|
271
|
-
(v: JsonValue) =>
|
|
272
|
-
transactDoc(activeTab.value, (t) =>
|
|
273
|
-
mutateUpdateProperty(t, path, "sort", v || undefined),
|
|
274
|
-
),
|
|
275
|
-
null,
|
|
276
|
-
null,
|
|
277
|
-
`prop|${path.join("/")}|sort`,
|
|
278
|
-
)
|
|
279
|
-
: nothing
|
|
280
|
-
}
|
|
281
|
-
<div style="display:flex;gap:8px;margin-top:4px">
|
|
282
|
-
${
|
|
283
|
-
!node.filter
|
|
284
|
-
? html`<span
|
|
285
|
-
class="kv-add"
|
|
286
|
-
@click=${() =>
|
|
287
|
-
transactDoc(activeTab.value, (t) =>
|
|
288
|
-
mutateUpdateProperty(t, path, "filter", { $ref: "#/state/" }),
|
|
289
|
-
)}
|
|
290
|
-
>+ Add filter</span
|
|
291
|
-
>`
|
|
292
|
-
: nothing
|
|
293
|
-
}
|
|
294
|
-
${
|
|
295
|
-
!node.sort
|
|
296
|
-
? html`<span
|
|
297
|
-
class="kv-add"
|
|
298
|
-
@click=${() =>
|
|
299
|
-
transactDoc(activeTab.value, (t) =>
|
|
300
|
-
mutateUpdateProperty(t, path, "sort", { $ref: "#/state/" }),
|
|
301
|
-
)}
|
|
302
|
-
>+ Add sort</span
|
|
303
|
-
>`
|
|
304
|
-
: nothing
|
|
305
|
-
}
|
|
306
|
-
</div>
|
|
307
|
-
${
|
|
308
|
-
node.map
|
|
309
|
-
? html`
|
|
310
|
-
<sp-action-button
|
|
311
|
-
size="s"
|
|
312
|
-
style="margin-top:8px;width:100%"
|
|
313
|
-
@click=${() => {
|
|
314
|
-
activeTab.value!.session.selection = [...path, "map"];
|
|
315
|
-
}}
|
|
316
|
-
>Edit template →</sp-action-button
|
|
317
|
-
>
|
|
318
|
-
`
|
|
319
|
-
: nothing
|
|
320
|
-
}
|
|
321
|
-
`;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Switch fields template */
|
|
325
|
-
function renderSwitchFieldsTemplate(
|
|
326
|
-
node: JxMutableNode,
|
|
327
|
-
path: JxPath,
|
|
328
|
-
mapSignals: SignalOption[] | null,
|
|
329
|
-
) {
|
|
330
|
-
const caseNames = Object.keys(node.cases || {});
|
|
331
|
-
return html`
|
|
332
|
-
${bindableFieldRow(
|
|
333
|
-
"Expression",
|
|
334
|
-
"text",
|
|
335
|
-
node.$switch,
|
|
336
|
-
(v: JsonValue) =>
|
|
337
|
-
transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, "$switch", v)),
|
|
338
|
-
null,
|
|
339
|
-
mapSignals,
|
|
340
|
-
`prop|${path.join("/")}|$switch`,
|
|
341
|
-
// No literal rung: a $switch is inherently dynamic.
|
|
342
|
-
// De-escalating to literal would delete the key and demote the node mid-cycle.
|
|
343
|
-
["ref", "template"],
|
|
344
|
-
)}
|
|
345
|
-
<div
|
|
346
|
-
style="font-size:var(--spectrum-font-size-50, 11px);font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
|
|
347
|
-
>
|
|
348
|
-
Cases
|
|
349
|
-
</div>
|
|
350
|
-
${caseNames.map((caseName) => {
|
|
351
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
352
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
353
|
-
return html`
|
|
354
|
-
<div class="field-row" style="display:flex;align-items:center;gap:4px;margin-bottom:3px">
|
|
355
|
-
<input
|
|
356
|
-
class="field-input"
|
|
357
|
-
.value=${live(caseName)}
|
|
358
|
-
style="flex:1"
|
|
359
|
-
@input=${(e: Event) => {
|
|
360
|
-
clearTimeout(debounce);
|
|
361
|
-
debounce = setTimeout(() => {
|
|
362
|
-
if (
|
|
363
|
-
(e.target as HTMLInputElement).value &&
|
|
364
|
-
(e.target as HTMLInputElement).value !== caseName
|
|
365
|
-
) {
|
|
366
|
-
transactDoc(activeTab.value, (t) =>
|
|
367
|
-
mutateRenameSwitchCase(t, path, caseName, (e.target as HTMLInputElement).value),
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
}, 500);
|
|
371
|
-
}}
|
|
372
|
-
/>
|
|
373
|
-
<span
|
|
374
|
-
class="bind-toggle"
|
|
375
|
-
title="Edit case"
|
|
376
|
-
style="cursor:pointer"
|
|
377
|
-
@click=${(e: Event) => {
|
|
378
|
-
e.stopPropagation();
|
|
379
|
-
activeTab.value!.session.selection = [...path, "cases", caseName];
|
|
380
|
-
}}
|
|
381
|
-
>→</span
|
|
382
|
-
>
|
|
383
|
-
<span
|
|
384
|
-
style="cursor:pointer;color:var(--danger);font-size:var(--spectrum-font-size-50, 11px)"
|
|
385
|
-
@click=${(e: Event) => {
|
|
386
|
-
e.stopPropagation();
|
|
387
|
-
transactDoc(activeTab.value!, (t) => mutateRemoveSwitchCase(t, path, caseName));
|
|
388
|
-
}}
|
|
389
|
-
>✕</span
|
|
390
|
-
>
|
|
391
|
-
</div>
|
|
392
|
-
`;
|
|
393
|
-
})}
|
|
394
|
-
<span
|
|
395
|
-
class="kv-add"
|
|
396
|
-
@click=${() => {
|
|
397
|
-
transactDoc(activeTab.value, (t) =>
|
|
398
|
-
mutateAddSwitchCase(t, path, `case${caseNames.length + 1}`),
|
|
399
|
-
);
|
|
400
|
-
}}
|
|
401
|
-
>+ Add case</span
|
|
402
|
-
>
|
|
403
|
-
`;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/** Component props fields template */
|
|
207
|
+
/**
|
|
208
|
+
* Component props, each row carrying §6.2's provenance chip — the SECOND cascade.
|
|
209
|
+
*
|
|
210
|
+
* A component instance's value for a prop either overrides the component's declared default or does
|
|
211
|
+
* not exist, and until now the two were the same blank field. The chip states which, and in the
|
|
212
|
+
* inherited case it names the donor AND jumps to it: the donor of a component default is the
|
|
213
|
+
* component definition, which this panel can already open.
|
|
214
|
+
*/
|
|
407
215
|
function renderComponentPropsFieldsTemplate(
|
|
408
216
|
node: JxMutableNode,
|
|
409
217
|
path: JxPath,
|
|
@@ -411,140 +219,147 @@ function renderComponentPropsFieldsTemplate(
|
|
|
411
219
|
navigateToComponent: (path: string) => void,
|
|
412
220
|
) {
|
|
413
221
|
const tab = activeTab.value;
|
|
414
|
-
const comp = componentRegistry.find((c) => c.tagName === node.tagName);
|
|
222
|
+
const comp = componentRegistry.find((c) => c.tagName === displayTagName(node.tagName));
|
|
223
|
+
const definitionOfThis = isComponentDefinitionOpen(node);
|
|
415
224
|
if (!comp || !comp.props) {
|
|
416
|
-
return
|
|
225
|
+
return renderEmptyState({
|
|
226
|
+
compact: true,
|
|
227
|
+
message: "This component is not in the project's library, so it has no settings to show.",
|
|
228
|
+
});
|
|
417
229
|
}
|
|
418
230
|
const isNpm = comp.source === "npm";
|
|
419
|
-
|
|
420
|
-
|
|
231
|
+
/* In the definition the "value" of a setting is its DECLARED DEFAULT — the state entry's
|
|
232
|
+
`default` — not a per-instance override, so the row reads and writes there instead. */
|
|
233
|
+
const stateEntries = (tab?.doc.document.state ?? {}) as Record<string, { default?: JsonValue }>;
|
|
234
|
+
const currentVals: Record<string, unknown> = definitionOfThis
|
|
235
|
+
? Object.fromEntries(comp.props.map((prop) => [prop.name, stateEntries[prop.name]?.default]))
|
|
236
|
+
: isNpm
|
|
237
|
+
? node.attributes || {}
|
|
238
|
+
: node.$props || {};
|
|
239
|
+
const updateFn = definitionOfThis
|
|
421
240
|
? (name: string, v?: JsonValue) =>
|
|
422
|
-
transactDoc(activeTab.value, (t) =>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
241
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: v }))
|
|
242
|
+
: isNpm
|
|
243
|
+
? (name: string, v?: JsonValue) =>
|
|
244
|
+
transactDoc(activeTab.value, (t) =>
|
|
245
|
+
mutateUpdateAttribute(
|
|
246
|
+
t,
|
|
247
|
+
path,
|
|
248
|
+
name,
|
|
249
|
+
v === "" ? undefined : (v as JxAttributeValue | undefined),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
: (name: string, v?: JsonValue) =>
|
|
253
|
+
transactDoc(activeTab.value, (t) => mutateUpdateProp(t, path, name, v));
|
|
432
254
|
|
|
433
255
|
const defs = tab!.doc.document.state || {};
|
|
434
|
-
const
|
|
435
|
-
([, d]) =>
|
|
436
|
-
!(d as Record<string, unknown>)?.$handler && (d as JxPrototypeDef)?.$prototype !== "Function",
|
|
437
|
-
);
|
|
256
|
+
const signalNames = bindableSignalNames(tab!.doc.document);
|
|
438
257
|
const extraSignals = mapSignals;
|
|
439
258
|
|
|
440
259
|
return html`
|
|
441
|
-
${comp.props.map(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
updateFn(prop.name);
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
// De-escalating to literal restores the bound signal's declared default (old unbind behavior).
|
|
458
|
-
const literalDefault = boundRef
|
|
459
|
-
? defaultAsString(defs[boundRef.startsWith("#/state/") ? boundRef.slice(8) : boundRef]) ||
|
|
460
|
-
undefined
|
|
461
|
-
: undefined;
|
|
462
|
-
|
|
463
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
464
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
465
|
-
let widgetTpl;
|
|
466
|
-
if (prop.format === "image") {
|
|
467
|
-
widgetTpl = renderMediaPicker(prop.name, staticVal, onChange);
|
|
468
|
-
} else if (prop.format === "color") {
|
|
469
|
-
widgetTpl = renderColorSelector(prop.name, staticVal, onChange);
|
|
470
|
-
} else if (prop.format === "date") {
|
|
471
|
-
widgetTpl = spTextField(
|
|
472
|
-
`cprop:${prop.name}`,
|
|
473
|
-
String(staticVal),
|
|
474
|
-
(v: string) => onChange(v),
|
|
475
|
-
{
|
|
476
|
-
placeholder: "YYYY-MM-DD",
|
|
477
|
-
},
|
|
478
|
-
);
|
|
479
|
-
} else if (parsed.kind === "boolean") {
|
|
480
|
-
widgetTpl = html`<sp-checkbox
|
|
481
|
-
size="s"
|
|
482
|
-
.checked=${live(Boolean(staticVal))}
|
|
483
|
-
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked || undefined)}
|
|
484
|
-
></sp-checkbox>`;
|
|
485
|
-
} else if (parsed.kind === "number") {
|
|
486
|
-
widgetTpl = html`<sp-number-field
|
|
487
|
-
size="s"
|
|
488
|
-
.value=${live(staticVal)}
|
|
489
|
-
@input=${(e: Event) => {
|
|
490
|
-
clearTimeout(debounce);
|
|
491
|
-
debounce = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
|
|
492
|
-
}}
|
|
493
|
-
></sp-number-field>`;
|
|
494
|
-
} else if (parsed.kind === "combobox") {
|
|
495
|
-
const options = /** @type {{ options?: string[] }} */ parsed.options as string[];
|
|
496
|
-
widgetTpl = html`<jx-value-selector
|
|
497
|
-
.value=${String(staticVal)}
|
|
498
|
-
size="s"
|
|
499
|
-
placeholder="—"
|
|
500
|
-
.options=${options.map((o) => ({
|
|
501
|
-
label: camelToLabel(o),
|
|
502
|
-
value: o,
|
|
503
|
-
}))}
|
|
504
|
-
@change=${(e: Event & { detail?: { value?: string } }) =>
|
|
505
|
-
onChange(e.detail?.value ?? (e.target as HTMLInputElement).value)}
|
|
506
|
-
></jx-value-selector>`;
|
|
507
|
-
} else {
|
|
508
|
-
widgetTpl = spTextField(`cprop:${prop.name}`, String(staticVal), (v: string) =>
|
|
509
|
-
onChange(v),
|
|
510
|
-
);
|
|
511
|
-
}
|
|
260
|
+
${comp.props.map((prop) => {
|
|
261
|
+
const rawValue = currentVals[prop.name];
|
|
262
|
+
const boundRef = isRef(rawValue) ? rawValue.$ref : null;
|
|
263
|
+
const hasVal = rawValue !== undefined && rawValue !== null;
|
|
264
|
+
const parsed = parseCemType(prop.type);
|
|
265
|
+
const onChange = (v?: JsonValue) => updateFn(prop.name, v);
|
|
266
|
+
const staticVal = slotMode(rawValue) === "literal" ? String(rawValue ?? "") : "";
|
|
267
|
+
|
|
268
|
+
// De-escalating to literal restores the bound signal's declared default (old unbind behavior).
|
|
269
|
+
const literalDefault = boundRef
|
|
270
|
+
? defaultAsString(defs[boundRef.startsWith("#/state/") ? boundRef.slice(8) : boundRef]) ||
|
|
271
|
+
undefined
|
|
272
|
+
: undefined;
|
|
512
273
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
274
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
275
|
+
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
276
|
+
let widgetTpl;
|
|
277
|
+
if (prop.format === "image") {
|
|
278
|
+
widgetTpl = renderMediaPicker(prop.name, staticVal, onChange);
|
|
279
|
+
} else if (prop.format === "color") {
|
|
280
|
+
widgetTpl = renderColorSelector(prop.name, staticVal, onChange);
|
|
281
|
+
} else if (prop.format === "date") {
|
|
282
|
+
widgetTpl = spTextField(
|
|
283
|
+
`cprop:${prop.name}`,
|
|
284
|
+
String(staticVal),
|
|
285
|
+
(v: string) => onChange(v),
|
|
286
|
+
{
|
|
287
|
+
placeholder: "YYYY-MM-DD",
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
} else if (parsed.kind === "boolean") {
|
|
291
|
+
widgetTpl = html`<sp-checkbox
|
|
292
|
+
size="s"
|
|
293
|
+
.checked=${live(Boolean(staticVal))}
|
|
294
|
+
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked || undefined)}
|
|
295
|
+
></sp-checkbox>`;
|
|
296
|
+
} else if (parsed.kind === "number") {
|
|
297
|
+
widgetTpl = html`<sp-number-field
|
|
298
|
+
size="s"
|
|
299
|
+
.value=${live(staticVal)}
|
|
300
|
+
@input=${(e: Event) => {
|
|
301
|
+
clearTimeout(debounce);
|
|
302
|
+
debounce = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
|
|
303
|
+
}}
|
|
304
|
+
></sp-number-field>`;
|
|
305
|
+
} else if (parsed.kind === "combobox") {
|
|
306
|
+
const options = parsed.options as string[];
|
|
307
|
+
widgetTpl = html`<jx-value-selector
|
|
308
|
+
.value=${String(staticVal)}
|
|
309
|
+
size="s"
|
|
310
|
+
placeholder="—"
|
|
311
|
+
.options=${options.map((o) => ({ label: camelToLabel(o), value: o }))}
|
|
312
|
+
@change=${(e: Event & { detail?: { value?: string } }) =>
|
|
313
|
+
onChange(e.detail?.value ?? (e.target as HTMLInputElement).value)}
|
|
314
|
+
></jx-value-selector>`;
|
|
315
|
+
} else {
|
|
316
|
+
widgetTpl = spTextField(`cprop:${prop.name}`, String(staticVal), (v: string) =>
|
|
317
|
+
onChange(v),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const slot = renderDynamicSlot({
|
|
322
|
+
caps: "componentProp",
|
|
323
|
+
extraSignals,
|
|
324
|
+
fieldKey: `cprop|${path.join("/")}|${prop.name}`,
|
|
325
|
+
literalDefault,
|
|
326
|
+
onChange,
|
|
327
|
+
staticWidget: widgetTpl,
|
|
328
|
+
stateDefs: signalNames,
|
|
329
|
+
value: rawValue,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return renderFieldRow({
|
|
333
|
+
hasValue: hasVal,
|
|
334
|
+
label: camelToLabel(prop.name),
|
|
335
|
+
labelExtra: slot.modeButton,
|
|
336
|
+
prop: prop.name,
|
|
337
|
+
provenance: componentPropProvenance(prop, rawValue, hasVal, {
|
|
338
|
+
// No donor and no jump in the definition: the value IS the component's default, and
|
|
339
|
+
// "→ the component definition" would open the document already in front of you.
|
|
340
|
+
isDefinition: definitionOfThis,
|
|
341
|
+
onClear: () => updateFn(prop.name),
|
|
342
|
+
...(definitionOfThis || !comp.path
|
|
343
|
+
? {}
|
|
344
|
+
: { openDefinition: () => navigateToComponent(comp.path!) }),
|
|
345
|
+
}),
|
|
346
|
+
widget: slot.widget,
|
|
347
|
+
});
|
|
348
|
+
})}
|
|
546
349
|
${
|
|
547
|
-
comp.
|
|
350
|
+
comp.props.length === 0
|
|
351
|
+
? renderEmptyState({
|
|
352
|
+
compact: true,
|
|
353
|
+
message: definitionOfThis
|
|
354
|
+
? "This component declares no settings yet. Add a state entry with a default to " +
|
|
355
|
+
"give whoever places it something to fill in."
|
|
356
|
+
: "This component has no settings to fill in yet.",
|
|
357
|
+
})
|
|
358
|
+
: nothing
|
|
359
|
+
}
|
|
360
|
+
${
|
|
361
|
+
// A link to the definition, from inside the definition, is a link to here.
|
|
362
|
+
comp.path && !definitionOfThis
|
|
548
363
|
? html`<span class="kv-add" @click=${() => navigateToComponent(comp.path!)}
|
|
549
364
|
>→ Edit definition</span
|
|
550
365
|
>`
|
|
@@ -553,9 +368,60 @@ function renderComponentPropsFieldsTemplate(
|
|
|
553
368
|
`;
|
|
554
369
|
}
|
|
555
370
|
|
|
556
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Is the selected node an INSTANCE of a component, or the definition of one?
|
|
373
|
+
*
|
|
374
|
+
* A component's own root tag has a hyphen, so "the tag contains a dash" answers yes to both — and
|
|
375
|
+
* that is what put the instance form in front of someone editing the component itself. The
|
|
376
|
+
* distinguishing fact is whose document this is: if the component's definition path IS the open
|
|
377
|
+
* file, there is no instance, no override and no donor to jump to.
|
|
378
|
+
*/
|
|
379
|
+
export function isComponentDefinitionOpen(node: JxMutableNode): boolean {
|
|
380
|
+
const comp = componentRegistry.find((c) => c.tagName === displayTagName(node.tagName));
|
|
381
|
+
const open = activeTab.value?.documentPath;
|
|
382
|
+
return comp?.path !== undefined && open !== undefined && comp.path === open;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Which of §6.2's four states a component-prop row is in.
|
|
387
|
+
*
|
|
388
|
+
* Bound beats set (a `$ref` IS a value, but "bound to `title`" is the more useful sentence); set
|
|
389
|
+
* beats inherited; a prop with no declared default and no value is plain Default and draws
|
|
390
|
+
* nothing.
|
|
391
|
+
*/
|
|
392
|
+
function componentPropProvenance(
|
|
393
|
+
prop: { name: string; default?: unknown; description?: string },
|
|
394
|
+
rawValue: unknown,
|
|
395
|
+
hasVal: boolean,
|
|
396
|
+
actions: {
|
|
397
|
+
onClear: () => void;
|
|
398
|
+
openDefinition?: (() => void) | undefined;
|
|
399
|
+
/** Editing the component itself, where there is no instance and therefore no donor. */
|
|
400
|
+
isDefinition?: boolean;
|
|
401
|
+
},
|
|
402
|
+
): FieldProvenance {
|
|
403
|
+
if (hasVal && slotMode(rawValue) !== "literal") {
|
|
404
|
+
return boundProvenance(rawValue, `Bound — ${prop.name}`);
|
|
405
|
+
}
|
|
406
|
+
if (hasVal) {
|
|
407
|
+
return { onClick: actions.onClear, state: "set", title: `Clear ${prop.name}` };
|
|
408
|
+
}
|
|
409
|
+
/* INHERITED is a statement about somewhere ELSE, so it cannot be true here. In the definition an
|
|
410
|
+
unset field means this component ships no default for that setting — which is `default`, the
|
|
411
|
+
state that draws nothing, not a badge pointing at itself. */
|
|
412
|
+
if (prop.default !== undefined && !actions.isDefinition) {
|
|
413
|
+
return {
|
|
414
|
+
donor: "the component default",
|
|
415
|
+
state: "inherited",
|
|
416
|
+
title: `Not set here — the component's default (${String(prop.default)}) applies`,
|
|
417
|
+
...(actions.openDefinition ? { onClick: actions.openDefinition } : {}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return { state: "default" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Custom attrs fields template — attributes this element's schema does not know about. */
|
|
557
424
|
function renderCustomAttrsFieldsTemplate(
|
|
558
|
-
_node: JxMutableNode,
|
|
559
425
|
path: JxPath,
|
|
560
426
|
attrs: Record<string, unknown>,
|
|
561
427
|
knownAttrNames: Set<string>,
|
|
@@ -563,10 +429,9 @@ function renderCustomAttrsFieldsTemplate(
|
|
|
563
429
|
const customAttrs = Object.entries(attrs).filter(([k]) => !knownAttrNames.has(k));
|
|
564
430
|
return html`
|
|
565
431
|
${customAttrs.map(([attr, val]) =>
|
|
566
|
-
|
|
567
|
-
attr,
|
|
568
|
-
|
|
569
|
-
(newAttr: string, newVal: string) => {
|
|
432
|
+
renderKvRow({
|
|
433
|
+
name: attr,
|
|
434
|
+
onCommit: (newAttr: string, newVal: string) => {
|
|
570
435
|
if (newAttr !== attr) {
|
|
571
436
|
transactDoc(activeTab.value, (t) => {
|
|
572
437
|
mutateUpdateAttribute(t, path, attr);
|
|
@@ -576,230 +441,18 @@ function renderCustomAttrsFieldsTemplate(
|
|
|
576
441
|
transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, newVal));
|
|
577
442
|
}
|
|
578
443
|
},
|
|
579
|
-
() => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr)),
|
|
580
|
-
|
|
444
|
+
onDelete: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr)),
|
|
445
|
+
value: String(val),
|
|
446
|
+
}),
|
|
581
447
|
)}
|
|
582
448
|
<span
|
|
583
449
|
class="kv-add"
|
|
584
|
-
@click=${() =>
|
|
585
|
-
transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "data-", ""))}
|
|
450
|
+
@click=${() => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "data-", ""))}
|
|
586
451
|
>+ Add attribute</span
|
|
587
452
|
>
|
|
588
453
|
`;
|
|
589
454
|
}
|
|
590
455
|
|
|
591
|
-
// ─── Media breakpoints ──────────────────────────────────────────────────────
|
|
592
|
-
|
|
593
|
-
/** Media breakpoint fields template */
|
|
594
|
-
function renderMediaFieldsTemplate(node: JxMutableNode) {
|
|
595
|
-
const media = node.$media || {};
|
|
596
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
597
|
-
let baseDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
598
|
-
const breakpoints = Object.entries(media).filter(([k]) => k !== "--");
|
|
599
|
-
|
|
600
|
-
return html`
|
|
601
|
-
<div class="kv-row" style="align-items:center">
|
|
602
|
-
<span class="field-label" style="width:auto;margin-right:4px">Base width</span>
|
|
603
|
-
<input
|
|
604
|
-
class="field-input"
|
|
605
|
-
style="width:70px;flex:none"
|
|
606
|
-
placeholder="320px"
|
|
607
|
-
.value=${live(media["--"] || "")}
|
|
608
|
-
@input=${(e: Event) => {
|
|
609
|
-
clearTimeout(baseDebounce);
|
|
610
|
-
baseDebounce = setTimeout(() => {
|
|
611
|
-
const val = (e.target as HTMLInputElement).value.trim();
|
|
612
|
-
transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, "--", val || undefined));
|
|
613
|
-
}, 400);
|
|
614
|
-
}}
|
|
615
|
-
/>
|
|
616
|
-
${
|
|
617
|
-
media["--"]
|
|
618
|
-
? html`<span
|
|
619
|
-
class="kv-del"
|
|
620
|
-
@click=${() => transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, "--"))}
|
|
621
|
-
>✕</span
|
|
622
|
-
>`
|
|
623
|
-
: nothing
|
|
624
|
-
}
|
|
625
|
-
</div>
|
|
626
|
-
|
|
627
|
-
${breakpoints.map(([name, query]) => mediaBreakpointRowTemplate(name, query))}
|
|
628
|
-
|
|
629
|
-
<div>
|
|
630
|
-
<span
|
|
631
|
-
class="kv-add"
|
|
632
|
-
style=${view.showAddBreakpointForm ? "display:none" : ""}
|
|
633
|
-
@click=${(_e: Event) => {
|
|
634
|
-
view.showAddBreakpointForm = true;
|
|
635
|
-
renderOnly("rightPanel");
|
|
636
|
-
}}
|
|
637
|
-
>+ Add breakpoint</span
|
|
638
|
-
>
|
|
639
|
-
${
|
|
640
|
-
view.showAddBreakpointForm
|
|
641
|
-
? html`
|
|
642
|
-
<div style="margin-top:4px">
|
|
643
|
-
<div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
|
|
644
|
-
<input
|
|
645
|
-
class="field-input"
|
|
646
|
-
placeholder="Name (e.g. Tablet)"
|
|
647
|
-
style="flex:1"
|
|
648
|
-
@input=${(e: Event) => {
|
|
649
|
-
view.addBreakpointPreview =
|
|
650
|
-
friendlyNameToMedia((e.target as HTMLInputElement).value) || "";
|
|
651
|
-
renderOnly("rightPanel");
|
|
652
|
-
}}
|
|
653
|
-
/>
|
|
654
|
-
<span
|
|
655
|
-
style="font-size:10px;color:var(--fg-dim);font-family:var(--font-mono);white-space:nowrap"
|
|
656
|
-
>${view.addBreakpointPreview}</span
|
|
657
|
-
>
|
|
658
|
-
</div>
|
|
659
|
-
<div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
|
|
660
|
-
<input
|
|
661
|
-
class="field-input add-bp-query"
|
|
662
|
-
value="(min-width: 768px)"
|
|
663
|
-
style="flex:1"
|
|
664
|
-
/>
|
|
665
|
-
</div>
|
|
666
|
-
<div style="display:flex;gap:4px">
|
|
667
|
-
<button
|
|
668
|
-
class="kv-add"
|
|
669
|
-
style="padding:2px 10px;cursor:pointer"
|
|
670
|
-
@click=${(e: Event) => {
|
|
671
|
-
const wrap = (e.target as HTMLElement).closest("div")?.parentElement;
|
|
672
|
-
const nameVal = wrap?.querySelector("input")?.value;
|
|
673
|
-
const queryVal = (
|
|
674
|
-
wrap?.querySelector(".add-bp-query") as HTMLInputElement | null
|
|
675
|
-
)?.value?.trim();
|
|
676
|
-
const key = friendlyNameToMedia(nameVal || "");
|
|
677
|
-
if (key && queryVal) {
|
|
678
|
-
view.showAddBreakpointForm = false;
|
|
679
|
-
view.addBreakpointPreview = "";
|
|
680
|
-
transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, key, queryVal));
|
|
681
|
-
}
|
|
682
|
-
}}
|
|
683
|
-
>
|
|
684
|
-
Add
|
|
685
|
-
</button>
|
|
686
|
-
<button
|
|
687
|
-
class="kv-add"
|
|
688
|
-
style="padding:2px 10px;cursor:pointer;color:var(--fg-dim)"
|
|
689
|
-
@click=${() => {
|
|
690
|
-
view.showAddBreakpointForm = false;
|
|
691
|
-
view.addBreakpointPreview = "";
|
|
692
|
-
renderOnly("rightPanel");
|
|
693
|
-
}}
|
|
694
|
-
>
|
|
695
|
-
Cancel
|
|
696
|
-
</button>
|
|
697
|
-
</div>
|
|
698
|
-
</div>
|
|
699
|
-
`
|
|
700
|
-
: nothing
|
|
701
|
-
}
|
|
702
|
-
</div>
|
|
703
|
-
`;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/** Single media breakpoint row template */
|
|
707
|
-
function mediaBreakpointRowTemplate(name: string, query: string) {
|
|
708
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
709
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
710
|
-
let currentRawLabel = name;
|
|
711
|
-
return html`
|
|
712
|
-
<div style="margin-bottom:6px;padding:4px 0;border-bottom:1px solid var(--border)">
|
|
713
|
-
<div style="display:flex;align-items:center;gap:4px;margin-bottom:2px">
|
|
714
|
-
<input
|
|
715
|
-
class="field-input"
|
|
716
|
-
.value=${live(mediaDisplayName(name))}
|
|
717
|
-
style="flex:1;font-weight:600;font-size:var(--spectrum-font-size-75, 12px)"
|
|
718
|
-
@input=${(e: Event) => {
|
|
719
|
-
const newKey = friendlyNameToMedia((e.target as HTMLInputElement).value);
|
|
720
|
-
currentRawLabel = newKey || "";
|
|
721
|
-
const rawEl = (e.target as HTMLElement).parentElement?.querySelector(".bp-raw-label");
|
|
722
|
-
if (rawEl) {
|
|
723
|
-
rawEl.textContent = currentRawLabel;
|
|
724
|
-
}
|
|
725
|
-
clearTimeout(debounceTimer);
|
|
726
|
-
debounceTimer = setTimeout(() => {
|
|
727
|
-
if (newKey && newKey !== name) {
|
|
728
|
-
const queryEl = (e.target as HTMLElement)
|
|
729
|
-
.closest("div[style]")
|
|
730
|
-
?.parentElement?.querySelector(".bp-query-input") as HTMLInputElement | null;
|
|
731
|
-
transactDoc(activeTab.value, (t) => {
|
|
732
|
-
mutateUpdateMedia(t, name);
|
|
733
|
-
mutateUpdateMedia(t, newKey, queryEl?.value || query);
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
}, 600);
|
|
737
|
-
}}
|
|
738
|
-
/>
|
|
739
|
-
<span
|
|
740
|
-
class="bp-raw-label"
|
|
741
|
-
style="font-size:10px;color:var(--fg-dim);font-family:var(--font-mono);white-space:nowrap"
|
|
742
|
-
>${name}</span
|
|
743
|
-
>
|
|
744
|
-
<span
|
|
745
|
-
class="kv-del"
|
|
746
|
-
@click=${() => transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, name))}
|
|
747
|
-
>✕</span
|
|
748
|
-
>
|
|
749
|
-
</div>
|
|
750
|
-
<div style="display:flex;gap:4px;align-items:center">
|
|
751
|
-
<input
|
|
752
|
-
class="field-input bp-query-input"
|
|
753
|
-
.value=${live(query)}
|
|
754
|
-
style="flex:1"
|
|
755
|
-
@input=${(e: Event) => {
|
|
756
|
-
clearTimeout(debounceTimer);
|
|
757
|
-
debounceTimer = setTimeout(
|
|
758
|
-
() =>
|
|
759
|
-
transactDoc(activeTab.value, (t) =>
|
|
760
|
-
mutateUpdateMedia(t, name, (e.target as HTMLInputElement).value),
|
|
761
|
-
),
|
|
762
|
-
400,
|
|
763
|
-
);
|
|
764
|
-
}}
|
|
765
|
-
/>
|
|
766
|
-
</div>
|
|
767
|
-
</div>
|
|
768
|
-
`;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// ─── Layout picker ──────────────────────────────────────────────────────────
|
|
772
|
-
|
|
773
|
-
/** @type {{ name: string; path: string }[] | null} */
|
|
774
|
-
let layoutEntries: { name: string; path: string }[] | null = null;
|
|
775
|
-
|
|
776
|
-
async function loadLayoutEntries() {
|
|
777
|
-
try {
|
|
778
|
-
const platform = getPlatform();
|
|
779
|
-
const listing = await platform.listDirectory("layouts");
|
|
780
|
-
layoutEntries = listing
|
|
781
|
-
.filter((f: DirEntry) => f.type === "file" && f.name.endsWith(".json"))
|
|
782
|
-
.map((f: DirEntry) => ({
|
|
783
|
-
name: f.name.replace(/\.json$/, ""),
|
|
784
|
-
path: `./layouts/${f.name}`,
|
|
785
|
-
}));
|
|
786
|
-
} catch {
|
|
787
|
-
layoutEntries = [];
|
|
788
|
-
}
|
|
789
|
-
renderOnly("rightPanel");
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
export function invalidateLayoutPickerCache() {
|
|
793
|
-
layoutEntries = null;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function isPageDocument(documentPath: string | undefined | null) {
|
|
797
|
-
if (!documentPath || !projectState?.isSiteProject) {
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
return documentPath.startsWith("pages/") || documentPath.startsWith("./pages/");
|
|
801
|
-
}
|
|
802
|
-
|
|
803
456
|
// ─── Page-route enumeration (for the Link-target Internal picker) ─────────────
|
|
804
457
|
|
|
805
458
|
/** @type {string[] | null} — cached list of internal routes derived from the pages/ tree. */
|
|
@@ -996,128 +649,102 @@ function isBoundAttrValue(value: unknown): boolean {
|
|
|
996
649
|
return isRef(value) || (typeof value === "string" && value.includes("${"));
|
|
997
650
|
}
|
|
998
651
|
|
|
999
|
-
|
|
1000
|
-
const tab = activeTab.value;
|
|
1001
|
-
if (!isPageDocument(tab!.documentPath)) {
|
|
1002
|
-
return nothing;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
if (layoutEntries === null) {
|
|
1006
|
-
void loadLayoutEntries();
|
|
1007
|
-
return nothing;
|
|
1008
|
-
}
|
|
652
|
+
// ─── Layout selection panel ─────────────────────────────────────────────────
|
|
1009
653
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
654
|
+
/*
|
|
655
|
+
* There is no `openLayoutAtNode`, and every line of it was wrong by the time it was deleted.
|
|
656
|
+
*
|
|
657
|
+
* Its docstring said `navigateToComponent` "swaps the tab's document (pushing the page onto the
|
|
658
|
+
* document stack)" — two releases stale: nothing has swapped since §14.1 landed and there is no
|
|
659
|
+
* document stack. Then it did four things, and the last three were the defect:
|
|
660
|
+
*
|
|
661
|
+
* 1. **Navigate.** Now the command's job, and the command opens the layout BESIDE the page rather
|
|
662
|
+
* than over the page it is teaching about — §8.2's promise since P3, never shipped.
|
|
663
|
+
* 2. **`setLayoutSelection(null)`.** Its stated reason — "the layout is now the open document, so
|
|
664
|
+
* its nodes are ordinary editable content" — stops being true the moment the layout opens beside
|
|
665
|
+
* the page rather than replacing it. Clearing it is precisely what killed the follow on its first
|
|
666
|
+
* frame, and it is what made the Inspector blank at the instant the layout appeared.
|
|
667
|
+
* 3. **Re-select against `activeTab`.** The focus deliberately stays in the page now, so the tab
|
|
668
|
+
* this reached was the wrong one. The selection is carried by the derivation instead —
|
|
669
|
+
* `pane-derive.ts`'s `DerivedTarget.select`, read from `shell.layoutSelection`'s `layoutPath`
|
|
670
|
+
* and applied to the companion pane's tab once the open resolves. For one release that sentence
|
|
671
|
+
* was written here and nothing implemented it: `layoutPath` had no consumer outside the canvas
|
|
672
|
+
* hit test, `derivationFor("layout")` stored `{resolved: null}`, and the exact regression this
|
|
673
|
+
* ledger entry describes — dropped into a layout file with nothing selected — was back while
|
|
674
|
+
* the comment denied it.
|
|
675
|
+
*
|
|
676
|
+
* The companion resolves `shell.layoutSelection.layoutFile` in preference to the page's own
|
|
677
|
+
* `$layout` for the same reason the old code did: they are different answers for a nested chain,
|
|
678
|
+
* and the one the author clicked is the one they meant.
|
|
679
|
+
* 4. **`renderOnly("rightPanel")`.** `panels/right-panel.ts:106` tracks `shell.layoutSelection`
|
|
680
|
+
* itself and `:212` reads it, so the repaint was already subscribed — a second, imperative
|
|
681
|
+
* subscription that only looked necessary because the function above had just cleared the state
|
|
682
|
+
* the real one is keyed on.
|
|
683
|
+
*/
|
|
1014
684
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
invalidateLayoutCache();
|
|
1047
|
-
}}
|
|
1048
|
-
>
|
|
1049
|
-
<sp-menu-item value="__default__"
|
|
1050
|
-
>Default${
|
|
1051
|
-
defaultLayout
|
|
1052
|
-
? ` (${defaultLayout.replace(/^\.\/layouts\//, "").replace(/\.json$/, "")})`
|
|
1053
|
-
: ""
|
|
1054
|
-
}</sp-menu-item
|
|
1055
|
-
>
|
|
1056
|
-
<sp-menu-item value="__none__">None</sp-menu-item>
|
|
1057
|
-
<sp-menu-divider></sp-menu-divider>
|
|
1058
|
-
${layoutEntries!.map(
|
|
1059
|
-
(l: { name: string; path: string }) =>
|
|
1060
|
-
html`<sp-menu-item value=${l.path}>${l.name}</sp-menu-item>`,
|
|
1061
|
-
)}
|
|
1062
|
-
</sp-picker>
|
|
1063
|
-
</div>
|
|
1064
|
-
${
|
|
1065
|
-
effectivePath
|
|
1066
|
-
? html`<div style="font-size:10px;color:var(--fg-dim);padding:2px 0;font-style:italic">
|
|
1067
|
-
Wraps page content via <slot> distribution
|
|
1068
|
-
</div>`
|
|
1069
|
-
: nothing
|
|
1070
|
-
}
|
|
1071
|
-
</div>
|
|
1072
|
-
</sp-accordion-item>
|
|
1073
|
-
`;
|
|
685
|
+
/**
|
|
686
|
+
* The Layout-element panel — what the inspector says when you click page chrome.
|
|
687
|
+
*
|
|
688
|
+
* It spent a release cycle unreachable: `shell.layoutSelection` had this reader and no writer, so
|
|
689
|
+
* clicking the site name in the header — the first click a new user makes — did nothing at all. The
|
|
690
|
+
* canvas hit test writes it now (§8.2), and `panels/right-panel.ts` tracks it, so the panel
|
|
691
|
+
* renders.
|
|
692
|
+
*/
|
|
693
|
+
/**
|
|
694
|
+
* The chip that opens the layout beside the page.
|
|
695
|
+
*
|
|
696
|
+
* **Drawn only where it can run**, which is why the condition lives at the call site rather than
|
|
697
|
+
* inside the handler. `canvas/iframe-host.ts` focuses the pane a layout click landed in — including
|
|
698
|
+
* a LENS, which draws layout chrome because it draws the same document — and from a derived pane
|
|
699
|
+
* `pane.derive` can only throw `an open document in a pane that is not itself derived`, into a
|
|
700
|
+
* floating `void registry.run(…)` that swallows it. A chip that does nothing when you press it is
|
|
701
|
+
* worse than no chip: the sentence above it already says where the element comes from, and in that
|
|
702
|
+
* shell the layout is either already beside you or one Unsplit away.
|
|
703
|
+
*/
|
|
704
|
+
function openLayoutTpl(): TemplateResult {
|
|
705
|
+
return html`<span
|
|
706
|
+
class="kv-add"
|
|
707
|
+
@click=${() => {
|
|
708
|
+
/* ONE command, addressed by id — the chip is a control, not a second definition site for
|
|
709
|
+
what "open the layout" means. It opens the layout BESIDE the page and leaves the keyboard
|
|
710
|
+
in the page, so the next click on layout chrome moves the side pane's selection instead of
|
|
711
|
+
teaching the author what a pane is. */
|
|
712
|
+
void activeRegistry()?.run("pane.derive", { preset: "layout" });
|
|
713
|
+
}}
|
|
714
|
+
>Open Layout →</span
|
|
715
|
+
>`;
|
|
1074
716
|
}
|
|
1075
717
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const {
|
|
1080
|
-
|
|
1081
|
-
layoutPath: string;
|
|
1082
|
-
};
|
|
1083
|
-
const tagName = el?.tagName?.toLowerCase() || "element";
|
|
1084
|
-
const className = el?.className || "";
|
|
1085
|
-
const displayPath = layoutPath || "layout";
|
|
718
|
+
function renderLayoutSelectionPanel() {
|
|
719
|
+
const selection = shell.layoutSelection as LayoutSelection;
|
|
720
|
+
const tagName = selection.tagName || "element";
|
|
721
|
+
const { className } = selection;
|
|
722
|
+
const displayPath = selection.layoutFile || "layout";
|
|
1086
723
|
|
|
1087
724
|
return html`
|
|
1088
725
|
<div class="style-sidebar">
|
|
1089
726
|
<sp-accordion allow-multiple size="s">
|
|
1090
727
|
<sp-accordion-item label="Layout Element" open>
|
|
1091
728
|
<div class="style-section-body">
|
|
1092
|
-
<div
|
|
1093
|
-
<span
|
|
1094
|
-
|
|
1095
|
-
>Layout</span
|
|
1096
|
-
>
|
|
1097
|
-
<code
|
|
1098
|
-
style="font-size:var(--spectrum-font-size-75, 12px);font-family:var(--font-mono)"
|
|
1099
|
-
><${tagName}></code
|
|
1100
|
-
>
|
|
729
|
+
<div class="layout-origin">
|
|
730
|
+
<span class="layout-origin-badge">Layout</span>
|
|
731
|
+
<code class="layout-origin-tag"><${tagName}></code>
|
|
1101
732
|
</div>
|
|
1102
733
|
${
|
|
1103
734
|
className
|
|
1104
|
-
?
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
>${className}</span
|
|
1111
|
-
>
|
|
1112
|
-
</div>`
|
|
735
|
+
? renderFieldRow({
|
|
736
|
+
hasValue: false,
|
|
737
|
+
label: "Class",
|
|
738
|
+
prop: "className",
|
|
739
|
+
widget: html`<span class="layout-origin-class">${className}</span>`,
|
|
740
|
+
})
|
|
1113
741
|
: nothing
|
|
1114
742
|
}
|
|
1115
|
-
<
|
|
1116
|
-
This element
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
>
|
|
743
|
+
<p class="layout-origin-note">
|
|
744
|
+
This element comes from ${displayPath}, which wraps every page that uses it. Open the
|
|
745
|
+
layout to edit it.
|
|
746
|
+
</p>
|
|
747
|
+
${deriveRefusal(workspace.activePaneId) === null ? openLayoutTpl() : nothing}
|
|
1121
748
|
</div>
|
|
1122
749
|
</sp-accordion-item>
|
|
1123
750
|
</sp-accordion>
|
|
@@ -1125,10 +752,112 @@ function renderLayoutSelectionPanel(ctx: { navigateToComponent: (path: string) =
|
|
|
1125
752
|
`;
|
|
1126
753
|
}
|
|
1127
754
|
|
|
755
|
+
// ─── Usages (§9.6) ───────────────────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* The Usage section's key, in the same per-tab `inspectorSections` record every other section uses,
|
|
759
|
+
* so `inspector.setSection` addresses it exactly like the rest and nothing needs a second store.
|
|
760
|
+
*/
|
|
761
|
+
const USAGES_SECTION = "__usages";
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The component instance's usage line — "Used on 7 pages →", expanding to the files.
|
|
765
|
+
*
|
|
766
|
+
* Three things this deliberately does NOT do. It does not render when the host cannot answer
|
|
767
|
+
* (`usagesSupported()` is `capability.findReferences`): a confident "0" for a component used
|
|
768
|
+
* everywhere is worse than no line at all. It does not ask on every paint — `peekUsages` is
|
|
769
|
+
* side-effect-free and only a cold target starts a request, which repaints once when it lands. And
|
|
770
|
+
* it does not swallow a failure: a failed count says so and offers Retry, because "we could not
|
|
771
|
+
* check" and "nothing uses this" are the two answers a user must never confuse.
|
|
772
|
+
*
|
|
773
|
+
* @param tagName — the instance's custom-element tag.
|
|
774
|
+
* @param componentPath — its definition file, when the registry knows one. Passing both asks the
|
|
775
|
+
* one question that covers file references AND element instances.
|
|
776
|
+
*/
|
|
777
|
+
function renderUsagesSection(tagName: string, componentPath: string | null) {
|
|
778
|
+
const query = { tagName, ...(componentPath ? { path: componentPath } : {}) };
|
|
779
|
+
const state = peekUsages(query);
|
|
780
|
+
// ONE gate, and it is the peek itself: a host with no `findReferences` answers "unsupported"
|
|
781
|
+
// Synchronously, and the section does not exist. A separate `usagesSupported()` guard above this
|
|
782
|
+
// Would make the arm below unreachable, which is a branch nothing can ever test.
|
|
783
|
+
if (state?.status === "unsupported") {
|
|
784
|
+
return nothing;
|
|
785
|
+
}
|
|
786
|
+
if (state === null) {
|
|
787
|
+
// Cold: ask once, and repaint the inspector when the answer arrives.
|
|
788
|
+
void loadUsages(query).then(() => renderOnly("rightPanel"));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
let body;
|
|
792
|
+
let heading = "Usage";
|
|
793
|
+
if (state === null || state.status === "pending") {
|
|
794
|
+
body = html`<div class="usage-note">Counting references…</div>`;
|
|
795
|
+
} else if (state.status === "failed") {
|
|
796
|
+
heading = "Usage · unknown";
|
|
797
|
+
body = html`
|
|
798
|
+
<div class="usage-note">
|
|
799
|
+
References could not be counted: ${state.message}. This is not the same as “unused”.
|
|
800
|
+
</div>
|
|
801
|
+
<sp-action-button
|
|
802
|
+
size="s"
|
|
803
|
+
quiet
|
|
804
|
+
@click=${() => {
|
|
805
|
+
void retryUsages(query).then(() => renderOnly("rightPanel"));
|
|
806
|
+
}}
|
|
807
|
+
>Retry</sp-action-button
|
|
808
|
+
>
|
|
809
|
+
`;
|
|
810
|
+
} else {
|
|
811
|
+
const files = usageFiles(state.result);
|
|
812
|
+
heading = usageHeadline(state.result);
|
|
813
|
+
body =
|
|
814
|
+
files.length === 0
|
|
815
|
+
? html`<div class="usage-note">
|
|
816
|
+
Nothing else in this project places <code><${tagName}></code> yet.
|
|
817
|
+
</div>`
|
|
818
|
+
: html`${files.map(
|
|
819
|
+
(file) => html`
|
|
820
|
+
<button
|
|
821
|
+
class="usage-row"
|
|
822
|
+
type="button"
|
|
823
|
+
title=${file.refs.map((r) => `${r.refType} ${r.ref} ×${r.count}`).join(", ")}
|
|
824
|
+
@click=${() => openUsage(file.path)}
|
|
825
|
+
>
|
|
826
|
+
<span class="usage-row-path">${file.path}</span>
|
|
827
|
+
<span class="usage-row-count">${file.count}</span>
|
|
828
|
+
</button>
|
|
829
|
+
`,
|
|
830
|
+
)}`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return html`
|
|
834
|
+
<sp-accordion-item
|
|
835
|
+
label=${heading}
|
|
836
|
+
?open=${isInspectorSectionOpen(USAGES_SECTION, false)}
|
|
837
|
+
@sp-accordion-item-toggle=${() =>
|
|
838
|
+
setInspectorSection(USAGES_SECTION, !isInspectorSectionOpen(USAGES_SECTION, false))}
|
|
839
|
+
>
|
|
840
|
+
<div class="style-section-body">${body}</div>
|
|
841
|
+
</sp-accordion-item>
|
|
842
|
+
`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** Open a referencing file in a tab — the "→" half of "Used on N pages →". */
|
|
846
|
+
function openUsage(path: string): void {
|
|
847
|
+
// Lazy import: `files/files.ts` imports this panel's siblings, and a static edge here would close
|
|
848
|
+
// The inspector → files → inspector cycle.
|
|
849
|
+
void import("../files/files").then((m) => m.openFileInTab(path));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Send the user to the tab that DOES answer, without this module importing the dock. */
|
|
853
|
+
function showLogicTab(): void {
|
|
854
|
+
void import("./right-panel").then((m) => m.setInspectorTab("events"));
|
|
855
|
+
}
|
|
856
|
+
|
|
1128
857
|
// ─── Main entry point ───────────────────────────────────────────────────────
|
|
1129
858
|
|
|
1130
859
|
/**
|
|
1131
|
-
*
|
|
860
|
+
* The Content tab — lit-html template with accordion sections.
|
|
1132
861
|
*
|
|
1133
862
|
* @param {{ navigateToComponent: (path: string) => void }} ctx
|
|
1134
863
|
*/
|
|
@@ -1137,53 +866,61 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1137
866
|
}) {
|
|
1138
867
|
const tab = activeTab.value;
|
|
1139
868
|
if (!tab) {
|
|
1140
|
-
return
|
|
869
|
+
return renderEmptyState({
|
|
870
|
+
actions: [openPageAction()],
|
|
871
|
+
message: "Open a page to inspect and style what you click.",
|
|
872
|
+
});
|
|
1141
873
|
}
|
|
1142
874
|
|
|
1143
875
|
// Layout element selected — show read-only info with link to open layout
|
|
1144
|
-
if (
|
|
1145
|
-
return renderLayoutSelectionPanel(
|
|
876
|
+
if (shell.layoutSelection) {
|
|
877
|
+
return renderLayoutSelectionPanel();
|
|
1146
878
|
}
|
|
1147
879
|
|
|
1148
|
-
|
|
1149
|
-
|
|
880
|
+
const selected = primarySelection(tab.session.selection);
|
|
881
|
+
if (!selected) {
|
|
882
|
+
return renderEmptyState({ message: clickAnythingTo("edit its content") });
|
|
1150
883
|
}
|
|
1151
|
-
const
|
|
884
|
+
const path: JxPath = selected;
|
|
885
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1152
886
|
if (!node) {
|
|
1153
|
-
return
|
|
887
|
+
return renderEmptyState({ message: staleSelectionMessage() });
|
|
1154
888
|
}
|
|
1155
889
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
890
|
+
// A repeating list has no content of its own — it has a source and a template, and both are
|
|
891
|
+
// Wiring. Content says where the answer lives rather than drawing an empty accordion (§6.5).
|
|
892
|
+
if (node.$prototype === "Array") {
|
|
893
|
+
return renderEmptyState({
|
|
894
|
+
actions: [{ label: "Open Logic", run: showLogicTab }],
|
|
895
|
+
detail: "Its items, filter, sort and template are wiring, so they live in Logic.",
|
|
896
|
+
message: "A repeating list has no content of its own.",
|
|
897
|
+
});
|
|
898
|
+
}
|
|
1163
899
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
900
|
+
// The whole selection the Content tab edits. `[path]` when one element is selected, which is
|
|
901
|
+
// Every existing call site's behaviour unchanged.
|
|
902
|
+
const targets: JxPath[] = tab.session.selection.length > 0 ? tab.session.selection : [path];
|
|
903
|
+
const doc = tab.doc.document;
|
|
904
|
+
const isCustomInstance = displayTagName(node.tagName).includes("-");
|
|
905
|
+
const chosenTag = isTagExpression(node.tagName) ? node.tagName.$expression : null;
|
|
906
|
+
const tagName = displayTagName(node.tagName) || "div";
|
|
907
|
+
const attrs = node.attributes || {};
|
|
1170
908
|
|
|
909
|
+
const mapSignals = mapSignalsFor(path);
|
|
1171
910
|
// Signals offered to attribute/textContent bindings (handlers and Functions excluded).
|
|
1172
|
-
const bindableSignals =
|
|
1173
|
-
.filter(
|
|
1174
|
-
([, d]) =>
|
|
1175
|
-
!(d as Record<string, unknown>)?.$handler &&
|
|
1176
|
-
(d as JxPrototypeDef)?.$prototype !== "Function",
|
|
1177
|
-
)
|
|
1178
|
-
.map(([defName]) => defName);
|
|
911
|
+
const bindableSignals = bindableSignalNames(tab.doc.document);
|
|
1179
912
|
|
|
1180
913
|
function renderAttrRow(attr: string, entry: HtmlMetaEntry, value: unknown) {
|
|
1181
914
|
const type = inferInputType(entry);
|
|
1182
915
|
const hasVal = value !== undefined && value !== "";
|
|
916
|
+
// One write per selected element, inside ONE transaction (§6.5). `targets` is `[path]` for a
|
|
917
|
+
// Single selection, so this is the same single mutation it has always been.
|
|
1183
918
|
const commitAttr = (v?: JsonValue) =>
|
|
1184
|
-
transactDoc(activeTab.value!, (t) =>
|
|
1185
|
-
|
|
1186
|
-
|
|
919
|
+
transactDoc(activeTab.value!, (t) => {
|
|
920
|
+
for (const target of targets) {
|
|
921
|
+
mutateUpdateAttribute(t, target, attr, v as JxAttributeValue | undefined);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
1187
924
|
|
|
1188
925
|
// Enhanced Link handling: only for anchors (a/area) with a plain (non-binding) value. Bindings
|
|
1189
926
|
// ($ref objects or ${…} template strings) fall through to the raw widget to stay editable.
|
|
@@ -1197,10 +934,9 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1197
934
|
}
|
|
1198
935
|
}
|
|
1199
936
|
|
|
1200
|
-
// Attribute strings are schema-legal at three rungs: literal, $ref binding, ${} template.
|
|
1201
937
|
const attrSlot = (staticWidget: unknown) =>
|
|
1202
938
|
renderDynamicSlot({
|
|
1203
|
-
caps:
|
|
939
|
+
caps: "attribute",
|
|
1204
940
|
extraSignals: mapSignals,
|
|
1205
941
|
fieldKey: `attr|${path.join("/")}|${attr}`,
|
|
1206
942
|
onChange: commitAttr,
|
|
@@ -1209,41 +945,37 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1209
945
|
value,
|
|
1210
946
|
});
|
|
1211
947
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
label: attrLabel(entry, attr),
|
|
1226
|
-
labelExtra: slot.modeButton,
|
|
1227
|
-
onClear: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr)),
|
|
1228
|
-
prop: attr,
|
|
1229
|
-
widget: slot.widget,
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const literalWidget = widgetForType(
|
|
1234
|
-
type,
|
|
1235
|
-
entry,
|
|
1236
|
-
attr,
|
|
1237
|
-
isRef(value) ? "" : String(value || ""),
|
|
1238
|
-
(v: string) => commitAttr(v || undefined),
|
|
1239
|
-
);
|
|
948
|
+
const literalWidget =
|
|
949
|
+
entry.type === "boolean"
|
|
950
|
+
? html`
|
|
951
|
+
<sp-checkbox
|
|
952
|
+
size="s"
|
|
953
|
+
.checked=${live(Boolean(value))}
|
|
954
|
+
@change=${(e: Event) => commitAttr((e.target as HTMLInputElement).checked ? "" : undefined)}
|
|
955
|
+
>
|
|
956
|
+
</sp-checkbox>
|
|
957
|
+
`
|
|
958
|
+
: widgetForType(type, entry, attr, isRef(value) ? "" : String(value || ""), (v: string) =>
|
|
959
|
+
commitAttr(v || undefined),
|
|
960
|
+
);
|
|
1240
961
|
const slot = attrSlot(literalWidget);
|
|
1241
962
|
return renderFieldRow({
|
|
1242
963
|
hasValue: hasVal,
|
|
1243
964
|
label: attrLabel(entry, attr),
|
|
1244
965
|
labelExtra: slot.modeButton,
|
|
1245
|
-
onClear: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr)),
|
|
1246
966
|
prop: attr,
|
|
967
|
+
provenance: attributeProvenance(
|
|
968
|
+
attr,
|
|
969
|
+
value,
|
|
970
|
+
hasVal,
|
|
971
|
+
() =>
|
|
972
|
+
transactDoc(activeTab.value, (t) => {
|
|
973
|
+
for (const target of targets) {
|
|
974
|
+
mutateUpdateAttribute(t, target, attr);
|
|
975
|
+
}
|
|
976
|
+
}),
|
|
977
|
+
mixedAcrossSelection(doc, targets, (n) => (n?.attributes ?? {})[attr] ?? null),
|
|
978
|
+
),
|
|
1247
979
|
widget: slot.widget,
|
|
1248
980
|
});
|
|
1249
981
|
}
|
|
@@ -1251,7 +983,7 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1251
983
|
// ── Collect applicable attributes from html-meta ──
|
|
1252
984
|
const applicableAttrs = {} as Record<string, HtmlMetaEntry>;
|
|
1253
985
|
for (const [attr, entry] of Object.entries(htmlMeta.$defs) as [string, HtmlMetaEntry][]) {
|
|
1254
|
-
if (!entry.$elements || entry.$elements.includes(tagName)) {
|
|
986
|
+
if (!entry.$elements || entry.$elements.includes(displayTagName(tagName))) {
|
|
1255
987
|
// The $attr field aliases a $defs key to a different attribute name.
|
|
1256
988
|
// This lets the same attribute (e.g. "name") carry per-element metadata.
|
|
1257
989
|
applicableAttrs[entry.$attr ?? attr] = entry;
|
|
@@ -1298,24 +1030,19 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1298
1030
|
}
|
|
1299
1031
|
|
|
1300
1032
|
function isSectionOpen(key: string) {
|
|
1301
|
-
|
|
1302
|
-
return tab!.session.ui.inspectorSections[key];
|
|
1303
|
-
}
|
|
1304
|
-
return autoOpen.has(key);
|
|
1033
|
+
return isInspectorSectionOpen(key, autoOpen.has(key));
|
|
1305
1034
|
}
|
|
1306
1035
|
|
|
1307
1036
|
function toggleSection(key: string) {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
[key]: !current,
|
|
1312
|
-
};
|
|
1037
|
+
// One writer: the accordion's own click and `inspector.setSection` land in the same function,
|
|
1038
|
+
// So the command and the control cannot disagree about what "open" means.
|
|
1039
|
+
setInspectorSection(key, !isSectionOpen(key));
|
|
1313
1040
|
}
|
|
1314
1041
|
|
|
1315
1042
|
// ── Build section templates ─────────────────────────────────────────
|
|
1316
1043
|
|
|
1317
1044
|
const textSlot = renderDynamicSlot({
|
|
1318
|
-
caps:
|
|
1045
|
+
caps: "textProperty",
|
|
1319
1046
|
extraSignals: mapSignals,
|
|
1320
1047
|
fieldKey: `prop|${path.join("/")}|textContent`,
|
|
1321
1048
|
onChange: (v?: JsonValue) =>
|
|
@@ -1332,6 +1059,31 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1332
1059
|
value: node.textContent,
|
|
1333
1060
|
});
|
|
1334
1061
|
|
|
1062
|
+
/**
|
|
1063
|
+
* A draft key for one field ON THIS NODE.
|
|
1064
|
+
*
|
|
1065
|
+
* The three Element rows used constant keys — `"prop:tagName"`, `"prop:$id"`, `"prop:className"`
|
|
1066
|
+
* — and `ui/field-input.ts`'s draft map is module-global, so every element shared one draft slot
|
|
1067
|
+
* per field. Type a class name, click a sibling before blurring, and the sibling's Class row
|
|
1068
|
+
* showed the text you had typed for the first one; blur it there and the commit landed on the
|
|
1069
|
+
* WRONG element. Plan §11.4 asks for "drafts keyed by node path (today all elements share one
|
|
1070
|
+
* draft slot per field)".
|
|
1071
|
+
*/
|
|
1072
|
+
const fieldKey = (name: string) => `prop:${path.join("/")}:${name}`;
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* The set-here chip for a document property, with the tooltip in the label's own words.
|
|
1076
|
+
*
|
|
1077
|
+
* The derived tooltip would read "Clear textContent"; the label above it reads "Text Content".
|
|
1078
|
+
* The row's key and the row's name are different vocabularies, and the tooltip belongs to the one
|
|
1079
|
+
* the user is looking at.
|
|
1080
|
+
*/
|
|
1081
|
+
const propertyChip = (name: string, human: string): FieldProvenance => ({
|
|
1082
|
+
onClick: () => transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, name)),
|
|
1083
|
+
state: "set",
|
|
1084
|
+
title: `Clear ${human}`,
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1335
1087
|
const elemT = html`
|
|
1336
1088
|
<sp-accordion-item
|
|
1337
1089
|
label="Element"
|
|
@@ -1339,210 +1091,122 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1339
1091
|
@sp-accordion-item-toggle=${() => toggleSection("__element")}
|
|
1340
1092
|
>
|
|
1341
1093
|
<div class="style-section-body">
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1094
|
+
${(() => {
|
|
1095
|
+
/* THE SHARED SLOT, not a hand-rolled control.
|
|
1096
|
+
`tagName` is a bindable position like any other — a literal name or a `TagExpression`
|
|
1097
|
+
— so it gets the same Value Source chip and the same expression editor as `href`, a
|
|
1098
|
+
style declaration or an event handler. The rungs are DERIVED from
|
|
1099
|
+
`SLOT_POSITION_SCHEMAS.elementTag`, which is why only Fixed value and Formula are
|
|
1100
|
+
offered: `TagName` carries a pattern, so `isFreeStringSchema` refuses a template rung,
|
|
1101
|
+
and a `${…}` in tag position is precisely what that pattern exists to reject.
|
|
1102
|
+
|
|
1103
|
+
The seed is ours because the generic one is `{ operator: "??" }`, which a
|
|
1104
|
+
`TagExpression` does not admit — clicking the chip would otherwise write a document
|
|
1105
|
+
that fails its own validator. */
|
|
1106
|
+
const tagSlot = renderDynamicSlot({
|
|
1107
|
+
caps: "elementTag",
|
|
1108
|
+
fieldKey: `${path.join(".")}:tagName`,
|
|
1109
|
+
onChange: (next) => {
|
|
1352
1110
|
transactDoc(activeTab.value, (t) =>
|
|
1353
|
-
mutateUpdateProperty(
|
|
1354
|
-
t,
|
|
1355
|
-
path,
|
|
1356
|
-
"tagName",
|
|
1357
|
-
(e.target as HTMLInputElement).value || undefined,
|
|
1358
|
-
),
|
|
1111
|
+
mutateUpdateProperty(t, path, "tagName", next ?? undefined),
|
|
1359
1112
|
);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1113
|
+
},
|
|
1114
|
+
seedFor: (mode) =>
|
|
1115
|
+
mode === "expression"
|
|
1116
|
+
? {
|
|
1117
|
+
$expression: {
|
|
1118
|
+
initial: tagName,
|
|
1119
|
+
operator: "?:",
|
|
1120
|
+
target: null,
|
|
1121
|
+
value: tagName,
|
|
1122
|
+
},
|
|
1123
|
+
}
|
|
1124
|
+
: undefined,
|
|
1125
|
+
staticWidget: spTextField(fieldKey("tagName"), tagName, (v: string) => {
|
|
1126
|
+
transactDoc(activeTab.value, (t) =>
|
|
1127
|
+
mutateUpdateProperty(t, path, "tagName", v || undefined),
|
|
1128
|
+
);
|
|
1129
|
+
}),
|
|
1130
|
+
stateDefs: bindableSignalNames(activeTab.value!.doc.document),
|
|
1131
|
+
value: node.tagName as JsonValue | undefined,
|
|
1132
|
+
});
|
|
1133
|
+
return renderFieldRow({
|
|
1134
|
+
hasValue: chosenTag !== null,
|
|
1135
|
+
label: "Tag",
|
|
1136
|
+
labelExtra: tagSlot.modeButton,
|
|
1137
|
+
prop: "tagName",
|
|
1138
|
+
widget: tagSlot.widget,
|
|
1139
|
+
});
|
|
1140
|
+
})()}
|
|
1141
|
+
${renderFieldRow({
|
|
1142
|
+
hasValue: Boolean(node.$id),
|
|
1143
|
+
label: "ID",
|
|
1144
|
+
prop: "$id",
|
|
1145
|
+
...(node.$id ? { provenance: propertyChip("$id", "ID") } : {}),
|
|
1146
|
+
widget: spTextField(fieldKey("$id"), String(node.$id || ""), (v: string) =>
|
|
1380
1147
|
transactDoc(activeTab.value, (t) =>
|
|
1381
1148
|
mutateUpdateProperty(t, path, "$id", v || undefined),
|
|
1382
1149
|
),
|
|
1383
|
-
)
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
title="Clear class"
|
|
1392
|
-
@click=${(e: Event) => {
|
|
1393
|
-
e.stopPropagation();
|
|
1394
|
-
transactDoc(activeTab.value, (t) =>
|
|
1395
|
-
mutateUpdateProperty(t, path, "className"),
|
|
1396
|
-
);
|
|
1397
|
-
}}
|
|
1398
|
-
></span>`
|
|
1399
|
-
: nothing
|
|
1400
|
-
}
|
|
1401
|
-
<sp-field-label size="s">Class</sp-field-label>
|
|
1402
|
-
</div>
|
|
1403
|
-
${spTextField("prop:className", String(node.className || ""), (v: string) =>
|
|
1150
|
+
),
|
|
1151
|
+
})}
|
|
1152
|
+
${renderFieldRow({
|
|
1153
|
+
hasValue: Boolean(node.className),
|
|
1154
|
+
label: "Class",
|
|
1155
|
+
prop: "className",
|
|
1156
|
+
...(node.className ? { provenance: propertyChip("className", "class") } : {}),
|
|
1157
|
+
widget: spTextField(fieldKey("className"), String(node.className || ""), (v: string) =>
|
|
1404
1158
|
transactDoc(activeTab.value, (t) =>
|
|
1405
1159
|
mutateUpdateProperty(t, path, "className", v || undefined),
|
|
1406
1160
|
),
|
|
1407
|
-
)
|
|
1408
|
-
|
|
1161
|
+
),
|
|
1162
|
+
})}
|
|
1409
1163
|
${
|
|
1410
1164
|
!Array.isArray(node.children) || node.children.length === 0
|
|
1411
|
-
?
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
transactDoc(activeTab.value, (t) =>
|
|
1422
|
-
mutateUpdateProperty(t, path, "textContent"),
|
|
1423
|
-
);
|
|
1424
|
-
}}
|
|
1425
|
-
></span>`
|
|
1426
|
-
: nothing
|
|
1427
|
-
}
|
|
1428
|
-
<sp-field-label size="s">Text Content</sp-field-label>
|
|
1429
|
-
${textSlot.modeButton}
|
|
1430
|
-
</div>
|
|
1431
|
-
${textSlot.widget}
|
|
1432
|
-
</div>
|
|
1433
|
-
`
|
|
1165
|
+
? renderFieldRow({
|
|
1166
|
+
hasValue: node.textContent !== undefined,
|
|
1167
|
+
label: "Text Content",
|
|
1168
|
+
labelExtra: textSlot.modeButton,
|
|
1169
|
+
prop: "textContent",
|
|
1170
|
+
widget: textSlot.widget,
|
|
1171
|
+
...(node.textContent === undefined
|
|
1172
|
+
? {}
|
|
1173
|
+
: { provenance: propertyChip("textContent", "text") }),
|
|
1174
|
+
})
|
|
1434
1175
|
: nothing
|
|
1435
1176
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
t,
|
|
1459
|
-
path,
|
|
1460
|
-
"hidden",
|
|
1461
|
-
(e.target as HTMLInputElement).checked || undefined,
|
|
1462
|
-
),
|
|
1463
|
-
)}
|
|
1464
|
-
>
|
|
1465
|
-
</sp-checkbox>
|
|
1466
|
-
</div>
|
|
1177
|
+
${renderFieldRow({
|
|
1178
|
+
hasValue: Boolean(node.hidden),
|
|
1179
|
+
label: "Hidden",
|
|
1180
|
+
prop: "hidden",
|
|
1181
|
+
...(node.hidden ? { provenance: propertyChip("hidden", "hidden") } : {}),
|
|
1182
|
+
widget: html`
|
|
1183
|
+
<sp-checkbox
|
|
1184
|
+
size="s"
|
|
1185
|
+
.checked=${live(Boolean(node.hidden))}
|
|
1186
|
+
@change=${(e: Event) =>
|
|
1187
|
+
transactDoc(activeTab.value, (t) =>
|
|
1188
|
+
mutateUpdateProperty(
|
|
1189
|
+
t,
|
|
1190
|
+
path,
|
|
1191
|
+
"hidden",
|
|
1192
|
+
(e.target as HTMLInputElement).checked || undefined,
|
|
1193
|
+
),
|
|
1194
|
+
)}
|
|
1195
|
+
>
|
|
1196
|
+
</sp-checkbox>
|
|
1197
|
+
`,
|
|
1198
|
+
})}
|
|
1467
1199
|
</div>
|
|
1468
1200
|
</sp-accordion-item>
|
|
1469
1201
|
`;
|
|
1470
1202
|
|
|
1471
|
-
const
|
|
1472
|
-
? html`
|
|
1473
|
-
<sp-accordion-item label="Repeater" open>
|
|
1474
|
-
<div class="style-section-body">
|
|
1475
|
-
${renderRepeaterFieldsTemplate(node, path, mapSignals)}
|
|
1476
|
-
</div>
|
|
1477
|
-
</sp-accordion-item>
|
|
1478
|
-
`
|
|
1479
|
-
: nothing;
|
|
1480
|
-
|
|
1481
|
-
const switchT = isSwitchNode
|
|
1482
|
-
? html`
|
|
1483
|
-
<sp-accordion-item label="Switch" open>
|
|
1484
|
-
<div class="style-section-body">
|
|
1485
|
-
${renderSwitchFieldsTemplate(node, path, mapSignals)}
|
|
1486
|
-
</div>
|
|
1487
|
-
</sp-accordion-item>
|
|
1488
|
-
`
|
|
1489
|
-
: nothing;
|
|
1490
|
-
|
|
1491
|
-
const observedAttrsT =
|
|
1492
|
-
isCustomElementDoc({ document: tab.doc.document }) && isRoot
|
|
1493
|
-
? (() => {
|
|
1494
|
-
const state = tab.doc.document.state || {};
|
|
1495
|
-
const entries = Object.entries(state).filter(
|
|
1496
|
-
([, d]) => (d as Record<string, unknown>).attribute,
|
|
1497
|
-
);
|
|
1498
|
-
return html`
|
|
1499
|
-
<sp-accordion-item label="Observed Attributes" ?open=${isSectionOpen("__observed")}>
|
|
1500
|
-
<div class="style-section-body">
|
|
1501
|
-
${
|
|
1502
|
-
entries.length === 0
|
|
1503
|
-
? html`<div class="empty-state">
|
|
1504
|
-
No attributes declared. Set "attribute" on a state entry.
|
|
1505
|
-
</div>`
|
|
1506
|
-
: entries.map(([key, d]) => {
|
|
1507
|
-
const def = d as Record<string, unknown>;
|
|
1508
|
-
return html`
|
|
1509
|
-
<div
|
|
1510
|
-
style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:var(--spectrum-font-size-50, 11px)"
|
|
1511
|
-
>
|
|
1512
|
-
<code style="font-family:var(--font-mono);color:var(--accent)"
|
|
1513
|
-
>${def.attribute}</code
|
|
1514
|
-
>
|
|
1515
|
-
<span style="color:var(--fg-dim)"> → </span>
|
|
1516
|
-
<span>${key}</span>
|
|
1517
|
-
${
|
|
1518
|
-
def.type
|
|
1519
|
-
? html`<span
|
|
1520
|
-
style="margin-left:auto;color:var(--fg-dim);font-size:10px"
|
|
1521
|
-
>${def.type}</span
|
|
1522
|
-
>`
|
|
1523
|
-
: nothing
|
|
1524
|
-
}
|
|
1525
|
-
${
|
|
1526
|
-
def.reflects
|
|
1527
|
-
? html`<span
|
|
1528
|
-
style="font-size:9px;background:var(--hover-bg);padding:1px 4px;border-radius:var(--radius)"
|
|
1529
|
-
>reflects</span
|
|
1530
|
-
>`
|
|
1531
|
-
: nothing
|
|
1532
|
-
}
|
|
1533
|
-
</div>
|
|
1534
|
-
`;
|
|
1535
|
-
})
|
|
1536
|
-
}
|
|
1537
|
-
</div>
|
|
1538
|
-
</sp-accordion-item>
|
|
1539
|
-
`;
|
|
1540
|
-
})()
|
|
1541
|
-
: nothing;
|
|
1542
|
-
|
|
1203
|
+
const editingDefinition = isCustomInstance && isComponentDefinitionOpen(node);
|
|
1543
1204
|
const compPropsT = isCustomInstance
|
|
1544
1205
|
? html`
|
|
1545
|
-
<sp-accordion-item
|
|
1206
|
+
<sp-accordion-item
|
|
1207
|
+
label=${editingDefinition ? "Component Defaults" : "Component Settings"}
|
|
1208
|
+
open
|
|
1209
|
+
>
|
|
1546
1210
|
<div class="style-section-body">
|
|
1547
1211
|
${renderComponentPropsFieldsTemplate(node, path, mapSignals, ctx.navigateToComponent)}
|
|
1548
1212
|
</div>
|
|
@@ -1550,24 +1214,29 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1550
1214
|
`
|
|
1551
1215
|
: nothing;
|
|
1552
1216
|
|
|
1217
|
+
// "Used on N pages →". Only for a component instance: an ordinary <div> is not a thing that can
|
|
1218
|
+
// Be reused, so the question does not arise.
|
|
1219
|
+
const usagesT = isCustomInstance
|
|
1220
|
+
? renderUsagesSection(
|
|
1221
|
+
displayTagName(tagName),
|
|
1222
|
+
componentRegistry.find((c) => c.tagName === displayTagName(tagName))?.path ?? null,
|
|
1223
|
+
)
|
|
1224
|
+
: nothing;
|
|
1225
|
+
|
|
1553
1226
|
const attrSectionTemplates = htmlMeta.$sections
|
|
1554
1227
|
.filter((sec) => attrSections[sec.key]!.length > 0)
|
|
1555
1228
|
.map((sec) => {
|
|
1556
1229
|
const sectionAttrs = attrSections[sec.key]!;
|
|
1557
|
-
const
|
|
1230
|
+
const setCount = sectionAttrs.filter(
|
|
1558
1231
|
(a: { name: string; entry: HtmlMetaEntry }) => attrs[a.name] !== undefined,
|
|
1559
|
-
);
|
|
1232
|
+
).length;
|
|
1560
1233
|
return html`
|
|
1561
1234
|
<sp-accordion-item
|
|
1562
1235
|
label=${sec.label}
|
|
1563
1236
|
?open=${isSectionOpen(sec.key)}
|
|
1564
1237
|
@sp-accordion-item-toggle=${() => toggleSection(sec.key)}
|
|
1565
1238
|
>
|
|
1566
|
-
${
|
|
1567
|
-
hasAnySet
|
|
1568
|
-
? html`<span slot="heading" class="set-dot set-dot--section"></span>`
|
|
1569
|
-
: nothing
|
|
1570
|
-
}
|
|
1239
|
+
${sectionDot(setCount)}
|
|
1571
1240
|
<div class="style-section-body">
|
|
1572
1241
|
${sectionAttrs.map((a: { name: string; entry: HtmlMetaEntry }) =>
|
|
1573
1242
|
renderAttrRow(a.name, a.entry, attrs[a.name]),
|
|
@@ -1585,107 +1254,245 @@ export function renderPropertiesPanelTemplate(ctx: {
|
|
|
1585
1254
|
?open=${isSectionOpen("__custom")}
|
|
1586
1255
|
@sp-accordion-item-toggle=${() => toggleSection("__custom")}
|
|
1587
1256
|
>
|
|
1588
|
-
${
|
|
1589
|
-
customAttrs.length > 0
|
|
1590
|
-
? html`<span slot="heading" class="set-dot set-dot--section"></span>`
|
|
1591
|
-
: nothing
|
|
1592
|
-
}
|
|
1257
|
+
${sectionDot(customAttrs.length)}
|
|
1593
1258
|
<div class="style-section-body">
|
|
1594
|
-
${renderCustomAttrsFieldsTemplate(
|
|
1259
|
+
${renderCustomAttrsFieldsTemplate(path, attrs, knownAttrNames)}
|
|
1595
1260
|
</div>
|
|
1596
1261
|
</sp-accordion-item>
|
|
1597
1262
|
`
|
|
1598
1263
|
: nothing;
|
|
1599
1264
|
|
|
1600
|
-
|
|
1601
|
-
? html`
|
|
1602
|
-
<sp-accordion-item
|
|
1603
|
-
label="Media"
|
|
1604
|
-
?open=${isSectionOpen("__media")}
|
|
1605
|
-
@sp-accordion-item-toggle=${() => toggleSection("__media")}
|
|
1606
|
-
>
|
|
1607
|
-
<div class="style-section-body">${renderMediaFieldsTemplate(node)}</div>
|
|
1608
|
-
</sp-accordion-item>
|
|
1609
|
-
`
|
|
1610
|
-
: nothing;
|
|
1611
|
-
|
|
1612
|
-
const cssPropsT =
|
|
1613
|
-
isCustomElementDoc({ document: tab.doc.document }) && isRoot
|
|
1614
|
-
? (() => {
|
|
1615
|
-
const style = node.style || {};
|
|
1616
|
-
const cssProps = Object.entries(style).filter(([k]) => k.startsWith("--"));
|
|
1617
|
-
if (cssProps.length === 0) {
|
|
1618
|
-
return nothing;
|
|
1619
|
-
}
|
|
1620
|
-
return html`
|
|
1621
|
-
<sp-accordion-item
|
|
1622
|
-
label="CSS Properties"
|
|
1623
|
-
?open=${isSectionOpen("__cssprops")}
|
|
1624
|
-
@sp-accordion-item-toggle=${() => toggleSection("__cssprops")}
|
|
1625
|
-
>
|
|
1626
|
-
<div class="style-section-body">
|
|
1627
|
-
${cssProps.map(
|
|
1628
|
-
([prop, val]) => html`
|
|
1629
|
-
<div
|
|
1630
|
-
style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:var(--spectrum-font-size-50, 11px)"
|
|
1631
|
-
>
|
|
1632
|
-
<code style="font-family:var(--font-mono);color:var(--accent)">${prop}</code>
|
|
1633
|
-
<span style="margin-left:auto;color:var(--fg-dim)">${String(val)}</span>
|
|
1634
|
-
</div>
|
|
1635
|
-
`,
|
|
1636
|
-
)}
|
|
1637
|
-
</div>
|
|
1638
|
-
</sp-accordion-item>
|
|
1639
|
-
`;
|
|
1640
|
-
})()
|
|
1641
|
-
: nothing;
|
|
1642
|
-
|
|
1643
|
-
const cssPartsT =
|
|
1644
|
-
isCustomElementDoc({ document: tab.doc.document }) && isRoot
|
|
1645
|
-
? (() => {
|
|
1646
|
-
const parts = collectCssParts(tab.doc.document);
|
|
1647
|
-
if (parts.length === 0) {
|
|
1648
|
-
return nothing;
|
|
1649
|
-
}
|
|
1650
|
-
return html`
|
|
1651
|
-
<sp-accordion-item
|
|
1652
|
-
label="CSS Parts"
|
|
1653
|
-
?open=${isSectionOpen("__cssparts")}
|
|
1654
|
-
@sp-accordion-item-toggle=${() => toggleSection("__cssparts")}
|
|
1655
|
-
>
|
|
1656
|
-
<div class="style-section-body">
|
|
1657
|
-
${parts.map(
|
|
1658
|
-
(p) => html`
|
|
1659
|
-
<div
|
|
1660
|
-
style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:var(--spectrum-font-size-50, 11px)"
|
|
1661
|
-
>
|
|
1662
|
-
<code style="font-family:var(--font-mono);color:var(--accent)"
|
|
1663
|
-
>${p.name}</code
|
|
1664
|
-
>
|
|
1665
|
-
<span style="color:var(--fg-dim)"><${p.tag}></span>
|
|
1666
|
-
</div>
|
|
1667
|
-
`,
|
|
1668
|
-
)}
|
|
1669
|
-
</div>
|
|
1670
|
-
</sp-accordion-item>
|
|
1671
|
-
`;
|
|
1672
|
-
})()
|
|
1673
|
-
: nothing;
|
|
1674
|
-
|
|
1675
|
-
const pageT = isRoot ? renderPageSection(node) : nothing;
|
|
1676
|
-
|
|
1677
|
-
// ── Assemble ──
|
|
1678
|
-
const tpl = html`
|
|
1265
|
+
return html`
|
|
1679
1266
|
<div class="style-sidebar">
|
|
1680
1267
|
<sp-accordion allow-multiple size="s">
|
|
1681
|
-
${
|
|
1682
|
-
${isMapNode ? nothing : switchT} ${isMapNode ? nothing : compPropsT}
|
|
1683
|
-
${isMapNode ? nothing : attrSectionTemplates} ${isMapNode ? nothing : customSectionT}
|
|
1684
|
-
${isMapNode ? nothing : mediaT} ${isMapNode ? nothing : cssPropsT}
|
|
1685
|
-
${isMapNode ? nothing : cssPartsT}
|
|
1268
|
+
${elemT} ${compPropsT} ${usagesT} ${attrSectionTemplates} ${customSectionT}
|
|
1686
1269
|
</sp-accordion>
|
|
1687
1270
|
</div>
|
|
1688
1271
|
`;
|
|
1272
|
+
}
|
|
1689
1273
|
|
|
1690
|
-
|
|
1274
|
+
/**
|
|
1275
|
+
* The collapsed-header provenance dot (§6.2), and the end of a false affordance.
|
|
1276
|
+
*
|
|
1277
|
+
* It inherited `.set-dot`'s `cursor: pointer` and its "turns red on hover" rule, which together say
|
|
1278
|
+
* "click me to clear this" — and nothing happened, because it never had a handler. It is not a
|
|
1279
|
+
* control and it is not deleted either: §6.2 puts a provenance dot on collapsed accordion headers
|
|
1280
|
+
* precisely so a section's state is legible while closed, and clicking anywhere in the header
|
|
1281
|
+
* already toggles the section, so a second, different meaning for the same click would be worse
|
|
1282
|
+
* than the silence. So it keeps the information and gives up the affordance: an inert, labelled
|
|
1283
|
+
* indicator that says how many values in the section are set.
|
|
1284
|
+
*/
|
|
1285
|
+
function sectionDot(setCount: number) {
|
|
1286
|
+
return setCount > 0
|
|
1287
|
+
? html`<span
|
|
1288
|
+
slot="heading"
|
|
1289
|
+
class="set-dot set-dot--section"
|
|
1290
|
+
aria-hidden="true"
|
|
1291
|
+
title=${`${setCount} value${setCount === 1 ? "" : "s"} set in this section`}
|
|
1292
|
+
></span>`
|
|
1293
|
+
: nothing;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** Which of §6.2's states an HTML-attribute row is in. Attributes have no third cascade layer. */
|
|
1297
|
+
function attributeProvenance(
|
|
1298
|
+
attr: string,
|
|
1299
|
+
value: unknown,
|
|
1300
|
+
hasVal: boolean,
|
|
1301
|
+
onClear: () => void,
|
|
1302
|
+
mixedCount = 0,
|
|
1303
|
+
): FieldProvenance {
|
|
1304
|
+
// Mixed first, for the same reason the Style tab decides it first: it is a fact about the
|
|
1305
|
+
// SELECTION, and the primary's own value says nothing about whether the others agree. A count of
|
|
1306
|
+
// 0 (a selection of one) can never reach this branch, so single-selection rows are untouched.
|
|
1307
|
+
if (mixedCount > 0) {
|
|
1308
|
+
return { donor: String(mixedCount), onClick: onClear, state: "mixed" };
|
|
1309
|
+
}
|
|
1310
|
+
if (hasVal && (isRef(value) || (typeof value === "string" && value.includes("${")))) {
|
|
1311
|
+
return boundProvenance(value, `Bound — ${attr}`);
|
|
1312
|
+
}
|
|
1313
|
+
return hasVal ? { onClick: onClear, state: "set", title: `Clear ${attr}` } : { state: "default" };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* How many selected elements disagree about `key` on `read` — 0 when they agree or there is one.
|
|
1318
|
+
*
|
|
1319
|
+
* The Content tab's two cascades (HTML attributes and component props) both need the same answer
|
|
1320
|
+
* about the same set, so the question is asked once here and answered with `unifyValues`, the same
|
|
1321
|
+
* comparison the Style tab uses.
|
|
1322
|
+
*
|
|
1323
|
+
* @param {JxMutableNode} doc
|
|
1324
|
+
* @param {readonly JxPath[]} targets
|
|
1325
|
+
* @param {(node: JxMutableNode | undefined) => unknown} read
|
|
1326
|
+
* @returns {number}
|
|
1327
|
+
*/
|
|
1328
|
+
export function mixedAcrossSelection(
|
|
1329
|
+
doc: JxMutableNode,
|
|
1330
|
+
targets: readonly JxPath[],
|
|
1331
|
+
read: (node: JxMutableNode | undefined) => unknown,
|
|
1332
|
+
): number {
|
|
1333
|
+
if (targets.length < 2) {
|
|
1334
|
+
return 0;
|
|
1335
|
+
}
|
|
1336
|
+
const values = targets.map((path) => read(getNodeAtPath(doc, path) as JxMutableNode | undefined));
|
|
1337
|
+
return unifyValues(values).mixed ? targets.length : 0;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// ─── Section state ────────────────────────────────────────────────────────────
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* The Inspector's fixed section keys — the accordion rows the inspector always draws.
|
|
1344
|
+
*
|
|
1345
|
+
* Attribute-schema sections add their own keys at render time (a class's `$section`), so this is a
|
|
1346
|
+
* floor, not a closed set: {@link inspectorSectionKeys} unions it with whatever the OPEN DOCUMENT
|
|
1347
|
+
* declares, and that union is what `inspector.setSection` validates against. A fixed enum would
|
|
1348
|
+
* refuse a section the user is looking at; no validation at all would accept the label the old
|
|
1349
|
+
* `inspector.toggleSection` step passed ("Element", not `__element`) and silently do nothing.
|
|
1350
|
+
*
|
|
1351
|
+
* `__media` left with the breakpoint definitions, which are Project Settings › Contexts now (P4).
|
|
1352
|
+
* `__observed`, `__cssprops` and `__cssparts` are drawn by the Logic tab; the key space is the
|
|
1353
|
+
* INSPECTOR's, not one tab's, so `inspector.setSection` keeps addressing all of them.
|
|
1354
|
+
*/
|
|
1355
|
+
export const INSPECTOR_SECTION_KEYS = [
|
|
1356
|
+
"__element",
|
|
1357
|
+
"__observed",
|
|
1358
|
+
"__usages",
|
|
1359
|
+
"__custom",
|
|
1360
|
+
"__cssprops",
|
|
1361
|
+
"__cssparts",
|
|
1362
|
+
] as const;
|
|
1363
|
+
|
|
1364
|
+
export type InspectorSectionKey = (typeof INSPECTOR_SECTION_KEYS)[number];
|
|
1365
|
+
|
|
1366
|
+
/** Section keys addressable right now: the fixed rows plus any already recorded for this tab. */
|
|
1367
|
+
export function inspectorSectionKeys(): string[] {
|
|
1368
|
+
const recorded = Object.keys(activeTab.value?.session.ui.inspectorSections ?? {});
|
|
1369
|
+
return [...new Set<string>([...INSPECTOR_SECTION_KEYS, ...recorded])];
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Whether one Inspector section is expanded, falling back to the caller's default.
|
|
1374
|
+
*
|
|
1375
|
+
* The fallback is the caller's because it is not a constant: an attribute section opens itself when
|
|
1376
|
+
* one of its attributes is set, and Usage never does. Only the panel drawing the row knows.
|
|
1377
|
+
*/
|
|
1378
|
+
export function isInspectorSectionOpen(section: string, fallback: boolean): boolean {
|
|
1379
|
+
const recorded = activeTab.value?.session.ui.inspectorSections?.[section];
|
|
1380
|
+
return recorded === undefined ? fallback : recorded;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Open or close one Inspector section, per tab.
|
|
1385
|
+
*
|
|
1386
|
+
* The pair `{ section, open }` is the whole point: `inspector.toggleSection` named a DELTA against
|
|
1387
|
+
* a section whose state depended on the selected node's own attributes (`autoOpen`), so the same
|
|
1388
|
+
* three manifest steps opened a section on one document and closed it on the next. Writing a new
|
|
1389
|
+
* object rather than mutating in place is what the reactive read in `isSectionOpen` depends on.
|
|
1390
|
+
*/
|
|
1391
|
+
export function setInspectorSection(section: string, open: boolean): void {
|
|
1392
|
+
const tab = activeTab.value;
|
|
1393
|
+
if (!tab) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
tab.session.ui.inspectorSections = { ...tab.session.ui.inspectorSections, [section]: open };
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* The Inspector's section verb. **This is the setter that retires `inspector.toggleSection`.**
|
|
1403
|
+
*
|
|
1404
|
+
* @returns {AnyCommand[]}
|
|
1405
|
+
*/
|
|
1406
|
+
export function inspectorCommands(): AnyCommand[] {
|
|
1407
|
+
return [
|
|
1408
|
+
{
|
|
1409
|
+
args: argsSchema({
|
|
1410
|
+
open: booleanProperty("True to expand the section, false to collapse it."),
|
|
1411
|
+
section: stringProperty(
|
|
1412
|
+
"The section key — one of the fixed rows (__element, __observed, __usages, " +
|
|
1413
|
+
"__custom, __cssprops, __cssparts) or an attribute schema's own $section.",
|
|
1414
|
+
),
|
|
1415
|
+
}),
|
|
1416
|
+
category: "View",
|
|
1417
|
+
id: "inspector.setSection",
|
|
1418
|
+
level: "document",
|
|
1419
|
+
menus: ["palette"],
|
|
1420
|
+
group: "4_docks",
|
|
1421
|
+
requires: "an open document",
|
|
1422
|
+
when: (ctx) => ctx.document.open,
|
|
1423
|
+
run: (_commandCtx, args) => {
|
|
1424
|
+
const open = booleanArg("inspector.setSection", args, "open");
|
|
1425
|
+
const section = stringArg("inspector.setSection", args, "section");
|
|
1426
|
+
const known = inspectorSectionKeys();
|
|
1427
|
+
if (!known.includes(section)) {
|
|
1428
|
+
throw new RangeError(
|
|
1429
|
+
`command "inspector.setSection" argument "section": "${section}" is not a section ` +
|
|
1430
|
+
`this document declares — declared: ${known.join(", ")}`,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
setInspectorSection(section, open);
|
|
1434
|
+
},
|
|
1435
|
+
title: "Show Inspector Section",
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
/**
|
|
1439
|
+
* Find Usages. Defined ONCE, here, and rendered twice: the palette gets it from the app
|
|
1440
|
+
* registry, and `editor/context-menu.ts` pulls the `context/element` records out of this same
|
|
1441
|
+
* set into the menu it builds — so the row and the palette entry cannot drift in title, chord
|
|
1442
|
+
* or availability. It reads `ctx.selection.kind`, which both contexts populate from the node
|
|
1443
|
+
* under the cursor, so one `run` serves the menu's target and the canvas selection alike.
|
|
1444
|
+
*
|
|
1445
|
+
* `when` is the capability, not a `try`: a host with no `findReferences` route hides the
|
|
1446
|
+
* command rather than offering a verb that answers "0 usages" for a component on every page.
|
|
1447
|
+
*/
|
|
1448
|
+
category: "Selection",
|
|
1449
|
+
id: "selection.findUsages",
|
|
1450
|
+
level: "selection",
|
|
1451
|
+
menus: ["context/element", "palette"],
|
|
1452
|
+
group: "4_identity",
|
|
1453
|
+
undo: "none",
|
|
1454
|
+
when: (ctx) => ctx.capability.findReferences && ctx.selection.isComponentInstance,
|
|
1455
|
+
requires: "a component instance, on a backend that can search the project",
|
|
1456
|
+
run: () => {
|
|
1457
|
+
const tagName = componentSelectionTag();
|
|
1458
|
+
if (!tagName) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const componentPath = componentRegistry.find((c) => c.tagName === tagName)?.path ?? null;
|
|
1462
|
+
setInspectorSection(USAGES_SECTION, true);
|
|
1463
|
+
void loadUsages({ tagName, ...(componentPath ? { path: componentPath } : {}) }).then(() =>
|
|
1464
|
+
renderOnly("rightPanel"),
|
|
1465
|
+
);
|
|
1466
|
+
},
|
|
1467
|
+
title: "Find Usages",
|
|
1468
|
+
},
|
|
1469
|
+
];
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* The tag of the selected component instance, or null.
|
|
1474
|
+
*
|
|
1475
|
+
* Read from the document rather than from a menu target so the one record works in both registries:
|
|
1476
|
+
* the context menu's own `getContext` reports the right-clicked node's tag as `selection.kind`, and
|
|
1477
|
+
* the app's reports the canvas selection's. This is the fallback for the app registry, where the
|
|
1478
|
+
* selection lives on the tab.
|
|
1479
|
+
*/
|
|
1480
|
+
function componentSelectionTag(): string | null {
|
|
1481
|
+
const tab = activeTab.value;
|
|
1482
|
+
const selection = primarySelection(tab?.session.selection);
|
|
1483
|
+
if (!tab || !selection) {
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
const node = getNodeAtPath(tab.doc.document, selection);
|
|
1487
|
+
const tagName = typeof node?.tagName === "string" ? node.tagName : "";
|
|
1488
|
+
return tagName.includes("-") ? tagName : null;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Register the Inspector's section verb.
|
|
1493
|
+
*
|
|
1494
|
+
* @param {CommandRegistry} registry
|
|
1495
|
+
*/
|
|
1496
|
+
export function registerInspectorCommands(registry: CommandRegistry): void {
|
|
1497
|
+
registry.registerAll(inspectorCommands());
|
|
1691
1498
|
}
|