@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
package/src/ui/schema-form.ts
CHANGED
|
@@ -1,34 +1,72 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Schema-form — reusable JSON-Schema → form
|
|
3
|
+
* Schema-form — the reusable JSON-Schema → form engine, and the FLOW half of it.
|
|
4
4
|
*
|
|
5
|
-
* Maps schema property types to
|
|
6
|
-
* number
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Maps schema property types to controls (enum → select, boolean → checkbox, number/integer →
|
|
6
|
+
* number field, `json-schema` format → JSON editor, array-of-objects → a row per item, other
|
|
7
|
+
* array/object → JSON text, default → text field); hosts commit edits through a single
|
|
8
|
+
* `onChange(patch)` callback; dynamic enum choices resolve through a {@link SchemaFormContext}.
|
|
9
|
+
* Custom controls register by name via {@link registerFormControl} and are consulted first for `ui`
|
|
10
|
+
* overrides.
|
|
11
|
+
*
|
|
12
|
+
* **This module decides; `surfaces/schema-form.json` draws.** Every question with an answer — which
|
|
13
|
+
* control a property gets, what its choices are, which rungs of the value ladder the position
|
|
14
|
+
* permits, what a keystroke is worth and whether a committed value is refused — is answered here
|
|
15
|
+
* and handed over as a list of already-decided rows. The document holds the markup, the ARIA and
|
|
16
|
+
* the style, and holds no schema knowledge at all.
|
|
17
|
+
*
|
|
18
|
+
* **Why a host element and not a template.** A Jx document CLEARS the host it is given, so a
|
|
19
|
+
* document and a lit template can never share a container. {@link mountSchemaForm} therefore hands
|
|
20
|
+
* back the element the form lives in; a caller that is still lit puts that element in its own tree
|
|
21
|
+
* exactly where it used to interpolate a template (studio-ui-guidelines.md §9.3, §9.4). Calling it
|
|
22
|
+
* again with the same `formKey` updates the standing form rather than building a second one, which
|
|
23
|
+
* is what keeps the caret in a field across the repaint a commit provokes.
|
|
11
24
|
*
|
|
12
25
|
* **Binding is the same ladder as everywhere else** (§6.6). A host that names somewhere a value can
|
|
13
26
|
* come from — route params, the document's signals — gets a Value Source chip on every scalar
|
|
14
|
-
* field,
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
27
|
+
* field, named by `ui/value-source.ts` and switched by `ui/dynamic-slot.ts`'s per-field, per-rung
|
|
28
|
+
* memory, which is shared rather than reimplemented: only the DRAWING differs between a form row
|
|
29
|
+
* and an inspector row. A host that names no source — the project settings forms — draws no chip
|
|
30
|
+
* and edits fixed values.
|
|
31
|
+
*
|
|
32
|
+
* **A registered control is MOUNTED into a host, and that is the whole contract now.** It used to
|
|
33
|
+
* be two: a function returning a lit template, and beside it a `mount` for the one built-in that
|
|
34
|
+
* had become a Jx document. The other two followed (`surfaces/secret-field.json`,
|
|
35
|
+
* `surfaces/reference-field.json`), and a contract with no producers is not a contract — so the
|
|
36
|
+
* template half went with them, and with it the last `lit-html` import in the form engine. What a
|
|
37
|
+
* control is handed is the empty `[part="control-host"]` this form's own document already draws for
|
|
38
|
+
* its field, or the `[part="cell-control-host"]` a row cell draws; what it hands back is a standing
|
|
39
|
+
* mount, updated rather than rebuilt on every repaint (specs/extensions.md §9.1).
|
|
19
40
|
*/
|
|
20
41
|
|
|
21
|
-
import { html, nothing } from "lit-html";
|
|
22
|
-
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
23
|
-
import { live } from "lit-html/directives/live.js";
|
|
24
|
-
import { styleMap } from "lit-html/directives/style-map.js";
|
|
25
42
|
import { isRef } from "@jxsuite/schema/guards";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
43
|
+
import { createSchemaFormSurface } from "../surfaces/schema-form";
|
|
44
|
+
import { openMenu } from "../surfaces/menu";
|
|
45
|
+
import type { MenuHandle } from "../surfaces/menu";
|
|
46
|
+
import { cloneValue } from "../tabs/doc-op-apply";
|
|
47
|
+
import {
|
|
48
|
+
effectiveSlotMode,
|
|
49
|
+
hasStashedSlotValue,
|
|
50
|
+
slotModeSeed,
|
|
51
|
+
stashSlotValue,
|
|
52
|
+
switchSlotMode,
|
|
53
|
+
} from "./dynamic-slot";
|
|
54
|
+
import {
|
|
55
|
+
VALUE_SOURCE_HINTS,
|
|
56
|
+
VALUE_SOURCE_LABELS,
|
|
57
|
+
configFieldSchema,
|
|
58
|
+
slotCaps,
|
|
59
|
+
} from "./value-source";
|
|
60
|
+
import type { SlotMode } from "./value-source";
|
|
30
61
|
import type { SignalOption } from "./dynamic-slot";
|
|
31
62
|
import type { JsonValue } from "../types";
|
|
63
|
+
import type {
|
|
64
|
+
SchemaFormCellView,
|
|
65
|
+
SchemaFormFieldView,
|
|
66
|
+
SchemaFormOption,
|
|
67
|
+
SchemaFormRowView,
|
|
68
|
+
SchemaFormSurface,
|
|
69
|
+
} from "../surfaces/schema-form";
|
|
32
70
|
|
|
33
71
|
/** A (possibly nested) JSON Schema node, covering both object and property level keys. */
|
|
34
72
|
export interface JsonSchema {
|
|
@@ -81,12 +119,38 @@ export interface SchemaFormControlArgs {
|
|
|
81
119
|
value: unknown;
|
|
82
120
|
onChange: (next: unknown) => void;
|
|
83
121
|
ctx: SchemaFormContext;
|
|
122
|
+
/**
|
|
123
|
+
* Ask the HOST to draw the form again, where it offered a way to.
|
|
124
|
+
*
|
|
125
|
+
* No built-in needs it any more, and that is the mount's doing rather than an omission: a control
|
|
126
|
+
* that owns its host draws its own second frame, so the `reference` picker's read landing and its
|
|
127
|
+
* Retry are its own business now. What it is still for is a commit that changes something OUTSIDE
|
|
128
|
+
* this field — a value another row's choices are derived from — which only the host can redraw.
|
|
129
|
+
*/
|
|
84
130
|
rerender?: (() => void) | undefined;
|
|
85
131
|
}
|
|
86
132
|
|
|
87
|
-
|
|
133
|
+
/**
|
|
134
|
+
* A control that OWNS a host element.
|
|
135
|
+
*
|
|
136
|
+
* The registry's whole contract, and the one a Jx document can satisfy: a document clears the host
|
|
137
|
+
* it is given and cannot be interpolated into a lit template (specs/studio-ui-guidelines.md §9.4),
|
|
138
|
+
* so a control is handed the empty host this form already draws for it and keeps the mount for the
|
|
139
|
+
* life of the field. `mount` is called ONCE per host; every later repaint of the form reaches the
|
|
140
|
+
* control through {@link SchemaFormControlHandle.update} instead, which is what keeps the caret in a
|
|
141
|
+
* name the reader is part-way through typing when the value beside it commits.
|
|
142
|
+
*/
|
|
143
|
+
export interface SchemaFormMountedControl {
|
|
144
|
+
mount: (host: HTMLElement, args: SchemaFormControlArgs) => SchemaFormControlHandle;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A standing mounted control: brought up to date, and taken down with the form. */
|
|
148
|
+
export interface SchemaFormControlHandle {
|
|
149
|
+
update: (args: SchemaFormControlArgs) => void;
|
|
150
|
+
dispose: () => void;
|
|
151
|
+
}
|
|
88
152
|
|
|
89
|
-
/** Options for {@link
|
|
153
|
+
/** Options for {@link mountSchemaForm}. */
|
|
90
154
|
export interface RenderFormOptions {
|
|
91
155
|
onChange: (patch: Record<string, unknown>) => void;
|
|
92
156
|
context?: SchemaFormContext | undefined;
|
|
@@ -101,9 +165,8 @@ export interface RenderFormOptions {
|
|
|
101
165
|
* Externally-produced diagnostics, keyed by property name — §7.1's inline tier, sourced.
|
|
102
166
|
*
|
|
103
167
|
* This is how a validator that runs over the WHOLE document reaches the one field it is about:
|
|
104
|
-
* `jx-validate`'s `project.json` errors
|
|
105
|
-
*
|
|
106
|
-
* for the same file open in the code view, and a host's own commit-time rejection.
|
|
168
|
+
* `jx-validate`'s `project.json` errors, Monaco's markers for the same file open in the code
|
|
169
|
+
* view, and a host's own commit-time rejection.
|
|
107
170
|
*
|
|
108
171
|
* A host message wins over {@link validateFieldValue}'s intrinsic check, because the host knows
|
|
109
172
|
* things the property schema alone does not — that this enum value names a connector that was
|
|
@@ -117,26 +180,29 @@ export interface RenderFormOptions {
|
|
|
117
180
|
* Report required-but-empty fields inline. Off by default, and that default is §7.1's rule
|
|
118
181
|
* literally applied: a form the user has not touched yet has not committed anything, so painting
|
|
119
182
|
* every required field red the moment it renders is telling them they got something wrong before
|
|
120
|
-
* they did anything. Required-ness is already shown — the
|
|
121
|
-
* validates on submit turns this on for the render that follows the rejected submit.
|
|
183
|
+
* they did anything. Required-ness is already shown — the row carries the required mark. A host
|
|
184
|
+
* that validates on submit turns this on for the render that follows the rejected submit.
|
|
122
185
|
*/
|
|
123
186
|
showRequired?: boolean | undefined;
|
|
124
187
|
}
|
|
125
188
|
|
|
126
189
|
// ─── Control registry ────────────────────────────────────────────────────────
|
|
127
190
|
|
|
128
|
-
const controlRegistry = new Map<string,
|
|
191
|
+
const controlRegistry = new Map<string, SchemaFormMountedControl>();
|
|
129
192
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Register (or replace) a named form control: a {@link SchemaFormMountedControl} that draws into the
|
|
195
|
+
* host it is given.
|
|
196
|
+
*
|
|
197
|
+
* There is no lookup beside this. `getFormControl` existed to hand a TEMPLATE control to a lit
|
|
198
|
+
* caller that would interpolate it, and every such caller is gone — `panels/frontmatter-fields.ts`
|
|
199
|
+
* projects a row rather than borrowing a widget, and both frontmatter surfaces are documents. A
|
|
200
|
+
* control needs a HOST, which only this engine has, so the dispatch below is the only reader.
|
|
201
|
+
*/
|
|
202
|
+
export function registerFormControl(name: string, control: SchemaFormMountedControl): void {
|
|
132
203
|
controlRegistry.set(name, control);
|
|
133
204
|
}
|
|
134
205
|
|
|
135
|
-
/** Look up a registered form control by name. */
|
|
136
|
-
export function getFormControl(name: string): SchemaFormControl | undefined {
|
|
137
|
-
return controlRegistry.get(name);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
206
|
// ─── References between collections ──────────────────────────────────────────
|
|
141
207
|
|
|
142
208
|
/** `#/content/<type>` — a property whose value is the id of an entry in another collection. */
|
|
@@ -219,368 +285,10 @@ export function parseNumericField(raw: string, integer: boolean): number {
|
|
|
219
285
|
return integer ? Math.trunc(Number(raw)) : Number(raw);
|
|
220
286
|
}
|
|
221
287
|
|
|
222
|
-
/** Plain textfield editing a `{ $ref }` value directly — the fallback when no binding control. */
|
|
223
|
-
function refTextField(key: string, refVal: string, onChange: (next: unknown) => void) {
|
|
224
|
-
return html`<sp-textfield
|
|
225
|
-
size="s"
|
|
226
|
-
label=${key}
|
|
227
|
-
placeholder=${key}
|
|
228
|
-
.value=${live(refVal)}
|
|
229
|
-
@change=${(e: Event) => {
|
|
230
|
-
const v = (e.target as HTMLInputElement).value.trim();
|
|
231
|
-
onChange(v ? { $ref: v } : undefined);
|
|
232
|
-
}}
|
|
233
|
-
></sp-textfield>`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Render a single inline field within an array-of-objects row. Dispatches by schema type: enum →
|
|
238
|
-
* picker, boolean → switch, number → number-field, else → textfield.
|
|
239
|
-
*
|
|
240
|
-
* @param {string} key
|
|
241
|
-
* @param {JsonSchema} schema
|
|
242
|
-
* @param {unknown} value
|
|
243
|
-
* @param {(val: unknown) => void} onChange
|
|
244
|
-
* @param {SchemaFormContext | undefined} ctx
|
|
245
|
-
* @param {Record<string, unknown>} [scope] - Scope for dependent enum refs (the parent form value)
|
|
246
|
-
* @param {() => void} [rerender] - Repaint hook, threaded so an inline reference can show its
|
|
247
|
-
* choices once they load — an async control with no way to ask for a second frame renders its
|
|
248
|
-
* loading state forever.
|
|
249
|
-
*/
|
|
250
|
-
export function renderInlineField(
|
|
251
|
-
key: string,
|
|
252
|
-
schema: JsonSchema,
|
|
253
|
-
value: unknown,
|
|
254
|
-
onChange: (val: unknown) => void,
|
|
255
|
-
ctx?: SchemaFormContext,
|
|
256
|
-
scope?: Record<string, unknown>,
|
|
257
|
-
rerender?: () => void,
|
|
258
|
-
) {
|
|
259
|
-
if (isRef(value)) {
|
|
260
|
-
return refTextField(key, value.$ref, onChange);
|
|
261
|
-
}
|
|
262
|
-
const referenceControl =
|
|
263
|
-
referenceTarget(schema) === null ? undefined : controlRegistry.get("reference");
|
|
264
|
-
if (referenceControl) {
|
|
265
|
-
return referenceControl({
|
|
266
|
-
ctx: ctx ?? NULL_FORM_CONTEXT,
|
|
267
|
-
key,
|
|
268
|
-
onChange,
|
|
269
|
-
rerender,
|
|
270
|
-
schema,
|
|
271
|
-
value,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
const enumValues = resolveFormEnum(schema.enum, ctx, scope);
|
|
275
|
-
|
|
276
|
-
if (enumValues) {
|
|
277
|
-
return html`<sp-picker
|
|
278
|
-
size="s"
|
|
279
|
-
label=${key}
|
|
280
|
-
value=${value !== undefined ? String(value) : "__none__"}
|
|
281
|
-
@change=${(e: Event) =>
|
|
282
|
-
onChange(
|
|
283
|
-
(e.target as HTMLInputElement).value === "__none__"
|
|
284
|
-
? undefined
|
|
285
|
-
: (e.target as HTMLInputElement).value,
|
|
286
|
-
)}
|
|
287
|
-
>
|
|
288
|
-
<sp-menu-item value="__none__">—</sp-menu-item>
|
|
289
|
-
${enumValues.map((v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`)}
|
|
290
|
-
</sp-picker>`;
|
|
291
|
-
}
|
|
292
|
-
if (schema.type === "boolean") {
|
|
293
|
-
return html`<sp-switch
|
|
294
|
-
size="s"
|
|
295
|
-
?checked=${Boolean(value)}
|
|
296
|
-
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
|
|
297
|
-
>${key}</sp-switch
|
|
298
|
-
>`;
|
|
299
|
-
}
|
|
300
|
-
if (schema.type === "integer" || schema.type === "number") {
|
|
301
|
-
return html`<sp-number-field
|
|
302
|
-
size="s"
|
|
303
|
-
label=${key}
|
|
304
|
-
.value=${value !== undefined ? value : nothing}
|
|
305
|
-
step=${schema.type === "integer" ? "1" : nothing}
|
|
306
|
-
@change=${(e: Event) => {
|
|
307
|
-
const parsed = parseNumericField(
|
|
308
|
-
(e.target as HTMLInputElement).value,
|
|
309
|
-
schema.type === "integer",
|
|
310
|
-
);
|
|
311
|
-
onChange(Number.isNaN(parsed) ? undefined : parsed);
|
|
312
|
-
}}
|
|
313
|
-
></sp-number-field>`;
|
|
314
|
-
}
|
|
315
|
-
return html`<sp-textfield
|
|
316
|
-
size="s"
|
|
317
|
-
label=${key}
|
|
318
|
-
placeholder=${key}
|
|
319
|
-
.value=${value ?? ""}
|
|
320
|
-
@input=${(e: Event) => onChange((e.target as HTMLInputElement).value || undefined)}
|
|
321
|
-
></sp-textfield>`;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Render a debounced multiline JSON text field for array/object schema properties. */
|
|
325
|
-
function renderJsonTextField(currentValue: unknown, ps: JsonSchema, commit: (v: unknown) => void) {
|
|
326
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
327
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
328
|
-
return html`<sp-textfield
|
|
329
|
-
multiline
|
|
330
|
-
size="s"
|
|
331
|
-
style="min-height:40px"
|
|
332
|
-
.value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
|
|
333
|
-
placeholder=${ps.default !== undefined ? JSON.stringify(ps.default) : nothing}
|
|
334
|
-
@input=${(e: Event) => {
|
|
335
|
-
clearTimeout(debounce);
|
|
336
|
-
debounce = setTimeout(() => {
|
|
337
|
-
try {
|
|
338
|
-
commit(JSON.parse((e.target as HTMLInputElement).value) as unknown);
|
|
339
|
-
} catch {}
|
|
340
|
-
}, 500);
|
|
341
|
-
}}
|
|
342
|
-
></sp-textfield>`;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ─── Per-property control dispatch ───────────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
/** Render the widget for one schema property, honoring registered-control overrides. */
|
|
348
|
-
function renderPropertyControl(
|
|
349
|
-
prop: string,
|
|
350
|
-
ps: JsonSchema,
|
|
351
|
-
value: Record<string, unknown>,
|
|
352
|
-
required: Set<string>,
|
|
353
|
-
opts: RenderFormOptions,
|
|
354
|
-
ctx: SchemaFormContext,
|
|
355
|
-
): TemplateResult {
|
|
356
|
-
const currentValue = value[prop];
|
|
357
|
-
const commit = (next: unknown) => opts.onChange({ [prop]: next });
|
|
358
|
-
const controlArgs: SchemaFormControlArgs = {
|
|
359
|
-
ctx,
|
|
360
|
-
key: prop,
|
|
361
|
-
onChange: commit,
|
|
362
|
-
rerender: opts.rerender,
|
|
363
|
-
schema: ps,
|
|
364
|
-
value: currentValue,
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Explicit ui override → consult the control registry first
|
|
368
|
-
const overrideName = opts.ui?.[prop]?.control;
|
|
369
|
-
if (overrideName) {
|
|
370
|
-
const custom = controlRegistry.get(overrideName);
|
|
371
|
-
if (custom) {
|
|
372
|
-
return custom(controlArgs);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/* A ref left in a form whose host named nowhere to bind: no ladder is drawn, so the pointer is
|
|
377
|
-
edited as the string it is rather than rendered as "[object Object]" in a typed widget. */
|
|
378
|
-
if (isRef(currentValue) && isBindableField(ps)) {
|
|
379
|
-
return refTextField(prop, currentValue.$ref, commit);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/* A relationship to another collection (`$ref: "#/content/<type>"`) is the registered `reference`
|
|
383
|
-
control, wherever the form is drawn — §9.2's "one picker" is this dispatch plus the single
|
|
384
|
-
`registerFormControl("reference", …)` in `ui/form-controls.ts`. It is deliberately NOT an enum:
|
|
385
|
-
the choices are entry files on disk, so they are read asynchronously and can be stale, and a
|
|
386
|
-
schema `enum` is a closed set the document itself declares. When the control is not registered
|
|
387
|
-
(a bare-Bun import of this engine), the field falls through to the plain textfield below rather
|
|
388
|
-
than rendering nothing. */
|
|
389
|
-
if (referenceTarget(ps) !== null) {
|
|
390
|
-
const referenceControl = controlRegistry.get("reference");
|
|
391
|
-
if (referenceControl) {
|
|
392
|
-
return referenceControl(controlArgs);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const enumValues = resolveFormEnum(opts.ui?.[prop]?.enum ?? ps.enum, ctx, value);
|
|
397
|
-
if (enumValues) {
|
|
398
|
-
return html`
|
|
399
|
-
<sp-picker
|
|
400
|
-
size="s"
|
|
401
|
-
value=${
|
|
402
|
-
currentValue !== undefined
|
|
403
|
-
? String(currentValue)
|
|
404
|
-
: ps.default !== undefined
|
|
405
|
-
? String(ps.default)
|
|
406
|
-
: "__none__"
|
|
407
|
-
}
|
|
408
|
-
@change=${(e: Event) =>
|
|
409
|
-
commit(
|
|
410
|
-
(e.target as HTMLInputElement).value === "__none__"
|
|
411
|
-
? undefined
|
|
412
|
-
: (e.target as HTMLInputElement).value,
|
|
413
|
-
)}
|
|
414
|
-
>
|
|
415
|
-
${!required.has(prop) ? html`<sp-menu-item value="__none__">—</sp-menu-item>` : nothing}
|
|
416
|
-
${enumValues.map((val: string) => html`<sp-menu-item value=${val}>${val}</sp-menu-item>`)}
|
|
417
|
-
</sp-picker>
|
|
418
|
-
`;
|
|
419
|
-
}
|
|
420
|
-
if (ps.type === "boolean") {
|
|
421
|
-
return html`<sp-checkbox
|
|
422
|
-
?checked=${currentValue ?? ps.default ?? false}
|
|
423
|
-
@change=${(e: Event) => commit((e.target as HTMLInputElement).checked)}
|
|
424
|
-
></sp-checkbox>`;
|
|
425
|
-
}
|
|
426
|
-
if (ps.type === "integer" || ps.type === "number") {
|
|
427
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
428
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
429
|
-
return html`<sp-number-field
|
|
430
|
-
size="s"
|
|
431
|
-
min=${ifDefined(ps.minimum)}
|
|
432
|
-
max=${ifDefined(ps.maximum)}
|
|
433
|
-
step=${ps.type === "integer" ? "1" : nothing}
|
|
434
|
-
.value=${currentValue !== undefined ? currentValue : nothing}
|
|
435
|
-
placeholder=${ps.default != null ? String(ps.default) : nothing}
|
|
436
|
-
@change=${(e: Event) => {
|
|
437
|
-
clearTimeout(debounce);
|
|
438
|
-
debounce = setTimeout(() => {
|
|
439
|
-
const parsed = parseNumericField(
|
|
440
|
-
(e.target as HTMLInputElement).value,
|
|
441
|
-
ps.type === "integer",
|
|
442
|
-
);
|
|
443
|
-
commit(Number.isNaN(parsed) ? undefined : parsed);
|
|
444
|
-
}, 400);
|
|
445
|
-
}}
|
|
446
|
-
></sp-number-field>`;
|
|
447
|
-
}
|
|
448
|
-
if (ps.format === "json-schema") {
|
|
449
|
-
const hasValue =
|
|
450
|
-
currentValue && typeof currentValue === "object" && Object.keys(currentValue).length > 0;
|
|
451
|
-
const cv = currentValue as Record<string, unknown>;
|
|
452
|
-
const isSchemaRef = hasValue && cv.$ref;
|
|
453
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
454
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
455
|
-
return html`
|
|
456
|
-
<div class="schema-param-editor">
|
|
457
|
-
${
|
|
458
|
-
hasValue && !isSchemaRef && cv.properties
|
|
459
|
-
? html`
|
|
460
|
-
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px">
|
|
461
|
-
${Object.entries(cv.properties as Record<string, Record<string, unknown>>).map(
|
|
462
|
-
([k, v]) => html`
|
|
463
|
-
<span
|
|
464
|
-
style="background:var(--bg);padding:1px 6px;border-radius:var(--radius);font-size:10px;color:var(--fg-dim)"
|
|
465
|
-
>${k}: ${v.type ?? "any"}</span
|
|
466
|
-
>
|
|
467
|
-
`,
|
|
468
|
-
)}
|
|
469
|
-
</div>
|
|
470
|
-
`
|
|
471
|
-
: nothing
|
|
472
|
-
}
|
|
473
|
-
<sp-textfield
|
|
474
|
-
multiline
|
|
475
|
-
size="s"
|
|
476
|
-
style=${styleMap({
|
|
477
|
-
fontFamily: "monospace",
|
|
478
|
-
fontSize: "11px",
|
|
479
|
-
minHeight: hasValue ? "80px" : "40px",
|
|
480
|
-
})}
|
|
481
|
-
.value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
|
|
482
|
-
placeholder=${ps.description ?? "JSON Schema defining the data shape…"}
|
|
483
|
-
@input=${(e: Event) => {
|
|
484
|
-
clearTimeout(debounce);
|
|
485
|
-
debounce = setTimeout(() => {
|
|
486
|
-
try {
|
|
487
|
-
commit(JSON.parse((e.target as HTMLInputElement).value) as unknown);
|
|
488
|
-
} catch {}
|
|
489
|
-
}, 500);
|
|
490
|
-
}}
|
|
491
|
-
></sp-textfield>
|
|
492
|
-
</div>
|
|
493
|
-
`;
|
|
494
|
-
}
|
|
495
|
-
if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
|
|
496
|
-
// Array of objects with defined schema → multi-row inline form
|
|
497
|
-
const rows: Record<string, unknown>[] = Array.isArray(currentValue)
|
|
498
|
-
? (currentValue as Record<string, unknown>[])
|
|
499
|
-
: [];
|
|
500
|
-
const itemProps = ps.items.properties;
|
|
501
|
-
return html`
|
|
502
|
-
<div class="array-object-field">
|
|
503
|
-
${rows.map(
|
|
504
|
-
(row: Record<string, unknown>, idx: number) => html`
|
|
505
|
-
<div
|
|
506
|
-
class="array-object-row"
|
|
507
|
-
style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
|
|
508
|
-
>
|
|
509
|
-
${Object.entries(itemProps).map(([propKey, propSchema]) =>
|
|
510
|
-
renderInlineField(
|
|
511
|
-
propKey,
|
|
512
|
-
propSchema,
|
|
513
|
-
row[propKey],
|
|
514
|
-
(val) => {
|
|
515
|
-
const updated = [...rows];
|
|
516
|
-
updated[idx] = { ...updated[idx], [propKey]: val };
|
|
517
|
-
commit(updated);
|
|
518
|
-
},
|
|
519
|
-
ctx,
|
|
520
|
-
value,
|
|
521
|
-
opts.rerender,
|
|
522
|
-
),
|
|
523
|
-
)}
|
|
524
|
-
<sp-action-button
|
|
525
|
-
quiet
|
|
526
|
-
size="s"
|
|
527
|
-
@click=${() => {
|
|
528
|
-
const updated = rows.filter((_: unknown, i: number) => i !== idx);
|
|
529
|
-
commit(updated.length > 0 ? updated : undefined);
|
|
530
|
-
opts.rerender?.();
|
|
531
|
-
}}
|
|
532
|
-
>
|
|
533
|
-
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
534
|
-
</sp-action-button>
|
|
535
|
-
</div>
|
|
536
|
-
`,
|
|
537
|
-
)}
|
|
538
|
-
<sp-action-button
|
|
539
|
-
quiet
|
|
540
|
-
size="s"
|
|
541
|
-
@click=${(e: Event) => {
|
|
542
|
-
e.stopPropagation();
|
|
543
|
-
const newRow: Record<string, unknown> = {};
|
|
544
|
-
for (const [k, v] of Object.entries(itemProps)) {
|
|
545
|
-
if (v.default !== undefined) {
|
|
546
|
-
newRow[k] = v.default;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
commit([...rows, newRow]);
|
|
550
|
-
opts.rerender?.();
|
|
551
|
-
}}
|
|
552
|
-
>+ Add</sp-action-button
|
|
553
|
-
>
|
|
554
|
-
</div>
|
|
555
|
-
`;
|
|
556
|
-
}
|
|
557
|
-
if (ps.type === "array" || ps.type === "object") {
|
|
558
|
-
return renderJsonTextField(currentValue, ps, commit) as TemplateResult;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
562
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
563
|
-
const ph = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
|
|
564
|
-
return html`<sp-textfield
|
|
565
|
-
size="s"
|
|
566
|
-
style="flex:1"
|
|
567
|
-
.value=${currentValue ?? ""}
|
|
568
|
-
placeholder=${ph || nothing}
|
|
569
|
-
title=${ps.description || nothing}
|
|
570
|
-
@input=${(e: Event) => {
|
|
571
|
-
clearTimeout(debounce);
|
|
572
|
-
debounce = setTimeout(() => commit((e.target as HTMLInputElement).value || undefined), 400);
|
|
573
|
-
}}
|
|
574
|
-
></sp-textfield>`;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// ─── The Value Source ladder (§6.6) ──────────────────────────────────────────
|
|
578
|
-
|
|
579
288
|
/**
|
|
580
289
|
* Whether a property is edited as a single value at all. The three that are not — a nested JSON
|
|
581
|
-
* Schema, an object and an array — are edited as raw JSON or as a
|
|
582
|
-
*
|
|
583
|
-
* work.
|
|
290
|
+
* Schema, an object and an array — are edited as raw JSON or as a row per item, and a chip offering
|
|
291
|
+
* to replace that editor with a signal pointer would be offering to delete the user's work.
|
|
584
292
|
*/
|
|
585
293
|
function isBindableField(ps: JsonSchema): boolean {
|
|
586
294
|
return ps.format !== "json-schema" && ps.type !== "object" && ps.type !== "array";
|
|
@@ -594,6 +302,17 @@ function refSourcesFor(ctx: SchemaFormContext): SignalOption[] {
|
|
|
594
302
|
];
|
|
595
303
|
}
|
|
596
304
|
|
|
305
|
+
/** The empty row's value in a select — an option list cannot offer "absent" as a value. */
|
|
306
|
+
const NONE = "__none__";
|
|
307
|
+
|
|
308
|
+
/** A value as a control reads it: a string, and never `"[object Object]"`. */
|
|
309
|
+
function asText(value: unknown): string {
|
|
310
|
+
if (value === undefined || value === null) {
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
return typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
|
|
314
|
+
}
|
|
315
|
+
|
|
597
316
|
// ─── Field validation (§7.1, inline tier) ────────────────────────────────────
|
|
598
317
|
|
|
599
318
|
/**
|
|
@@ -655,60 +374,807 @@ export function validateFieldValue(
|
|
|
655
374
|
return "";
|
|
656
375
|
}
|
|
657
376
|
|
|
658
|
-
// ───
|
|
377
|
+
// ─── What one property turned out to be ──────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* How this module reads a gesture back. The document reports `commit("port", "8080")` and nothing
|
|
381
|
+
* else; everything needed to turn that into a patch was decided when the row was derived, so the
|
|
382
|
+
* plan is where it is kept rather than re-derived from the schema on every keystroke.
|
|
383
|
+
*/
|
|
384
|
+
interface FieldPlan {
|
|
385
|
+
prop: string;
|
|
386
|
+
schema: JsonSchema;
|
|
387
|
+
/** How a committed string becomes a document value. */
|
|
388
|
+
commit: "text" | "ref" | "template" | "select" | "number" | "json" | "none";
|
|
389
|
+
/** Commit a keystroke after this long; `0` means only on change. */
|
|
390
|
+
debounceMs: number;
|
|
391
|
+
/** Item-property schemas, for an array-of-objects field. */
|
|
392
|
+
itemProps?: Record<string, JsonSchema> | undefined;
|
|
393
|
+
/** The rows currently held, so a cell edit can rebuild the array. */
|
|
394
|
+
rows?: Record<string, unknown>[] | undefined;
|
|
395
|
+
/** The ladder, when this position offers one. */
|
|
396
|
+
ladder?: { fieldKey: string; mode: SlotMode; offered: SlotMode[]; sources: SignalOption[] };
|
|
397
|
+
/** A registered control owns the whole field; args are rebuilt for it on every repaint. */
|
|
398
|
+
control?: SchemaFormMountedControl | undefined;
|
|
399
|
+
/**
|
|
400
|
+
* Registered controls owning one cell, keyed by the cell's host id.
|
|
401
|
+
*
|
|
402
|
+
* The same kind of entry a field's own control is, and reached by the same dispatch: a cell host
|
|
403
|
+
* is a host like any other. The one control a cell is ever given is `reference` (see
|
|
404
|
+
* {@link deriveCell}).
|
|
405
|
+
*/
|
|
406
|
+
cellControls?: Map<
|
|
407
|
+
string,
|
|
408
|
+
{ control: SchemaFormMountedControl; schema: JsonSchema; row: string; key: string }
|
|
409
|
+
>;
|
|
410
|
+
/**
|
|
411
|
+
* Cells that currently hold a pointer, by cell id.
|
|
412
|
+
*
|
|
413
|
+
* A cell edits the pointer STRING it is showing, so what it commits has to go back as `{ $ref }`
|
|
414
|
+
* — which the item property's declared type cannot say, because the property is typed as what the
|
|
415
|
+
* pointer resolves to.
|
|
416
|
+
*/
|
|
417
|
+
refCells?: Set<string>;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The debounce a JSON editor gets: long, because a half-typed object is not a refusal. */
|
|
421
|
+
const JSON_COMMIT_MS = 500;
|
|
422
|
+
/** The debounce a scalar gets, so the canvas follows a value being typed. */
|
|
423
|
+
const TEXT_COMMIT_MS = 400;
|
|
424
|
+
|
|
425
|
+
// ─── The controller ──────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
interface FormController {
|
|
428
|
+
readonly host: HTMLElement;
|
|
429
|
+
readonly update: (
|
|
430
|
+
schema: JsonSchema,
|
|
431
|
+
value: Record<string, unknown>,
|
|
432
|
+
opts: RenderFormOptions,
|
|
433
|
+
) => void;
|
|
434
|
+
readonly dispose: () => void;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Every standing form, by the key its caller named it with. */
|
|
438
|
+
const forms = new Map<string, FormController>();
|
|
439
|
+
|
|
440
|
+
/** Test hook: take every standing form down, so one test's mount never outlives it. */
|
|
441
|
+
export function resetSchemaForms(): void {
|
|
442
|
+
for (const controller of forms.values()) {
|
|
443
|
+
controller.dispose();
|
|
444
|
+
}
|
|
445
|
+
forms.clear();
|
|
446
|
+
}
|
|
659
447
|
|
|
660
448
|
/**
|
|
661
|
-
*
|
|
662
|
-
* as single-key patches (`undefined` values mean "unset the key").
|
|
449
|
+
* Mount (or update) a schema-driven form and hand back the element it lives in.
|
|
663
450
|
*
|
|
451
|
+
* @param {string} formKey Stable identity for this form — the same key updates the standing mount
|
|
452
|
+
* instead of building a second one, which is what keeps the caret in a field across a repaint.
|
|
664
453
|
* @param {JsonSchema} schema
|
|
665
|
-
* @param {Record<string, unknown>} value
|
|
454
|
+
* @param {Record<string, unknown>} value The record being edited
|
|
666
455
|
* @param {RenderFormOptions} opts
|
|
667
|
-
* @returns {
|
|
456
|
+
* @returns {HTMLElement} The form's own host, for a lit caller to place in its tree
|
|
668
457
|
*/
|
|
669
|
-
export function
|
|
458
|
+
export function mountSchemaForm(
|
|
459
|
+
formKey: string,
|
|
670
460
|
schema: JsonSchema,
|
|
671
461
|
value: Record<string, unknown>,
|
|
672
462
|
opts: RenderFormOptions,
|
|
673
|
-
):
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
463
|
+
): HTMLElement {
|
|
464
|
+
sweep(formKey);
|
|
465
|
+
let controller = forms.get(formKey);
|
|
466
|
+
if (!controller) {
|
|
467
|
+
controller = createController();
|
|
468
|
+
forms.set(formKey, controller);
|
|
469
|
+
}
|
|
470
|
+
controller.update(schema, value, opts);
|
|
471
|
+
return controller.host;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Drop forms whose host has been taken out of the page.
|
|
476
|
+
*
|
|
477
|
+
* A host is created here and inserted by its caller one lit render later, so "not connected" is the
|
|
478
|
+
* normal state of the form being mounted right now — which is why the key being asked for is exempt
|
|
479
|
+
* rather than the sweep being conditional on a count.
|
|
480
|
+
*/
|
|
481
|
+
function sweep(exceptKey: string): void {
|
|
482
|
+
for (const [key, controller] of forms) {
|
|
483
|
+
if (key !== exceptKey && !controller.host.isConnected) {
|
|
484
|
+
controller.dispose();
|
|
485
|
+
forms.delete(key);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function createController(): FormController {
|
|
491
|
+
let schema: JsonSchema = {};
|
|
492
|
+
let value: Record<string, unknown> = {};
|
|
493
|
+
let opts: RenderFormOptions = { onChange: () => {} };
|
|
494
|
+
let ctx: SchemaFormContext = NULL_FORM_CONTEXT;
|
|
495
|
+
const plans = new Map<string, FieldPlan>();
|
|
496
|
+
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
497
|
+
const controlHosts = new Map<string, HTMLElement>();
|
|
498
|
+
/** Standing mounted controls, by host id, with the element each was mounted into. */
|
|
499
|
+
const mountedControls = new Map<string, { host: HTMLElement; handle: SchemaFormControlHandle }>();
|
|
500
|
+
/** The rung picker, while one is up. One at a time, and it goes down with the form. */
|
|
501
|
+
let sourceMenu: MenuHandle | null = null;
|
|
502
|
+
|
|
503
|
+
const later = (key: string, ms: number, run: () => void): void => {
|
|
504
|
+
clearTimeout(timers.get(key));
|
|
505
|
+
if (ms <= 0) {
|
|
506
|
+
run();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
timers.set(key, setTimeout(run, ms));
|
|
510
|
+
};
|
|
511
|
+
const now = (key: string, run: () => void): void => {
|
|
512
|
+
clearTimeout(timers.get(key));
|
|
513
|
+
timers.delete(key);
|
|
514
|
+
run();
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const patch = (prop: string, next: unknown): void => {
|
|
518
|
+
opts.onChange({ [prop]: next });
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
/** Turn a committed string into the document value this plan says it is. */
|
|
522
|
+
const write = (plan: FieldPlan, raw: string): void => {
|
|
523
|
+
if (plan.commit === "none") {
|
|
524
|
+
// The value is not a string this module writes: a control or a row editor owns it.
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
switch (plan.commit) {
|
|
528
|
+
case "ref": {
|
|
529
|
+
const trimmed = raw.trim();
|
|
530
|
+
patch(plan.prop, trimmed ? { $ref: trimmed } : undefined);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
case "template": {
|
|
534
|
+
patch(plan.prop, raw);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
case "select": {
|
|
538
|
+
patch(plan.prop, raw === NONE ? undefined : raw);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
case "number": {
|
|
542
|
+
const parsed = parseNumericField(raw, plan.schema.type === "integer");
|
|
543
|
+
patch(plan.prop, Number.isNaN(parsed) ? undefined : parsed);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
case "json": {
|
|
547
|
+
try {
|
|
548
|
+
patch(plan.prop, JSON.parse(raw) as unknown);
|
|
549
|
+
} catch {
|
|
550
|
+
/* A half-typed object is not a refusal: the last parsable text is what the document
|
|
551
|
+
keeps, and the field goes on showing what is actually in it. */
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
default: {
|
|
556
|
+
patch(plan.prop, raw || undefined);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/** The field's items, each shallow-copied so a cell edit never writes through to the document. */
|
|
562
|
+
const cellRows = (plan: FieldPlan): Record<string, unknown>[] => {
|
|
563
|
+
const copies: Record<string, unknown>[] = [];
|
|
564
|
+
for (const item of plan.rows ?? []) {
|
|
565
|
+
copies.push({ ...item });
|
|
566
|
+
}
|
|
567
|
+
return copies;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const actions = {
|
|
571
|
+
addRow: (field: string) => {
|
|
572
|
+
const plan = plans.get(field);
|
|
573
|
+
if (!plan?.itemProps) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const seed: Record<string, unknown> = {};
|
|
577
|
+
for (const [k, v] of Object.entries(plan.itemProps)) {
|
|
578
|
+
if (v.default !== undefined) {
|
|
579
|
+
seed[k] = v.default;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
patch(field, [...cellRows(plan), seed]);
|
|
583
|
+
opts.rerender?.();
|
|
584
|
+
},
|
|
585
|
+
commit: (key: string, raw: string) => {
|
|
586
|
+
const plan = plans.get(key);
|
|
587
|
+
if (plan) {
|
|
588
|
+
now(key, () => write(plan, raw));
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
commitChecked: (key: string, checked: boolean) => {
|
|
592
|
+
const plan = plans.get(key);
|
|
593
|
+
if (plan) {
|
|
594
|
+
patch(plan.prop, checked);
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
edit: (key: string, raw: string) => {
|
|
598
|
+
const plan = plans.get(key);
|
|
599
|
+
if (plan && plan.debounceMs > 0) {
|
|
600
|
+
later(key, plan.debounceMs, () => write(plan, raw));
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
editCell: (field: string, row: string, cell: string, raw: string) => {
|
|
604
|
+
writeCell(field, row, cell, raw);
|
|
605
|
+
},
|
|
606
|
+
editCellChecked: (field: string, row: string, cell: string, checked: boolean) => {
|
|
607
|
+
writeCell(field, row, cell, checked);
|
|
608
|
+
},
|
|
609
|
+
editJson: (key: string, raw: string) => {
|
|
610
|
+
const plan = plans.get(key);
|
|
611
|
+
if (plan) {
|
|
612
|
+
later(key, JSON_COMMIT_MS, () => write(plan, raw));
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
pickSource: (key: string, anchor: HTMLElement) => {
|
|
616
|
+
openSourceMenu(key, anchor);
|
|
617
|
+
},
|
|
618
|
+
removeRow: (field: string, row: string) => {
|
|
619
|
+
const plan = plans.get(field);
|
|
620
|
+
if (!plan) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const idx = Number(row);
|
|
624
|
+
const kept = cellRows(plan).filter((_, i) => i !== idx);
|
|
625
|
+
patch(field, kept.length > 0 ? kept : undefined);
|
|
626
|
+
opts.rerender?.();
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
function writeCell(field: string, row: string, cell: string, next: unknown): void {
|
|
631
|
+
const plan = plans.get(field);
|
|
632
|
+
if (!plan?.itemProps) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const idx = Number(row);
|
|
636
|
+
const rows = cellRows(plan);
|
|
637
|
+
const target = rows[idx];
|
|
638
|
+
if (!target) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const cellSchema = plan.itemProps[cell];
|
|
642
|
+
rows[idx] = {
|
|
643
|
+
...target,
|
|
644
|
+
[cell]: coerceCell(cellSchema, next, plan.refCells?.has(`${field}/${row}/${cell}`) ?? false),
|
|
645
|
+
};
|
|
646
|
+
patch(field, rows);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** A cell's committed string, as the item property's declared type. */
|
|
650
|
+
function coerceCell(
|
|
651
|
+
cellSchema: JsonSchema | undefined,
|
|
652
|
+
next: unknown,
|
|
653
|
+
isPointer: boolean,
|
|
654
|
+
): unknown {
|
|
655
|
+
if (typeof next === "boolean") {
|
|
656
|
+
return next;
|
|
657
|
+
}
|
|
658
|
+
const raw = String(next);
|
|
659
|
+
if (isPointer) {
|
|
660
|
+
const trimmed = raw.trim();
|
|
661
|
+
return trimmed ? { $ref: trimmed } : undefined;
|
|
662
|
+
}
|
|
663
|
+
if (cellSchema?.type === "integer" || cellSchema?.type === "number") {
|
|
664
|
+
const parsed = parseNumericField(raw, cellSchema.type === "integer");
|
|
665
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
666
|
+
}
|
|
667
|
+
if (raw === NONE || raw === "") {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
return raw;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** The rung picker: every source this position permits, one action away (§6.3). */
|
|
674
|
+
function openSourceMenu(key: string, anchor: HTMLElement): void {
|
|
675
|
+
const plan = plans.get(key);
|
|
676
|
+
if (!plan?.ladder) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const { fieldKey, mode, offered, sources } = plan.ladder;
|
|
680
|
+
sourceMenu?.close();
|
|
681
|
+
sourceMenu = openMenu({
|
|
682
|
+
label: "Value source",
|
|
683
|
+
onClosed: () => {
|
|
684
|
+
sourceMenu = null;
|
|
685
|
+
},
|
|
686
|
+
opener: anchor,
|
|
687
|
+
region: "value-source",
|
|
688
|
+
rows: offered.map((rung) => ({
|
|
689
|
+
checked: (rung === mode ? "true" : "false") as "true" | "false",
|
|
690
|
+
destructive: false,
|
|
691
|
+
disabled: false,
|
|
692
|
+
dividerAbove: false,
|
|
693
|
+
id: rung,
|
|
694
|
+
title: VALUE_SOURCE_LABELS[rung],
|
|
695
|
+
})),
|
|
696
|
+
run: (id) => {
|
|
697
|
+
const next = id as SlotMode;
|
|
698
|
+
if (next === mode) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
/* Leaving From data… seeds an empty Fixed value stash with the property's declared default,
|
|
702
|
+
so unbind-restores-default survives a detour through Mixed text. */
|
|
703
|
+
const literalDefault = plan.schema.default as JsonValue | undefined;
|
|
704
|
+
if (
|
|
705
|
+
mode === "ref" &&
|
|
706
|
+
literalDefault !== undefined &&
|
|
707
|
+
!hasStashedSlotValue(fieldKey, "literal")
|
|
708
|
+
) {
|
|
709
|
+
stashSlotValue(fieldKey, "literal", literalDefault);
|
|
710
|
+
}
|
|
711
|
+
patch(
|
|
712
|
+
plan.prop,
|
|
713
|
+
switchSlotMode(
|
|
714
|
+
fieldKey,
|
|
715
|
+
mode,
|
|
716
|
+
next,
|
|
717
|
+
cloneValue(value[plan.prop] as JsonValue | undefined),
|
|
718
|
+
slotModeSeed(next, { extraSignals: sources, literalDefault, stateDefs: [] }),
|
|
719
|
+
),
|
|
720
|
+
);
|
|
721
|
+
},
|
|
710
722
|
});
|
|
711
|
-
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* The control a host id names, and the arguments it is drawn from right now.
|
|
727
|
+
*
|
|
728
|
+
* One answer for both kinds of host, because a cell host is a host: a field's id is its property
|
|
729
|
+
* and a cell's is its `field/row/property` triple, and what differs between them is only where
|
|
730
|
+
* the value comes from and where a commit goes.
|
|
731
|
+
*/
|
|
732
|
+
function controlFor(
|
|
733
|
+
id: string,
|
|
734
|
+
): { control: SchemaFormMountedControl; args: SchemaFormControlArgs } | undefined {
|
|
735
|
+
const plan = plans.get(id);
|
|
736
|
+
if (plan?.control) {
|
|
737
|
+
return {
|
|
738
|
+
args: {
|
|
739
|
+
ctx,
|
|
740
|
+
key: plan.prop,
|
|
741
|
+
onChange: (next) => patch(plan.prop, next),
|
|
742
|
+
rerender: opts.rerender,
|
|
743
|
+
schema: plan.schema,
|
|
744
|
+
value: value[plan.prop],
|
|
745
|
+
},
|
|
746
|
+
control: plan.control,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
for (const owner of plans.values()) {
|
|
750
|
+
const entry = owner.cellControls?.get(id);
|
|
751
|
+
if (!entry) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const row = (owner.rows ?? [])[Number(entry.row)] ?? {};
|
|
755
|
+
return {
|
|
756
|
+
args: {
|
|
757
|
+
ctx,
|
|
758
|
+
key: entry.key,
|
|
759
|
+
onChange: (next) => writeCell(owner.prop, entry.row, entry.key, next),
|
|
760
|
+
rerender: opts.rerender,
|
|
761
|
+
schema: entry.schema,
|
|
762
|
+
value: row[entry.key],
|
|
763
|
+
},
|
|
764
|
+
control: entry.control,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Draw one registered control into the host the document announced for it.
|
|
772
|
+
*
|
|
773
|
+
* Connectedness is deliberately NOT consulted. A host is announced as it is CREATED, which is one
|
|
774
|
+
* reconcile step before it is in the page — so "is it connected" answers no for exactly the node
|
|
775
|
+
* that needs drawing, and a check there is how the control came out empty on first mount.
|
|
776
|
+
*/
|
|
777
|
+
function paintControl(id: string, host: HTMLElement): void {
|
|
778
|
+
const found = controlFor(id);
|
|
779
|
+
if (!found) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
/* A control is built once per host and updated after that. The host element is what identifies
|
|
783
|
+
it: the reconciler may replace the node, and a handle left pointing at the old one would go
|
|
784
|
+
on updating a document nobody can see. */
|
|
785
|
+
const standing = mountedControls.get(id);
|
|
786
|
+
if (standing?.host === host) {
|
|
787
|
+
standing.handle.update(found.args);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
standing?.handle.dispose();
|
|
791
|
+
mountedControls.set(id, { handle: found.control.mount(host, found.args), host });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Take down a mounted control whose field is no longer one — or is no longer here at all. */
|
|
795
|
+
function sweepMountedControls(): void {
|
|
796
|
+
for (const [id, standing] of mountedControls) {
|
|
797
|
+
if (!controlFor(id)) {
|
|
798
|
+
standing.handle.dispose();
|
|
799
|
+
mountedControls.delete(id);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Redraw every control this form has been handed a host for. */
|
|
805
|
+
function paintControls(): void {
|
|
806
|
+
sweepMountedControls();
|
|
807
|
+
for (const [id, host] of controlHosts) {
|
|
808
|
+
paintControl(id, host);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const surface: SchemaFormSurface = createSchemaFormSurface(
|
|
813
|
+
{ fields: [] },
|
|
814
|
+
actions,
|
|
815
|
+
(id, host) => {
|
|
816
|
+
controlHosts.set(id, host);
|
|
817
|
+
paintControl(id, host);
|
|
818
|
+
},
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
dispose() {
|
|
823
|
+
for (const timer of timers.values()) {
|
|
824
|
+
clearTimeout(timer);
|
|
825
|
+
}
|
|
826
|
+
timers.clear();
|
|
827
|
+
sourceMenu?.close();
|
|
828
|
+
sourceMenu = null;
|
|
829
|
+
for (const standing of mountedControls.values()) {
|
|
830
|
+
standing.handle.dispose();
|
|
831
|
+
}
|
|
832
|
+
mountedControls.clear();
|
|
833
|
+
controlHosts.clear();
|
|
834
|
+
plans.clear();
|
|
835
|
+
surface.dispose();
|
|
836
|
+
},
|
|
837
|
+
host: surface.host,
|
|
838
|
+
update(nextSchema, nextValue, nextOpts) {
|
|
839
|
+
schema = nextSchema;
|
|
840
|
+
value = nextValue;
|
|
841
|
+
opts = nextOpts;
|
|
842
|
+
ctx = nextOpts.context ?? NULL_FORM_CONTEXT;
|
|
843
|
+
plans.clear();
|
|
844
|
+
surface.update({ fields: deriveFields(schema, value, opts, ctx, plans) });
|
|
845
|
+
paintControls();
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ─── Derivation: one schema property becomes one already-decided row ─────────
|
|
851
|
+
|
|
852
|
+
/** Every row the document draws, and the plan that reads each one's gestures back. */
|
|
853
|
+
function deriveFields(
|
|
854
|
+
schema: JsonSchema,
|
|
855
|
+
value: Record<string, unknown>,
|
|
856
|
+
opts: RenderFormOptions,
|
|
857
|
+
ctx: SchemaFormContext,
|
|
858
|
+
plans: Map<string, FieldPlan>,
|
|
859
|
+
): SchemaFormFieldView[] {
|
|
860
|
+
const required = new Set(schema.required);
|
|
861
|
+
const sources = refSourcesFor(ctx);
|
|
862
|
+
return Object.entries(schema.properties ?? {}).map(([prop, ps]) =>
|
|
863
|
+
deriveField(prop, ps, {
|
|
864
|
+
ctx,
|
|
865
|
+
opts,
|
|
866
|
+
plans,
|
|
867
|
+
required: required.has(prop),
|
|
868
|
+
sources,
|
|
869
|
+
value,
|
|
870
|
+
}),
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
interface DeriveArgs {
|
|
875
|
+
ctx: SchemaFormContext;
|
|
876
|
+
opts: RenderFormOptions;
|
|
877
|
+
plans: Map<string, FieldPlan>;
|
|
878
|
+
required: boolean;
|
|
879
|
+
sources: SignalOption[];
|
|
880
|
+
value: Record<string, unknown>;
|
|
881
|
+
}
|
|
712
882
|
|
|
713
|
-
|
|
883
|
+
function deriveField(prop: string, ps: JsonSchema, args: DeriveArgs): SchemaFormFieldView {
|
|
884
|
+
const { ctx, opts, plans, required, sources, value } = args;
|
|
885
|
+
const current = value[prop];
|
|
886
|
+
const plan: FieldPlan = { commit: "text", debounceMs: TEXT_COMMIT_MS, prop, schema: ps };
|
|
887
|
+
plans.set(prop, plan);
|
|
888
|
+
|
|
889
|
+
// Host diagnostics win over the intrinsic check — see RenderFormOptions.errors.
|
|
890
|
+
const error =
|
|
891
|
+
opts.errors?.[prop] || validateFieldValue(ps, current, Boolean(opts.showRequired) && required);
|
|
892
|
+
const count = opts.errorCounts?.[prop];
|
|
893
|
+
|
|
894
|
+
/* A `ui.control` override owns its whole field — the secret control writes an env-var NAME rather
|
|
895
|
+
than the value it was handed, so a rung switch above it would be editing a different thing than
|
|
896
|
+
the one on screen. */
|
|
897
|
+
const overrideName = opts.ui?.[prop]?.control;
|
|
898
|
+
const override = overrideName ? controlRegistry.get(overrideName) : undefined;
|
|
899
|
+
const laddered = sources.length > 0 && isBindableField(ps) && !overrideName;
|
|
900
|
+
|
|
901
|
+
const row: SchemaFormFieldView = {
|
|
902
|
+
addLabel: `Add ${prop}`,
|
|
903
|
+
checked: false,
|
|
904
|
+
chips: [],
|
|
905
|
+
description: ps.description ?? "",
|
|
906
|
+
error,
|
|
907
|
+
errorCount: count !== undefined && count > 1 ? `×${count}` : "",
|
|
908
|
+
hasChip: false,
|
|
909
|
+
hasChips: false,
|
|
910
|
+
hasCount: count !== undefined && count > 1,
|
|
911
|
+
hasError: error !== "",
|
|
912
|
+
hasOptions: false,
|
|
913
|
+
invalid: error !== "",
|
|
914
|
+
key: prop,
|
|
915
|
+
kind: "text",
|
|
916
|
+
label: prop,
|
|
917
|
+
max: "",
|
|
918
|
+
min: "",
|
|
919
|
+
mono: false,
|
|
920
|
+
multiline: false,
|
|
921
|
+
options: [],
|
|
922
|
+
placeholder: "",
|
|
923
|
+
prop: ps.name || prop,
|
|
924
|
+
required,
|
|
925
|
+
rows: [],
|
|
926
|
+
source: "fixed",
|
|
927
|
+
sourceHint: "",
|
|
928
|
+
sourceLabel: "",
|
|
929
|
+
sourceLocked: false,
|
|
930
|
+
sourceName: "",
|
|
931
|
+
step: "",
|
|
932
|
+
value: "",
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
if (laddered) {
|
|
936
|
+
const fieldKey = `${ctx.fieldKeyPrefix ?? ""}.${prop}`;
|
|
937
|
+
const mode = effectiveSlotMode(fieldKey, current);
|
|
938
|
+
/* A From data… rung with nothing to point at is a dead end — but this engine only ladders a
|
|
939
|
+
field once the host has named a source, so the rung always has something in it. */
|
|
940
|
+
const offered = slotCaps({ schema: configFieldSchema(ps) });
|
|
941
|
+
plan.ladder = { fieldKey, mode, offered, sources };
|
|
942
|
+
row.hasChip = true;
|
|
943
|
+
row.sourceLabel = VALUE_SOURCE_LABELS[mode];
|
|
944
|
+
row.sourceHint = VALUE_SOURCE_HINTS[mode];
|
|
945
|
+
row.source = mode === "literal" ? "fixed" : "bound";
|
|
946
|
+
row.sourceLocked = !offered.some((m) => m !== mode);
|
|
947
|
+
row.sourceName = row.sourceLocked
|
|
948
|
+
? `Value source: ${row.sourceLabel} (no other source available here)`
|
|
949
|
+
: `Value source: ${row.sourceLabel} — click to change`;
|
|
950
|
+
if (mode === "ref") {
|
|
951
|
+
plan.commit = "ref";
|
|
952
|
+
plan.debounceMs = 0;
|
|
953
|
+
row.kind = "pointer";
|
|
954
|
+
row.value = isRef(current) ? current.$ref : "";
|
|
955
|
+
row.options = sources.map((s) => ({ label: s.label, value: s.value }));
|
|
956
|
+
row.hasOptions = row.options.length > 0;
|
|
957
|
+
return row;
|
|
958
|
+
}
|
|
959
|
+
if (mode === "template") {
|
|
960
|
+
plan.commit = "template";
|
|
961
|
+
plan.debounceMs = 0;
|
|
962
|
+
row.kind = "template";
|
|
963
|
+
row.value = String(current ?? "");
|
|
964
|
+
return row;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (override) {
|
|
969
|
+
plan.commit = "none";
|
|
970
|
+
plan.control = override;
|
|
971
|
+
row.kind = "control";
|
|
972
|
+
return row;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/* A ref left in a form whose host named nowhere to bind: no ladder is drawn, so the pointer is
|
|
976
|
+
edited as the string it is rather than rendered as "[object Object]" in a typed widget. */
|
|
977
|
+
if (isRef(current) && isBindableField(ps)) {
|
|
978
|
+
plan.commit = "ref";
|
|
979
|
+
plan.debounceMs = 0;
|
|
980
|
+
row.kind = "text";
|
|
981
|
+
row.mono = true;
|
|
982
|
+
row.value = current.$ref;
|
|
983
|
+
row.placeholder = prop;
|
|
984
|
+
return row;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/* A relationship to another collection (`$ref: "#/content/<type>"`) is the registered `reference`
|
|
988
|
+
control, wherever the form is drawn — §9.2's "one picker" is this dispatch plus the single
|
|
989
|
+
`registerFormControl("reference", …)` in `ui/form-controls.ts`. It is deliberately NOT an enum:
|
|
990
|
+
the choices are entry files on disk, so they are read asynchronously and can be stale, and a
|
|
991
|
+
schema `enum` is a closed set the document itself declares. When the control is not registered
|
|
992
|
+
(a bare-Bun import of this engine), the field falls through to the plain text control below. */
|
|
993
|
+
if (referenceTarget(ps) !== null) {
|
|
994
|
+
const reference = controlRegistry.get("reference");
|
|
995
|
+
if (reference) {
|
|
996
|
+
plan.commit = "none";
|
|
997
|
+
plan.control = reference;
|
|
998
|
+
row.kind = "control";
|
|
999
|
+
return row;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const enumValues = resolveFormEnum(opts.ui?.[prop]?.enum ?? ps.enum, ctx, value);
|
|
1004
|
+
if (enumValues) {
|
|
1005
|
+
plan.commit = "select";
|
|
1006
|
+
plan.debounceMs = 0;
|
|
1007
|
+
row.kind = "select";
|
|
1008
|
+
row.value =
|
|
1009
|
+
current !== undefined
|
|
1010
|
+
? String(current)
|
|
1011
|
+
: ps.default !== undefined
|
|
1012
|
+
? String(ps.default)
|
|
1013
|
+
: NONE;
|
|
1014
|
+
row.options = [
|
|
1015
|
+
...(required ? [] : [{ label: "—", value: NONE }]),
|
|
1016
|
+
...enumValues.map((v) => ({ label: v, value: v })),
|
|
1017
|
+
];
|
|
1018
|
+
return row;
|
|
1019
|
+
}
|
|
1020
|
+
if (ps.type === "boolean") {
|
|
1021
|
+
plan.commit = "none";
|
|
1022
|
+
row.kind = "checkbox";
|
|
1023
|
+
row.checked = Boolean(current ?? ps.default ?? false);
|
|
1024
|
+
return row;
|
|
1025
|
+
}
|
|
1026
|
+
if (ps.type === "integer" || ps.type === "number") {
|
|
1027
|
+
plan.commit = "number";
|
|
1028
|
+
plan.debounceMs = 0;
|
|
1029
|
+
row.kind = "number";
|
|
1030
|
+
row.value = current !== undefined ? String(current) : "";
|
|
1031
|
+
row.placeholder = ps.default != null ? String(ps.default) : "";
|
|
1032
|
+
row.min = ps.minimum !== undefined ? String(ps.minimum) : "";
|
|
1033
|
+
row.max = ps.maximum !== undefined ? String(ps.maximum) : "";
|
|
1034
|
+
row.step = ps.type === "integer" ? "1" : "";
|
|
1035
|
+
return row;
|
|
1036
|
+
}
|
|
1037
|
+
if (ps.format === "json-schema") {
|
|
1038
|
+
plan.commit = "json";
|
|
1039
|
+
row.kind = "json";
|
|
1040
|
+
row.value = current !== undefined ? JSON.stringify(current, null, 2) : "";
|
|
1041
|
+
row.placeholder = ps.description ?? "JSON Schema defining the data shape…";
|
|
1042
|
+
row.chips = shapeChips(current);
|
|
1043
|
+
row.hasChips = row.chips.length > 0;
|
|
1044
|
+
return row;
|
|
1045
|
+
}
|
|
1046
|
+
if (ps.type === "array" && ps.items?.type === "object" && ps.items.properties) {
|
|
1047
|
+
return arrayOfObjects(prop, ps.items.properties, args, plan, row);
|
|
1048
|
+
}
|
|
1049
|
+
if (ps.type === "array" || ps.type === "object") {
|
|
1050
|
+
plan.commit = "json";
|
|
1051
|
+
row.kind = "json";
|
|
1052
|
+
row.value = current !== undefined ? JSON.stringify(current, null, 2) : "";
|
|
1053
|
+
row.placeholder = ps.default !== undefined ? JSON.stringify(ps.default) : "";
|
|
1054
|
+
return row;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
row.kind = "text";
|
|
1058
|
+
row.value = asText(current);
|
|
1059
|
+
row.placeholder = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
|
|
1060
|
+
return row;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** The property chips above a JSON-Schema field: what shape is in there right now. */
|
|
1064
|
+
function shapeChips(current: unknown): { key: string; label: string }[] {
|
|
1065
|
+
if (!current || typeof current !== "object") {
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
const held = current as Record<string, unknown>;
|
|
1069
|
+
if (held.$ref || typeof held.properties !== "object" || held.properties === null) {
|
|
1070
|
+
return [];
|
|
1071
|
+
}
|
|
1072
|
+
return Object.entries(held.properties as Record<string, Record<string, unknown>>).map(
|
|
1073
|
+
([key, spec]) => ({ key, label: `${key}: ${String(spec.type ?? "any")}` }),
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** An array whose items have a declared shape: one row per item, one cell per property. */
|
|
1078
|
+
function arrayOfObjects(
|
|
1079
|
+
prop: string,
|
|
1080
|
+
itemProps: Record<string, JsonSchema>,
|
|
1081
|
+
args: DeriveArgs,
|
|
1082
|
+
plan: FieldPlan,
|
|
1083
|
+
row: SchemaFormFieldView,
|
|
1084
|
+
): SchemaFormFieldView {
|
|
1085
|
+
const held = args.value[prop];
|
|
1086
|
+
const rows = Array.isArray(held) ? (held as Record<string, unknown>[]) : [];
|
|
1087
|
+
plan.commit = "none";
|
|
1088
|
+
plan.itemProps = itemProps;
|
|
1089
|
+
plan.rows = rows;
|
|
1090
|
+
plan.cellControls = new Map();
|
|
1091
|
+
plan.refCells = new Set();
|
|
1092
|
+
row.kind = "rows";
|
|
1093
|
+
row.rows = rows.map((item, idx) => deriveRow(prop, String(idx), item, itemProps, args, plan));
|
|
1094
|
+
return row;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function deriveRow(
|
|
1098
|
+
prop: string,
|
|
1099
|
+
key: string,
|
|
1100
|
+
item: Record<string, unknown>,
|
|
1101
|
+
itemProps: Record<string, JsonSchema>,
|
|
1102
|
+
args: DeriveArgs,
|
|
1103
|
+
plan: FieldPlan,
|
|
1104
|
+
): SchemaFormRowView {
|
|
1105
|
+
return {
|
|
1106
|
+
cells: Object.entries(itemProps).map(([cellKey, cellSchema]) =>
|
|
1107
|
+
deriveCell(prop, key, cellKey, cellSchema, item[cellKey], args, plan),
|
|
1108
|
+
),
|
|
1109
|
+
field: prop,
|
|
1110
|
+
key,
|
|
1111
|
+
removeLabel: `Remove ${prop} ${Number(key) + 1}`,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function deriveCell(
|
|
1116
|
+
prop: string,
|
|
1117
|
+
rowKey: string,
|
|
1118
|
+
cellKey: string,
|
|
1119
|
+
cellSchema: JsonSchema,
|
|
1120
|
+
held: unknown,
|
|
1121
|
+
args: DeriveArgs,
|
|
1122
|
+
plan: FieldPlan,
|
|
1123
|
+
): SchemaFormCellView {
|
|
1124
|
+
const id = `${prop}/${rowKey}/${cellKey}`;
|
|
1125
|
+
const cell: SchemaFormCellView = {
|
|
1126
|
+
checked: false,
|
|
1127
|
+
field: prop,
|
|
1128
|
+
id,
|
|
1129
|
+
key: cellKey,
|
|
1130
|
+
kind: "text",
|
|
1131
|
+
label: cellKey,
|
|
1132
|
+
options: [],
|
|
1133
|
+
row: rowKey,
|
|
1134
|
+
value: "",
|
|
1135
|
+
};
|
|
1136
|
+
if (isRef(held)) {
|
|
1137
|
+
cell.value = held.$ref;
|
|
1138
|
+
plan.refCells?.add(id);
|
|
1139
|
+
return cell;
|
|
1140
|
+
}
|
|
1141
|
+
if (referenceTarget(cellSchema) !== null) {
|
|
1142
|
+
/* A CELL's control is mounted into a host of the cell's own, exactly as a field's is: the one
|
|
1143
|
+
control a cell is ever given is the reference picker, and that is a document now
|
|
1144
|
+
(`surfaces/reference-field.json`). What the cell host must never become is a second writer of
|
|
1145
|
+
a node the document already owns. */
|
|
1146
|
+
const reference = controlRegistry.get("reference");
|
|
1147
|
+
if (reference) {
|
|
1148
|
+
cell.kind = "control";
|
|
1149
|
+
plan.cellControls?.set(id, {
|
|
1150
|
+
control: reference,
|
|
1151
|
+
key: cellKey,
|
|
1152
|
+
row: rowKey,
|
|
1153
|
+
schema: cellSchema,
|
|
1154
|
+
});
|
|
1155
|
+
return cell;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const enumValues = resolveFormEnum(cellSchema.enum, args.ctx, args.value);
|
|
1159
|
+
if (enumValues) {
|
|
1160
|
+
cell.kind = "select";
|
|
1161
|
+
cell.value = held !== undefined ? String(held) : NONE;
|
|
1162
|
+
cell.options = [
|
|
1163
|
+
{ label: "—", value: NONE },
|
|
1164
|
+
...enumValues.map((v: string): SchemaFormOption => ({ label: v, value: v })),
|
|
1165
|
+
];
|
|
1166
|
+
return cell;
|
|
1167
|
+
}
|
|
1168
|
+
if (cellSchema.type === "boolean") {
|
|
1169
|
+
cell.kind = "checkbox";
|
|
1170
|
+
cell.checked = Boolean(held);
|
|
1171
|
+
return cell;
|
|
1172
|
+
}
|
|
1173
|
+
if (cellSchema.type === "integer" || cellSchema.type === "number") {
|
|
1174
|
+
cell.kind = "number";
|
|
1175
|
+
cell.value = held !== undefined ? String(held) : "";
|
|
1176
|
+
return cell;
|
|
1177
|
+
}
|
|
1178
|
+
cell.value = asText(held);
|
|
1179
|
+
return cell;
|
|
714
1180
|
}
|