@jxsuite/studio 0.35.0 → 0.37.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/dist/iframe-entry.js +18 -9
- package/dist/iframe-entry.js.map +4 -4
- package/dist/studio.js +12278 -2990
- package/dist/studio.js.map +73 -21
- package/package.json +10 -5
- package/src/canvas/canvas-render.ts +85 -12
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-overlay.ts +30 -1
- package/src/collab/collab-session.ts +903 -0
- package/src/collab/collab-state.ts +71 -0
- package/src/collab/monaco-cursors.ts +69 -0
- package/src/collab/presence-chips.ts +58 -0
- package/src/files/file-ops.ts +6 -0
- package/src/files/files.ts +6 -0
- package/src/panels/ai-panel.ts +25 -3
- package/src/panels/git-panel.ts +5 -0
- package/src/panels/toolbar.ts +15 -3
- package/src/panels/welcome-screen.ts +26 -0
- package/src/platforms/cloud.ts +805 -0
- package/src/platforms/devserver.ts +107 -0
- package/src/project-list.ts +38 -0
- package/src/publish/pages-service.ts +186 -0
- package/src/publish/publish-panel.ts +360 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/cf-settings.ts +58 -0
- package/src/services/settings-store.ts +7 -1
- package/src/studio.ts +25 -0
- package/src/tabs/doc-op-apply.ts +5 -81
- package/src/tabs/patch-ops.ts +7 -23
- package/src/tabs/transact.ts +138 -12
- package/src/types.ts +95 -174
- package/src/workspace/workspace.ts +4 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive per-tab collab state, kept OUTSIDE the Tab type: UI reads it through collabState(tab),
|
|
3
|
+
* and non-collab builds never touch it. The path registry answers "is this file co-edited?" for the
|
|
4
|
+
* fs-sync/reload guards without importing the session machinery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { reactive } from "../reactivity";
|
|
8
|
+
import type { Tab } from "../tabs/tab";
|
|
9
|
+
import type { CollabAwarenessState } from "@jxsuite/collab/awareness-types";
|
|
10
|
+
|
|
11
|
+
export type CollabTabStatus = "detached" | "connecting" | "synced" | "offline";
|
|
12
|
+
|
|
13
|
+
export interface PeerPresence {
|
|
14
|
+
clientId: number;
|
|
15
|
+
state: CollabAwarenessState;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TabCollabState {
|
|
19
|
+
status: CollabTabStatus;
|
|
20
|
+
/** True once a session is attached and past its initial sync. */
|
|
21
|
+
active: boolean;
|
|
22
|
+
readOnly: boolean;
|
|
23
|
+
/** Other clients' awareness states (this project connection, all docs). */
|
|
24
|
+
peers: PeerPresence[];
|
|
25
|
+
/**
|
|
26
|
+
* True while source holds the canonical lock (someone is co-editing the code view): structural
|
|
27
|
+
* surfaces soft-freeze and the canvas previews the source reconciler's parses.
|
|
28
|
+
*/
|
|
29
|
+
sourceCanonical: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const states = new WeakMap<Tab, TabCollabState>();
|
|
33
|
+
const attachedPaths = new Map<string, number>();
|
|
34
|
+
|
|
35
|
+
export function collabState(tab: Tab): TabCollabState {
|
|
36
|
+
let state = states.get(tab);
|
|
37
|
+
if (!state) {
|
|
38
|
+
state = reactive({
|
|
39
|
+
active: false,
|
|
40
|
+
peers: [],
|
|
41
|
+
readOnly: false,
|
|
42
|
+
sourceCanonical: false,
|
|
43
|
+
status: "detached",
|
|
44
|
+
}) as TabCollabState;
|
|
45
|
+
states.set(tab, state);
|
|
46
|
+
}
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isCollabActive(tab: Tab): boolean {
|
|
51
|
+
return states.get(tab)?.active === true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Track which project-relative paths have live sessions (fs-event/reload suppression). */
|
|
55
|
+
export function registerCollabPath(path: string): void {
|
|
56
|
+
attachedPaths.set(path, (attachedPaths.get(path) ?? 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function unregisterCollabPath(path: string): void {
|
|
60
|
+
const count = attachedPaths.get(path) ?? 0;
|
|
61
|
+
if (count <= 1) {
|
|
62
|
+
attachedPaths.delete(path);
|
|
63
|
+
} else {
|
|
64
|
+
attachedPaths.set(path, count - 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** True while some tab co-edits this path — its Y.Doc is ahead of any provider write-back. */
|
|
69
|
+
export function isCollabPath(path: string): boolean {
|
|
70
|
+
return attachedPaths.has(path.replaceAll("\\", "/"));
|
|
71
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote cursor colors for y-monaco. MonacoBinding decorates other clients' text cursors with
|
|
3
|
+
* per-client class names (`yRemoteSelection-<id>`, `yRemoteSelectionHead-<id>`) but ships no
|
|
4
|
+
* styling — this module maintains one injected <style> element with a rule set per remote client,
|
|
5
|
+
* colored from the presence palette (`user.color`) and labeled with the author's name on hover of
|
|
6
|
+
* the cursor head.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The slice of y-protocols Awareness the style manager uses (duck-typed; stays yjs-free). */
|
|
10
|
+
export interface AwarenessLike {
|
|
11
|
+
clientID: number;
|
|
12
|
+
getStates: () => Map<number, unknown>;
|
|
13
|
+
on: (event: "change", cb: () => void) => void;
|
|
14
|
+
off: (event: "change", cb: () => void) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PeerStateShape {
|
|
18
|
+
user?: { color?: string; name?: string; login?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** CSS-escape for the content string (names come from GitHub logins/display names). */
|
|
22
|
+
function cssString(value: string): string {
|
|
23
|
+
return `"${value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`)}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One client's rules: translucent selection band, solid caret, name flag above the caret. */
|
|
27
|
+
export function cursorRulesFor(clientId: number, color: string, label: string): string {
|
|
28
|
+
return [
|
|
29
|
+
`.yRemoteSelection-${clientId}{background-color:${color}44;}`,
|
|
30
|
+
`.yRemoteSelectionHead-${clientId}{position:absolute;border-left:2px solid ${color};height:100%;box-sizing:border-box;}`,
|
|
31
|
+
`.yRemoteSelectionHead-${clientId}::after{content:${cssString(label)};position:absolute;top:-1.2em;left:-2px;padding:0 4px;border-radius:3px 3px 3px 0;font:10px/1.4 sans-serif;color:#fff;white-space:nowrap;background-color:${color};opacity:0;transition:opacity .15s;pointer-events:none;}`,
|
|
32
|
+
`.yRemoteSelectionHead-${clientId}:hover::after{opacity:1;}`,
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Full stylesheet text for every remote client with a presence identity. */
|
|
37
|
+
export function cursorStylesheet(awareness: AwarenessLike): string {
|
|
38
|
+
const rules: string[] = [];
|
|
39
|
+
for (const [clientId, raw] of awareness.getStates()) {
|
|
40
|
+
if (clientId === awareness.clientID) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const { user } = raw as PeerStateShape;
|
|
44
|
+
if (!user?.color) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
rules.push(cursorRulesFor(clientId, user.color, user.name ?? user.login ?? String(clientId)));
|
|
48
|
+
}
|
|
49
|
+
return rules.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keep an injected <style> element in sync with the awareness roster while a code view is bound.
|
|
54
|
+
* Returns the disposer (removes the element and the listener).
|
|
55
|
+
*/
|
|
56
|
+
export function attachCursorStyles(awareness: AwarenessLike, doc: Document): () => void {
|
|
57
|
+
const style = doc.createElement("style");
|
|
58
|
+
style.dataset["jxCollabCursors"] = "true";
|
|
59
|
+
doc.head.append(style);
|
|
60
|
+
const refresh = () => {
|
|
61
|
+
style.textContent = cursorStylesheet(awareness);
|
|
62
|
+
};
|
|
63
|
+
refresh();
|
|
64
|
+
awareness.on("change", refresh);
|
|
65
|
+
return () => {
|
|
66
|
+
awareness.off("change", refresh);
|
|
67
|
+
style.remove();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence chips — who else is in this co-editing session, plus the sync-status pill that replaces
|
|
3
|
+
* the dirty dot for collab tabs. Pure lit templates over collabState(tab); the toolbar includes
|
|
4
|
+
* them and its render effect tracks the underlying reactive state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, nothing } from "lit-html";
|
|
8
|
+
import type { TemplateResult } from "lit-html";
|
|
9
|
+
import { collabState } from "./collab-state";
|
|
10
|
+
import type { PeerPresence } from "./collab-state";
|
|
11
|
+
import type { Tab } from "../tabs/tab";
|
|
12
|
+
|
|
13
|
+
function initialOf(peer: PeerPresence): string {
|
|
14
|
+
const name = peer.state.user.name ?? peer.state.user.login;
|
|
15
|
+
return (name[0] ?? "?").toUpperCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function titleOf(peer: PeerPresence, docPath: string | null): string {
|
|
19
|
+
const { user } = peer.state;
|
|
20
|
+
const who = user.name ? `${user.name} (${user.login})` : user.login;
|
|
21
|
+
const here = peer.state.focusedPath && peer.state.focusedPath === docPath;
|
|
22
|
+
return here ? who : `${who} — ${peer.state.focusedPath ?? "browsing"}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const STATUS_LABEL: Record<string, string> = {
|
|
26
|
+
connecting: "Connecting…",
|
|
27
|
+
offline: "Offline — changes sync on reconnect",
|
|
28
|
+
synced: "Live",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Chips + status pill for the toolbar; `nothing` while the tab isn't co-editing. */
|
|
32
|
+
export function presenceChipsTemplate(tab: Tab | null): TemplateResult | typeof nothing {
|
|
33
|
+
if (!tab) {
|
|
34
|
+
return nothing;
|
|
35
|
+
}
|
|
36
|
+
const state = collabState(tab);
|
|
37
|
+
if (state.status === "detached") {
|
|
38
|
+
return nothing;
|
|
39
|
+
}
|
|
40
|
+
const label = STATUS_LABEL[state.status] ?? state.status;
|
|
41
|
+
return html`
|
|
42
|
+
<div class="jx-presence" title=${label}>
|
|
43
|
+
<span class="jx-presence-status" data-status=${state.status}>${label}</span>
|
|
44
|
+
${state.peers.map(
|
|
45
|
+
(peer) => html`
|
|
46
|
+
<span
|
|
47
|
+
class="jx-presence-chip"
|
|
48
|
+
style="background:${peer.state.user.color}"
|
|
49
|
+
title=${titleOf(peer, tab.documentPath)}
|
|
50
|
+
>${peer.state.user.avatarUrl
|
|
51
|
+
? html`<img src=${peer.state.user.avatarUrl} alt=${initialOf(peer)} />`
|
|
52
|
+
: initialOf(peer)}</span
|
|
53
|
+
>
|
|
54
|
+
`,
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
}
|
package/src/files/file-ops.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { statusMessage } from "../panels/statusbar";
|
|
|
13
13
|
import { validateComponentSlots } from "../services/cem-export";
|
|
14
14
|
import { getPlatform } from "../platform";
|
|
15
15
|
import { activeTab, openTab } from "../workspace/workspace";
|
|
16
|
+
import { collabSave } from "../collab/collab-session";
|
|
16
17
|
import { isEditing, stopEditing } from "../editor/inline-edit";
|
|
17
18
|
import {
|
|
18
19
|
defaultContentFormat,
|
|
@@ -162,6 +163,11 @@ export async function saveFile() {
|
|
|
162
163
|
return;
|
|
163
164
|
}
|
|
164
165
|
try {
|
|
166
|
+
// A co-edited tab persists through its provider (a direct file write would reset the room).
|
|
167
|
+
if (await collabSave(tab)) {
|
|
168
|
+
savedMessage(tab, "Synced");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
165
171
|
const output = await serializeDocument(tab);
|
|
166
172
|
|
|
167
173
|
if (tab.documentPath) {
|
package/src/files/files.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { ensureDependenciesInstalled } from "../packages/ensure-deps";
|
|
|
20
20
|
import { maybePromptJxsuiteUpdate } from "../packages/jxsuite-update";
|
|
21
21
|
import { autoSyncProjectOnOpen } from "../packages/pull-package-sync";
|
|
22
22
|
import { markLocalMutation } from "./fs-events";
|
|
23
|
+
import { isCollabPath } from "../collab/collab-state";
|
|
23
24
|
import {
|
|
24
25
|
draggable,
|
|
25
26
|
dropTargetForElements,
|
|
@@ -1078,6 +1079,11 @@ export async function openFileInTab(path: string) {
|
|
|
1078
1079
|
*/
|
|
1079
1080
|
/** Reload an open tab from disk when an external change arrives — but only if it is not dirty. */
|
|
1080
1081
|
export function reloadCleanTab(path: string): void {
|
|
1082
|
+
// Co-edited docs never reload from disk: the shared Y.Doc is ahead of the provider's write-back
|
|
1083
|
+
// (Which is what produced this event), and genuine external changes arrive as a collab reset.
|
|
1084
|
+
if (isCollabPath(path)) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1081
1087
|
for (const [, tab] of workspace.tabs.entries()) {
|
|
1082
1088
|
if (tab.documentPath === path && !tab.doc.dirty) {
|
|
1083
1089
|
void reloadFileInTab(path);
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { TemplateResult } from "lit-html";
|
|
|
21
21
|
import { effect, effectScope } from "../reactivity";
|
|
22
22
|
import { createDocumentAssistant } from "../services/document-assistant";
|
|
23
23
|
import { hasOpenAiKey } from "../services/ai-settings";
|
|
24
|
+
import { fetchAvailableModels, isProxyConfigured } from "../services/ai-models";
|
|
24
25
|
import { createAiCredentialsForm } from "../ui/ai-credentials-form";
|
|
25
26
|
import { clearMarkdownCache } from "./ai-chat/chat-markdown";
|
|
26
27
|
import { renderChatHeader, renderMessageList } from "./ai-chat/chat-view";
|
|
@@ -36,6 +37,23 @@ let mounted = false;
|
|
|
36
37
|
/** Whether the OpenAI key form is showing (gate when no key, or re-edit via the gear). */
|
|
37
38
|
let keyEditing = false;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* One-time proxy probe: managed platforms (cloud Workers AI) and env-keyed dev servers report
|
|
42
|
+
* `configured` from /models, unlocking the assistant without a locally stored key. Fired lazily on
|
|
43
|
+
* the first gated render.
|
|
44
|
+
*/
|
|
45
|
+
let proxyProbe: Promise<void> | null = null;
|
|
46
|
+
|
|
47
|
+
function ensureProxyProbe() {
|
|
48
|
+
proxyProbe ??= fetchAvailableModels({ force: true })
|
|
49
|
+
.catch(() => {
|
|
50
|
+
// Unreachable proxy — the key gate stays up.
|
|
51
|
+
})
|
|
52
|
+
.then(() => {
|
|
53
|
+
scheduleAiRender();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
/** Which pane the panel shows once the key gate is passed. */
|
|
40
58
|
let view: "chat" | "sessions" = "chat";
|
|
41
59
|
|
|
@@ -245,9 +263,13 @@ const composer = createComposer({
|
|
|
245
263
|
|
|
246
264
|
/** @returns {TemplateResult} */
|
|
247
265
|
export function renderAiPanelTemplate(): TemplateResult {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
266
|
+
/* The document assistant authenticates via the AI proxy. Gate the chat
|
|
267
|
+
behind the key form until a key is stored locally OR the proxy reports
|
|
268
|
+
itself configured (managed platforms, env-keyed dev servers). */
|
|
269
|
+
if ((!hasOpenAiKey() && !isProxyConfigured()) || keyEditing) {
|
|
270
|
+
if (!keyEditing) {
|
|
271
|
+
ensureProxyProbe();
|
|
272
|
+
}
|
|
251
273
|
return renderKeyGate();
|
|
252
274
|
}
|
|
253
275
|
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { html, nothing } from "lit-html";
|
|
8
8
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
9
|
+
import { flushAllCollab } from "../collab/collab-session";
|
|
9
10
|
import type {
|
|
10
11
|
GitBranchesResult,
|
|
11
12
|
GitDiffState,
|
|
@@ -263,6 +264,9 @@ export function renderGitPanel(
|
|
|
263
264
|
return;
|
|
264
265
|
}
|
|
265
266
|
updateUi("gitCommitMessage", "");
|
|
267
|
+
// Fold co-editing sessions into the backend's tree first so the commit never misses
|
|
268
|
+
// Trailing keystrokes (the mirror is debounced).
|
|
269
|
+
await flushAllCollab();
|
|
266
270
|
await gitAction("gitCommit", msg);
|
|
267
271
|
};
|
|
268
272
|
|
|
@@ -275,6 +279,7 @@ export function renderGitPanel(
|
|
|
275
279
|
updateUi("gitCommitMessage", "");
|
|
276
280
|
updateUi("gitLoading", true);
|
|
277
281
|
updateUi("gitError", null);
|
|
282
|
+
await flushAllCollab();
|
|
278
283
|
const plat = getPlatform();
|
|
279
284
|
try {
|
|
280
285
|
await plat.gitCommit(msg);
|
package/src/panels/toolbar.ts
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
|
+
import { openPublishPanel } from "../publish/publish-panel";
|
|
8
9
|
import { updateSession } from "../store";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
canRedo as tabCanRedo,
|
|
12
|
+
canUndo as tabCanUndo,
|
|
13
|
+
redo as tabRedo,
|
|
14
|
+
undo as tabUndo,
|
|
15
|
+
} from "../tabs/transact";
|
|
16
|
+
import { collabState } from "../collab/collab-state";
|
|
17
|
+
import { presenceChipsTemplate } from "../collab/presence-chips";
|
|
10
18
|
import { effect, effectScope } from "../reactivity";
|
|
11
19
|
import { activeTab } from "../workspace/workspace";
|
|
12
20
|
import { applyPanelCollapse, view } from "../view";
|
|
@@ -100,6 +108,8 @@ export function mount(rootEl: HTMLElement, ctx: ToolbarCtx) {
|
|
|
100
108
|
void tab.session.ui.gitStatus;
|
|
101
109
|
void tab.history.index;
|
|
102
110
|
void tab.history.snapshots.length;
|
|
111
|
+
void collabState(tab).status;
|
|
112
|
+
void collabState(tab).peers.length;
|
|
103
113
|
}
|
|
104
114
|
render();
|
|
105
115
|
});
|
|
@@ -321,8 +331,8 @@ function toolbarTemplate() {
|
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
const allowedModes = new Set(tab.capabilities.modes);
|
|
324
|
-
const canUndo = tab
|
|
325
|
-
const canRedo = tab
|
|
334
|
+
const canUndo = tabCanUndo(tab);
|
|
335
|
+
const canRedo = tabCanRedo(tab);
|
|
326
336
|
const canSave = tab.doc.dirty;
|
|
327
337
|
|
|
328
338
|
const S = {
|
|
@@ -472,6 +482,7 @@ function toolbarTemplate() {
|
|
|
472
482
|
${recentProjectsTpl}
|
|
473
483
|
</div>
|
|
474
484
|
${tbBtnTpl("Manage", openBrowseModal, "sp-icon-view-list")}
|
|
485
|
+
${tbBtnTpl("Publish", openPublishPanel)}
|
|
475
486
|
<sp-action-button size="s" title="Save" ?disabled=${!canSave} @click=${ctx.saveFile}>
|
|
476
487
|
${toolbarIconMap["sp-icon-save-floppy"]}<span class="tb-label">Save</span>
|
|
477
488
|
</sp-action-button>
|
|
@@ -493,6 +504,7 @@ function toolbarTemplate() {
|
|
|
493
504
|
${toolbarIconMap["sp-icon-redo"]}<span class="tb-label">Redo</span>
|
|
494
505
|
</sp-action-button>
|
|
495
506
|
</sp-action-group>
|
|
507
|
+
${presenceChipsTemplate(tab)}
|
|
496
508
|
<div class="tb-spacer"></div>
|
|
497
509
|
<sp-action-button
|
|
498
510
|
class="tb-search-trigger"
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
|
+
import { getProjectList } from "../project-list";
|
|
8
9
|
import { clearRecentProjects, getRecentProjects, removeRecentProject } from "../recent-projects";
|
|
9
10
|
import { renderOnly } from "../store";
|
|
10
11
|
import { platformSupportsClone } from "./git-panel";
|
|
@@ -27,6 +28,8 @@ export function initWelcome(ctx: WelcomeCtx) {
|
|
|
27
28
|
export function renderWelcome(host: HTMLElement) {
|
|
28
29
|
const ctx = _ctx as WelcomeCtx;
|
|
29
30
|
const recent = getRecentProjects();
|
|
31
|
+
// Catalogue entries already in Recent stay in that section only.
|
|
32
|
+
const catalogue = getProjectList().filter((p) => !recent.some((r) => r.root === p.root));
|
|
30
33
|
const showClone = platformSupportsClone();
|
|
31
34
|
|
|
32
35
|
litRender(
|
|
@@ -84,6 +87,29 @@ export function renderWelcome(host: HTMLElement) {
|
|
|
84
87
|
: nothing}
|
|
85
88
|
</div>
|
|
86
89
|
|
|
90
|
+
${catalogue.length > 0
|
|
91
|
+
? html`
|
|
92
|
+
<div class="welcome-section">
|
|
93
|
+
<h2 class="welcome-section-title">Projects</h2>
|
|
94
|
+
${catalogue.map(
|
|
95
|
+
(p) => html`
|
|
96
|
+
<div class="welcome-recent-row">
|
|
97
|
+
<button
|
|
98
|
+
class="welcome-recent welcome-catalogue"
|
|
99
|
+
@click=${() => ctx.openRecentProject(p.root)}
|
|
100
|
+
title=${p.root}
|
|
101
|
+
>
|
|
102
|
+
<span class="welcome-recent-name">${p.name}</span>
|
|
103
|
+
<span class="welcome-recent-path">
|
|
104
|
+
${p.description ?? shortenPath(p.root)}
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
`,
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
`
|
|
112
|
+
: nothing}
|
|
87
113
|
${recent.length > 0
|
|
88
114
|
? html`
|
|
89
115
|
<div class="welcome-section">
|