@jxsuite/studio 3.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
- package/dist/chunks/Overlay.dev-etqjwyf9.js +23 -0
- package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
- package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-48xg86wj.js +49 -0
- package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-43tfn7cs.js} +33 -25
- package/dist/chunks/entry-commands-e63f1xc0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-txv08xxg.js} +34 -26
- package/dist/chunks/file-ops-wbh4hhsx.js +48 -0
- package/dist/chunks/files-3kfkwhk4.js +93 -0
- package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
- package/dist/chunks/{format-host-8fr3545e.js → format-host-w16q81tf.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-532whr1y.js} +22 -16
- package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8er6gbxy.js} +6 -4
- package/dist/chunks/layers-panel-07hm7g5j.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-vx9bhp1d.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-v9st96ja.js +29 -0
- package/dist/chunks/quick-search-zccjp6gv.js +66 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-vm3b6kwy.js +46 -0
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-wdt9y92a.js → studio-0bq6eh7v.js} +2 -2
- package/dist/chunks/studio-1ngbejft.js +181 -0
- package/dist/chunks/studio-1wagb550.js +118 -0
- package/dist/chunks/{studio-j595e748.js → studio-2b946z26.js} +172 -95
- package/dist/chunks/{studio-pkp61mh8.js → studio-3bqpr0zh.js} +25 -17
- package/dist/chunks/{studio-k4jvjcfx.js → studio-4b5z1x43.js} +46 -12
- package/dist/chunks/{studio-ry1vz5sv.js → studio-4n2b5gzb.js} +15 -13
- package/dist/chunks/{studio-p5dja96s.js → studio-4r1pv6zf.js} +2 -2
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-tzx3xmvx.js → studio-5fx3g9jg.js} +33 -52
- package/dist/chunks/{studio-rh1cnamv.js → studio-5myknm0a.js} +565 -135
- package/dist/chunks/studio-6yw4y3my.js +91 -0
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-6z5eyjwn.js → studio-950k0dx6.js} +72 -135
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-hcxzyk3x.js → studio-ahn3pdgx.js} +6 -6
- package/dist/chunks/studio-b55y6ewx.js +498 -0
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-dc8s8heb.js → studio-cmb03bg5.js} +6904 -8321
- package/dist/chunks/studio-dm19q43r.js +195 -0
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ef49pnzx.js} +11 -9
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-5efrtn1m.js → studio-ew0djmhg.js} +23 -19
- package/dist/chunks/{studio-tr20m8g8.js → studio-f3wqjtk5.js} +39 -3
- package/dist/chunks/{studio-xntfdy1x.js → studio-fbf92ckf.js} +36 -23
- package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
- package/dist/chunks/{studio-7p35254k.js → studio-gfgb8pe6.js} +36 -193
- package/dist/chunks/studio-jc994dnr.js +1066 -0
- package/dist/chunks/{studio-hj2se583.js → studio-jkftxyvz.js} +69 -3423
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/studio-msd847zn.js +1315 -0
- package/dist/chunks/{studio-g5gdqnyd.js → studio-mtfnaqs9.js} +19 -19
- package/dist/chunks/{studio-7ejemhej.js → studio-n6j9t10q.js} +6 -6
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-6kv3apa9.js → studio-q9jd1a60.js} +27 -11
- package/dist/chunks/studio-qcy05dp8.js +1743 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-qjy2z9bv.js} +29 -132
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-r5xbtctf.js → studio-trxrabqf.js} +292 -2297
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-3dkcd4c5.js → studio-x4qkdat5.js} +62 -57
- package/dist/chunks/studio-x813zp48.js +195 -0
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/{studio-8ezdegcx.js → studio-zqazsct2.js} +40 -36
- package/dist/chunks/workspace-fjx52ryv.js +89 -0
- package/dist/iframe-entry.js +141 -56
- package/dist/manifest.json +7 -0
- package/dist/studio.js +2605 -1950
- package/favicon.ico +0 -0
- package/index.html +1 -0
- package/package.json +20 -18
- package/src/about/about-modal.ts +4 -3
- package/src/browse/library-pane.ts +57 -3
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-render.ts +22 -6
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +13 -42
- package/src/files/media-usage.ts +41 -131
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-view.ts +16 -2
- package/src/hosting/document.ts +2 -0
- package/src/hosting/layout.ts +8 -0
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +357 -163
- package/src/new-project/location-fields.ts +8 -5
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +31 -97
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +261 -70
- package/src/platforms/devserver.ts +67 -33
- package/src/preview/preview-overlay.ts +226 -0
- package/src/publish/publish-panel.ts +62 -2
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +532 -0
- package/src/services/ai-models.ts +108 -1
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +101 -1
- package/src/services/document-assistant.ts +45 -4
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +17 -0
- package/src/services/tool-executor.ts +45 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +193 -12
- package/src/settings/preferences-dialog.ts +93 -59
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/settings-document.ts +65 -9
- package/src/studio.ts +33 -6
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +129 -7
- package/src/ui/ai-managed-connect.ts +62 -13
- package/src/ui/ai-model-picker.ts +189 -0
- package/src/ui/cf-account-picker.ts +200 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/utils/studio-utils.ts +22 -52
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +147 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/content-source-7fy9qrm0.js +0 -40
- package/dist/chunks/files-xcybanxe.js +0 -70
- package/dist/chunks/layers-panel-27essgkc.js +0 -60
- package/dist/chunks/publish-panel-09sy2bab.js +0 -22
- package/dist/chunks/quick-search-sefb61v8.js +0 -55
- package/dist/chunks/right-panel-9r32jd5j.js +0 -39
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-bw5dz6km.js +0 -608
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Cf-account-picker.ts — "which Cloudflare account?", the question nothing in Studio asked.
|
|
4
|
+
*
|
|
5
|
+
* A Cloudflare grant can reach several accounts, and the broker stores exactly one id per
|
|
6
|
+
* connection. Until one is chosen every Cloudflare-backed call answers `cf_account_required`, so a
|
|
7
|
+
* multi-account user finished the hosted OAuth flow and landed in a state that reads as connected
|
|
8
|
+
* and behaves as broken: the assistant 401s, publishing 401s, and no surface says why. The broker
|
|
9
|
+
* has had `/cf/accounts` and `/cf/select-account` the whole time; this is the first caller.
|
|
10
|
+
*
|
|
11
|
+
* ONE picker, invoked wherever a connect resolves account-less — the assistant's managed-connect
|
|
12
|
+
* CTA, the publish panel, and a button on the Preferences row. A second picker per surface is how
|
|
13
|
+
* three surfaces end up disagreeing about what "connected" means.
|
|
14
|
+
*
|
|
15
|
+
* Cloud-only by construction: the PAL members it drives are the brokered ones, so a platform
|
|
16
|
+
* holding a pasted API token (desktop, dev server) keeps its own local account field and this
|
|
17
|
+
* resolves null without opening anything.
|
|
18
|
+
*
|
|
19
|
+
* @docs studio/ai
|
|
20
|
+
* @license MIT
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
24
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
25
|
+
import type { TemplateResult } from "lit-html";
|
|
26
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
27
|
+
import { notifyCredentialsChanged } from "../settings/preferences-accounts";
|
|
28
|
+
import { showDialog } from "./layers";
|
|
29
|
+
import { overlayRegion } from "./regions";
|
|
30
|
+
import type { CfAccountSummary } from "../types";
|
|
31
|
+
|
|
32
|
+
/** Whether this platform brokers the connection, and so can be asked which accounts it reaches. */
|
|
33
|
+
function canPick(): boolean {
|
|
34
|
+
if (!hasPlatform()) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const platform = getPlatform();
|
|
38
|
+
return Boolean(platform.cfAccounts && platform.cfSelectAccount);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ask which Cloudflare account this connection should use, and store the answer.
|
|
43
|
+
*
|
|
44
|
+
* Resolves the chosen account, or null — dismissed, or a platform that does not broker connections
|
|
45
|
+
* at all. A caller treats null as "still unusable" rather than as a failure: nothing was changed,
|
|
46
|
+
* and the surface that opened this is the one that knows what to say about it.
|
|
47
|
+
*
|
|
48
|
+
* @returns {Promise<CfAccountSummary | null>}
|
|
49
|
+
*/
|
|
50
|
+
export function openCfAccountPicker(): Promise<CfAccountSummary | null> {
|
|
51
|
+
if (!canPick()) {
|
|
52
|
+
return Promise.resolve(null);
|
|
53
|
+
}
|
|
54
|
+
return showDialog<CfAccountSummary | null>(
|
|
55
|
+
(done) => {
|
|
56
|
+
let accounts: CfAccountSummary[] = [];
|
|
57
|
+
let loading = true;
|
|
58
|
+
/** The last refusal — a listing that failed, or an account the broker would not accept. */
|
|
59
|
+
let failure = "";
|
|
60
|
+
/** The id being committed, so its own row can say so rather than the whole list going grey. */
|
|
61
|
+
let choosing = "";
|
|
62
|
+
let settled = false;
|
|
63
|
+
let wrapperEl: HTMLElement | null = null;
|
|
64
|
+
|
|
65
|
+
function finish(account: CfAccountSummary | null): void {
|
|
66
|
+
settled = true;
|
|
67
|
+
done(account);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function repaint(): void {
|
|
71
|
+
/* Resolved lazily, and only while the dialog is still up: `done` releases the slot, so a
|
|
72
|
+
late `load()` landing after a dismissal would otherwise paint into a detached host. */
|
|
73
|
+
const host = settled ? null : wrapperEl?.parentElement;
|
|
74
|
+
if (host) {
|
|
75
|
+
litRender(build(), host);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function load(): Promise<void> {
|
|
80
|
+
loading = true;
|
|
81
|
+
failure = "";
|
|
82
|
+
repaint();
|
|
83
|
+
try {
|
|
84
|
+
accounts = (await getPlatform().cfAccounts?.()) ?? [];
|
|
85
|
+
} catch (error) {
|
|
86
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
87
|
+
}
|
|
88
|
+
loading = false;
|
|
89
|
+
repaint();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function choose(account: CfAccountSummary): Promise<void> {
|
|
93
|
+
choosing = account.id;
|
|
94
|
+
failure = "";
|
|
95
|
+
repaint();
|
|
96
|
+
try {
|
|
97
|
+
await getPlatform().cfSelectAccount?.({ id: account.id, name: account.name });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
100
|
+
choosing = "";
|
|
101
|
+
repaint();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
/* The stored account is a credential like any other: the assistant's gate, the publish
|
|
105
|
+
panel and the Preferences row all re-read from the same announcement. */
|
|
106
|
+
notifyCredentialsChanged();
|
|
107
|
+
finish(account);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listTpl(): TemplateResult | typeof nothing {
|
|
111
|
+
if (loading) {
|
|
112
|
+
return html`<p class="cf-account-picker-empty">Reading your Cloudflare accounts…</p>`;
|
|
113
|
+
}
|
|
114
|
+
/* Nothing to show and a reason why. The retry is offered only here: a commit that was
|
|
115
|
+
refused (below) keeps the list, because listing again is not what would fix it. */
|
|
116
|
+
if (accounts.length === 0) {
|
|
117
|
+
return html`
|
|
118
|
+
<p class="cf-account-picker-empty">
|
|
119
|
+
${
|
|
120
|
+
failure
|
|
121
|
+
? `Cloudflare could not be reached: ${failure}`
|
|
122
|
+
: "This Cloudflare login reaches no accounts. Create one at dash.cloudflare.com, then try again."
|
|
123
|
+
}
|
|
124
|
+
</p>
|
|
125
|
+
${
|
|
126
|
+
failure
|
|
127
|
+
? html`<sp-button
|
|
128
|
+
size="s"
|
|
129
|
+
treatment="outline"
|
|
130
|
+
@click=${() => {
|
|
131
|
+
void load();
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
Try again
|
|
135
|
+
</sp-button>`
|
|
136
|
+
: nothing
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
/* The row shape is Preferences › Accounts', deliberately reused: this IS an account list,
|
|
141
|
+
and a second stylesheet entry for the same shape is how two lists drift apart. */
|
|
142
|
+
return html`
|
|
143
|
+
${failure ? html`<p class="cf-account-picker-error">${failure}</p>` : nothing}
|
|
144
|
+
<div class="prefs-accounts">
|
|
145
|
+
${accounts.map(
|
|
146
|
+
(account) => html`
|
|
147
|
+
<div class="prefs-account" data-account=${account.id}>
|
|
148
|
+
<div class="prefs-account-text">
|
|
149
|
+
<span class="prefs-account-label">${account.name}</span>
|
|
150
|
+
<span class="prefs-account-detail">${account.id}</span>
|
|
151
|
+
</div>
|
|
152
|
+
<sp-button
|
|
153
|
+
size="s"
|
|
154
|
+
variant="accent"
|
|
155
|
+
?disabled=${choosing !== ""}
|
|
156
|
+
@click=${() => {
|
|
157
|
+
void choose(account);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
${choosing === account.id ? "Selecting…" : "Use this account"}
|
|
161
|
+
</sp-button>
|
|
162
|
+
</div>
|
|
163
|
+
`,
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function build(): TemplateResult {
|
|
170
|
+
return html`
|
|
171
|
+
<sp-dialog-wrapper
|
|
172
|
+
open
|
|
173
|
+
underlay
|
|
174
|
+
headline="Choose a Cloudflare account"
|
|
175
|
+
cancel-label="Cancel"
|
|
176
|
+
@cancel=${() => finish(null)}
|
|
177
|
+
@close=${() => finish(null)}
|
|
178
|
+
${ref((el?: Element) => {
|
|
179
|
+
if (el) {
|
|
180
|
+
wrapperEl = el as HTMLElement;
|
|
181
|
+
}
|
|
182
|
+
})}
|
|
183
|
+
>
|
|
184
|
+
<div class="cf-account-picker">
|
|
185
|
+
<p class="cf-account-picker-lede">
|
|
186
|
+
Your Cloudflare login reaches more than one account. Publishing and the assistant
|
|
187
|
+
both run against the one you pick; you can change it in Preferences › Accounts.
|
|
188
|
+
</p>
|
|
189
|
+
${listTpl()}
|
|
190
|
+
</div>
|
|
191
|
+
</sp-dialog-wrapper>
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void load();
|
|
196
|
+
return build();
|
|
197
|
+
},
|
|
198
|
+
{ region: overlayRegion("dialog", "cf-accounts") },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `choiceField` — one labelled choice, rendered the way every Spectrum picker in Studio is.
|
|
3
|
+
*
|
|
4
|
+
* It exists so the New File dialog's format picker and the Convert dialog's target picker are the
|
|
5
|
+
* SAME control rather than two that drift: same divider idiom for a sentinel row, same `live()`
|
|
6
|
+
* guard, same "one kind is not a choice" collapse to static text.
|
|
7
|
+
*
|
|
8
|
+
* **It imports lit only.** Both callers live in modules the other imports — `ui/layers.ts` is
|
|
9
|
+
* imported by `format/convert-file.ts`, which also wants this fragment — so a shared control that
|
|
10
|
+
* reached into either would be a cycle, and `import/no-cycle` is an error here.
|
|
11
|
+
*
|
|
12
|
+
* `.value=${live(...)}` is not stylistic. `sp-picker` writes its own `value` when the reader picks
|
|
13
|
+
* a row, so lit's dirty check sees the value it last committed and skips the re-commit that would
|
|
14
|
+
* put the control back where the model says it is. `scripts/check-lit-conventions.ts` fails on the
|
|
15
|
+
* unguarded form.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { html, nothing } from "lit-html";
|
|
19
|
+
import { live } from "lit-html/directives/live.js";
|
|
20
|
+
import type { TemplateResult } from "lit-html";
|
|
21
|
+
|
|
22
|
+
/** One row of a {@link choiceField}. */
|
|
23
|
+
export interface ChoiceOption {
|
|
24
|
+
value: string;
|
|
25
|
+
label: string;
|
|
26
|
+
/** Draw an `sp-menu-divider` immediately before this row — how a sentinel row is set apart. */
|
|
27
|
+
dividerBefore?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What a {@link choiceField} renders and reports. */
|
|
31
|
+
export interface ChoiceSpec {
|
|
32
|
+
label: string;
|
|
33
|
+
options: readonly ChoiceOption[];
|
|
34
|
+
value: string;
|
|
35
|
+
onChange: (next: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A labelled picker, or static text when there is nothing to choose.
|
|
40
|
+
*
|
|
41
|
+
* The collapse is the rule `panels/pane-context.ts` already states about editor kinds: **one kind
|
|
42
|
+
* is not a choice**. A picker whose menu holds a single row is a control that cannot do anything,
|
|
43
|
+
* and drawing it invites the reader to open it and find out.
|
|
44
|
+
*
|
|
45
|
+
* @param spec - The label, the rows, the current value and the change sink.
|
|
46
|
+
* @returns The fragment, for a caller's own template.
|
|
47
|
+
*/
|
|
48
|
+
export function choiceField(spec: ChoiceSpec): TemplateResult {
|
|
49
|
+
const current = spec.options.find((option) => option.value === spec.value);
|
|
50
|
+
if (spec.options.length < 2) {
|
|
51
|
+
return html`<div class="choice-row">
|
|
52
|
+
<sp-field-label size="s">${spec.label}</sp-field-label>
|
|
53
|
+
<span class="choice-static">${current?.label ?? spec.value}</span>
|
|
54
|
+
</div>`;
|
|
55
|
+
}
|
|
56
|
+
return html`<div class="choice-row">
|
|
57
|
+
<sp-field-label size="s">${spec.label}</sp-field-label>
|
|
58
|
+
<sp-picker
|
|
59
|
+
size="s"
|
|
60
|
+
label=${spec.label}
|
|
61
|
+
.value=${live(spec.value)}
|
|
62
|
+
@change=${(e: Event) => spec.onChange((e.target as HTMLInputElement).value)}
|
|
63
|
+
>
|
|
64
|
+
${spec.options.map(
|
|
65
|
+
(option) =>
|
|
66
|
+
html`${
|
|
67
|
+
option.dividerBefore === true ? html`<sp-menu-divider></sp-menu-divider>` : nothing
|
|
68
|
+
}<sp-menu-item value=${option.value}>${option.label}</sp-menu-item>`,
|
|
69
|
+
)}
|
|
70
|
+
</sp-picker>
|
|
71
|
+
</div>`;
|
|
72
|
+
}
|
package/src/ui/layers.ts
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
import { html, render as litRender, nothing } from "lit-html";
|
|
16
16
|
import { ref } from "lit-html/directives/ref.js";
|
|
17
17
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
18
|
+
import { choiceField } from "./choice-field";
|
|
18
19
|
import { overlayRegion, REGION_ATTR } from "./regions";
|
|
19
20
|
import { dismiss, toasts } from "../services/notify";
|
|
20
21
|
import { activeRegistry } from "../commands/active-registry";
|
|
21
22
|
import { effect, effectScope } from "../reactivity";
|
|
23
|
+
import type { ChoiceOption } from "./choice-field";
|
|
22
24
|
import type { Notification, Severity } from "../services/notify";
|
|
23
25
|
import type { EffectScope } from "@vue/reactivity";
|
|
24
26
|
import type { TemplateResult } from "lit-html";
|
|
@@ -338,6 +340,31 @@ export function showSaveDiscardDialog(
|
|
|
338
340
|
);
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A picker rendered above the field, whose selection the dialog OWNS.
|
|
345
|
+
*
|
|
346
|
+
* Owned, not merely displayed: the choice reaches `validate` as its second argument, and picking a
|
|
347
|
+
* row re-runs it. That is the whole reason this lives here rather than being smuggled in through
|
|
348
|
+
* `message` — `check` and `rerender` are private to {@link showPromptDialog}, so a caller-built
|
|
349
|
+
* control could change the selection but could never make the field's refusal catch up with it. The
|
|
350
|
+
* New File dialog's format picker is exactly that case: switching from Markdown to JSON changes
|
|
351
|
+
* whether the typed name is already taken, with no keystroke to notice.
|
|
352
|
+
*/
|
|
353
|
+
export interface PromptChoice {
|
|
354
|
+
/** Label above the picker. */
|
|
355
|
+
label: string;
|
|
356
|
+
/**
|
|
357
|
+
* The rows, re-read on EVERY render rather than captured once. A snapshot taken when the options
|
|
358
|
+
* object is built freezes a list the project can still be loading — the same defect
|
|
359
|
+
* `content/entry-commands.ts`'s `derivedEnumProperty` exists to prevent one layer up.
|
|
360
|
+
*/
|
|
361
|
+
options: () => readonly ChoiceOption[];
|
|
362
|
+
/** Which row starts selected. */
|
|
363
|
+
initial: string;
|
|
364
|
+
/** Told what was picked, for a caller keeping its own derived state. */
|
|
365
|
+
onChange?: (next: string) => void;
|
|
366
|
+
}
|
|
367
|
+
|
|
341
368
|
/** Options accepted by {@link showPromptDialog}. */
|
|
342
369
|
export interface PromptDialogOptions {
|
|
343
370
|
/** Label on the confirming button. */
|
|
@@ -346,17 +373,21 @@ export interface PromptDialogOptions {
|
|
|
346
373
|
cancelLabel?: string;
|
|
347
374
|
/** Explanatory copy rendered above the text field. */
|
|
348
375
|
message?: string | TemplateResult;
|
|
349
|
-
/** Placeholder shown while the field is empty. */
|
|
350
|
-
placeholder?: string;
|
|
376
|
+
/** Placeholder shown while the field is empty. Read on every render, so it may follow the choice. */
|
|
377
|
+
placeholder?: string | (() => string);
|
|
351
378
|
/** How much of the pre-filled value to select once the field takes focus. */
|
|
352
379
|
select?: "all" | "stem" | "none";
|
|
353
380
|
/**
|
|
354
381
|
* Validate the raw field value. Return an empty string when valid, or a message to show as
|
|
355
382
|
* negative help text (which also blocks confirmation). Defaults to "must not be blank".
|
|
383
|
+
*
|
|
384
|
+
* `chosen` is the {@link PromptChoice} selection, or `""` when the dialog offers no choice.
|
|
356
385
|
*/
|
|
357
|
-
validate?: (value: string) => string;
|
|
386
|
+
validate?: (value: string, chosen: string) => string;
|
|
358
387
|
/** Pre-filled value. */
|
|
359
388
|
value?: string;
|
|
389
|
+
/** A picker above the field whose selection this dialog owns. */
|
|
390
|
+
choice?: PromptChoice;
|
|
360
391
|
}
|
|
361
392
|
|
|
362
393
|
/**
|
|
@@ -375,6 +406,7 @@ export function showPromptDialog(
|
|
|
375
406
|
): Promise<string | null> {
|
|
376
407
|
const {
|
|
377
408
|
cancelLabel = "Cancel",
|
|
409
|
+
choice,
|
|
378
410
|
confirmLabel = "OK",
|
|
379
411
|
message,
|
|
380
412
|
placeholder = "",
|
|
@@ -384,12 +416,22 @@ export function showPromptDialog(
|
|
|
384
416
|
} = opts;
|
|
385
417
|
|
|
386
418
|
const check = (candidate: string) =>
|
|
387
|
-
validate ? validate(candidate) : candidate.trim() ? "" : "Enter a value.";
|
|
419
|
+
validate ? validate(candidate, chosen) : candidate.trim() ? "" : "Enter a value.";
|
|
388
420
|
|
|
389
421
|
let value = initialValue;
|
|
422
|
+
let chosen = choice?.initial ?? "";
|
|
390
423
|
let error = "";
|
|
391
424
|
let wrapperEl: HTMLElement | null = null;
|
|
392
425
|
let focusRequested = false;
|
|
426
|
+
/*
|
|
427
|
+
* Whether the reader has typed yet.
|
|
428
|
+
*
|
|
429
|
+
* A dialog opens with a valid prefill and NO error, and picking a format must not be the thing
|
|
430
|
+
* that first paints one under a field nobody has touched — "untitled" is not yet a mistake. So a
|
|
431
|
+
* pick refreshes an error that is already showing and never mints the first one; the confirm
|
|
432
|
+
* still refuses, because `confirm()` runs `check` unconditionally.
|
|
433
|
+
*/
|
|
434
|
+
let touched = false;
|
|
393
435
|
|
|
394
436
|
return showDialog<string | null>((done) => {
|
|
395
437
|
function rerender() {
|
|
@@ -411,6 +453,7 @@ export function showPromptDialog(
|
|
|
411
453
|
}
|
|
412
454
|
|
|
413
455
|
function onInput(e: Event) {
|
|
456
|
+
touched = true;
|
|
414
457
|
value = (e.target as HTMLInputElement).value || "";
|
|
415
458
|
const next = check(value);
|
|
416
459
|
if (next !== error) {
|
|
@@ -419,6 +462,23 @@ export function showPromptDialog(
|
|
|
419
462
|
}
|
|
420
463
|
}
|
|
421
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Take a pick, and re-render UNCONDITIONALLY.
|
|
467
|
+
*
|
|
468
|
+
* `onInput` re-renders only when the error string changed, which is right for a keystroke: the
|
|
469
|
+
* template it would rebuild is identical. A pick is not that. It can change the placeholder,
|
|
470
|
+
* the picker's own selected row, and — for the New File dialog — whether the composed filename
|
|
471
|
+
* is already taken, all with the error text unchanged. Comparing error strings here would leave
|
|
472
|
+
* a dialog showing "about.md already exists" after the reader switched the format to JSON.
|
|
473
|
+
*/
|
|
474
|
+
function onPick(next: string) {
|
|
475
|
+
chosen = next;
|
|
476
|
+
choice?.onChange?.(next);
|
|
477
|
+
const candidate = check(value);
|
|
478
|
+
error = touched || error ? candidate : "";
|
|
479
|
+
rerender();
|
|
480
|
+
}
|
|
481
|
+
|
|
422
482
|
function onKeydown(e: KeyboardEvent) {
|
|
423
483
|
if (e.key === "Enter") {
|
|
424
484
|
confirm();
|
|
@@ -472,9 +532,27 @@ export function showPromptDialog(
|
|
|
472
532
|
// Spectrum resets <p> margins to 0, so without this the copy sits flush on the field.
|
|
473
533
|
message ? html`<p style="margin:0 0 8px">${message}</p>` : nothing
|
|
474
534
|
}
|
|
535
|
+
${
|
|
536
|
+
/*
|
|
537
|
+
* Above the field, and in ONE template position in every mode.
|
|
538
|
+
*
|
|
539
|
+
* The `sp-textfield` below must never move or be rebuilt by a conditional branch: lit
|
|
540
|
+
* would commit a new element, `onFieldRef` would fire again, the `focusRequested` latch
|
|
541
|
+
* would refuse to re-focus it, and the caret would be stranded mid-name. Only the
|
|
542
|
+
* placeholder, the validation rules and the help text vary between modes.
|
|
543
|
+
*/
|
|
544
|
+
choice
|
|
545
|
+
? choiceField({
|
|
546
|
+
label: choice.label,
|
|
547
|
+
onChange: onPick,
|
|
548
|
+
options: choice.options(),
|
|
549
|
+
value: chosen,
|
|
550
|
+
})
|
|
551
|
+
: nothing
|
|
552
|
+
}
|
|
475
553
|
<sp-textfield
|
|
476
554
|
style="width:100%"
|
|
477
|
-
placeholder=${placeholder}
|
|
555
|
+
placeholder=${typeof placeholder === "function" ? placeholder() : placeholder}
|
|
478
556
|
value=${value}
|
|
479
557
|
?invalid=${Boolean(error)}
|
|
480
558
|
@input=${onInput}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* These are all side-effect-free functions used by style/properties/events panels.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
7
|
+
import { collectionForFile } from "../content/collection-match";
|
|
8
|
+
import { defaultContentFormat } from "../format/format-host";
|
|
9
9
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -117,15 +117,6 @@ export function abbreviateValue(val: string) {
|
|
|
117
117
|
return map[val] || val;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/**
|
|
121
|
-
* The placeholder a content type's `source` may carry to say "one directory per locale".
|
|
122
|
-
*
|
|
123
|
-
* Held here rather than imported out of the parser extension's content loader ({@link
|
|
124
|
-
* file://../../../../extensions/parser/src/content-loader.ts}): that module reads the filesystem
|
|
125
|
-
* and this one runs in a browser. It is also an extension, and core may not import one.
|
|
126
|
-
*/
|
|
127
|
-
const LOCALE_PLACEHOLDER = "{locale}";
|
|
128
|
-
|
|
129
120
|
/**
|
|
130
121
|
* True when a schema `format` names a MEDIA reference.
|
|
131
122
|
*
|
|
@@ -184,8 +175,14 @@ export function inferInputType(entry: Record<string, unknown>) {
|
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
/**
|
|
187
|
-
* Match a document path to a content type and return its schema.
|
|
188
|
-
*
|
|
178
|
+
* Match a document path to a content type and return its schema.
|
|
179
|
+
*
|
|
180
|
+
* Delegates to `content/collection-match.ts`, which is the ONE matcher — the reason it takes a
|
|
181
|
+
* config rather than reading `projectState`. Two rules are added here and belong here, because they
|
|
182
|
+
* are about the FORM rather than about membership: a collection that declares no `schema` has no
|
|
183
|
+
* fields to draw, and a file whose extension is not the collection's is not one of its entries.
|
|
184
|
+
* Membership itself — the `{locale}` expansion, the recursive subdirectory, the single-file
|
|
185
|
+
* catalogue, longest-source-wins — is the matcher's, and is no longer restated here.
|
|
189
186
|
*
|
|
190
187
|
* @param {string | null} documentPath — project-relative path (e.g. "content/products/widget.md")
|
|
191
188
|
* @param {ProjectConfig | null | undefined} projectConfig — parsed project.json
|
|
@@ -198,47 +195,20 @@ export function findContentTypeSchema(
|
|
|
198
195
|
if (!documentPath || !projectConfig?.content) {
|
|
199
196
|
return null;
|
|
200
197
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (hasExt) {
|
|
213
|
-
if (docPath === src || docPath.endsWith(`/${src}`)) {
|
|
214
|
-
return { name, schema: def.schema };
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
const ext =
|
|
218
|
-
def.format === "json"
|
|
219
|
-
? ".json"
|
|
220
|
-
: (formatByName(def.format)?.extensions[0] ??
|
|
221
|
-
defaultContentFormat()?.extensions[0] ??
|
|
222
|
-
".json");
|
|
223
|
-
/* A localized source is N directories, one content type: `content/posts/{locale}` matches
|
|
224
|
-
`content/posts/fr/hello.md` and carries the SAME schema, because a translation is the same
|
|
225
|
-
post. Without this the placeholder was compared literally, matched no path anybody has, and
|
|
226
|
-
a translated entry showed no frontmatter fields at all. */
|
|
227
|
-
const prefix = src.includes(LOCALE_PLACEHOLDER)
|
|
228
|
-
? new RegExp(
|
|
229
|
-
`^${src
|
|
230
|
-
.split(LOCALE_PLACEHOLDER)
|
|
231
|
-
.map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`))
|
|
232
|
-
.join("[^/]+")}/`,
|
|
233
|
-
)
|
|
234
|
-
: null;
|
|
235
|
-
const matches = prefix ? prefix.test(docPath) : docPath.startsWith(`${src}/`);
|
|
236
|
-
if (matches && docPath.endsWith(ext)) {
|
|
237
|
-
return { name, schema: def.schema };
|
|
238
|
-
}
|
|
198
|
+
const match = collectionForFile(documentPath, projectConfig);
|
|
199
|
+
if (!match || !match.def.schema) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
/* A single-file source is matched AS that file, so its extension is settled by construction.
|
|
203
|
+
A directory-backed one is not: a `.json` sitting beside `.md` posts is somebody else's file,
|
|
204
|
+
and drawing the collection's fields over it would invite edits nothing will ever read. */
|
|
205
|
+
if (!match.fileBacked) {
|
|
206
|
+
const ext = match.ext ?? defaultContentFormat()?.extensions[0] ?? ".json";
|
|
207
|
+
if (!documentPath.toLowerCase().endsWith(ext.toLowerCase())) {
|
|
208
|
+
return null;
|
|
239
209
|
}
|
|
240
210
|
}
|
|
241
|
-
return
|
|
211
|
+
return { name: match.name, schema: match.def.schema };
|
|
242
212
|
}
|
|
243
213
|
|
|
244
214
|
/**
|
package/styles/forced-colors.css
CHANGED
|
@@ -48,6 +48,13 @@
|
|
|
48
48
|
outline-offset: -2px;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/* The rail's gear while its menu is open. That state is drawn with a `background`, which this
|
|
52
|
+
mode re-forces to the system canvas — so it says nothing. An outline says it instead. */
|
|
53
|
+
.rail-item--menu-open {
|
|
54
|
+
outline: 2px solid Highlight;
|
|
55
|
+
outline-offset: -2px;
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
/* The drop target. A 3px translucent ring is invisible here twice over — shadow and alpha. */
|
|
52
59
|
.drop-target,
|
|
53
60
|
.is-drop-target {
|