@jxsuite/studio 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/data/elements-meta.json +5 -15
- package/data/html-meta.json +51 -6
- package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
- package/dist/chunks/action-group-sp6tna8v.js +118 -0
- package/dist/chunks/adapter-nj7tnjfn.js +13 -0
- package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
- package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
- package/dist/chunks/color-area-v17df7b8.js +154 -0
- package/dist/chunks/color-field-anwybnkn.js +208 -0
- package/dist/chunks/color-slider-44ez96fc.js +79 -0
- package/dist/chunks/color-xgg3kfw7.js +35 -0
- package/dist/chunks/combobox-5e7jev23.js +236 -0
- package/dist/chunks/components-2w1f35qs.js +18 -0
- package/dist/chunks/content-source-3wqeas45.js +46 -0
- package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
- package/dist/chunks/dialog-s890b918.js +85 -0
- package/dist/chunks/editor-3qhbg51x.js +35 -0
- package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
- package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
- package/dist/chunks/file-ops-3btj6st0.js +47 -0
- package/dist/chunks/files-ef8fgs3w.js +72 -0
- package/dist/chunks/format-host-3c33nwr8.js +40 -0
- package/dist/chunks/fs-events-13tj6sex.js +18 -0
- package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
- package/dist/chunks/icons-yy10wk9r.js +402 -0
- package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
- package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
- package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
- package/dist/chunks/iframe-host-jgesd924.js +115 -0
- package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
- package/dist/chunks/index-n4j7qxyr.js +109 -0
- package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
- package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
- package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
- package/dist/chunks/layers-1nq5evsh.js +29 -0
- package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
- package/dist/chunks/listbox-6v2ac08j.js +44 -0
- package/dist/chunks/lit-html-m244w9nh.js +17 -0
- package/dist/chunks/media-picker-2qbetq73.js +33 -0
- package/dist/chunks/menu-q99sy9rf.js +285 -0
- package/dist/chunks/menu-rf7n72mf.js +14 -0
- package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
- package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
- package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
- package/dist/chunks/notify-628t0anh.js +21 -0
- package/dist/chunks/number-field-5cndyft8.js +72 -0
- package/dist/chunks/parse-0zx14y15.js +10 -0
- package/dist/chunks/platform-ts9cg1a4.js +15 -0
- package/dist/chunks/popover-47z3ytva.js +25 -0
- package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
- package/dist/chunks/quick-search-srzz83k2.js +45 -0
- package/dist/chunks/references-n553f0e1.js +26 -0
- package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
- package/dist/chunks/section-registry-5pq32t0q.js +25 -0
- package/dist/chunks/select-2r16ez41.js +49 -0
- package/dist/chunks/split-g7nm1rgb.js +191 -0
- package/dist/chunks/studio-0hqbpbd0.js +62 -0
- package/dist/chunks/studio-18avnhjp.js +2430 -0
- package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
- package/dist/chunks/{studio-msd847zn.js → studio-25hkdrkw.js} +357 -86
- package/dist/chunks/studio-26d7myd1.js +258 -0
- package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
- package/dist/chunks/studio-3gxv60tn.js +422 -0
- package/dist/chunks/{studio-fbf92ckf.js → studio-3hxkd92t.js} +275 -163
- package/dist/chunks/studio-4m0haseg.js +185 -0
- package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
- package/dist/chunks/{studio-1wagb550.js → studio-5523bpzt.js} +11 -11
- package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
- package/dist/chunks/studio-5jy81t3s.js +17463 -0
- package/dist/chunks/studio-5rt7sqa0.js +1168 -0
- package/dist/chunks/studio-5te5tqng.js +81 -0
- package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
- package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
- package/dist/chunks/studio-6xa21ej9.js +28 -0
- package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
- package/dist/chunks/{studio-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
- package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
- package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
- package/dist/chunks/studio-7xptrg0j.js +954 -0
- package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
- package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
- package/dist/chunks/studio-8qtrpzr3.js +839 -0
- package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
- package/dist/chunks/studio-9xxz1q69.js +1356 -0
- package/dist/chunks/studio-akbww0j5.js +13615 -0
- package/dist/chunks/{studio-x813zp48.js → studio-arfpe389.js} +30 -30
- package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
- package/dist/chunks/studio-cpvb2ed1.js +1171 -0
- package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
- package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
- package/dist/chunks/studio-d1ja2x9r.js +362 -0
- package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
- package/dist/chunks/{studio-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
- package/dist/chunks/studio-erdatzvp.js +1190 -0
- package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
- package/dist/chunks/studio-fabxk6r9.js +2811 -0
- package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
- package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
- package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
- package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
- package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
- package/dist/chunks/studio-pm6hgy7r.js +4763 -0
- package/dist/chunks/studio-q0a3yk4n.js +33 -0
- package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
- package/dist/chunks/studio-qahxvnan.js +111 -0
- package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
- package/dist/chunks/{studio-2b946z26.js → studio-rkstz940.js} +3658 -6134
- package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
- package/dist/chunks/studio-rpyah2ew.js +2805 -0
- package/dist/chunks/studio-rs76k67r.js +12 -0
- package/dist/chunks/{studio-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
- package/dist/chunks/{studio-6yw4y3my.js → studio-sgdc5pfc.js} +8 -8
- package/dist/chunks/studio-tb3ev01j.js +763 -0
- package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
- package/dist/chunks/studio-vhv5tgxm.js +51 -0
- package/dist/chunks/{studio-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
- package/dist/chunks/studio-wqpngpsk.js +174 -0
- package/dist/chunks/{studio-b55y6ewx.js → studio-x3rty6tr.js} +58 -58
- package/dist/chunks/studio-xsykpvsh.js +8448 -0
- package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
- package/dist/chunks/studio-yxa81f45.js +12 -0
- package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
- package/dist/chunks/tabs-wbp1cdws.js +156 -0
- package/dist/chunks/textfield-abk6qyee.js +73 -0
- package/dist/chunks/toast-780pvfbv.js +27 -0
- package/dist/chunks/toast-host-3sqv46sn.js +116 -0
- package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
- package/dist/chunks/tooltip-eptb614w.js +369 -0
- package/dist/chunks/tree-a73p0pqm.js +367 -0
- package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
- package/dist/chunks/workspace-wrew8zk4.js +69 -0
- package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
- package/dist/iframe-entry.js +1780 -667
- package/dist/studio.js +52819 -36480
- package/dist/workers/editor.worker.js +1040 -1085
- package/dist/workers/json.worker.js +1946 -1991
- package/dist/workers/ts.worker.js +4960 -5005
- package/index.html +1 -0
- package/package.json +18 -52
- package/src/about/about-modal.ts +72 -123
- package/src/browse/library-commands.ts +20 -49
- package/src/browse/library-layouts.ts +87 -218
- package/src/browse/library-pane.ts +406 -438
- package/src/canvas/canvas-helpers.ts +0 -49
- package/src/canvas/canvas-live-render.ts +25 -2
- package/src/canvas/canvas-render.ts +666 -231
- package/src/canvas/canvas-surface.ts +0 -1
- package/src/canvas/canvas-utils.ts +310 -117
- package/src/canvas/dialog-path.ts +76 -0
- package/src/canvas/dialog-state.ts +67 -0
- package/src/canvas/diff-marks.ts +252 -0
- package/src/canvas/diff-toolbar.ts +349 -0
- package/src/canvas/diff-view.ts +123 -0
- package/src/canvas/edit-width-drag.ts +187 -0
- package/src/canvas/edit-width.ts +150 -0
- package/src/canvas/iframe-entry.ts +125 -8
- package/src/canvas/iframe-host.ts +271 -15
- package/src/canvas/iframe-interaction.ts +133 -13
- package/src/canvas/iframe-patch.ts +7 -1
- package/src/canvas/iframe-position.ts +5 -6
- package/src/canvas/iframe-protocol.ts +106 -5
- package/src/canvas/iframe-render.ts +373 -5
- package/src/canvas/iframe-slash.ts +2 -2
- package/src/canvas/iframe-subtree.ts +5 -8
- package/src/canvas/path-mapping.ts +25 -3
- package/src/canvas/popover-path.ts +106 -0
- package/src/canvas/popover-state.ts +155 -0
- package/src/canvas/surface-registry.ts +21 -10
- package/src/collab/presence-chips.ts +51 -73
- package/src/commands/app-commands.ts +10 -0
- package/src/commands/budget.ts +12 -0
- package/src/commands/command-args.ts +369 -6
- package/src/commands/defaults.ts +44 -14
- package/src/commands/live-context.ts +11 -5
- package/src/commands/reference.ts +12 -2
- package/src/commands/registry.ts +165 -8
- package/src/content/entry-commands.ts +16 -23
- package/src/content/entry-editor.ts +144 -101
- package/src/editor/canvas-slash-bridge.ts +2 -2
- package/src/editor/context-menu.ts +49 -200
- package/src/editor/convert-to-component.ts +42 -82
- package/src/editor/convert-to-repeater.ts +132 -173
- package/src/editor/inline-edit.ts +11 -0
- package/src/editor/shortcuts.ts +65 -26
- package/src/editor/slash-menu.ts +150 -157
- package/src/files/file-ops.ts +117 -11
- package/src/files/files.ts +943 -660
- package/src/files/json-layout.ts +474 -0
- package/src/files/serialize-document.ts +8 -1
- package/src/format/convert-file.ts +16 -10
- package/src/format/format-host.ts +50 -1
- package/src/github/github-auth.ts +71 -29
- package/src/github/github-publish.ts +57 -65
- package/src/grid/cell-editors.ts +46 -4
- package/src/grid/cell-popovers.ts +65 -62
- package/src/grid/grid-open.ts +86 -64
- package/src/grid/grid-panel.ts +390 -469
- package/src/grid/redirects-grid.ts +40 -44
- package/src/hosting/layout.ts +2 -0
- package/src/i18n/i18n-commands.ts +127 -62
- package/src/media/media-pane.ts +83 -133
- package/src/new-project/add-repo-modal.ts +125 -176
- package/src/new-project/import-tab.ts +181 -252
- package/src/new-project/location-fields.ts +93 -160
- package/src/new-project/new-project-modal.ts +376 -367
- package/src/packages/jxsuite-update.ts +76 -18
- package/src/panels/activity-panel.ts +137 -104
- package/src/panels/ai-chat/chat-markdown.ts +15 -13
- package/src/panels/ai-chat/chat-view.ts +307 -472
- package/src/panels/ai-chat/composer.ts +175 -163
- package/src/panels/ai-chat/sessions-view.ts +31 -62
- package/src/panels/ai-panel.ts +210 -185
- package/src/panels/block-action-bar.ts +437 -643
- package/src/panels/bottom-dock.ts +112 -47
- package/src/panels/chat-panel.ts +19 -14
- package/src/panels/data-explorer.ts +141 -102
- package/src/panels/data-grid.ts +183 -150
- package/src/panels/dnd.ts +75 -20
- package/src/panels/editors.ts +26 -19
- package/src/panels/elements-panel.ts +243 -186
- package/src/panels/empty-state.ts +26 -42
- package/src/panels/events-panel.ts +950 -729
- package/src/panels/formula-workspace.ts +434 -207
- package/src/panels/frontmatter-fields.ts +185 -153
- package/src/panels/frontmatter-panel.ts +411 -187
- package/src/panels/git-diff-open.ts +148 -0
- package/src/panels/git-panel.ts +679 -605
- package/src/panels/head-panel.ts +617 -435
- package/src/panels/i18n-panel.ts +153 -133
- package/src/panels/imports-panel.ts +388 -414
- package/src/panels/jump-bar.ts +174 -147
- package/src/panels/layers-panel.ts +785 -671
- package/src/panels/left-panel.ts +166 -94
- package/src/panels/navigator-panels.ts +1 -1
- package/src/panels/pane-context.ts +529 -677
- package/src/panels/pane-grid.ts +265 -241
- package/src/panels/panel-registry.ts +15 -4
- package/src/panels/problems-panel.ts +177 -99
- package/src/panels/properties-panel.ts +1525 -966
- package/src/panels/provenance.ts +31 -113
- package/src/panels/quick-search.ts +217 -172
- package/src/panels/right-panel.ts +135 -169
- package/src/panels/seo-modal.ts +346 -167
- package/src/panels/settings-menu.ts +109 -646
- package/src/panels/settings-pane.ts +102 -86
- package/src/panels/signals-panel.ts +1318 -1172
- package/src/panels/statement-editor.ts +556 -428
- package/src/panels/style-panel.ts +1367 -630
- package/src/panels/style-utils.ts +36 -32
- package/src/panels/stylebook-doc.ts +30 -73
- package/src/panels/stylebook-layers-panel.ts +202 -109
- package/src/panels/stylebook-panel.ts +124 -88
- package/src/panels/tab-strip.ts +385 -287
- package/src/platforms/cloud.ts +106 -16
- package/src/platforms/devserver.ts +13 -0
- package/src/publish/deploy-checklist.ts +124 -78
- package/src/publish/publish-commands.ts +4 -12
- package/src/publish/publish-panel.ts +134 -328
- package/src/services/a11y-report.ts +100 -43
- package/src/services/ai-command-tools.ts +468 -0
- package/src/services/ai-session-store.ts +6 -0
- package/src/services/ai-system-prompt.ts +110 -18
- package/src/services/ai-tools.ts +28 -143
- package/src/services/ai-write-report.ts +174 -0
- package/src/services/document-assistant.ts +50 -2
- package/src/services/idle.ts +16 -14
- package/src/services/live-surfaces.ts +237 -0
- package/src/services/model-uri.ts +75 -0
- package/src/services/notify.ts +63 -6
- package/src/services/popover-report.ts +325 -0
- package/src/services/surface-registry.ts +56 -0
- package/src/services/tool-executor.ts +6 -0
- package/src/settings/contexts-section.ts +301 -299
- package/src/settings/contributed-section.ts +361 -222
- package/src/settings/css-vars-editor.ts +320 -325
- package/src/settings/defs-editor.ts +572 -535
- package/src/settings/dependencies-editor.ts +79 -107
- package/src/settings/extension-commands.ts +379 -0
- package/src/settings/extension-rows.ts +113 -0
- package/src/settings/extensions-section.ts +287 -0
- package/src/settings/general-settings.ts +130 -181
- package/src/settings/head-editor.ts +10 -305
- package/src/settings/locales-section.ts +146 -196
- package/src/settings/preferences-accounts.ts +1 -1
- package/src/settings/preferences-dialog.ts +350 -330
- package/src/settings/project-sections.ts +108 -181
- package/src/settings/schema-field-ui.ts +11 -399
- package/src/settings/section-registry.ts +8 -1
- package/src/settings/settings-document.ts +31 -29
- package/src/shell/tree.ts +42 -87
- package/src/shell.ts +48 -36
- package/src/studio.ts +90 -67
- package/src/style/token-ref.ts +0 -39
- package/src/surfaces/about.json +263 -0
- package/src/surfaces/about.ts +163 -0
- package/src/surfaces/add-repo.json +486 -0
- package/src/surfaces/add-repo.ts +236 -0
- package/src/surfaces/ai-chat.json +2185 -0
- package/src/surfaces/ai-chat.ts +549 -0
- package/src/surfaces/ai-credentials-form.json +332 -0
- package/src/surfaces/ai-credentials-form.ts +151 -0
- package/src/surfaces/ai-managed-connect.json +108 -0
- package/src/surfaces/ai-managed-connect.ts +123 -0
- package/src/surfaces/ai-model-picker.json +114 -0
- package/src/surfaces/ai-model-picker.ts +139 -0
- package/src/surfaces/block-action-bar.json +640 -0
- package/src/surfaces/block-action-bar.ts +368 -0
- package/src/surfaces/bottom-dock.json +154 -0
- package/src/surfaces/bottom-dock.ts +125 -0
- package/src/surfaces/canvas-stage.json +444 -0
- package/src/surfaces/canvas-stage.ts +253 -0
- package/src/surfaces/cf-account-picker.json +300 -0
- package/src/surfaces/cf-account-picker.ts +214 -0
- package/src/surfaces/commandbar.json +984 -0
- package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
- package/src/surfaces/convert-repeater.json +309 -0
- package/src/surfaces/convert-repeater.ts +276 -0
- package/src/surfaces/data-actions.json +157 -0
- package/src/surfaces/data-actions.ts +115 -0
- package/src/surfaces/dialog.json +289 -0
- package/src/surfaces/dialog.ts +343 -0
- package/src/surfaces/diff-toolbar.json +266 -0
- package/src/surfaces/diff-toolbar.ts +139 -0
- package/src/surfaces/doc-header.json +739 -0
- package/src/surfaces/doc-header.ts +223 -0
- package/src/surfaces/empty-state.json +152 -0
- package/src/surfaces/empty-state.ts +173 -0
- package/src/surfaces/entry-editor.json +281 -0
- package/src/surfaces/entry-editor.ts +191 -0
- package/src/surfaces/expression-editor.json +977 -0
- package/src/surfaces/expression-editor.ts +266 -0
- package/src/surfaces/files-panel.json +592 -0
- package/src/surfaces/files-panel.ts +301 -0
- package/src/surfaces/formula-palette.json +364 -0
- package/src/surfaces/formula-palette.ts +340 -0
- package/src/surfaces/git-panel.json +1822 -0
- package/src/surfaces/git-panel.ts +272 -0
- package/src/surfaces/github-auth.json +112 -0
- package/src/surfaces/github-auth.ts +139 -0
- package/src/surfaces/github-publish.json +206 -0
- package/src/surfaces/github-publish.ts +169 -0
- package/src/surfaces/grid-cell.json +218 -0
- package/src/surfaces/grid-cell.ts +120 -0
- package/src/surfaces/grid-open.json +147 -0
- package/src/surfaces/grid-open.ts +87 -0
- package/src/surfaces/grid-panel.json +706 -0
- package/src/surfaces/grid-panel.ts +206 -0
- package/src/surfaces/grid-replace.json +179 -0
- package/src/surfaces/grid-replace.ts +89 -0
- package/src/surfaces/grid-views.json +446 -0
- package/src/surfaces/grid-views.ts +172 -0
- package/src/surfaces/inspector-dock.json +187 -0
- package/src/surfaces/inspector-dock.ts +161 -0
- package/src/surfaces/jump-bar.json +243 -0
- package/src/surfaces/jump-bar.ts +129 -0
- package/src/surfaces/jxsuite-update.json +146 -0
- package/src/surfaces/jxsuite-update.ts +149 -0
- package/src/surfaces/library-pane.json +1509 -0
- package/src/surfaces/library-pane.ts +363 -0
- package/src/surfaces/logic-panel.json +1949 -0
- package/src/surfaces/logic-panel.ts +369 -0
- package/src/surfaces/logic-workspace.json +685 -0
- package/src/surfaces/logic-workspace.ts +238 -0
- package/src/surfaces/media-browser.json +303 -0
- package/src/surfaces/media-browser.ts +145 -0
- package/src/surfaces/media-field.json +163 -0
- package/src/surfaces/media-field.ts +116 -0
- package/src/surfaces/media-pane.json +513 -0
- package/src/surfaces/media-pane.ts +197 -0
- package/src/surfaces/menu.json +232 -0
- package/src/surfaces/menu.ts +375 -0
- package/src/surfaces/navigator-dock.json +113 -0
- package/src/surfaces/navigator-dock.ts +130 -0
- package/src/surfaces/new-project.json +1654 -0
- package/src/surfaces/new-project.ts +411 -0
- package/src/surfaces/palette.json +535 -0
- package/src/surfaces/pane-context.json +1497 -0
- package/src/surfaces/pane-context.ts +513 -0
- package/src/surfaces/pane-grid.json +188 -0
- package/src/surfaces/pane-grid.ts +250 -0
- package/src/surfaces/panel-activity.json +563 -0
- package/src/surfaces/panel-activity.ts +187 -0
- package/src/surfaces/panel-data.json +141 -0
- package/src/surfaces/panel-data.ts +156 -0
- package/src/surfaces/panel-deploy-checklist.json +283 -0
- package/src/surfaces/panel-deploy-checklist.ts +175 -0
- package/src/surfaces/panel-elements.json +367 -0
- package/src/surfaces/panel-elements.ts +175 -0
- package/src/surfaces/panel-i18n.json +569 -0
- package/src/surfaces/panel-i18n.ts +212 -0
- package/src/surfaces/panel-imports.json +780 -0
- package/src/surfaces/panel-imports.ts +209 -0
- package/src/surfaces/panel-outline.json +734 -0
- package/src/surfaces/panel-outline.ts +326 -0
- package/src/surfaces/panel-page.json +1017 -0
- package/src/surfaces/panel-page.ts +240 -0
- package/src/surfaces/panel-problems.json +497 -0
- package/src/surfaces/panel-problems.ts +157 -0
- package/src/surfaces/panel-signals.json +1973 -0
- package/src/surfaces/panel-signals.ts +436 -0
- package/src/surfaces/panel-stylebook-layers.json +317 -0
- package/src/surfaces/panel-stylebook-layers.ts +155 -0
- package/src/surfaces/preferences.json +850 -0
- package/src/surfaces/preferences.ts +332 -0
- package/src/surfaces/progress-modal.json +110 -0
- package/src/surfaces/progress-modal.ts +150 -0
- package/src/surfaces/properties-panel.json +1578 -0
- package/src/surfaces/properties-panel.ts +337 -0
- package/src/surfaces/publish.json +945 -0
- package/src/surfaces/publish.ts +392 -0
- package/src/surfaces/push-plan.json +110 -0
- package/src/surfaces/push-plan.ts +87 -0
- package/src/surfaces/rail.json +255 -0
- package/src/surfaces/rail.ts +206 -0
- package/src/surfaces/reference-field.json +191 -0
- package/src/surfaces/reference-field.ts +168 -0
- package/src/surfaces/schema-builder.json +1011 -0
- package/src/surfaces/schema-builder.ts +181 -0
- package/src/surfaces/schema-form.json +1013 -0
- package/src/surfaces/schema-form.ts +287 -0
- package/src/surfaces/secret-field.json +60 -0
- package/src/surfaces/secret-field.ts +146 -0
- package/src/surfaces/seo.json +932 -0
- package/src/surfaces/seo.ts +334 -0
- package/src/surfaces/settings-contexts.json +538 -0
- package/src/surfaces/settings-contexts.ts +196 -0
- package/src/surfaces/settings-contributed.json +489 -0
- package/src/surfaces/settings-contributed.ts +199 -0
- package/src/surfaces/settings-css-vars.json +820 -0
- package/src/surfaces/settings-css-vars.ts +199 -0
- package/src/surfaces/settings-defs.json +1363 -0
- package/src/surfaces/settings-defs.ts +232 -0
- package/src/surfaces/settings-deploy.json +108 -0
- package/src/surfaces/settings-deploy.ts +135 -0
- package/src/surfaces/settings-extensions.json +486 -0
- package/src/surfaces/settings-extensions.ts +201 -0
- package/src/surfaces/settings-head.json +779 -0
- package/src/surfaces/settings-head.ts +397 -0
- package/src/surfaces/settings-locales.json +420 -0
- package/src/surfaces/settings-locales.ts +137 -0
- package/src/surfaces/settings-overview.json +423 -0
- package/src/surfaces/settings-overview.ts +218 -0
- package/src/surfaces/settings-packages.json +549 -0
- package/src/surfaces/settings-packages.ts +158 -0
- package/src/surfaces/settings-pane.json +147 -0
- package/src/surfaces/settings-pane.ts +150 -0
- package/src/surfaces/settings-rawjson.json +104 -0
- package/src/surfaces/settings-rawjson.ts +107 -0
- package/src/surfaces/shell.json +99 -0
- package/src/surfaces/shell.ts +30 -0
- package/src/surfaces/slash-menu.json +180 -0
- package/src/surfaces/slash-menu.ts +272 -0
- package/src/surfaces/statements.json +607 -0
- package/src/surfaces/statements.ts +238 -0
- package/src/surfaces/statusbar.json +94 -0
- package/src/{panels → surfaces}/statusbar.ts +163 -90
- package/src/surfaces/style-panel.json +1643 -0
- package/src/surfaces/style-panel.ts +338 -0
- package/src/surfaces/stylebook-chrome.json +122 -0
- package/src/surfaces/stylebook-chrome.ts +122 -0
- package/src/surfaces/tab-strip.json +448 -0
- package/src/surfaces/tab-strip.ts +200 -0
- package/src/surfaces/target-line.json +419 -0
- package/src/surfaces/target-line.ts +349 -0
- package/src/surfaces/toasts.json +189 -0
- package/src/surfaces/welcome.json +571 -0
- package/src/surfaces/welcome.ts +338 -0
- package/src/tabs/project-config.ts +71 -15
- package/src/tabs/tab.ts +52 -1
- package/src/types.ts +22 -0
- package/src/ui/ai-credentials-form.ts +104 -100
- package/src/ui/ai-managed-connect.ts +54 -36
- package/src/ui/ai-model-picker.ts +75 -47
- package/src/ui/cf-account-picker.ts +76 -141
- package/src/ui/color-selector.ts +80 -301
- package/src/ui/dynamic-slot.ts +43 -283
- package/src/ui/expression-editor.ts +797 -576
- package/src/ui/field-input.ts +8 -181
- package/src/ui/form-controls.ts +838 -366
- package/src/ui/formula-chips.ts +80 -85
- package/src/ui/kit.ts +33 -0
- package/src/ui/layers.ts +293 -593
- package/src/ui/media-picker.ts +203 -284
- package/src/ui/panel-resize.ts +251 -64
- package/src/ui/popover-surface.ts +153 -0
- package/src/ui/progress-modal.ts +22 -60
- package/src/ui/regions.ts +13 -7
- package/src/ui/schema-form.ts +906 -440
- package/src/ui/surface.ts +122 -0
- package/src/ui/unit-selector.ts +15 -131
- package/src/ui/value-source.ts +14 -0
- package/src/ui/virtual-window.ts +88 -9
- package/src/utils/canvas-media.ts +72 -0
- package/src/utils/edit-display.ts +7 -2
- package/src/utils/element-selectors.ts +69 -0
- package/src/utils/link-target.ts +2 -2
- package/src/utils/studio-utils.ts +0 -10
- package/src/view.ts +15 -0
- package/src/workspace/pane-derive.ts +36 -14
- package/src/workspace/workspace.ts +15 -6
- package/styles/canvas.css +72 -467
- package/styles/forced-colors.css +48 -82
- package/styles/forced-colors.json +41 -0
- package/styles/inspector.css +2 -1301
- package/styles/overlays.css +96 -2823
- package/styles/panels.css +6 -1423
- package/styles/shell-frame.css +272 -0
- package/styles/shell-frame.json +227 -0
- package/styles/shell.css +0 -1568
- package/styles/tokens.css +65 -136
- package/styles/tokens.json +84 -0
- package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
- package/dist/chunks/adapter-jkcyhgtr.js +0 -14
- package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
- package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
- package/dist/chunks/components-zmx10t62.js +0 -21
- package/dist/chunks/content-source-48xg86wj.js +0 -49
- package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
- package/dist/chunks/editor-cptq5jp1.js +0 -37
- package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
- package/dist/chunks/extension-sections-txv08xxg.js +0 -311
- package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
- package/dist/chunks/files-3kfkwhk4.js +0 -93
- package/dist/chunks/flow-qbfgrpeh.js +0 -634
- package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
- package/dist/chunks/focus-visible-6eg70z18.js +0 -167
- package/dist/chunks/format-host-w16q81tf.js +0 -43
- package/dist/chunks/fs-events-hrrg328g.js +0 -21
- package/dist/chunks/git-panel-532whr1y.js +0 -59
- package/dist/chunks/index-zqxmqxmh.js +0 -109
- package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
- package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
- package/dist/chunks/layers-q75tj2mf.js +0 -45
- package/dist/chunks/lit-html-j055mkh8.js +0 -22
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
- package/dist/chunks/notify-yjm79n4s.js +0 -30
- package/dist/chunks/parse-24d0gay3.js +0 -19
- package/dist/chunks/platform-ba1j1gbh.js +0 -16
- package/dist/chunks/publish-panel-v9st96ja.js +0 -29
- package/dist/chunks/quick-search-zccjp6gv.js +0 -66
- package/dist/chunks/references-3qx8fkyp.js +0 -29
- package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
- package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
- package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
- package/dist/chunks/studio-0a5qrh01.js +0 -3
- package/dist/chunks/studio-0bq6eh7v.js +0 -12
- package/dist/chunks/studio-18j3bmcz.js +0 -14
- package/dist/chunks/studio-1ngbejft.js +0 -181
- package/dist/chunks/studio-4b5z1x43.js +0 -424
- package/dist/chunks/studio-4n2b5gzb.js +0 -357
- package/dist/chunks/studio-4rzyrtjz.js +0 -426
- package/dist/chunks/studio-5fx3g9jg.js +0 -4325
- package/dist/chunks/studio-5myknm0a.js +0 -1903
- package/dist/chunks/studio-7afd21nw.js +0 -67
- package/dist/chunks/studio-7nte2hj4.js +0 -613
- package/dist/chunks/studio-8jpzqk5s.js +0 -128
- package/dist/chunks/studio-8q517dfr.js +0 -24
- package/dist/chunks/studio-950k0dx6.js +0 -6939
- package/dist/chunks/studio-9n4mrmnb.js +0 -184
- package/dist/chunks/studio-a0tswy0w.js +0 -58
- package/dist/chunks/studio-ahnzd9rg.js +0 -23
- package/dist/chunks/studio-cmb03bg5.js +0 -13890
- package/dist/chunks/studio-d1ks410h.js +0 -175
- package/dist/chunks/studio-ef49pnzx.js +0 -387
- package/dist/chunks/studio-eqq1w638.js +0 -113
- package/dist/chunks/studio-ew0djmhg.js +0 -341
- package/dist/chunks/studio-gfgb8pe6.js +0 -1652
- package/dist/chunks/studio-h4rxscfc.js +0 -10
- package/dist/chunks/studio-jc994dnr.js +0 -1066
- package/dist/chunks/studio-kyh9avvv.js +0 -28
- package/dist/chunks/studio-m3hwn7y1.js +0 -120
- package/dist/chunks/studio-mtfnaqs9.js +0 -659
- package/dist/chunks/studio-n6j9t10q.js +0 -318
- package/dist/chunks/studio-n8b9wqwy.js +0 -291
- package/dist/chunks/studio-qcy05dp8.js +0 -1743
- package/dist/chunks/studio-qq6jym04.js +0 -53
- package/dist/chunks/studio-rdx8jcxy.js +0 -83
- package/dist/chunks/studio-rn9ta2mz.js +0 -111
- package/dist/chunks/studio-t7qb9db8.js +0 -665
- package/dist/chunks/studio-tmc5emrw.js +0 -8
- package/dist/chunks/studio-z6nw2ek2.js +0 -18
- package/dist/chunks/studio-zczps70q.js +0 -378
- package/dist/chunks/studio-zqazsct2.js +0 -1823
- package/dist/chunks/workspace-fjx52ryv.js +0 -89
- package/src/panels/activity-bar.ts +0 -251
- package/src/panels/panel-scheduler.ts +0 -219
- package/src/panels/style-inputs.ts +0 -237
- package/src/panels/target-line.ts +0 -274
- package/src/panels/welcome-screen.ts +0 -346
- package/src/ui/button-group.ts +0 -102
- package/src/ui/choice-field.ts +0 -72
- package/src/ui/field-row.ts +0 -249
- package/src/ui/formula-palette.ts +0 -213
- package/src/ui/icons.ts +0 -52
- package/src/ui/jx-theme.ts +0 -130
- package/src/ui/spectrum.ts +0 -340
- package/src/ui/value-selector.ts +0 -174
- package/src/ui/widgets.ts +0 -172
- /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
- /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
- /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
- /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Style panel —
|
|
4
|
-
*
|
|
3
|
+
* Style panel — the FLOW behind the Inspector's Style tab (§6).
|
|
4
|
+
*
|
|
5
|
+
* The tab itself is a Jx document over the kit (`surfaces/style-panel.json`, mounted by
|
|
6
|
+
* `surfaces/style-panel.ts`), so this module draws no markup: it projects. What stays here is
|
|
7
|
+
* everything that is a DECISION — which compound coordinate the tab is editing, what css-meta says
|
|
8
|
+
* a property is, where a value came from, what a commit writes and into how many elements, and
|
|
9
|
+
* which of the four lists a control offers. The surface reads values.
|
|
5
10
|
*
|
|
6
11
|
* The panel resolves a compound coordinate before it can commit anything: `(selection, breakpoint,
|
|
7
12
|
* scheme layer, nested selector)`. It has always computed that tuple — it is the per-field key and
|
|
@@ -10,27 +15,48 @@
|
|
|
10
15
|
* gone: the breakpoint and scheme axes are selected on the pane context bar (region ⑦), and the
|
|
11
16
|
* selector is the one axis this tab owns.
|
|
12
17
|
*
|
|
13
|
-
* Every row then says where its value came from. `provenance.ts` supplies the
|
|
18
|
+
* Every row then says where its value came from. `provenance.ts` supplies the vocabulary;
|
|
14
19
|
* `computeInheritedSources()` supplies the donor breakpoint the cascade walk always knew and this
|
|
15
20
|
* panel used to throw away.
|
|
21
|
+
*
|
|
22
|
+
* Three consequences of the conversion are worth knowing before editing it.
|
|
23
|
+
*
|
|
24
|
+
* **The tab keeps ITSELF up to date, and must never go back through the scheduler.**
|
|
25
|
+
* `panels/panel-scheduler.ts`'s focus guard exists because a lit repaint of the whole dock could
|
|
26
|
+
* take the field a reader is typing into; {@link mountStyleTab} owns an `effect()` instead, so a
|
|
27
|
+
* commit, a breakpoint change and a settled usage query each reach the document as one write per
|
|
28
|
+
* bound property, and an equal write is skipped. `renderOnly("rightPanel")` is gone from this file
|
|
29
|
+
* with it.
|
|
30
|
+
*
|
|
31
|
+
* **Two controls are lists, and the list is the KIT MENU** (`surfaces/menu.ts`, §12.5). The unit
|
|
32
|
+
* picker and the keyword suggestions were each an `sp-overlay` + `sp-menu` of their own; the Value
|
|
33
|
+
* Source picker was a third. All three are `openMenu()` now, which is also how roving focus,
|
|
34
|
+
* typeahead and Escape arrive here without a line of keyboard code.
|
|
35
|
+
*
|
|
36
|
+
* **Nothing here is an island any more.** One control was: `specs/ui.md` §5.6 (Colour) was Pending,
|
|
37
|
+
* so a colour row drew an announced `[part="control-host"]` and a lit `paintColorControl` filled
|
|
38
|
+
* it, and that was the last Spectrum on this surface. §5.6 landed. The row is a `jx-color-field` in
|
|
39
|
+
* the document beside every other control, its palette is a `jx-swatch-group` slotted into the
|
|
40
|
+
* picker, and `ui/color-selector.ts` is what remains: which colours THIS PROJECT has named, which
|
|
41
|
+
* is a question no kit element can answer.
|
|
42
|
+
*
|
|
43
|
+
* @docs studio/design/style-inspector
|
|
16
44
|
*/
|
|
17
45
|
|
|
18
|
-
import { html, nothing } from "lit-html";
|
|
19
46
|
import { getNestedStyle } from "@jxsuite/schema/guards";
|
|
20
|
-
import {
|
|
21
|
-
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
47
|
+
import { effect, effectScope, reactive } from "../reactivity";
|
|
22
48
|
import {
|
|
23
|
-
|
|
49
|
+
cancelStyleDebounce,
|
|
24
50
|
debouncedStyleCommit,
|
|
25
51
|
getNodeAtPath,
|
|
26
52
|
isNestedSelector,
|
|
27
|
-
renderOnly,
|
|
28
53
|
updateUi,
|
|
29
54
|
} from "../store";
|
|
30
55
|
import { activeTab } from "../workspace/workspace";
|
|
31
56
|
import { primarySelection, unifyValues } from "../tabs/selection";
|
|
32
57
|
import { shell } from "../shell";
|
|
33
58
|
import { activeRegistry } from "../commands/active-registry";
|
|
59
|
+
import { selectorsForNode } from "../utils/element-selectors";
|
|
34
60
|
import { selectStylebookTag } from "./stylebook-panel";
|
|
35
61
|
import {
|
|
36
62
|
mutateUpdateMediaNestedStyle,
|
|
@@ -41,29 +67,35 @@ import {
|
|
|
41
67
|
mutateUpdateStyle,
|
|
42
68
|
transactDoc,
|
|
43
69
|
} from "../tabs/transact";
|
|
44
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
abbreviateValue,
|
|
72
|
+
friendlyNameToVar,
|
|
73
|
+
inferInputType,
|
|
74
|
+
kebabToLabel,
|
|
75
|
+
propLabel,
|
|
76
|
+
varDisplayName,
|
|
77
|
+
} from "../utils/studio-utils";
|
|
45
78
|
import { stringArg } from "../commands/command-args";
|
|
46
79
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
47
|
-
import { renderFieldRow } from "../ui/field-row";
|
|
48
80
|
import { showPromptDialog } from "../ui/layers";
|
|
49
|
-
import { renderDynamicSlot } from "../ui/dynamic-slot";
|
|
50
81
|
import { parseMediaEntries, schemeOfQuery } from "../utils/canvas-media";
|
|
51
82
|
import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
|
|
52
83
|
import { computeInheritedSources } from "../utils/inherited-style";
|
|
53
84
|
import { loadUsages, peekUsages, usageFiles } from "../services/references";
|
|
54
85
|
import { mediaDisplayName } from "./shared";
|
|
55
|
-
import { countProvenance, renderProvenanceChip, renderProvenanceDots } from "./provenance";
|
|
56
86
|
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
countProvenance,
|
|
88
|
+
provenanceSummaryText,
|
|
89
|
+
provenanceText,
|
|
90
|
+
provenanceTitle,
|
|
91
|
+
} from "./provenance";
|
|
61
92
|
import {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from "
|
|
93
|
+
attachTargetLine,
|
|
94
|
+
openSelectorMenu,
|
|
95
|
+
resetTargetLine as resetTargetSelector,
|
|
96
|
+
setTargetLine,
|
|
97
|
+
} from "../surfaces/target-line";
|
|
98
|
+
import { clickAnythingTo, openPageAction, staleSelectionMessage } from "./empty-state";
|
|
67
99
|
import {
|
|
68
100
|
allConditionsPass,
|
|
69
101
|
autoOpenSections,
|
|
@@ -73,12 +105,34 @@ import {
|
|
|
73
105
|
expandBorderSide,
|
|
74
106
|
expandShorthand,
|
|
75
107
|
getCssInitialMap,
|
|
108
|
+
getFontVars,
|
|
109
|
+
TYPO_PREVIEW_CHANNELS,
|
|
76
110
|
getLonghands,
|
|
77
111
|
} from "./style-utils";
|
|
78
|
-
import {
|
|
79
|
-
|
|
112
|
+
import { UNIT_RE } from "../ui/unit-selector";
|
|
113
|
+
import { colorTokens, resolvedColor } from "../ui/color-selector";
|
|
114
|
+
import { resolveTokenValue, toTokenRef, tokenRefName } from "../style/token-ref";
|
|
115
|
+
import { mountStylePanelSurface } from "../surfaces/style-panel";
|
|
116
|
+
import { openMenu } from "../surfaces/menu";
|
|
117
|
+
import type { MenuHandle } from "../surfaces/menu";
|
|
118
|
+
import { cloneValue } from "../tabs/doc-op-apply";
|
|
119
|
+
import { effectiveSlotMode, slotModeSeed, switchSlotMode } from "../ui/dynamic-slot";
|
|
120
|
+
import { slotCaps, VALUE_SOURCE_HINTS, VALUE_SOURCE_LABELS } from "../ui/value-source";
|
|
121
|
+
import { dialogPathFor } from "../canvas/dialog-path";
|
|
122
|
+
|
|
123
|
+
import type { SlotMode } from "../ui/value-source";
|
|
80
124
|
import type { FieldProvenance, ProvenanceState } from "./provenance";
|
|
81
|
-
import type { TargetScope, TargetSegment } from "
|
|
125
|
+
import type { TargetScope, TargetSegment } from "../surfaces/target-line";
|
|
126
|
+
import type {
|
|
127
|
+
StyleButtonView,
|
|
128
|
+
StyleOptionView,
|
|
129
|
+
StylePanelActions,
|
|
130
|
+
StylePanelSurface,
|
|
131
|
+
StylePanelView,
|
|
132
|
+
StyleRowView,
|
|
133
|
+
StyleSectionView,
|
|
134
|
+
} from "../surfaces/style-panel";
|
|
135
|
+
import type { EffectScope } from "../reactivity";
|
|
82
136
|
import type { Tab } from "../tabs/tab";
|
|
83
137
|
import type { JxPath } from "../state";
|
|
84
138
|
import type { JsonValue } from "../types";
|
|
@@ -256,217 +310,6 @@ function mixedStyleProps(
|
|
|
256
310
|
return mixed;
|
|
257
311
|
}
|
|
258
312
|
|
|
259
|
-
// ─── Row renderers ──────────────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
function renderStyleRow(
|
|
262
|
-
entry: CssPropertyEntry,
|
|
263
|
-
prop: string,
|
|
264
|
-
value: string,
|
|
265
|
-
onCommit: (v: string | undefined) => void,
|
|
266
|
-
isWarning: boolean,
|
|
267
|
-
gridMode: boolean,
|
|
268
|
-
inheritedValue: string | undefined,
|
|
269
|
-
templateSignals: string[] = [],
|
|
270
|
-
fieldKey: string = prop,
|
|
271
|
-
provenance?: FieldProvenance,
|
|
272
|
-
) {
|
|
273
|
-
const chip: FieldProvenance = provenance ?? DEFAULT_PROVENANCE;
|
|
274
|
-
const type = inferInputType(entry);
|
|
275
|
-
const hasVal = value !== undefined && value !== "";
|
|
276
|
-
const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
|
|
277
|
-
const spanVal = gridMode && (entry as Record<string, unknown>).$span === 2 ? 2 : undefined;
|
|
278
|
-
// The rungs come from `StyleObject.additionalProperties`, not from a list here (§6.6 rule 2).
|
|
279
|
-
// That derives literal + ${} template: a CSS value carrying `${…}` IS a signal binding, which
|
|
280
|
-
// `emitStyleString` compiles to a reactive declaration, and `$ref` is not admitted by JxStyle.
|
|
281
|
-
// Naming the POSITION rather than the answer is the point — a schema that later admits another
|
|
282
|
-
// Rung cannot then be contradicted by a hand-written array sitting here.
|
|
283
|
-
const slot = renderDynamicSlot({
|
|
284
|
-
caps: "styleProperty",
|
|
285
|
-
fieldKey,
|
|
286
|
-
onChange: (v?: JsonValue) => onCommit(v === undefined || v === "" ? undefined : String(v)),
|
|
287
|
-
staticWidget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
|
|
288
|
-
stateDefs: templateSignals,
|
|
289
|
-
value,
|
|
290
|
-
});
|
|
291
|
-
// `hasValue` is false because the chip supersedes the row's derived set-dot: the dot could only
|
|
292
|
-
// Say set-or-not, and three of the four states this panel can now answer with were previously
|
|
293
|
-
// Indistinguishable from "unset".
|
|
294
|
-
return renderFieldRow({
|
|
295
|
-
prop,
|
|
296
|
-
label: propLabel(entry, prop),
|
|
297
|
-
hasValue: false,
|
|
298
|
-
widget: slot.widget,
|
|
299
|
-
provenance: chip,
|
|
300
|
-
labelExtra: slot.modeButton,
|
|
301
|
-
...(spanVal != null && { span: spanVal }),
|
|
302
|
-
warning: isWarning,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** What {@link renderShorthandRow} needs to draw one shorthand and its longhand children. */
|
|
307
|
-
interface ShorthandRowOptions {
|
|
308
|
-
prop: string;
|
|
309
|
-
entry: CssPropertyEntry;
|
|
310
|
-
/** The style block at the edited coordinate — the primary element's, as every row reads it. */
|
|
311
|
-
style: Record<string, unknown>;
|
|
312
|
-
/** The write, already fanned across the selection. Called INSIDE a transaction. */
|
|
313
|
-
mutate: StyleMutateFn;
|
|
314
|
-
inherited: Record<string, string | number>;
|
|
315
|
-
ctx: ProvenanceCtx;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* A shorthand property row: the header field, plus one row per longhand when it is expanded.
|
|
320
|
-
*
|
|
321
|
-
* Two things were wrong with it, and they were the same thing. It took a single-target mutation
|
|
322
|
-
* while every other row in the tab took the selection-wide one, so "set padding on six cards in one
|
|
323
|
-
* decision" — the sentence §6.5 makes, with padding as its example — wrote to one card. And it drew
|
|
324
|
-
* its own `.set-dot` rather than a provenance chip, so a shorthand `mixedStyleProps` had ALREADY
|
|
325
|
-
* flagged as mixed offered a plain "Clear padding" affordance instead of saying so.
|
|
326
|
-
*
|
|
327
|
-
* Both are fixed by taking the same two things every other row takes: `mutate`, which fans across
|
|
328
|
-
* the selection, and `ctx`, which answers where a value came from.
|
|
329
|
-
*/
|
|
330
|
-
function renderShorthandRow({
|
|
331
|
-
prop: shortProp,
|
|
332
|
-
entry,
|
|
333
|
-
style,
|
|
334
|
-
mutate,
|
|
335
|
-
inherited,
|
|
336
|
-
ctx,
|
|
337
|
-
}: ShorthandRowOptions) {
|
|
338
|
-
const tab = activeTab.value!;
|
|
339
|
-
const longhands = getLonghands(shortProp) as CssLonghand[];
|
|
340
|
-
const shortVal = style[shortProp];
|
|
341
|
-
const hasLonghands = longhands.some((l: CssLonghand) => style[l.name] !== undefined);
|
|
342
|
-
const isExpanded = tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
|
|
343
|
-
const hasAnyVal = shortVal !== undefined || hasLonghands;
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Drop every longhand, then write the shorthand — the one write order this row has.
|
|
347
|
-
*
|
|
348
|
-
* The longhands go unconditionally rather than "the ones the panel can see set", because what the
|
|
349
|
-
* panel can see is the PRIMARY element's style: a second selected element with a `paddingTop` of
|
|
350
|
-
* its own would otherwise keep it and quietly out-rank the shorthand just written over it.
|
|
351
|
-
* Deleting a property an element does not have is a no-op on that element.
|
|
352
|
-
*/
|
|
353
|
-
const writeShorthand = (val?: string | undefined) =>
|
|
354
|
-
transactDoc(activeTab.value, (t) => {
|
|
355
|
-
for (const l of longhands) {
|
|
356
|
-
mutate(t, l.name);
|
|
357
|
-
}
|
|
358
|
-
mutate(t, shortProp, val);
|
|
359
|
-
});
|
|
360
|
-
const clearAll = () =>
|
|
361
|
-
transactDoc(activeTab.value, (t) => {
|
|
362
|
-
mutate(t, shortProp);
|
|
363
|
-
for (const l of longhands) {
|
|
364
|
-
mutate(t, l.name);
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const headerWidget = html`
|
|
369
|
-
<div class="style-shorthand-header">
|
|
370
|
-
<sp-textfield
|
|
371
|
-
size="s"
|
|
372
|
-
.value=${live(shortVal || "")}
|
|
373
|
-
placeholder=${
|
|
374
|
-
!shortVal && hasLonghands
|
|
375
|
-
? longhands.map((l: CssLonghand) => style[l.name] || "0").join(" ")
|
|
376
|
-
: !shortVal && inherited[shortProp]
|
|
377
|
-
? inherited[shortProp]
|
|
378
|
-
: !shortVal && longhands.some((l: CssLonghand) => inherited[l.name])
|
|
379
|
-
? longhands.map((l: CssLonghand) => inherited[l.name] || "0").join(" ")
|
|
380
|
-
: ""
|
|
381
|
-
}
|
|
382
|
-
@input=${debouncedStyleCommit(`short:${shortProp}`, 400, (e: Event) => {
|
|
383
|
-
writeShorthand((e.target as HTMLInputElement).value || undefined);
|
|
384
|
-
})}
|
|
385
|
-
></sp-textfield>
|
|
386
|
-
<sp-action-button
|
|
387
|
-
size="xs"
|
|
388
|
-
quiet
|
|
389
|
-
@click=${(e: Event) => {
|
|
390
|
-
e.stopPropagation();
|
|
391
|
-
activeTab.value!.session.ui.styleShorthands = {
|
|
392
|
-
...activeTab.value!.session.ui.styleShorthands,
|
|
393
|
-
[shortProp]: !isExpanded,
|
|
394
|
-
};
|
|
395
|
-
}}
|
|
396
|
-
>
|
|
397
|
-
${
|
|
398
|
-
isExpanded
|
|
399
|
-
? html`<sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>`
|
|
400
|
-
: html`<sp-icon-chevron-right slot="icon"></sp-icon-chevron-right>`
|
|
401
|
-
}
|
|
402
|
-
</sp-action-button>
|
|
403
|
-
</div>
|
|
404
|
-
`;
|
|
405
|
-
|
|
406
|
-
return html`
|
|
407
|
-
${renderFieldRow({
|
|
408
|
-
prop: shortProp,
|
|
409
|
-
label: propLabel(entry, shortProp),
|
|
410
|
-
hasValue: hasAnyVal,
|
|
411
|
-
widget: headerWidget,
|
|
412
|
-
provenance: shorthandProvenance(shortProp, ctx, hasAnyVal ? clearAll : undefined),
|
|
413
|
-
})}
|
|
414
|
-
${
|
|
415
|
-
isExpanded
|
|
416
|
-
? (() => {
|
|
417
|
-
const isBorderSide =
|
|
418
|
-
(entry as Record<string, unknown>).$shorthandType === "border-side";
|
|
419
|
-
const expanded = shortVal
|
|
420
|
-
? isBorderSide
|
|
421
|
-
? expandBorderSide(shortVal as string)
|
|
422
|
-
: expandShorthand(shortVal as string, longhands.length)
|
|
423
|
-
: null;
|
|
424
|
-
const compress = isBorderSide ? compressBorderSide : compressShorthand;
|
|
425
|
-
const emptyVal = isBorderSide ? "" : "0";
|
|
426
|
-
/** This longhand becomes `val`; the others keep whatever they show. */
|
|
427
|
-
const recompress = (idx: number, val: string) =>
|
|
428
|
-
writeShorthand(
|
|
429
|
-
compress(
|
|
430
|
-
longhands.map((l: CssLonghand, i: number) =>
|
|
431
|
-
i === idx ? val : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
432
|
-
) as string[],
|
|
433
|
-
),
|
|
434
|
-
);
|
|
435
|
-
return longhands.map(({ name, entry: lEntry }: CssLonghand, idx: number) => {
|
|
436
|
-
const lVal = style[name] ?? (expanded ? expanded[idx] : "");
|
|
437
|
-
const hasLVal = lVal !== undefined && lVal !== "";
|
|
438
|
-
return html`
|
|
439
|
-
<div class="style-row style-row--child" data-prop=${name}>
|
|
440
|
-
<div class="style-row-label">
|
|
441
|
-
${renderProvenanceChip(
|
|
442
|
-
name,
|
|
443
|
-
longhandProvenance(name, shortProp, hasLVal, ctx, () =>
|
|
444
|
-
recompress(idx, emptyVal),
|
|
445
|
-
),
|
|
446
|
-
)}
|
|
447
|
-
<sp-field-label size="s" title=${name}
|
|
448
|
-
>${propLabel(lEntry, name)}</sp-field-label
|
|
449
|
-
>
|
|
450
|
-
</div>
|
|
451
|
-
${widgetForType(
|
|
452
|
-
inferInputType(lEntry),
|
|
453
|
-
lEntry,
|
|
454
|
-
name,
|
|
455
|
-
lVal as string,
|
|
456
|
-
(newVal: string) => recompress(idx, newVal || emptyVal),
|
|
457
|
-
{
|
|
458
|
-
placeholder: !lVal && inherited[name] ? String(inherited[name]) : "",
|
|
459
|
-
},
|
|
460
|
-
)}
|
|
461
|
-
</div>
|
|
462
|
-
`;
|
|
463
|
-
});
|
|
464
|
-
})()
|
|
465
|
-
: nothing
|
|
466
|
-
}
|
|
467
|
-
`;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
313
|
// ─── The edit target ─────────────────────────────────────────────────────────
|
|
471
314
|
|
|
472
315
|
/** Forget the selector menu's element handle — the Inspector unmounted, or a test starts clean. */
|
|
@@ -478,13 +321,15 @@ export function resetSelectorMenu(): void {
|
|
|
478
321
|
* Whether the "show affected" list under a project-wide warning band is open.
|
|
479
322
|
*
|
|
480
323
|
* Module-local rather than a `session.ui` field: it is a disclosure on a project-level warning, and
|
|
481
|
-
* a per-document field would make the same warning open in one tab and closed in another.
|
|
324
|
+
* a per-document field would make the same warning open in one tab and closed in another. Reactive
|
|
325
|
+
* because the tab is now driven by an `effect()` — a plain field would move the disclosure and
|
|
326
|
+
* nothing would redraw.
|
|
482
327
|
*/
|
|
483
|
-
|
|
328
|
+
const _disclosure = reactive({ showAffected: false, usageRevision: 0 });
|
|
484
329
|
|
|
485
330
|
/** Fold the affected list away — the Inspector unmounted, or a test starts clean. */
|
|
486
331
|
export function resetAffectedDisclosure(): void {
|
|
487
|
-
|
|
332
|
+
_disclosure.showAffected = false;
|
|
488
333
|
}
|
|
489
334
|
|
|
490
335
|
/**
|
|
@@ -580,21 +425,32 @@ function projectScope(label: string, tagName: string | null): TargetScope {
|
|
|
580
425
|
return { affected: "how many pages that is, is unknown", kind: "project", label };
|
|
581
426
|
}
|
|
582
427
|
const query = { tagName };
|
|
428
|
+
// The cache behind `peekUsages` is a plain Map, so a settled sweep is invisible to the effect
|
|
429
|
+
// Driving this tab. The revision is what makes it visible, and reading it here is what enrols
|
|
430
|
+
// The projection in it.
|
|
431
|
+
void _disclosure.usageRevision;
|
|
583
432
|
const state = peekUsages(query);
|
|
584
433
|
if (state === null) {
|
|
585
|
-
// First
|
|
586
|
-
//
|
|
587
|
-
void loadUsages(query).then(() =>
|
|
434
|
+
// First projection for this tag: ask once, and re-project when the answer lands. `loadUsages`
|
|
435
|
+
// Joins an in-flight request, so a tab re-projecting sixty times a second is still one sweep.
|
|
436
|
+
void loadUsages(query).then(() => {
|
|
437
|
+
/* The bump is conditional on the cache having actually SETTLED, and that condition is what
|
|
438
|
+
makes the loop terminate. Bumping unconditionally would re-run the effect, which would find
|
|
439
|
+
`peekUsages` still null, ask again and bump again — a spin, not a repaint. A host that
|
|
440
|
+
cannot answer this query at all is exactly the state that produces it. */
|
|
441
|
+
if (peekUsages(query) !== null) {
|
|
442
|
+
_disclosure.usageRevision += 1;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
588
445
|
}
|
|
589
446
|
return {
|
|
590
447
|
kind: "project",
|
|
591
448
|
label,
|
|
592
449
|
affected: affectedSentence(state),
|
|
593
450
|
affectedFiles: state?.status === "ready" ? usageFiles(state.result) : [],
|
|
594
|
-
showAffected:
|
|
451
|
+
showAffected: _disclosure.showAffected,
|
|
595
452
|
onToggleAffected: () => {
|
|
596
|
-
|
|
597
|
-
renderOnly("rightPanel");
|
|
453
|
+
_disclosure.showAffected = !_disclosure.showAffected;
|
|
598
454
|
},
|
|
599
455
|
};
|
|
600
456
|
}
|
|
@@ -841,25 +697,737 @@ function sectionProvenance(
|
|
|
841
697
|
);
|
|
842
698
|
}
|
|
843
699
|
|
|
844
|
-
// ───
|
|
700
|
+
// ─── The row register ────────────────────────────────────────────────────────
|
|
701
|
+
|
|
702
|
+
/** One choice a row's list offers. `divider` draws a rule above the entry. */
|
|
703
|
+
interface StyleChoice {
|
|
704
|
+
value: string;
|
|
705
|
+
label: string;
|
|
706
|
+
divider?: boolean;
|
|
707
|
+
checked?: boolean;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* What one projected row can DO, held beside the row the document draws.
|
|
712
|
+
*
|
|
713
|
+
* The document names a row by its `key` and nothing else, so this is where a key becomes a verb.
|
|
714
|
+
* Every handler is optional because rows differ in what they own: a longhand child has no Value
|
|
715
|
+
* Source ladder, a custom pair has no unit list, and the add-a-property field has no value to
|
|
716
|
+
* clear.
|
|
717
|
+
*/
|
|
718
|
+
interface RowActions {
|
|
719
|
+
/** The reader is typing — debounced under {@link RowActions.debounceId}. */
|
|
720
|
+
edit?: (value: string) => void;
|
|
721
|
+
/** The reader left the field or pressed Enter. Cancels the pending debounce first. */
|
|
722
|
+
commit?: (value: string) => void;
|
|
723
|
+
debounceId?: string;
|
|
724
|
+
/** The provenance chip, a custom pair's remove and a nested rule's remove are one verb. */
|
|
725
|
+
chip?: () => void;
|
|
726
|
+
/** The list this row's chooser offers, and what taking one from it does. */
|
|
727
|
+
choices?: StyleChoice[];
|
|
728
|
+
choicesLabel?: string;
|
|
729
|
+
choose?: (value: string) => void;
|
|
730
|
+
/** The Value Source ladder for this position (§6.3). */
|
|
731
|
+
source?: {
|
|
732
|
+
mode: SlotMode;
|
|
733
|
+
caps: SlotMode[];
|
|
734
|
+
fieldKey: string;
|
|
735
|
+
value: unknown;
|
|
736
|
+
stateDefs: string[];
|
|
737
|
+
onChange: (value?: JsonValue) => void;
|
|
738
|
+
};
|
|
739
|
+
/** Expand or collapse a shorthand's longhands. */
|
|
740
|
+
toggle?: () => void;
|
|
741
|
+
/** Rename a custom property, keeping each element's own value. */
|
|
742
|
+
rename?: (name: string) => void;
|
|
743
|
+
/** Seed a new custom property. Returns whether anything was written. */
|
|
744
|
+
add?: (name: string) => boolean;
|
|
745
|
+
/** Open a nested rule as the active selector. */
|
|
746
|
+
open?: () => void;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Every row on screen, by key. Rebuilt whole on every projection; read by every action. */
|
|
750
|
+
let _rows = new Map<string, RowActions>();
|
|
751
|
+
|
|
752
|
+
/** What each section's clear-all dot does. Rebuilt with the sections it belongs to. */
|
|
753
|
+
let _sectionClears = new Map<string, () => void>();
|
|
754
|
+
|
|
755
|
+
/** What "+ Add" under Relative Styling does. Rebuilt with the section. */
|
|
756
|
+
let _nestedAdd: (() => void) | null = null;
|
|
757
|
+
|
|
758
|
+
/** The value each button-group row is holding — pressing the selected button again clears it. */
|
|
759
|
+
let _buttonValues = new Map<string, string>();
|
|
760
|
+
|
|
761
|
+
/** Look one row up. A key the projection no longer has is a stale click, and does nothing. */
|
|
762
|
+
function rowActions(key: string): RowActions | undefined {
|
|
763
|
+
return _rows.get(key);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ─── Widgets ─────────────────────────────────────────────────────────────────
|
|
767
|
+
|
|
768
|
+
/** Whether a typed value is a bare number — the unit control's own test. */
|
|
769
|
+
function isNumericValue(value: string): boolean {
|
|
770
|
+
return /^-?\d*\.?\d*$/.test(value);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Kit glyph names for the values a button-group offers.
|
|
775
|
+
*
|
|
776
|
+
* Only names the kit ACTUALLY has (`@jxsuite/ui/icons`) are here, and every other value falls back
|
|
777
|
+
* to `abbreviateValue`, which is the same fallback the group has always had for a value with no
|
|
778
|
+
* icon. The alternative was to guess an approximation — `align-left` for `justify-content:
|
|
779
|
+
* flex-start`, say, which is a text-alignment glyph — and a glyph that means something else is
|
|
780
|
+
* worse than three letters that mean what they say. The seventeen names still missing are recorded
|
|
781
|
+
* against the kit rather than approximated here.
|
|
782
|
+
*/
|
|
783
|
+
const BUTTON_GLYPHS: Readonly<Record<string, string>> = {
|
|
784
|
+
"arrow-down": "arrow-down",
|
|
785
|
+
"arrow-left": "arrow-left",
|
|
786
|
+
"arrow-right": "arrow-right",
|
|
787
|
+
"arrow-up": "arrow-up",
|
|
788
|
+
"text-align-center": "text-align-center",
|
|
789
|
+
"text-align-justify": "text-align-justify",
|
|
790
|
+
"text-align-left": "text-align-left",
|
|
791
|
+
"text-align-right": "text-align-right",
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
/** The buttons a button-group row draws, and the overflow list it hands to the kit menu. */
|
|
795
|
+
function buttonGroup(
|
|
796
|
+
entry: CssPropertyEntry,
|
|
797
|
+
rowKey: string,
|
|
798
|
+
value: string,
|
|
799
|
+
): { buttons: StyleButtonView[]; extra: string[] } {
|
|
800
|
+
const values = (entry.$buttonValues || entry.enum || []) as string[];
|
|
801
|
+
const iconMap = (entry.$icons || {}) as Record<string, string>;
|
|
802
|
+
const buttonValues = entry.$buttonValues as string[] | undefined;
|
|
803
|
+
const enumValues = entry.enum as string[] | undefined;
|
|
804
|
+
const extra =
|
|
805
|
+
buttonValues && enumValues && enumValues.length > buttonValues.length
|
|
806
|
+
? enumValues.filter((v) => !buttonValues.includes(v))
|
|
807
|
+
: [];
|
|
808
|
+
const buttons = values.map((v) => {
|
|
809
|
+
const icon = BUTTON_GLYPHS[iconMap[v] ?? ""] ?? "";
|
|
810
|
+
return {
|
|
811
|
+
icon,
|
|
812
|
+
row: rowKey,
|
|
813
|
+
selected: v === value,
|
|
814
|
+
text: icon === "" ? abbreviateValue(v) : "",
|
|
815
|
+
title: v,
|
|
816
|
+
value: v,
|
|
817
|
+
};
|
|
818
|
+
});
|
|
819
|
+
return { buttons, extra };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** How a keyword reads in a list: `Inline block` for `inline-block`, `Flex` for `flex`. */
|
|
823
|
+
function keywordLabel(value: string): string {
|
|
824
|
+
return value.includes("-") ? kebabToLabel(value) : value.replace(/^./, (c) => c.toUpperCase());
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** The keyword list a button row's overflow menu offers, in the words the menu prints. */
|
|
828
|
+
function keywordChoices(options: string[], value: string): StyleChoice[] {
|
|
829
|
+
return options.map((v) => ({ checked: v === value, label: keywordLabel(v), value: v }));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* The keyword list a `keywords` row suggests under its field.
|
|
834
|
+
*
|
|
835
|
+
* A typography row's options are their own preview: the axis the row moves is set on each option as
|
|
836
|
+
* the value it offers, through the `jx-option` channel `TYPO_PREVIEW_CHANNELS` names, and every
|
|
837
|
+
* such option is set in the element's own `face` — so a weight row shows `700` at 700 in the
|
|
838
|
+
* typeface the element actually uses, rather than in the panel's. Any other row's options are the
|
|
839
|
+
* words alone.
|
|
840
|
+
*/
|
|
841
|
+
function keywordOptions(options: string[], prop: string, face: string): StyleOptionView[] {
|
|
842
|
+
const channel = TYPO_PREVIEW_CHANNELS[prop];
|
|
843
|
+
return options.map((v) => {
|
|
844
|
+
const option: StyleOptionView = { label: keywordLabel(v), value: v };
|
|
845
|
+
if (channel) {
|
|
846
|
+
option[channel] = v;
|
|
847
|
+
option.face = face;
|
|
848
|
+
}
|
|
849
|
+
return option;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* The typeface the edited element is set in, for the typography rows to preview their values in:
|
|
855
|
+
* its own `fontFamily`, else the base context's when a breakpoint is being edited (which is what
|
|
856
|
+
* `inheritedStyle` holds — the cascade within the coordinate, not the ancestors'), followed through
|
|
857
|
+
* the effective style to the stack at the end of the chain. Empty when the element has no typeface
|
|
858
|
+
* of its own or the reference cannot be followed, in which case the rows are set in the panel's.
|
|
859
|
+
*/
|
|
860
|
+
function previewFace(ctx: EditorCtx): string {
|
|
861
|
+
const raw = ctx.activeStyle.fontFamily ?? ctx.inheritedStyle.fontFamily;
|
|
862
|
+
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
863
|
+
return "";
|
|
864
|
+
}
|
|
865
|
+
const resolved = resolveTokenValue(getEffectiveStyle(ctx.tab.doc.document?.style), raw);
|
|
866
|
+
return resolved === undefined ? "" : String(resolved);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** The Modern Font Stacks a `fontFamily` entry offers, as css-meta.json states them. */
|
|
870
|
+
function fontPresets(entry: CssPropertyEntry): { title: string; value: string }[] {
|
|
871
|
+
return Array.isArray(entry.presets) ? (entry.presets as { title: string; value: string }[]) : [];
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* The font list: the project's `--font-*` tokens first — the site's and the document's together,
|
|
876
|
+
* which is where a project declares its fonts once — then the presets it has not minted yet. Every
|
|
877
|
+
* row is its own specimen through `face`, which is the reason this is a combobox over `jx-option`
|
|
878
|
+
* rows rather than the kit menu it used to be: a menu row is an action and cannot draw itself in a
|
|
879
|
+
* typeface (ui.md §5.1), and a font is the one thing a reader chooses by looking.
|
|
880
|
+
*
|
|
881
|
+
* A row's VALUE is a token's name, minted or not, because that is what the field holds after a pick
|
|
882
|
+
* and what the commit turns into `var(--name)`. A token row says its name beside its title, so what
|
|
883
|
+
* the pick will write is in the list; a preset row is bare, which is what tells the two groups
|
|
884
|
+
* apart in a list that draws no divider.
|
|
885
|
+
*/
|
|
886
|
+
function fontOptions(entry: CssPropertyEntry): StyleOptionView[] {
|
|
887
|
+
const fontVars = getFontVars();
|
|
888
|
+
const style = getEffectiveStyle(activeTab.value?.doc.document?.style);
|
|
889
|
+
const options: StyleOptionView[] = fontVars.map((fv) => ({
|
|
890
|
+
description: fv.name,
|
|
891
|
+
face: String(resolveTokenValue(style, fv.value) ?? fv.value),
|
|
892
|
+
label: varDisplayName(fv.name, "--font-"),
|
|
893
|
+
value: fv.name,
|
|
894
|
+
}));
|
|
895
|
+
for (const preset of fontPresets(entry)) {
|
|
896
|
+
const name = friendlyNameToVar(preset.title, "--font-");
|
|
897
|
+
if (!fontVars.some((fv) => fv.name === name)) {
|
|
898
|
+
options.push({ face: preset.value, label: preset.title, value: name });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return options;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* What the font field WRITES. A token name becomes the reference that points at it; free text —
|
|
906
|
+
* `Georgia, serif` — is the value itself; and the empty string clears. A name that is a preset's
|
|
907
|
+
* and no token yet is minted into the document's own style first, so the token exists before
|
|
908
|
+
* anything references it — but only on a COMMIT, never on the debounced edit that follows each
|
|
909
|
+
* keystroke: a reader halfway through typing `--font-slab-serif` has not asked for a token, and one
|
|
910
|
+
* minted early would be left behind when they finished typing something else.
|
|
911
|
+
*/
|
|
912
|
+
function commitFont(
|
|
913
|
+
entry: CssPropertyEntry,
|
|
914
|
+
raw: string,
|
|
915
|
+
mint: boolean,
|
|
916
|
+
onCommit: (value: string) => void,
|
|
917
|
+
): void {
|
|
918
|
+
const name = raw.trim();
|
|
919
|
+
if (!name.startsWith("--")) {
|
|
920
|
+
onCommit(raw);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (mint && getEffectiveStyle(activeTab.value?.doc.document?.style)[name] === undefined) {
|
|
924
|
+
const preset = fontPresets(entry).find((p) => friendlyNameToVar(p.title, "--font-") === name);
|
|
925
|
+
if (preset) {
|
|
926
|
+
transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], name, preset.value));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
onCommit(toTokenRef(name));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** A blank row, so every `$switch` case reads a path that exists whatever the row draws. */
|
|
933
|
+
function blankRow(key: string, prop: string, kind: StyleRowView["kind"]): StyleRowView {
|
|
934
|
+
return {
|
|
935
|
+
buttons: [],
|
|
936
|
+
child: false,
|
|
937
|
+
chip: "none",
|
|
938
|
+
chipState: "default",
|
|
939
|
+
chipText: "",
|
|
940
|
+
chipTitle: "",
|
|
941
|
+
choicesHint: "",
|
|
942
|
+
choicesLabel: "",
|
|
943
|
+
expandIcon: "caret-right",
|
|
944
|
+
expandLabel: "",
|
|
945
|
+
expanded: false,
|
|
946
|
+
hasChoices: false,
|
|
947
|
+
hasLabel: false,
|
|
948
|
+
hasSource: false,
|
|
949
|
+
hasTokens: false,
|
|
950
|
+
key,
|
|
951
|
+
kind,
|
|
952
|
+
label: "",
|
|
953
|
+
max: "",
|
|
954
|
+
min: "",
|
|
955
|
+
mono: false,
|
|
956
|
+
name: "",
|
|
957
|
+
openTitle: "",
|
|
958
|
+
options: [],
|
|
959
|
+
overflowSelected: false,
|
|
960
|
+
placeholder: "",
|
|
961
|
+
prop,
|
|
962
|
+
removeLabel: "",
|
|
963
|
+
resolved: "",
|
|
964
|
+
sourceHint: "",
|
|
965
|
+
sourceLabel: "",
|
|
966
|
+
sourceState: "literal",
|
|
967
|
+
span: false,
|
|
968
|
+
step: "",
|
|
969
|
+
tokens: [],
|
|
970
|
+
value: "",
|
|
971
|
+
warning: false,
|
|
972
|
+
widget: "text",
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Fill a row's chip fields from the provenance answer, and register its click. */
|
|
977
|
+
function applyChip(row: StyleRowView, actions: RowActions, prov: FieldProvenance): void {
|
|
978
|
+
if (prov.state === "default") {
|
|
979
|
+
row.chip = "none";
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
row.chipState = prov.state;
|
|
983
|
+
row.chipText = provenanceText(prov);
|
|
984
|
+
row.chipTitle = provenanceTitle(row.prop, prov);
|
|
985
|
+
if (prov.onClick) {
|
|
986
|
+
row.chip = "button";
|
|
987
|
+
actions.chip = prov.onClick;
|
|
988
|
+
} else {
|
|
989
|
+
row.chip = "text";
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ─── The projection ──────────────────────────────────────────────────────────
|
|
994
|
+
|
|
995
|
+
/** What {@link buildEditor} is handed once, having resolved the coordinate. */
|
|
996
|
+
interface EditorCtx {
|
|
997
|
+
tab: Tab;
|
|
998
|
+
/** The block at the edited coordinate, as the rows display it. */
|
|
999
|
+
activeStyle: JxStyle;
|
|
1000
|
+
activeSelector: string | null;
|
|
1001
|
+
editMedia: string | null;
|
|
1002
|
+
inheritedStyle: Record<string, string | number>;
|
|
1003
|
+
provCtx: ProvenanceCtx;
|
|
1004
|
+
templateSignals: string[];
|
|
1005
|
+
/** One write, fanned across every selected element, inside the caller's transaction. */
|
|
1006
|
+
mutate: StyleMutateFn;
|
|
1007
|
+
/** The same write in a transaction of its own. */
|
|
1008
|
+
commit: (prop: string, value?: string | Record<string, unknown> | undefined) => void;
|
|
1009
|
+
/** The key prefix a row's identity is built from — the coordinate, spelled once. */
|
|
1010
|
+
coord: string;
|
|
1011
|
+
targets: readonly JxPath[];
|
|
1012
|
+
filter: string;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* The rungs a CSS declaration admits.
|
|
1017
|
+
*
|
|
1018
|
+
* Derived from `StyleObject.additionalProperties` rather than listed (§6.6 rule 2), minus `ref` —
|
|
1019
|
+
* and that subtraction is the rule the panel already wrote down being enforced rather than a
|
|
1020
|
+
* narrowing of it. **`JxStyle` has no `$ref` branch at all**: a CSS value carrying `${…}` IS the
|
|
1021
|
+
* signal binding, which `emitStyleString` compiles to a reactive declaration, and a `{ $ref }` in a
|
|
1022
|
+
* declaration is a document the schema refuses. The generic ladder offered the rung anyway whenever
|
|
1023
|
+
* the document happened to declare a signal — the old panel's own test asserted "literal and
|
|
1024
|
+
* template only" and passed only because its fixture had none — so a document with one signal in it
|
|
1025
|
+
* put a control on every style row that writes an invalid style block.
|
|
1026
|
+
*/
|
|
1027
|
+
function styleCaps(): SlotMode[] {
|
|
1028
|
+
return slotCaps("styleProperty").filter((mode) => mode !== "ref");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** One ordinary property row: the label, the chip, the ladder and whichever control it draws. */
|
|
1032
|
+
function fieldRow(
|
|
1033
|
+
ctx: EditorCtx,
|
|
1034
|
+
entry: CssPropertyEntry,
|
|
1035
|
+
prop: string,
|
|
1036
|
+
value: string,
|
|
1037
|
+
opts: {
|
|
1038
|
+
child?: boolean;
|
|
1039
|
+
warning?: boolean;
|
|
1040
|
+
span?: boolean;
|
|
1041
|
+
provenance: FieldProvenance;
|
|
1042
|
+
onCommit: (value: string | undefined) => void;
|
|
1043
|
+
placeholder?: string;
|
|
1044
|
+
},
|
|
1045
|
+
): { row: StyleRowView; actions: RowActions } {
|
|
1046
|
+
const key = `${ctx.coord}|${prop}${opts.child ? "|child" : ""}`;
|
|
1047
|
+
const row = blankRow(key, prop, "field");
|
|
1048
|
+
const actions: RowActions = {};
|
|
1049
|
+
const type = inferInputType(entry);
|
|
1050
|
+
const cssInitial = getCssInitialMap().get(prop) ?? "";
|
|
1051
|
+
const placeholder = opts.placeholder || cssInitial;
|
|
1052
|
+
const commitLiteral = (v: string) => opts.onCommit(v === "" ? undefined : v);
|
|
1053
|
+
|
|
1054
|
+
row.label = propLabel(entry, prop);
|
|
1055
|
+
row.hasLabel = true;
|
|
1056
|
+
row.child = opts.child === true;
|
|
1057
|
+
row.warning = opts.warning === true;
|
|
1058
|
+
row.span = opts.span === true;
|
|
1059
|
+
row.value = value;
|
|
1060
|
+
row.placeholder = placeholder;
|
|
1061
|
+
applyChip(row, actions, opts.provenance);
|
|
1062
|
+
|
|
1063
|
+
// The Value Source ladder (§6.3), and only where the position has one to offer. A longhand child
|
|
1064
|
+
// Is not a document position of its own — its value is a slice of the shorthand above it — so it
|
|
1065
|
+
// Carries no chip, which is what it has always done.
|
|
1066
|
+
if (!opts.child) {
|
|
1067
|
+
const caps = styleCaps();
|
|
1068
|
+
const fieldKey = `${ctx.coord}|${prop}`;
|
|
1069
|
+
const mode = effectiveSlotMode(fieldKey, value === "" ? undefined : value);
|
|
1070
|
+
row.hasSource = true;
|
|
1071
|
+
row.sourceState = mode;
|
|
1072
|
+
row.sourceLabel = VALUE_SOURCE_LABELS[mode];
|
|
1073
|
+
row.sourceHint = `Value source: ${VALUE_SOURCE_LABELS[mode]} — click to change`;
|
|
1074
|
+
actions.source = {
|
|
1075
|
+
caps,
|
|
1076
|
+
fieldKey,
|
|
1077
|
+
mode,
|
|
1078
|
+
onChange: (v) => opts.onCommit(v === undefined || v === "" ? undefined : String(v)),
|
|
1079
|
+
stateDefs: ctx.templateSignals,
|
|
1080
|
+
value: value === "" ? undefined : value,
|
|
1081
|
+
};
|
|
1082
|
+
if (mode === "template") {
|
|
1083
|
+
// The `${…}` rung: a mixed-text value is committed when the field is LEFT, never while it is
|
|
1084
|
+
// Typed — a half-written `${state.m` is not a binding, and writing one would move the row's
|
|
1085
|
+
// Rung out from under the caret.
|
|
1086
|
+
row.widget = "text";
|
|
1087
|
+
row.mono = true;
|
|
1088
|
+
row.placeholder = "${state.…}";
|
|
1089
|
+
actions.commit = (v) => opts.onCommit(v === "" ? undefined : v);
|
|
1090
|
+
return { actions, row };
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
switch (type) {
|
|
1095
|
+
case "color": {
|
|
1096
|
+
row.widget = "color";
|
|
1097
|
+
row.tokens = colorTokens();
|
|
1098
|
+
row.hasTokens = row.tokens.length > 0;
|
|
1099
|
+
/* The literal behind a token, for the chip: `var(--color-accent)` resolves in the canvas and
|
|
1100
|
+
nowhere in this page, so without it a pick from the palette drew the no-colour chip. */
|
|
1101
|
+
row.resolved = resolvedColor(value);
|
|
1102
|
+
/* Both verbs, as a text row has: the field says `input` on every frame of a drag and
|
|
1103
|
+
`change` when the reader lets go, so the drag is debounced into one write per pause and
|
|
1104
|
+
the release lands immediately. */
|
|
1105
|
+
actions.edit = commitLiteral;
|
|
1106
|
+
actions.commit = commitLiteral;
|
|
1107
|
+
actions.debounceId = `color:${key}`;
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
case "button-group": {
|
|
1111
|
+
const { buttons, extra } = buttonGroup(entry, key, value);
|
|
1112
|
+
row.widget = "buttons";
|
|
1113
|
+
row.buttons = buttons;
|
|
1114
|
+
row.hasChoices = extra.length > 0;
|
|
1115
|
+
row.overflowSelected = extra.includes(value);
|
|
1116
|
+
row.choicesLabel = "";
|
|
1117
|
+
row.choicesHint = `More ${row.label.toLowerCase()} values`;
|
|
1118
|
+
actions.choices = keywordChoices(extra, value);
|
|
1119
|
+
actions.choose = commitLiteral;
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
case "number-unit": {
|
|
1123
|
+
const units = (entry.$units ?? []) as string[];
|
|
1124
|
+
const keywords = (entry.$keywords ?? []) as string[];
|
|
1125
|
+
const match = UNIT_RE.exec(value);
|
|
1126
|
+
const isKeyword = !match && value !== "" && keywords.includes(value);
|
|
1127
|
+
const currentUnit = isKeyword ? (units[0] ?? "") : (match?.[2] ?? units[0] ?? "");
|
|
1128
|
+
row.widget = "group";
|
|
1129
|
+
row.value = unitDisplayValue(value, match, isKeyword);
|
|
1130
|
+
row.placeholder = unitPlaceholder(placeholder);
|
|
1131
|
+
row.hasChoices = units.length > 0 || keywords.length > 0;
|
|
1132
|
+
row.choicesLabel = currentUnit || units[0] || "";
|
|
1133
|
+
row.choicesHint = `Unit for ${row.label.toLowerCase()}`;
|
|
1134
|
+
actions.choices = [
|
|
1135
|
+
...units.map((u) => ({ checked: u === currentUnit, label: u, value: u })),
|
|
1136
|
+
...keywords.map((kw, index) => ({
|
|
1137
|
+
checked: kw === value,
|
|
1138
|
+
label: kw,
|
|
1139
|
+
value: kw,
|
|
1140
|
+
...(index === 0 && units.length > 0 ? { divider: true } : {}),
|
|
1141
|
+
})),
|
|
1142
|
+
];
|
|
1143
|
+
actions.choose = (chosen) => {
|
|
1144
|
+
if (keywords.includes(chosen)) {
|
|
1145
|
+
opts.onCommit(chosen);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const numPart = UNIT_RE.exec(value)?.[1];
|
|
1149
|
+
if (numPart) {
|
|
1150
|
+
opts.onCommit(numPart + chosen);
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
{
|
|
1154
|
+
const commitUnitValue = (raw: string) => {
|
|
1155
|
+
const v = raw.trim();
|
|
1156
|
+
if (v === "") {
|
|
1157
|
+
opts.onCommit("");
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
opts.onCommit(isNumericValue(v) && units.length > 0 ? v + currentUnit : v);
|
|
1161
|
+
};
|
|
1162
|
+
actions.edit = commitUnitValue;
|
|
1163
|
+
actions.commit = commitUnitValue;
|
|
1164
|
+
actions.debounceId = `nui:${key}`;
|
|
1165
|
+
}
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
case "number": {
|
|
1169
|
+
row.widget = "number";
|
|
1170
|
+
row.min = entry.minimum === undefined ? "" : String(entry.minimum);
|
|
1171
|
+
row.max = entry.maximum === undefined ? "" : String(entry.maximum);
|
|
1172
|
+
row.step = typeof entry.maximum === "number" && entry.maximum <= 1 ? "0.1" : "";
|
|
1173
|
+
actions.commit = commitLiteral;
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
case "select":
|
|
1177
|
+
case "combobox": {
|
|
1178
|
+
const options = Array.isArray(entry.enum)
|
|
1179
|
+
? (entry.enum as string[])
|
|
1180
|
+
: Array.isArray(entry.examples)
|
|
1181
|
+
? (entry.examples as string[])
|
|
1182
|
+
: [];
|
|
1183
|
+
// A keyword row is the combobox contract exactly: the field IS the value, and the rows
|
|
1184
|
+
// Under it are the values worth offering. `allows-custom-value` is what makes an enum a
|
|
1185
|
+
// List of suggestions rather than a whitelist — `full-width` is a text-transform whether or
|
|
1186
|
+
// Not the catalogue names it. The font row is the same element over a different list, with
|
|
1187
|
+
// A token showing as its own name rather than as the `var()` around it: the field edits
|
|
1188
|
+
// Which token this is, and `var(--font-body)` is punctuation the reader did not type. What
|
|
1189
|
+
// Differs is what a commit WRITES, and that is `commitFont`'s.
|
|
1190
|
+
row.widget = "keywords";
|
|
1191
|
+
if (prop === "fontFamily") {
|
|
1192
|
+
row.value = tokenRefName(value) ?? value;
|
|
1193
|
+
row.options = fontOptions(entry);
|
|
1194
|
+
actions.edit = (v) => commitFont(entry, v, false, commitLiteral);
|
|
1195
|
+
actions.commit = (v) => commitFont(entry, v, true, commitLiteral);
|
|
1196
|
+
} else {
|
|
1197
|
+
row.options = keywordOptions(options, prop, previewFace(ctx));
|
|
1198
|
+
actions.edit = commitLiteral;
|
|
1199
|
+
actions.commit = commitLiteral;
|
|
1200
|
+
}
|
|
1201
|
+
actions.debounceId = `kw:${key}`;
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
default: {
|
|
1205
|
+
row.widget = "text";
|
|
1206
|
+
actions.edit = commitLiteral;
|
|
1207
|
+
actions.commit = commitLiteral;
|
|
1208
|
+
actions.debounceId = `text:${key}`;
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return { actions, row };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/** What a unit field SHOWS: the number alone, or the keyword, or whatever could not be parsed. */
|
|
1216
|
+
function unitDisplayValue(
|
|
1217
|
+
value: string,
|
|
1218
|
+
match: RegExpExecArray | null,
|
|
1219
|
+
isKeyword: boolean,
|
|
1220
|
+
): string {
|
|
1221
|
+
if (isKeyword) {
|
|
1222
|
+
return value;
|
|
1223
|
+
}
|
|
1224
|
+
if (match) {
|
|
1225
|
+
return match[1] ?? "";
|
|
1226
|
+
}
|
|
1227
|
+
if (value === "") {
|
|
1228
|
+
return "";
|
|
1229
|
+
}
|
|
1230
|
+
// Intentional partial parse: a shorthand like "10px 20px" shows its leading number.
|
|
1231
|
+
// oxlint-disable-next-line unicorn/prefer-number-coercion
|
|
1232
|
+
const num = Number.parseFloat(value);
|
|
1233
|
+
return Number.isNaN(num) ? value : String(num);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** A unit field's placeholder is the inherited NUMBER — "500", not "500px". */
|
|
1237
|
+
function unitPlaceholder(placeholder: string): string {
|
|
1238
|
+
return UNIT_RE.exec(placeholder)?.[1] ?? placeholder ?? "0";
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** A shorthand header row and, when it is expanded, the longhand children under it. */
|
|
1242
|
+
function shorthandRows(
|
|
1243
|
+
ctx: EditorCtx,
|
|
1244
|
+
entry: CssPropertyEntry,
|
|
1245
|
+
shortProp: string,
|
|
1246
|
+
): { row: StyleRowView; actions: RowActions }[] {
|
|
1247
|
+
const longhands = getLonghands(shortProp) as CssLonghand[];
|
|
1248
|
+
const style = ctx.activeStyle as Record<string, unknown>;
|
|
1249
|
+
const shortVal = style[shortProp];
|
|
1250
|
+
const hasLonghands = longhands.some((l) => style[l.name] !== undefined);
|
|
1251
|
+
const isExpanded = ctx.tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
|
|
1252
|
+
const hasAnyVal = shortVal !== undefined || hasLonghands;
|
|
1253
|
+
const inherited = ctx.inheritedStyle;
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Drop every longhand, then write the shorthand — the one write order this row has.
|
|
1257
|
+
*
|
|
1258
|
+
* The longhands go unconditionally rather than "the ones the panel can see set", because what the
|
|
1259
|
+
* panel can see is the PRIMARY element's style: a second selected element with a `paddingTop` of
|
|
1260
|
+
* its own would otherwise keep it and quietly out-rank the shorthand just written over it.
|
|
1261
|
+
* Deleting a property an element does not have is a no-op on that element.
|
|
1262
|
+
*/
|
|
1263
|
+
const writeShorthand = (val?: string | undefined) =>
|
|
1264
|
+
transactDoc(activeTab.value, (t) => {
|
|
1265
|
+
for (const l of longhands) {
|
|
1266
|
+
ctx.mutate(t, l.name);
|
|
1267
|
+
}
|
|
1268
|
+
ctx.mutate(t, shortProp, val);
|
|
1269
|
+
});
|
|
1270
|
+
const clearAll = () =>
|
|
1271
|
+
transactDoc(activeTab.value, (t) => {
|
|
1272
|
+
ctx.mutate(t, shortProp);
|
|
1273
|
+
for (const l of longhands) {
|
|
1274
|
+
ctx.mutate(t, l.name);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const key = `${ctx.coord}|${shortProp}`;
|
|
1279
|
+
const row = blankRow(key, shortProp, "shorthand");
|
|
1280
|
+
const actions: RowActions = {};
|
|
1281
|
+
row.widget = "shorthand";
|
|
1282
|
+
row.hasLabel = true;
|
|
1283
|
+
row.label = propLabel(entry, shortProp);
|
|
1284
|
+
row.value = typeof shortVal === "string" ? shortVal : "";
|
|
1285
|
+
row.placeholder = shorthandPlaceholder(shortVal, longhands, style, inherited, shortProp);
|
|
1286
|
+
row.expanded = isExpanded;
|
|
1287
|
+
row.expandIcon = isExpanded ? "caret-down" : "caret-right";
|
|
1288
|
+
row.expandLabel = `${isExpanded ? "Collapse" : "Expand"} ${row.label.toLowerCase()}`;
|
|
1289
|
+
applyChip(
|
|
1290
|
+
row,
|
|
1291
|
+
actions,
|
|
1292
|
+
shorthandProvenance(shortProp, ctx.provCtx, hasAnyVal ? clearAll : undefined),
|
|
1293
|
+
);
|
|
1294
|
+
actions.edit = (v) => writeShorthand(v || undefined);
|
|
1295
|
+
actions.commit = (v) => writeShorthand(v || undefined);
|
|
1296
|
+
actions.debounceId = `short:${key}`;
|
|
1297
|
+
actions.toggle = () => {
|
|
1298
|
+
const tab = activeTab.value;
|
|
1299
|
+
if (tab) {
|
|
1300
|
+
tab.session.ui.styleShorthands = {
|
|
1301
|
+
...tab.session.ui.styleShorthands,
|
|
1302
|
+
[shortProp]: !isExpanded,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const out = [{ actions, row }];
|
|
1308
|
+
if (!isExpanded) {
|
|
1309
|
+
return out;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const isBorderSide = entry.$shorthandType === "border-side";
|
|
1313
|
+
const expanded =
|
|
1314
|
+
typeof shortVal === "string" && shortVal
|
|
1315
|
+
? isBorderSide
|
|
1316
|
+
? expandBorderSide(shortVal)
|
|
1317
|
+
: expandShorthand(shortVal, longhands.length)
|
|
1318
|
+
: null;
|
|
1319
|
+
const compress = isBorderSide ? compressBorderSide : compressShorthand;
|
|
1320
|
+
const emptyVal = isBorderSide ? "" : "0";
|
|
1321
|
+
/** This longhand becomes `val`; the others keep whatever they show. */
|
|
1322
|
+
const recompress = (idx: number, val: string) =>
|
|
1323
|
+
writeShorthand(
|
|
1324
|
+
compress(
|
|
1325
|
+
longhands.map((l, i) =>
|
|
1326
|
+
i === idx ? val : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
1327
|
+
) as string[],
|
|
1328
|
+
),
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
for (const [idx, { name, entry: lEntry }] of longhands.entries()) {
|
|
1332
|
+
const lVal = style[name] ?? (expanded ? expanded[idx] : "");
|
|
1333
|
+
const hasLVal = lVal !== undefined && lVal !== "";
|
|
1334
|
+
out.push(
|
|
1335
|
+
fieldRow(ctx, lEntry, name, String(lVal ?? ""), {
|
|
1336
|
+
child: true,
|
|
1337
|
+
onCommit: (newVal) => recompress(idx, newVal || emptyVal),
|
|
1338
|
+
placeholder: !lVal && inherited[name] !== undefined ? String(inherited[name]) : "",
|
|
1339
|
+
provenance: longhandProvenance(name, shortProp, hasLVal, ctx.provCtx, () =>
|
|
1340
|
+
recompress(idx, emptyVal),
|
|
1341
|
+
),
|
|
1342
|
+
}),
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
return out;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** The shorthand header's placeholder: the longhands it would compress, or what shows through. */
|
|
1349
|
+
function shorthandPlaceholder(
|
|
1350
|
+
shortVal: unknown,
|
|
1351
|
+
longhands: CssLonghand[],
|
|
1352
|
+
style: Record<string, unknown>,
|
|
1353
|
+
inherited: Record<string, string | number>,
|
|
1354
|
+
shortProp: string,
|
|
1355
|
+
): string {
|
|
1356
|
+
if (shortVal) {
|
|
1357
|
+
return "";
|
|
1358
|
+
}
|
|
1359
|
+
if (longhands.some((l) => style[l.name] !== undefined)) {
|
|
1360
|
+
return longhands.map((l) => style[l.name] || "0").join(" ");
|
|
1361
|
+
}
|
|
1362
|
+
if (inherited[shortProp] !== undefined) {
|
|
1363
|
+
return String(inherited[shortProp]);
|
|
1364
|
+
}
|
|
1365
|
+
if (longhands.some((l) => inherited[l.name] !== undefined)) {
|
|
1366
|
+
return longhands.map((l) => inherited[l.name] || "0").join(" ");
|
|
1367
|
+
}
|
|
1368
|
+
return "";
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// ─── The whole view ──────────────────────────────────────────────────────────
|
|
1372
|
+
|
|
1373
|
+
/** The three teaching states, and the one that offers a button. */
|
|
1374
|
+
function emptyView(message: string, withOpen: boolean): StylePanelView {
|
|
1375
|
+
return {
|
|
1376
|
+
emptyActionLabel: withOpen ? openPageAction().label : "",
|
|
1377
|
+
emptyMessage: message,
|
|
1378
|
+
filter: "",
|
|
1379
|
+
hasEmptyAction: withOpen,
|
|
1380
|
+
sections: [],
|
|
1381
|
+
view: "empty",
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/** What the tab is showing, from the state it can read right now. */
|
|
1386
|
+
function buildView(canvasMode: () => string): StylePanelView {
|
|
1387
|
+
_rows = new Map();
|
|
1388
|
+
const tab = activeTab.value;
|
|
1389
|
+
if (!tab) {
|
|
1390
|
+
return emptyView("Open a page to style what you click.", true);
|
|
1391
|
+
}
|
|
1392
|
+
if (canvasMode() === "stylebook" && shell.stylebook.selection) {
|
|
1393
|
+
const node = tab.doc.document;
|
|
1394
|
+
if (!node) {
|
|
1395
|
+
return emptyView("Open a page to style what you click.", true);
|
|
1396
|
+
}
|
|
1397
|
+
// No separate `Styling: <h1>` header. It said the same thing the Target Line's scope chip says,
|
|
1398
|
+
// Except that it said it as a caption, after the fact, and without the blast radius (§6.1).
|
|
1399
|
+
return buildEditor(tab, node, {
|
|
1400
|
+
effectiveStyle: getEffectiveStyle(node.style),
|
|
1401
|
+
stylebookSelector: shell.stylebook.selection,
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
const selected = primarySelection(tab.session.selection);
|
|
1405
|
+
if (!selected) {
|
|
1406
|
+
return emptyView(clickAnythingTo("style it"), false);
|
|
1407
|
+
}
|
|
1408
|
+
const node = getNodeAtPath(tab.doc.document, selected) as JxMutableNode | undefined;
|
|
1409
|
+
if (!node) {
|
|
1410
|
+
return emptyView(staleSelectionMessage(), false);
|
|
1411
|
+
}
|
|
1412
|
+
return buildEditor(tab, node, {});
|
|
1413
|
+
}
|
|
845
1414
|
|
|
846
1415
|
/**
|
|
1416
|
+
* Resolve the coordinate, then project every section under it.
|
|
1417
|
+
*
|
|
1418
|
+
* @param {Tab} tab
|
|
847
1419
|
* @param {JxMutableNode} node
|
|
848
|
-
* @param {string | null}
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* tag catalogue entry being styled, which is what makes the edit tag-wide rather than
|
|
853
|
-
* element-wide and therefore what the scope chip reports.
|
|
1420
|
+
* @param {{ effectiveStyle?: JxStyle; stylebookSelector?: string | null }} opts — `effectiveStyle`
|
|
1421
|
+
* is the site-merged style Stylebook edits against; `stylebookSelector` is the tag catalogue
|
|
1422
|
+
* entry being styled, which is what makes the edit tag-wide rather than element-wide and
|
|
1423
|
+
* therefore what the scope chip reports.
|
|
854
1424
|
*/
|
|
855
|
-
function
|
|
1425
|
+
function buildEditor(
|
|
1426
|
+
tab: Tab,
|
|
856
1427
|
node: JxMutableNode,
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
opts: { effectiveStyle?: JxStyle | undefined; stylebookSelector?: string | null } = {},
|
|
860
|
-
) {
|
|
1428
|
+
opts: { effectiveStyle?: JxStyle | undefined; stylebookSelector?: string | null },
|
|
1429
|
+
): StylePanelView {
|
|
861
1430
|
const { effectiveStyle, stylebookSelector = null } = opts;
|
|
862
|
-
const tab = activeTab.value!;
|
|
863
1431
|
const sel = primarySelection(tab.session.selection) as JxPath;
|
|
864
1432
|
/** The document's OWN style — what a commit writes into. */
|
|
865
1433
|
const ownRootStyle: JxStyle = node.style || {};
|
|
@@ -874,7 +1442,8 @@ function styleSidebarTemplate(
|
|
|
874
1442
|
.map(([defName]) => defName);
|
|
875
1443
|
const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
|
|
876
1444
|
const mediaNames = sizeBreakpoints.map((bp) => bp.name);
|
|
877
|
-
const mediaTab =
|
|
1445
|
+
const mediaTab = tab.session.ui.activeMedia || null;
|
|
1446
|
+
const { activeSelector } = tab.session.ui;
|
|
878
1447
|
|
|
879
1448
|
// ── Scheme-layer routing (spec §9.5) ────────────────────────────────────────
|
|
880
1449
|
// With the tab-bar scheme control forcing a scheme that has a matching declared scheme query,
|
|
@@ -895,29 +1464,49 @@ function styleSidebarTemplate(
|
|
|
895
1464
|
? (style[`@${editMedia}`] as Record<string, unknown>) || {}
|
|
896
1465
|
: style;
|
|
897
1466
|
const declaredSelectors = new Set(Object.keys(contextStyle).filter((s) => isNestedSelector(s)));
|
|
1467
|
+
/* Element-aware, not the flat global list: `:popover-open` on a `<p>` is a rule that can never
|
|
1468
|
+
match, and a menu that offers unmatchable states is a menu people stop reading. What the
|
|
1469
|
+
element already DECLARES and the active selector are unioned in after, so nothing an author has
|
|
1470
|
+
written can drop out of it. */
|
|
898
1471
|
const selectorOptions = [
|
|
899
1472
|
...new Set([
|
|
900
|
-
...
|
|
1473
|
+
...selectorsForNode(node),
|
|
901
1474
|
...declaredSelectors,
|
|
902
1475
|
...(activeSelector ? [activeSelector] : []),
|
|
903
1476
|
]),
|
|
904
1477
|
];
|
|
905
1478
|
|
|
906
1479
|
// ── The Target Line (§6.1) ─────────────────────────────────────────────────
|
|
907
|
-
// The three widgets this replaces are gone: the breakpoint `<sp-tabs>` strip, the
|
|
908
|
-
// `.selector-select` picker and the `.style-scheme-badge`. The first two named axes the pane
|
|
909
|
-
// Context bar already selects; the third emitted a class with no CSS rule anywhere in the repo.
|
|
910
1480
|
const stylebookTag = stylebookTagOf(stylebookSelector);
|
|
911
1481
|
const elementLabel =
|
|
912
1482
|
stylebookTag ?? (typeof node.tagName === "string" ? node.tagName : "element");
|
|
913
|
-
|
|
1483
|
+
setTargetLine({
|
|
914
1484
|
segments: targetSegments(elementLabel, mediaTab, mediaNames.length > 0, schemeLayer),
|
|
915
1485
|
selector: {
|
|
916
1486
|
value: activeSelector,
|
|
917
1487
|
options: selectorOptions,
|
|
918
1488
|
declared: declaredSelectors,
|
|
919
1489
|
onSelect: (value) => {
|
|
920
|
-
|
|
1490
|
+
const current = activeTab.value;
|
|
1491
|
+
if (!current) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
current.session.ui.activeSelector = value;
|
|
1495
|
+
/* §6.2's own rule: a control that selects a rendering context has to change the rendering,
|
|
1496
|
+
or it is a control over a label. `:hover` gets away with not doing this because you can
|
|
1497
|
+
hover the element; `:popover-open` cannot, because a closed popover is not on the screen
|
|
1498
|
+
to be put into that state by hand. This is the ONE state the canvas simulates — extending
|
|
1499
|
+
it to `:hover` and `:focus` is a larger decision and is deliberately not taken here. */
|
|
1500
|
+
if (value?.startsWith(":popover-open")) {
|
|
1501
|
+
runCommand("canvas.setPopoverOpen", { open: true });
|
|
1502
|
+
}
|
|
1503
|
+
// The dialog's open states, by the same rule, when the selection is in a dialog to open.
|
|
1504
|
+
if (
|
|
1505
|
+
(value?.startsWith("[open]") || value?.startsWith(":modal")) &&
|
|
1506
|
+
dialogPathFor(current) !== null
|
|
1507
|
+
) {
|
|
1508
|
+
runCommand("canvas.setDialogOpen", { open: true });
|
|
1509
|
+
}
|
|
921
1510
|
},
|
|
922
1511
|
onAddCustom: () => {
|
|
923
1512
|
// One flow, through `ui/layers.ts` — the imperative `<input>` this replaces was appended to
|
|
@@ -929,9 +1518,8 @@ function styleSidebarTemplate(
|
|
|
929
1518
|
validate: (v) =>
|
|
930
1519
|
isNestedSelector(v.trim()) ? "" : 'A selector must start with ":", ".", "&" or "[".',
|
|
931
1520
|
}).then((value) => {
|
|
932
|
-
if (value) {
|
|
933
|
-
activeTab.value
|
|
934
|
-
renderOnly("rightPanel");
|
|
1521
|
+
if (value && activeTab.value) {
|
|
1522
|
+
activeTab.value.session.ui.activeSelector = value.trim();
|
|
935
1523
|
}
|
|
936
1524
|
});
|
|
937
1525
|
},
|
|
@@ -939,24 +1527,6 @@ function styleSidebarTemplate(
|
|
|
939
1527
|
scope: resolveScope(tab, stylebookTag),
|
|
940
1528
|
});
|
|
941
1529
|
|
|
942
|
-
// ── Filter bar ─────────────────────────────────────────────────────────────
|
|
943
|
-
// One control. The "Active" toggle is gone: it existed only because provenance was invisible
|
|
944
|
-
// With a section closed, and a heading that says "3 set here · 2 inherited" answers the same
|
|
945
|
-
// Question without hiding two thirds of the panel to do it (§6.2).
|
|
946
|
-
const filterBarT = html`
|
|
947
|
-
<div class="style-filter-bar">
|
|
948
|
-
<sp-textfield
|
|
949
|
-
size="s"
|
|
950
|
-
class="style-filter-input"
|
|
951
|
-
placeholder="Filter properties…"
|
|
952
|
-
.value=${live(tab.session.ui.styleFilter || "")}
|
|
953
|
-
@input=${(e: Event) => {
|
|
954
|
-
activeTab.value!.session.ui.styleFilter = (e.target as HTMLInputElement).value;
|
|
955
|
-
}}
|
|
956
|
-
></sp-textfield>
|
|
957
|
-
</div>
|
|
958
|
-
`;
|
|
959
|
-
|
|
960
1530
|
// ── Determine the active style object ──────────────────────────────────────
|
|
961
1531
|
const activeStyle = resolveContextStyle(style, activeSelector, editMedia);
|
|
962
1532
|
// The SAME coordinate, resolved against the document's own style. In Stylebook `style` is the
|
|
@@ -987,11 +1557,6 @@ function styleSidebarTemplate(
|
|
|
987
1557
|
};
|
|
988
1558
|
const commitStyle = (prop: string, val?: string | Record<string, unknown> | undefined) =>
|
|
989
1559
|
transactDoc(activeTab.value, (t) => mutateTargets(t, prop, val));
|
|
990
|
-
/** One target's OWN block at this coordinate — what a write that preserves values reads first. */
|
|
991
|
-
const targetStyleOf = (t: Tab, target: JxPath): JxStyle => {
|
|
992
|
-
const targetNode = getNodeAtPath(t.doc.document, target) as JxMutableNode | undefined;
|
|
993
|
-
return resolveContextStyle(targetNode?.style ?? {}, activeSelector, editMedia);
|
|
994
|
-
};
|
|
995
1560
|
|
|
996
1561
|
// ── Compute inherited style, and NAME THE DONOR ────────────────────────────
|
|
997
1562
|
// Scheme layer: the base styles show through as inherited; breakpoint tabs inherit from lower
|
|
@@ -1034,390 +1599,562 @@ function styleSidebarTemplate(
|
|
|
1034
1599
|
...(mixedProps.size > 0 ? { mixed: { count: targets.length, props: mixedProps } } : {}),
|
|
1035
1600
|
};
|
|
1036
1601
|
|
|
1037
|
-
// Auto-open sections that have properties
|
|
1602
|
+
// Auto-open sections that have properties. Written back only when it actually moves, so the
|
|
1603
|
+
// Effect this runs inside settles after one extra pass rather than chasing its own write.
|
|
1038
1604
|
const newSections = autoOpenSections({ style: activeStyle }, tab.session.ui.styleSections);
|
|
1039
1605
|
if (JSON.stringify(newSections) !== JSON.stringify(tab.session.ui.styleSections)) {
|
|
1040
1606
|
tab.session.ui.styleSections = newSections;
|
|
1041
1607
|
}
|
|
1042
1608
|
|
|
1043
|
-
|
|
1609
|
+
const filterText = (tab.session.ui.styleFilter || "").toLowerCase();
|
|
1610
|
+
const ctx: EditorCtx = {
|
|
1611
|
+
activeSelector,
|
|
1612
|
+
activeStyle,
|
|
1613
|
+
commit: commitStyle,
|
|
1614
|
+
coord: `style|${sel.join("/")}|${editMedia ?? ""}|${activeSelector ?? ""}`,
|
|
1615
|
+
editMedia,
|
|
1616
|
+
filter: filterText,
|
|
1617
|
+
inheritedStyle,
|
|
1618
|
+
mutate: mutateTargets,
|
|
1619
|
+
provCtx,
|
|
1620
|
+
tab,
|
|
1621
|
+
targets,
|
|
1622
|
+
templateSignals,
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
const sections = [...propertySections(ctx), ...nestedSection(ctx), ...customSection(ctx)];
|
|
1626
|
+
return {
|
|
1627
|
+
emptyActionLabel: "",
|
|
1628
|
+
emptyMessage: "",
|
|
1629
|
+
filter: tab.session.ui.styleFilter || "",
|
|
1630
|
+
hasEmptyAction: false,
|
|
1631
|
+
sections,
|
|
1632
|
+
view: "editor",
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/** Register a projected row's verbs, and hand back the row. */
|
|
1637
|
+
function register(entry: { row: StyleRowView; actions: RowActions }): StyleRowView {
|
|
1638
|
+
_rows.set(entry.row.key, entry.actions);
|
|
1639
|
+
return entry.row;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** The css-meta sections, in their declared order, minus the catch-all. */
|
|
1643
|
+
function propertySections(ctx: EditorCtx): StyleSectionView[] {
|
|
1644
|
+
const { activeStyle, tab } = ctx;
|
|
1645
|
+
const isFiltering = ctx.filter.length > 0;
|
|
1044
1646
|
const sectionProps: Record<string, { prop: string; entry: CssPropertyEntry }[]> = {};
|
|
1045
1647
|
for (const sec of cssMeta.$sections) {
|
|
1046
1648
|
sectionProps[sec.key] = [];
|
|
1047
1649
|
}
|
|
1048
|
-
|
|
1049
1650
|
for (const [prop, entry] of Object.entries(cssMeta.$defs) as [string, CssPropertyEntry][]) {
|
|
1050
|
-
if (typeof
|
|
1651
|
+
if (typeof entry.$shorthand === "string") {
|
|
1051
1652
|
continue;
|
|
1052
1653
|
}
|
|
1053
|
-
|
|
1054
|
-
sectionProps[sec]!.push({ entry, prop });
|
|
1654
|
+
sectionProps[(entry.$section as string) || "other"]!.push({ entry, prop });
|
|
1055
1655
|
}
|
|
1056
1656
|
for (const sec of cssMeta.$sections) {
|
|
1057
|
-
sectionProps[sec.key]!.sort(
|
|
1058
|
-
(
|
|
1059
|
-
a: { prop: string; entry: CssPropertyEntry },
|
|
1060
|
-
b: { prop: string; entry: CssPropertyEntry },
|
|
1061
|
-
) =>
|
|
1062
|
-
((a.entry as Record<string, unknown>).$order as number) -
|
|
1063
|
-
((b.entry as Record<string, unknown>).$order as number),
|
|
1064
|
-
);
|
|
1657
|
+
sectionProps[sec.key]!.sort((a, b) => (a.entry.$order as number) - (b.entry.$order as number));
|
|
1065
1658
|
}
|
|
1066
1659
|
|
|
1067
|
-
const
|
|
1068
|
-
for (const
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1660
|
+
const out: StyleSectionView[] = [];
|
|
1661
|
+
for (const sec of cssMeta.$sections) {
|
|
1662
|
+
if (sec.key === "other") {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
const entries = sectionProps[sec.key]!;
|
|
1666
|
+
const sectionActiveProps = entries.filter(({ prop, entry }) => {
|
|
1667
|
+
if (activeStyle[prop] !== undefined) {
|
|
1668
|
+
return true;
|
|
1669
|
+
}
|
|
1670
|
+
return (
|
|
1671
|
+
inferInputType(entry) === "shorthand" &&
|
|
1672
|
+
(getLonghands(prop) as CssLonghand[]).some((l) => activeStyle[l.name] !== undefined)
|
|
1673
|
+
);
|
|
1674
|
+
});
|
|
1675
|
+
const clearSection = () => {
|
|
1676
|
+
transactDoc(activeTab.value, (t) => {
|
|
1677
|
+
for (const { prop, entry } of sectionActiveProps) {
|
|
1678
|
+
if (activeStyle[prop] !== undefined) {
|
|
1679
|
+
ctx.mutate(t, prop);
|
|
1680
|
+
}
|
|
1681
|
+
if (inferInputType(entry) === "shorthand") {
|
|
1682
|
+
for (const l of getLonghands(prop) as CssLonghand[]) {
|
|
1683
|
+
ctx.mutate(t, l.name);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// The heading's own answer to "is anything in here set, inherited or bound" — the question the
|
|
1691
|
+
// Retired "Active" toggle answered by hiding everything that was not.
|
|
1692
|
+
const counts = countProvenance(sectionProvenance(entries, ctx.provCtx));
|
|
1693
|
+
const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
|
|
1694
|
+
const section = sectionShell(sec.key, sec.label, isOpen, counts, {
|
|
1695
|
+
canClear: sectionActiveProps.length > 0,
|
|
1696
|
+
clearTitle: `Clear all ${sec.label.toLowerCase()} properties`,
|
|
1697
|
+
layout: sec.$layout === "grid" ? "grid" : "",
|
|
1698
|
+
onClear: clearSection,
|
|
1699
|
+
});
|
|
1700
|
+
if (isOpen) {
|
|
1701
|
+
section.rows = sectionRows(ctx, entries, sec.$layout === "grid");
|
|
1702
|
+
}
|
|
1703
|
+
if (isFiltering && section.rows.length === 0) {
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
out.push(section);
|
|
1707
|
+
}
|
|
1708
|
+
return out;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** One section's rows: every property that is set, or whose `$show` condition passes. */
|
|
1712
|
+
function sectionRows(
|
|
1713
|
+
ctx: EditorCtx,
|
|
1714
|
+
entries: { prop: string; entry: CssPropertyEntry }[],
|
|
1715
|
+
grid: boolean,
|
|
1716
|
+
): StyleRowView[] {
|
|
1717
|
+
const { activeStyle } = ctx;
|
|
1718
|
+
const rows: StyleRowView[] = [];
|
|
1719
|
+
for (const { prop, entry } of entries) {
|
|
1720
|
+
const val = activeStyle[prop];
|
|
1721
|
+
const hasVal = val !== undefined;
|
|
1722
|
+
const condMet = allConditionsPass(entry, activeStyle);
|
|
1723
|
+
const type = inferInputType(entry);
|
|
1724
|
+
if (!hasVal && !condMet) {
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (ctx.filter) {
|
|
1728
|
+
const label = propLabel(entry, prop).toLowerCase();
|
|
1729
|
+
if (!prop.includes(ctx.filter) && !label.includes(ctx.filter)) {
|
|
1072
1730
|
continue;
|
|
1073
1731
|
}
|
|
1074
|
-
otherProps.push(prop);
|
|
1075
1732
|
}
|
|
1733
|
+
if (type === "shorthand") {
|
|
1734
|
+
const longhands = getLonghands(prop) as CssLonghand[];
|
|
1735
|
+
const hasAny = hasVal || longhands.some((l) => activeStyle[l.name] !== undefined);
|
|
1736
|
+
if (!hasAny && !condMet) {
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
for (const built of shorthandRows(ctx, entry, prop)) {
|
|
1740
|
+
rows.push(register(built));
|
|
1741
|
+
}
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const inherited = ctx.inheritedStyle[prop];
|
|
1745
|
+
const built = fieldRow(ctx, entry, prop, (val as string) ?? "", {
|
|
1746
|
+
onCommit: (newVal) => ctx.commit(prop, newVal || undefined),
|
|
1747
|
+
placeholder: !hasVal && inherited !== undefined ? String(inherited) : "",
|
|
1748
|
+
provenance: provenanceOf(prop, ctx.provCtx),
|
|
1749
|
+
span: grid && entry.$span === 2,
|
|
1750
|
+
warning: hasVal && !condMet,
|
|
1751
|
+
});
|
|
1752
|
+
rows.push(register(built));
|
|
1753
|
+
}
|
|
1754
|
+
return rows;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/** A section with its tally worked out and no rows yet. */
|
|
1758
|
+
function sectionShell(
|
|
1759
|
+
key: string,
|
|
1760
|
+
label: string,
|
|
1761
|
+
open: boolean,
|
|
1762
|
+
counts: ReturnType<typeof countProvenance>,
|
|
1763
|
+
opts: { canClear: boolean; clearTitle: string; layout: string; onClear: () => void },
|
|
1764
|
+
): StyleSectionView {
|
|
1765
|
+
const marks: { state: string }[] = [];
|
|
1766
|
+
if (counts.inherited > 0) {
|
|
1767
|
+
marks.push({ state: "inherited" });
|
|
1076
1768
|
}
|
|
1769
|
+
if (counts.bound > 0) {
|
|
1770
|
+
marks.push({ state: "bound" });
|
|
1771
|
+
}
|
|
1772
|
+
if (counts.mixed > 0) {
|
|
1773
|
+
marks.push({ state: "mixed" });
|
|
1774
|
+
}
|
|
1775
|
+
const canClear = counts.set > 0 && opts.canClear;
|
|
1776
|
+
if (canClear) {
|
|
1777
|
+
_sectionClears.set(key, opts.onClear);
|
|
1778
|
+
}
|
|
1779
|
+
return {
|
|
1780
|
+
canClear,
|
|
1781
|
+
clearTitle: opts.clearTitle,
|
|
1782
|
+
hasMarks: marks.length > 0,
|
|
1783
|
+
key,
|
|
1784
|
+
label,
|
|
1785
|
+
layout: opts.layout,
|
|
1786
|
+
marks,
|
|
1787
|
+
open,
|
|
1788
|
+
rows: [],
|
|
1789
|
+
tally: provenanceSummaryText(counts),
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1077
1792
|
|
|
1793
|
+
/** Relative Styling — the nested rules declared at this coordinate, and the way to add one. */
|
|
1794
|
+
function nestedSection(ctx: EditorCtx): StyleSectionView[] {
|
|
1078
1795
|
const nestedRules: string[] = [];
|
|
1079
|
-
for (const [prop, val] of Object.entries(activeStyle)) {
|
|
1796
|
+
for (const [prop, val] of Object.entries(ctx.activeStyle)) {
|
|
1080
1797
|
if (val !== null && typeof val === "object" && !Array.isArray(val) && !prop.startsWith("@")) {
|
|
1081
1798
|
nestedRules.push(prop);
|
|
1082
1799
|
}
|
|
1083
1800
|
}
|
|
1801
|
+
if (nestedRules.length === 0) {
|
|
1802
|
+
return [];
|
|
1803
|
+
}
|
|
1804
|
+
const open = ctx.tab.session.ui.styleSections.nested ?? true;
|
|
1805
|
+
const section = sectionShell("nested", "Relative Styling", open, countProvenance([]), {
|
|
1806
|
+
canClear: false,
|
|
1807
|
+
clearTitle: "",
|
|
1808
|
+
layout: "",
|
|
1809
|
+
onClear: () => {},
|
|
1810
|
+
});
|
|
1811
|
+
if (!open) {
|
|
1812
|
+
return [section];
|
|
1813
|
+
}
|
|
1814
|
+
for (const rule of nestedRules) {
|
|
1815
|
+
const key = `${ctx.coord}|nested|${rule}`;
|
|
1816
|
+
const row = blankRow(key, rule, "nested");
|
|
1817
|
+
row.widget = "nested";
|
|
1818
|
+
row.name = rule;
|
|
1819
|
+
row.openTitle = `Edit ${rule} under this rule`;
|
|
1820
|
+
row.removeLabel = `Remove ${rule}`;
|
|
1821
|
+
_rows.set(key, {
|
|
1822
|
+
chip: () => ctx.commit(rule),
|
|
1823
|
+
open: () => {
|
|
1824
|
+
const compound = ctx.activeSelector ? `${ctx.activeSelector} ${rule}` : rule;
|
|
1825
|
+
selectStylebookTag(compound, undefined, { panCanvas: true });
|
|
1826
|
+
},
|
|
1827
|
+
});
|
|
1828
|
+
section.rows.push(row);
|
|
1829
|
+
}
|
|
1830
|
+
const addKey = `${ctx.coord}|nested|+add`;
|
|
1831
|
+
const addRow = blankRow(addKey, "+add", "nested-add");
|
|
1832
|
+
addRow.widget = "nestedadd";
|
|
1833
|
+
_rows.set(addKey, {});
|
|
1834
|
+
section.rows.push(addRow);
|
|
1835
|
+
_nestedAdd = () => {
|
|
1836
|
+
void showPromptDialog("Add Nested Selector", {
|
|
1837
|
+
confirmLabel: "Add",
|
|
1838
|
+
message: "Enter a selector to nest under the current rule.",
|
|
1839
|
+
placeholder: "th, :hover, .active",
|
|
1840
|
+
validate: (v) => (v.trim() ? "" : "Enter a selector."),
|
|
1841
|
+
}).then((name) => {
|
|
1842
|
+
if (name) {
|
|
1843
|
+
ctx.commit(name, {});
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
};
|
|
1847
|
+
return [section];
|
|
1848
|
+
}
|
|
1084
1849
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1850
|
+
/** Custom — every declaration css-meta does not know about, plus the field that adds one. */
|
|
1851
|
+
function customSection(ctx: EditorCtx): StyleSectionView[] {
|
|
1852
|
+
const otherProps: string[] = [];
|
|
1853
|
+
for (const prop of Object.keys(ctx.activeStyle)) {
|
|
1854
|
+
if (!(cssMeta.$defs as Record<string, unknown>)[prop]) {
|
|
1855
|
+
const val = ctx.activeStyle[prop];
|
|
1856
|
+
if (val !== null && typeof val === "object") {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
otherProps.push(prop);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const open = ctx.tab.session.ui.styleSections.other ?? otherProps.length > 0;
|
|
1863
|
+
const section = sectionShell("other", "Custom", open, countProvenance([]), {
|
|
1864
|
+
canClear: false,
|
|
1865
|
+
clearTitle: "",
|
|
1866
|
+
layout: "",
|
|
1867
|
+
onClear: () => {},
|
|
1868
|
+
});
|
|
1869
|
+
if (!open) {
|
|
1870
|
+
return [section];
|
|
1871
|
+
}
|
|
1872
|
+
const cssInitialMap = getCssInitialMap();
|
|
1873
|
+
/** One target's OWN block at this coordinate — what a write that preserves values reads first. */
|
|
1874
|
+
const targetStyleOf = (t: Tab, target: JxPath): JxStyle => {
|
|
1875
|
+
const targetNode = getNodeAtPath(t.doc.document, target) as JxMutableNode | undefined;
|
|
1876
|
+
return resolveContextStyle(targetNode?.style ?? {}, ctx.activeSelector, ctx.editMedia);
|
|
1877
|
+
};
|
|
1878
|
+
for (const prop of otherProps) {
|
|
1879
|
+
const key = `${ctx.coord}|custom|${prop}`;
|
|
1880
|
+
const row = blankRow(key, prop, "custom");
|
|
1881
|
+
row.widget = "kv";
|
|
1882
|
+
row.name = prop;
|
|
1883
|
+
row.value = String(ctx.activeStyle[prop]);
|
|
1884
|
+
row.placeholder = cssInitialMap.get(prop) ?? "";
|
|
1885
|
+
row.removeLabel = `Remove ${prop}`;
|
|
1886
|
+
_rows.set(key, {
|
|
1887
|
+
chip: () => ctx.commit(prop),
|
|
1888
|
+
debounceId: `custom:${key}`,
|
|
1889
|
+
edit: (value) => ctx.commit(prop, value),
|
|
1890
|
+
rename: (raw) => {
|
|
1891
|
+
const newProp = raw.trim();
|
|
1892
|
+
if (!newProp || newProp === prop) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
// A rename is the one write that cannot be fanned out with a single value: each element
|
|
1896
|
+
// Keeps ITS OWN value under the new key. The old code batched half of the rename (the
|
|
1897
|
+
// Delete) against the primary and the other half against the primary too, while the value
|
|
1898
|
+
// Cell beside it wrote to the whole selection — so renaming a property the selection shared
|
|
1899
|
+
// Moved it on one element and left it on the rest.
|
|
1110
1900
|
transactDoc(activeTab.value, (t) => {
|
|
1111
|
-
for (const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
if (inferInputType(entry) === "shorthand") {
|
|
1116
|
-
for (const l of getLonghands(prop) as CssLonghand[]) {
|
|
1117
|
-
mutateTargets(t, l.name);
|
|
1118
|
-
}
|
|
1901
|
+
for (const target of ctx.targets) {
|
|
1902
|
+
const current = targetStyleOf(t, target)[prop];
|
|
1903
|
+
if (current === undefined) {
|
|
1904
|
+
continue;
|
|
1119
1905
|
}
|
|
1906
|
+
const write = contextMutate(target, ctx.activeSelector, ctx.editMedia);
|
|
1907
|
+
write(t, prop);
|
|
1908
|
+
write(t, newProp, String(current));
|
|
1120
1909
|
}
|
|
1121
1910
|
});
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
</span>
|
|
1135
|
-
`;
|
|
1136
|
-
|
|
1137
|
-
const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
|
|
1138
|
-
|
|
1139
|
-
if (!isOpen) {
|
|
1140
|
-
return html`
|
|
1141
|
-
<sp-accordion-item
|
|
1142
|
-
label=${sec.label}
|
|
1143
|
-
.open=${false}
|
|
1144
|
-
@sp-accordion-item-toggle=${(e: Event) => {
|
|
1145
|
-
activeTab.value!.session.ui.styleSections = {
|
|
1146
|
-
...activeTab.value!.session.ui.styleSections,
|
|
1147
|
-
[sec.key]: (e.target as HTMLElement & { open: boolean }).open,
|
|
1148
|
-
};
|
|
1149
|
-
}}
|
|
1150
|
-
>
|
|
1151
|
-
${headingT}
|
|
1152
|
-
</sp-accordion-item>
|
|
1153
|
-
`;
|
|
1911
|
+
},
|
|
1912
|
+
});
|
|
1913
|
+
section.rows.push(row);
|
|
1914
|
+
}
|
|
1915
|
+
const addKey = `${ctx.coord}|custom|+add`;
|
|
1916
|
+
const addRow = blankRow(addKey, "+add", "custom-add");
|
|
1917
|
+
addRow.widget = "kvadd";
|
|
1918
|
+
_rows.set(addKey, {
|
|
1919
|
+
add: (raw) => {
|
|
1920
|
+
const prop = raw.trim();
|
|
1921
|
+
if (!prop) {
|
|
1922
|
+
return false;
|
|
1154
1923
|
}
|
|
1924
|
+
ctx.commit(prop, cssInitialMap.get(prop) || "");
|
|
1925
|
+
return true;
|
|
1926
|
+
},
|
|
1927
|
+
});
|
|
1928
|
+
section.rows.push(addRow);
|
|
1929
|
+
return [section];
|
|
1930
|
+
}
|
|
1155
1931
|
|
|
1156
|
-
|
|
1157
|
-
for (const { prop, entry } of entries) {
|
|
1158
|
-
const val = activeStyle[prop];
|
|
1159
|
-
const hasVal = val !== undefined;
|
|
1160
|
-
const condMet = allConditionsPass(entry, activeStyle);
|
|
1161
|
-
const type = inferInputType(entry);
|
|
1162
|
-
if (!hasVal && !condMet) {
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1932
|
+
// ─── The menus ───────────────────────────────────────────────────────────────
|
|
1165
1933
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1934
|
+
/**
|
|
1935
|
+
* The popover slot every list on this tab opens in; its region is `overlay.menu:style-value`.
|
|
1936
|
+
*
|
|
1937
|
+
* ONE slot, because only one of these can be open at a time — a unit list, a keyword list and a
|
|
1938
|
+
* Value Source picker are three uses of the same gesture, not three overlays — and a second menu
|
|
1939
|
+
* opening in the same slot would otherwise leave the first one's document mounted under it.
|
|
1940
|
+
*/
|
|
1941
|
+
const MENU_REGION = "style-value";
|
|
1172
1942
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
prop,
|
|
1183
|
-
entry,
|
|
1184
|
-
style: activeStyle,
|
|
1185
|
-
mutate: mutateTargets,
|
|
1186
|
-
inherited: inheritedStyle,
|
|
1187
|
-
ctx: provCtx,
|
|
1188
|
-
}),
|
|
1189
|
-
);
|
|
1190
|
-
} else {
|
|
1191
|
-
const isWarning = hasVal && !condMet;
|
|
1192
|
-
if (hasVal || condMet) {
|
|
1193
|
-
rows.push(
|
|
1194
|
-
renderStyleRow(
|
|
1195
|
-
entry,
|
|
1196
|
-
prop,
|
|
1197
|
-
(val as string) ?? "",
|
|
1198
|
-
(newVal: string | undefined) => commitStyle(prop, newVal || undefined),
|
|
1199
|
-
isWarning,
|
|
1200
|
-
sec.$layout === "grid",
|
|
1201
|
-
inheritedStyle[prop] as string | undefined,
|
|
1202
|
-
templateSignals,
|
|
1203
|
-
`style|${sel.join("/")}|${editMedia ?? ""}|${activeSelector ?? ""}|${prop}`,
|
|
1204
|
-
provenanceOf(prop, provCtx),
|
|
1205
|
-
),
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1943
|
+
/** The list that is open, so opening another closes it first. */
|
|
1944
|
+
let _menu: MenuHandle | null = null;
|
|
1945
|
+
|
|
1946
|
+
/** Close the row list — every choice does this, so the row is readable straight after. */
|
|
1947
|
+
function closeRowMenu(): void {
|
|
1948
|
+
const menu = _menu;
|
|
1949
|
+
_menu = null;
|
|
1950
|
+
menu?.close();
|
|
1951
|
+
}
|
|
1210
1952
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1953
|
+
/** Open a list under the control that asked for it. One menu at a time, the kit's. */
|
|
1954
|
+
function openRowMenu(
|
|
1955
|
+
anchor: unknown,
|
|
1956
|
+
label: string,
|
|
1957
|
+
choices: StyleChoice[],
|
|
1958
|
+
run: (value: string) => void,
|
|
1959
|
+
): void {
|
|
1960
|
+
const trigger = anchor instanceof HTMLElement ? anchor : null;
|
|
1961
|
+
closeRowMenu();
|
|
1962
|
+
if (!trigger || choices.length === 0) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
_menu = openMenu({
|
|
1966
|
+
label,
|
|
1967
|
+
onClosed: (closed) => {
|
|
1968
|
+
if (_menu === closed) {
|
|
1969
|
+
_menu = null;
|
|
1213
1970
|
}
|
|
1971
|
+
},
|
|
1972
|
+
opener: trigger,
|
|
1973
|
+
region: MENU_REGION,
|
|
1974
|
+
rows: choices.map((choice) => ({
|
|
1975
|
+
...(choice.checked === undefined
|
|
1976
|
+
? {}
|
|
1977
|
+
: { checked: (choice.checked ? "true" : "false") as "true" | "false" }),
|
|
1978
|
+
destructive: false,
|
|
1979
|
+
disabled: false,
|
|
1980
|
+
dividerAbove: choice.divider === true,
|
|
1981
|
+
id: choice.value,
|
|
1982
|
+
title: choice.label,
|
|
1983
|
+
})),
|
|
1984
|
+
run: (value) => run(value),
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1214
1987
|
|
|
1215
|
-
|
|
1216
|
-
<sp-accordion-item
|
|
1217
|
-
label=${sec.label}
|
|
1218
|
-
.open=${live(isOpen)}
|
|
1219
|
-
@sp-accordion-item-toggle=${(e: Event) => {
|
|
1220
|
-
activeTab.value!.session.ui.styleSections = {
|
|
1221
|
-
...activeTab.value!.session.ui.styleSections,
|
|
1222
|
-
[sec.key]: (e.target as HTMLElement & { open: boolean }).open,
|
|
1223
|
-
};
|
|
1224
|
-
}}
|
|
1225
|
-
>
|
|
1226
|
-
${headingT}
|
|
1227
|
-
<div class=${sec.$layout === "grid" ? "style-section-body--grid" : ""}>${rows}</div>
|
|
1228
|
-
</sp-accordion-item>
|
|
1229
|
-
`;
|
|
1230
|
-
});
|
|
1988
|
+
// ─── The mount ───────────────────────────────────────────────────────────────
|
|
1231
1989
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
panCanvas: true,
|
|
1340
|
-
});
|
|
1341
|
-
}}
|
|
1342
|
-
>
|
|
1343
|
-
${rule}
|
|
1344
|
-
</button>
|
|
1345
|
-
<sp-action-button size="xs" quiet @click=${() => commitStyle(rule)}>
|
|
1346
|
-
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
1347
|
-
</sp-action-button>
|
|
1348
|
-
</div>
|
|
1349
|
-
`,
|
|
1350
|
-
)}
|
|
1351
|
-
<button
|
|
1352
|
-
style="padding:6px 10px;background:none;border:1px dashed var(--spectrum-gray-400, #333);border-radius:var(--radius);color:var(--spectrum-gray-700, #a1a1aa);font-size:var(--spectrum-font-size-75, 12px);cursor:pointer"
|
|
1353
|
-
@click=${async () => {
|
|
1354
|
-
const name = await showPromptDialog("Add Nested Selector", {
|
|
1355
|
-
confirmLabel: "Add",
|
|
1356
|
-
message: "Enter a selector to nest under the current rule.",
|
|
1357
|
-
placeholder: "th, :hover, .active",
|
|
1358
|
-
validate: (v) => (v.trim() ? "" : "Enter a selector."),
|
|
1359
|
-
});
|
|
1360
|
-
if (name) {
|
|
1361
|
-
commitStyle(name, {});
|
|
1362
|
-
}
|
|
1363
|
-
}}
|
|
1364
|
-
>
|
|
1365
|
-
+ Add
|
|
1366
|
-
</button>
|
|
1367
|
-
</div>
|
|
1368
|
-
</sp-accordion-item>
|
|
1369
|
-
`
|
|
1370
|
-
: nothing;
|
|
1371
|
-
|
|
1372
|
-
return html`
|
|
1373
|
-
<div class="style-sidebar">
|
|
1374
|
-
${targetLineT} ${filterBarT}
|
|
1375
|
-
<sp-accordion allow-multiple size="s">
|
|
1376
|
-
${sectionTemplates} ${nestedSectionT} ${customSectionT}
|
|
1377
|
-
</sp-accordion>
|
|
1378
|
-
</div>
|
|
1379
|
-
`;
|
|
1990
|
+
/** Everything the document can ask for, resolved through {@link rowActions}. */
|
|
1991
|
+
const ACTIONS: StylePanelActions = {
|
|
1992
|
+
addCustom: (key, field) => {
|
|
1993
|
+
const add = rowActions(key)?.add;
|
|
1994
|
+
if (!add || !(field instanceof HTMLInputElement)) {
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (add(field.value)) {
|
|
1998
|
+
field.value = "";
|
|
1999
|
+
}
|
|
2000
|
+
},
|
|
2001
|
+
addNested: () => _nestedAdd?.(),
|
|
2002
|
+
chipClick: (key) => rowActions(key)?.chip?.(),
|
|
2003
|
+
clearSection: (key) => _sectionClears.get(key)?.(),
|
|
2004
|
+
commitText: (key, value) => {
|
|
2005
|
+
const actions = rowActions(key);
|
|
2006
|
+
if (!actions) {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (actions.debounceId) {
|
|
2010
|
+
cancelStyleDebounce(actions.debounceId);
|
|
2011
|
+
}
|
|
2012
|
+
actions.commit?.(value);
|
|
2013
|
+
},
|
|
2014
|
+
editText: (key, value) => {
|
|
2015
|
+
const actions = rowActions(key);
|
|
2016
|
+
if (!actions?.edit) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
if (actions.debounceId) {
|
|
2020
|
+
debouncedStyleCommit(actions.debounceId, 400, actions.edit)(value);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
actions.edit(value);
|
|
2024
|
+
},
|
|
2025
|
+
openNested: (key) => rowActions(key)?.open?.(),
|
|
2026
|
+
pickChoice: (key, anchor) => {
|
|
2027
|
+
const actions = rowActions(key);
|
|
2028
|
+
if (!actions?.choose || !actions.choices) {
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const { choose } = actions;
|
|
2032
|
+
openRowMenu(anchor, "Values", actions.choices, (value) => choose(value));
|
|
2033
|
+
},
|
|
2034
|
+
pickSource: (key, anchor) => {
|
|
2035
|
+
const source = rowActions(key)?.source;
|
|
2036
|
+
if (!source) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
openRowMenu(
|
|
2040
|
+
anchor,
|
|
2041
|
+
"Value source",
|
|
2042
|
+
source.caps.map((mode) => ({
|
|
2043
|
+
checked: mode === source.mode,
|
|
2044
|
+
label: `${VALUE_SOURCE_LABELS[mode]} — ${VALUE_SOURCE_HINTS[mode]}`,
|
|
2045
|
+
value: mode,
|
|
2046
|
+
})),
|
|
2047
|
+
(chosen) => {
|
|
2048
|
+
const next = chosen as SlotMode;
|
|
2049
|
+
if (next === source.mode) {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
source.onChange(
|
|
2053
|
+
switchSlotMode(
|
|
2054
|
+
source.fieldKey,
|
|
2055
|
+
source.mode,
|
|
2056
|
+
next,
|
|
2057
|
+
cloneValue(source.value as JsonValue | undefined),
|
|
2058
|
+
slotModeSeed(next, { stateDefs: source.stateDefs }),
|
|
2059
|
+
),
|
|
2060
|
+
);
|
|
2061
|
+
},
|
|
2062
|
+
);
|
|
2063
|
+
},
|
|
2064
|
+
pressButton: (key, value) => {
|
|
2065
|
+
const actions = rowActions(key);
|
|
2066
|
+
actions?.choose?.(value === currentButtonValue(key) ? "" : value);
|
|
2067
|
+
},
|
|
2068
|
+
renameCustom: (key, name) => rowActions(key)?.rename?.(name),
|
|
2069
|
+
runEmptyAction: () => openPageAction().run(),
|
|
2070
|
+
setFilter: (value) => {
|
|
2071
|
+
const tab = activeTab.value;
|
|
2072
|
+
if (tab) {
|
|
2073
|
+
tab.session.ui.styleFilter = value;
|
|
2074
|
+
}
|
|
2075
|
+
},
|
|
2076
|
+
toggleSection: (key, open) => {
|
|
2077
|
+
const tab = activeTab.value;
|
|
2078
|
+
if (!tab) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
tab.session.ui.styleSections = { ...tab.session.ui.styleSections, [key]: open };
|
|
2082
|
+
},
|
|
2083
|
+
toggleShorthand: (key) => rowActions(key)?.toggle?.(),
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
function currentButtonValue(key: string): string {
|
|
2087
|
+
return _buttonValues.get(key) ?? "";
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/** The standing mount: one per window, because the Inspector draws one Style tab. */
|
|
2091
|
+
let _standing: { host: HTMLElement; handle: StylePanelSurface; scope: EffectScope } | null = null;
|
|
2092
|
+
|
|
2093
|
+
/** What the Style tab cannot read for itself: which mode the canvas beside it is in. */
|
|
2094
|
+
export interface StyleHostDeps {
|
|
2095
|
+
/** `stylebook` edits a tag catalogue entry; anything else edits the element selection. */
|
|
2096
|
+
getCanvasMode: () => string;
|
|
1380
2097
|
}
|
|
1381
2098
|
|
|
1382
|
-
|
|
2099
|
+
/** The default the tab falls back to when the dock names no mode. */
|
|
2100
|
+
const EDIT_MODE = () => "edit";
|
|
2101
|
+
|
|
2102
|
+
/** The canvas mode the dock last reported — Stylebook edits a tag, Edit edits a selection. */
|
|
2103
|
+
let _canvasMode: () => string = EDIT_MODE;
|
|
2104
|
+
|
|
2105
|
+
/** Remember the button values, so a second press on the selected one clears it. */
|
|
2106
|
+
function rememberButtons(view: StylePanelView): void {
|
|
2107
|
+
_buttonValues = new Map();
|
|
2108
|
+
for (const section of view.sections) {
|
|
2109
|
+
for (const row of section.rows) {
|
|
2110
|
+
if (row.widget === "buttons") {
|
|
2111
|
+
_buttonValues.set(row.key, row.buttons.find((b) => b.selected)?.value ?? "");
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1383
2116
|
|
|
1384
2117
|
/**
|
|
1385
|
-
*
|
|
2118
|
+
* Give the Style tab somewhere to draw, or take it away.
|
|
2119
|
+
*
|
|
2120
|
+
* The document is mounted once and then only ASSIGNED to: its own `effect()` re-projects whenever
|
|
2121
|
+
* anything the projection read moves, and the surface writes one property per binding with an equal
|
|
2122
|
+
* write skipped. It must NOT be routed through `panels/panel-scheduler.ts` — that scheduler defers
|
|
2123
|
+
* a repaint while a field in the dock has focus, because a lit render of the whole panel replaces
|
|
2124
|
+
* the node the reader is typing into, and here the deferral would buy nothing and make the canvas
|
|
2125
|
+
* lag the keystroke.
|
|
1386
2126
|
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
2127
|
+
* `null` takes the tab down: the dock unmounted, or a test is starting clean.
|
|
2128
|
+
*
|
|
2129
|
+
* @param {HTMLElement | null} el The Inspector's Style tab body.
|
|
2130
|
+
* @param {StyleHostDeps} [ctx] What the tab cannot read for itself.
|
|
1389
2131
|
*/
|
|
1390
|
-
export function
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
2132
|
+
export function bindStyleHost(el: HTMLElement | null, ctx?: StyleHostDeps): void {
|
|
2133
|
+
closeRowMenu();
|
|
2134
|
+
_standing?.scope.stop();
|
|
2135
|
+
_standing?.handle.dispose();
|
|
2136
|
+
_standing = null;
|
|
2137
|
+
_rows = new Map();
|
|
2138
|
+
_sectionClears = new Map();
|
|
2139
|
+
_nestedAdd = null;
|
|
2140
|
+
if (!el) {
|
|
2141
|
+
return;
|
|
1399
2142
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2143
|
+
_canvasMode = ctx?.getCanvasMode ?? EDIT_MODE;
|
|
2144
|
+
const first = buildView(_canvasMode);
|
|
2145
|
+
rememberButtons(first);
|
|
2146
|
+
const handle = mountStylePanelSurface(el, first, ACTIONS, {
|
|
2147
|
+
target: (element) => attachTargetLine(element ?? undefined),
|
|
2148
|
+
});
|
|
2149
|
+
const scope = effectScope();
|
|
2150
|
+
scope.run(() => {
|
|
2151
|
+
effect(() => {
|
|
2152
|
+
const view = buildView(_canvasMode);
|
|
2153
|
+
rememberButtons(view);
|
|
2154
|
+
handle.update(view);
|
|
1410
2155
|
});
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
if (!selected) {
|
|
1414
|
-
return renderEmptyState({ message: clickAnythingTo("style it") });
|
|
1415
|
-
}
|
|
1416
|
-
const node = getNodeAtPath(tab.doc.document, selected);
|
|
1417
|
-
if (!node) {
|
|
1418
|
-
return renderEmptyState({ message: staleSelectionMessage() });
|
|
1419
|
-
}
|
|
1420
|
-
return styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector);
|
|
2156
|
+
});
|
|
2157
|
+
_standing = { handle, host: el, scope };
|
|
1421
2158
|
}
|
|
1422
2159
|
|
|
1423
2160
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
@@ -1428,9 +2165,9 @@ export function renderStylePanelTemplate(ctx: { getCanvasMode: () => string }) {
|
|
|
1428
2165
|
* `style.openSelectorMenu` is the one command here that addresses a CONTROL rather than a state,
|
|
1429
2166
|
* and it earns that on purpose: the shot it serves photographs the open menu, so the menu is the
|
|
1430
2167
|
* subject. What it does not do is hand a CSS selector back to the runner for a synthetic mouse
|
|
1431
|
-
* press — the element comes from the Target Line's own
|
|
1432
|
-
* the handle with it. The menu it opens is
|
|
1433
|
-
*
|
|
2168
|
+
* press — the element comes from the Target Line's own handle, so a refactor of that markup moves
|
|
2169
|
+
* the handle with it. The menu it opens is a segment of the Target Line rather than a picker on a
|
|
2170
|
+
* retired toolbar; the id, and therefore the shot, is unchanged.
|
|
1434
2171
|
*
|
|
1435
2172
|
* `style.setSelector` is the state half of the same idea, and it is what a caller that wants a
|
|
1436
2173
|
* RESULT rather than a picture should use: the menu exists to choose a selector, and choosing one
|