@1agh/maude 0.30.0 → 0.32.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/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- package/apps/studio/annotations-layer.tsx +6 -1
- package/apps/studio/api.ts +225 -66
- package/apps/studio/bin/chat-open.sh +44 -0
- package/apps/studio/canvas-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +22 -2
- package/apps/studio/client/app.jsx +788 -26
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/panels/ChatPanel.jsx +796 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +598 -0
- package/apps/studio/client/panels/ReadinessList.jsx +189 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +625 -3
- package/apps/studio/client/styles/5-maude-overrides.css +25 -0
- package/apps/studio/client/styles/6-acp-chat.css +821 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +11 -1
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- package/apps/studio/cursors-overlay.tsx +17 -1
- package/apps/studio/dist/client.bundle.js +29097 -1534
- package/apps/studio/dist/comment-mount.js +4 -2
- package/apps/studio/dist/styles.css +5816 -1614
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +399 -1
- package/apps/studio/participants-chrome.tsx +69 -9
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/readiness.ts +220 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +65 -2
- package/apps/studio/sync/agent.ts +81 -1
- package/apps/studio/sync/codec.ts +24 -0
- package/apps/studio/sync/cold-start.ts +40 -0
- package/apps/studio/sync/hub-link.ts +137 -0
- package/apps/studio/test/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +95 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- package/apps/studio/test/canvas-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +35 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -0
- package/apps/studio/test/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csrf-write-guard.test.ts +26 -0
- package/apps/studio/test/editing-presence.test.ts +103 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- package/apps/studio/test/readiness.test.ts +127 -0
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +61 -1
- package/apps/studio/test/tour-overlay.test.tsx +18 -0
- package/apps/studio/tool-palette.tsx +18 -9
- package/apps/studio/use-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +414 -187
- package/apps/studio/whats-new.json +82 -0
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design.mjs +1 -0
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +30 -8
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// Phase 29 (epic E4) Task 3 — the first-run onboarding wizard.
|
|
2
|
+
//
|
|
3
|
+
// Mounts full-screen on first launch (app.jsx checks `app_is_first_run` via the
|
|
4
|
+
// Tauri shell) OVER the empty welcome project the sidecar boots. Three doors,
|
|
5
|
+
// GitHub first: (A) Continue with GitHub — sign in (device flow) → open a shared
|
|
6
|
+
// project or start a new one; (B) Open a folder on this computer; (C) Connect to a
|
|
7
|
+
// team hub (advanced). Every door ends by switching the sidecar to a real project
|
|
8
|
+
// (`open_local_project`), which reloads the webview — at which point first-run is
|
|
9
|
+
// false and the wizard no longer mounts. Built 1:1 with `.design/ui/Onboarding.tsx`;
|
|
10
|
+
// CSS (`ob-*`) lives in client/styles/3-shell-maude.css. Renders nothing outside the
|
|
11
|
+
// native app (no Tauri → no first-run concept).
|
|
12
|
+
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import ReadinessList, { useReadiness } from './ReadinessList.jsx';
|
|
16
|
+
import {
|
|
17
|
+
cloneRepo,
|
|
18
|
+
createProject,
|
|
19
|
+
fetchIdentity,
|
|
20
|
+
getCrashReporting,
|
|
21
|
+
hubLink,
|
|
22
|
+
initDesign,
|
|
23
|
+
isNativeApp,
|
|
24
|
+
isSignedIn,
|
|
25
|
+
listRepos,
|
|
26
|
+
onDeviceCode,
|
|
27
|
+
openLocalProject,
|
|
28
|
+
openVerification,
|
|
29
|
+
pickDirectory,
|
|
30
|
+
setCrashReporting,
|
|
31
|
+
signIn,
|
|
32
|
+
} from '../github.js';
|
|
33
|
+
|
|
34
|
+
function Icon({ name, size = 16, className }) {
|
|
35
|
+
const p = {
|
|
36
|
+
check: <polyline points="3 8.2 6.4 11.5 13 4.2" />,
|
|
37
|
+
'arrow-right': (<><line x1="2.5" y1="8" x2="13" y2="8" /><polyline points="9 4 13 8 9 12" /></>),
|
|
38
|
+
'chevron-right': <polyline points="6 3.5 10.5 8 6 12.5" />,
|
|
39
|
+
'chevron-down': <polyline points="3.5 6 8 10.5 12.5 6" />,
|
|
40
|
+
folder: <path d="M2 4.5h4l1.3 1.5H14V13H2z" />,
|
|
41
|
+
'folder-open': (<><path d="M2 4.5h4l1.3 1.5H14" /><path d="M2 6h12.5l-1.4 7H3.4z" /></>),
|
|
42
|
+
lock: (<><rect x="3.5" y="7" width="9" height="6.5" rx="1.2" /><path d="M5.5 7V5.2a2.5 2.5 0 0 1 5 0V7" /></>),
|
|
43
|
+
globe: (<><circle cx="8" cy="8" r="5.5" /><path d="M2.5 8h11M8 2.5c1.7 1.5 2.6 3.5 2.6 5.5S9.7 12.5 8 13.5C6.3 12 5.4 10 5.4 8S6.3 3.5 8 2.5z" /></>),
|
|
44
|
+
plus: (<><line x1="8" y1="3" x2="8" y2="13" /><line x1="3" y1="8" x2="13" y2="8" /></>),
|
|
45
|
+
download: (<><line x1="8" y1="2.5" x2="8" y2="10" /><polyline points="4.5 7 8 10.5 11.5 7" /><polyline points="3 12.8 3 13.6 13 13.6 13 12.8" /></>),
|
|
46
|
+
link: (<><path d="M6.5 9.5 9.5 6.5" /><path d="M7 4.5 8.4 3.1a2.6 2.6 0 0 1 3.7 3.7L10.7 8.2" /><path d="M9 11.5 7.6 12.9a2.6 2.6 0 0 1-3.7-3.7L5.3 7.8" /></>),
|
|
47
|
+
key: (<><circle cx="5" cy="5" r="2.6" /><path d="M6.9 6.9 13 13M11 11l1.4-1.4M9.2 9.2l1.6-1.6" /></>),
|
|
48
|
+
server: (<><rect x="2.5" y="3" width="11" height="4" rx="1" /><rect x="2.5" y="9" width="11" height="4" rx="1" /><circle cx="5" cy="5" r="0.5" fill="currentColor" /><circle cx="5" cy="11" r="0.5" fill="currentColor" /></>),
|
|
49
|
+
x: (<><line x1="3.5" y1="3.5" x2="12.5" y2="12.5" /><line x1="12.5" y1="3.5" x2="3.5" y2="12.5" /></>),
|
|
50
|
+
copy: (<><rect x="5.5" y="5.5" width="7.5" height="7.5" rx="1.2" /><path d="M3 10.5V3.2A.7.7 0 0 1 3.7 2.5H10" /></>),
|
|
51
|
+
external: (<><path d="M6 3.5H3.2A.7.7 0 0 0 2.5 4.2v8.6a.7.7 0 0 0 .7.7h8.6a.7.7 0 0 0 .7-.7V10" /><line x1="8" y1="8" x2="13" y2="3" /><polyline points="9.5 3 13 3 13 6.5" /></>),
|
|
52
|
+
back: <polyline points="10 3.5 5.5 8 10 12.5" />,
|
|
53
|
+
spinner: <path d="M8 2.2a5.8 5.8 0 1 0 5.8 5.8" />,
|
|
54
|
+
}[name];
|
|
55
|
+
return (
|
|
56
|
+
<svg className={className} width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
57
|
+
{p}
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function GitHubMark({ size = 18 }) {
|
|
63
|
+
return (
|
|
64
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
65
|
+
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.6 7.6 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The maude mark — the canonical spark-on-bubble tile (source of truth:
|
|
71
|
+
// system/maude/preview/logo.tsx + logo.css `.lg-mark`): a four-point star in
|
|
72
|
+
// --accent-fg on the one indigo, bottom-right corner squared (a message bubble),
|
|
73
|
+
// with the in-app accent-tint halo. NOT a hand-drawn glyph.
|
|
74
|
+
function Mark({ size = 30 }) {
|
|
75
|
+
return (
|
|
76
|
+
<span className="ob-mark" style={{ width: size, height: size }} role="img" aria-label="maude">
|
|
77
|
+
<svg viewBox="0 0 32 32" fill="none">
|
|
78
|
+
<path d="M16 5l2.8 8.2L27 16l-8.2 2.8L16 27l-2.8-8.2L5 16l8.2-2.8z" fill="currentColor" />
|
|
79
|
+
</svg>
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Rail({ signedInAs }) {
|
|
85
|
+
return (
|
|
86
|
+
<aside className="ob-rail">
|
|
87
|
+
<div className="ob-rail-brand">
|
|
88
|
+
<Mark size={30} />
|
|
89
|
+
<span className="ob-rail-wordmark">maude</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="ob-rail-lede">
|
|
92
|
+
<p className="ob-rail-h">Design together.<br />No setup, no terminal.</p>
|
|
93
|
+
<p>Open Maude, and you're in a real project in under two minutes — sharing canvases with your team as you go.</p>
|
|
94
|
+
</div>
|
|
95
|
+
{signedInAs ? (
|
|
96
|
+
<div className="ob-rail-signed">
|
|
97
|
+
<span className="ob-rail-signed-dot" aria-hidden="true" />
|
|
98
|
+
<span>Signed in as <b>@{signedInAs}</b></span>
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<ul className="ob-rail-reassure">
|
|
102
|
+
<li><span className="ob-rail-tick" aria-hidden="true"><Icon name="check" size={12} /></span>Sign in once — Maude remembers you</li>
|
|
103
|
+
<li><span className="ob-rail-tick" aria-hidden="true"><Icon name="check" size={12} /></span>Your work saves on its own</li>
|
|
104
|
+
<li><span className="ob-rail-tick" aria-hidden="true"><Icon name="check" size={12} /></span>Invite anyone by their name</li>
|
|
105
|
+
</ul>
|
|
106
|
+
)}
|
|
107
|
+
<div className="ob-rail-foot">You can change any of this later.</div>
|
|
108
|
+
</aside>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Opt-in crash reporting (Phase 32 / Task 4). Default OFF — the box starts
|
|
113
|
+
// unchecked and only writes a local crash log when the user explicitly ticks it.
|
|
114
|
+
// Nothing leaves the machine (local-file backend; no third party, no network).
|
|
115
|
+
function CrashOptIn() {
|
|
116
|
+
const [on, setOn] = useState(false);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
let alive = true;
|
|
119
|
+
getCrashReporting()
|
|
120
|
+
.then((v) => {
|
|
121
|
+
if (alive) setOn(!!v);
|
|
122
|
+
})
|
|
123
|
+
.catch(() => {});
|
|
124
|
+
return () => {
|
|
125
|
+
alive = false;
|
|
126
|
+
};
|
|
127
|
+
}, []);
|
|
128
|
+
return (
|
|
129
|
+
<label className="ob-crash-optin">
|
|
130
|
+
<input
|
|
131
|
+
type="checkbox"
|
|
132
|
+
checked={on}
|
|
133
|
+
onChange={(e) => {
|
|
134
|
+
const next = e.target.checked;
|
|
135
|
+
setOn(next);
|
|
136
|
+
setCrashReporting(next).catch(() => setOn(!next));
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
<span>
|
|
140
|
+
Send crash reports to help improve Maude. Optional — a crash writes a local log
|
|
141
|
+
(stack trace + OS + version, no file contents) you can attach to an issue.
|
|
142
|
+
</span>
|
|
143
|
+
</label>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// AI-editing readiness (Phase 33 / DDR-128). NON-BLOCKING: the bundled core
|
|
148
|
+
// (canvas browser, version history, sharing) works with zero install, so this is a
|
|
149
|
+
// quiet, collapsible strip — never a gate. It surfaces what AI editing additionally
|
|
150
|
+
// needs (a paired Claude Code with the maude plugins + the `maude` CLI) so a missing
|
|
151
|
+
// piece is visible instead of a silent no-op when the user later types /design:edit.
|
|
152
|
+
// Stays silent until the probe answers (endpoint unreachable → render nothing).
|
|
153
|
+
function AiReadiness() {
|
|
154
|
+
const { report, loading, refresh } = useReadiness();
|
|
155
|
+
if (!report) return null;
|
|
156
|
+
const ready = report.ready;
|
|
157
|
+
return (
|
|
158
|
+
<details className="ob-readiness">
|
|
159
|
+
<summary className="ob-readiness-sum">
|
|
160
|
+
<span className={`ob-readiness-dot ob-readiness-dot--${ready ? 'ok' : 'warn'}`} aria-hidden="true" />
|
|
161
|
+
<span className="ob-readiness-lede">
|
|
162
|
+
{ready ? 'AI editing is ready' : 'AI editing needs a couple of things'}
|
|
163
|
+
</span>
|
|
164
|
+
<span className="ob-readiness-hint">
|
|
165
|
+
{ready ? 'optional — view details' : 'optional · the rest of Maude works without it'}
|
|
166
|
+
</span>
|
|
167
|
+
</summary>
|
|
168
|
+
<div className="ob-readiness-body">
|
|
169
|
+
<p className="ob-readiness-note">
|
|
170
|
+
Everything else — the canvas browser, version history, sharing — works right now. AI
|
|
171
|
+
editing additionally pairs with a Claude Code you already have installed and runs on your
|
|
172
|
+
own Pro/Max subscription.
|
|
173
|
+
</p>
|
|
174
|
+
<ReadinessList report={report} loading={loading} refresh={refresh} />
|
|
175
|
+
</div>
|
|
176
|
+
</details>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── A · Welcome (door picker) ────────────────────────────────────────────────
|
|
181
|
+
function Welcome({ onGithub, onLocal, onHub, signing }) {
|
|
182
|
+
return (
|
|
183
|
+
<main className="ob-main">
|
|
184
|
+
<header className="ob-head">
|
|
185
|
+
<span className="ob-eyebrow">Welcome</span>
|
|
186
|
+
<h1>How would you like to start?</h1>
|
|
187
|
+
<p>Most people sign in with GitHub — it's the simplest way to work with a team.</p>
|
|
188
|
+
</header>
|
|
189
|
+
<div className="ob-doors">
|
|
190
|
+
<button type="button" className="ob-door ob-door--primary" aria-label="Continue with GitHub — recommended" onClick={onGithub} disabled={signing}>
|
|
191
|
+
<span className="ob-door-icon ob-door-icon--gh"><GitHubMark size={26} /></span>
|
|
192
|
+
<span className="ob-door-tx">
|
|
193
|
+
<span className="ob-door-title">Continue with GitHub<span className="ob-door-tag">Recommended</span></span>
|
|
194
|
+
<span className="ob-door-sub">Start a shared project, or open one a teammate shared with you.</span>
|
|
195
|
+
</span>
|
|
196
|
+
<span className="ob-door-cta">
|
|
197
|
+
<span className="btn btn--primary ob-door-btn"><GitHubMark size={15} /> {signing ? 'Starting…' : 'Sign in with GitHub'}</span>
|
|
198
|
+
</span>
|
|
199
|
+
</button>
|
|
200
|
+
<button type="button" className="ob-door" aria-label="Open a folder on this computer" onClick={onLocal}>
|
|
201
|
+
<span className="ob-door-icon"><Icon name="folder-open" size={22} /></span>
|
|
202
|
+
<span className="ob-door-tx">
|
|
203
|
+
<span className="ob-door-title">Open a folder on this computer</span>
|
|
204
|
+
<span className="ob-door-sub">Already have a project folder? Open it and keep designing.</span>
|
|
205
|
+
</span>
|
|
206
|
+
<span className="ob-door-go" aria-hidden="true"><Icon name="chevron-right" size={16} /></span>
|
|
207
|
+
</button>
|
|
208
|
+
<button type="button" className="ob-door ob-door--advanced" aria-label="Connect to a team hub — advanced" onClick={onHub}>
|
|
209
|
+
<span className="ob-door-icon ob-door-icon--quiet"><Icon name="server" size={18} /></span>
|
|
210
|
+
<span className="ob-door-tx">
|
|
211
|
+
<span className="ob-door-title">Connect to a team hub <span className="ob-door-adv">Advanced</span></span>
|
|
212
|
+
<span className="ob-door-sub">Your team runs its own Maude hub? Paste the link they gave you.</span>
|
|
213
|
+
</span>
|
|
214
|
+
<span className="ob-door-go" aria-hidden="true"><Icon name="chevron-right" size={15} /></span>
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
<p className="ob-foot-note">Maude never touches the terminal. Everything here happens in the app.</p>
|
|
218
|
+
<AiReadiness />
|
|
219
|
+
<CrashOptIn />
|
|
220
|
+
</main>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function BackBar({ onBack }) {
|
|
225
|
+
return (
|
|
226
|
+
<button type="button" className="ob-back" onClick={onBack}>
|
|
227
|
+
<Icon name="back" size={14} /> Back
|
|
228
|
+
</button>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── B · GitHub door (pick or create) ─────────────────────────────────────────
|
|
233
|
+
function GitHubDoor({ identity, onBack }) {
|
|
234
|
+
const [repos, setRepos] = useState(null);
|
|
235
|
+
const [err, setErr] = useState('');
|
|
236
|
+
const [busy, setBusy] = useState('');
|
|
237
|
+
const [creating, setCreating] = useState(false);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
(async () => {
|
|
241
|
+
const r = await listRepos();
|
|
242
|
+
if (r.ok && r.json?.ok) setRepos(r.json.repos || []);
|
|
243
|
+
else { setErr(r.json?.error || "Couldn't load your projects."); setRepos([]); }
|
|
244
|
+
})();
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
async function open(repo) {
|
|
248
|
+
setErr(''); setBusy(repo.full_name);
|
|
249
|
+
try {
|
|
250
|
+
const parentDir = await pickDirectory();
|
|
251
|
+
if (!parentDir) { setBusy(''); return; }
|
|
252
|
+
const r = await cloneRepo({ cloneUrl: repo.clone_url, parentDir, name: repo.name });
|
|
253
|
+
if (!(r.ok && r.json?.ok)) { setErr(r.json?.error || "Couldn't open that project."); setBusy(''); return; }
|
|
254
|
+
if (r.json.hasDesign === false) {
|
|
255
|
+
const init = await initDesign(r.json.path);
|
|
256
|
+
if (!(init.ok && init.json?.ok)) { setErr(init.json?.error || "Couldn't set it up."); setBusy(''); return; }
|
|
257
|
+
}
|
|
258
|
+
await openLocalProject(r.json.path); // switches the sidecar → webview reloads
|
|
259
|
+
} catch (e) {
|
|
260
|
+
setErr(String(e?.message || e || 'Something went wrong opening the project.'));
|
|
261
|
+
setBusy('');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (creating) return <CreateInline identity={identity} onBack={() => setCreating(false)} />;
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<main className="ob-main">
|
|
269
|
+
<BackBar onBack={onBack} />
|
|
270
|
+
<header className="ob-head">
|
|
271
|
+
<span className="ob-eyebrow">You're signed in</span>
|
|
272
|
+
<h1>Open a project, or start a new one</h1>
|
|
273
|
+
<p>Pick up a shared project below, or create a fresh one — it's private until you invite someone.</p>
|
|
274
|
+
</header>
|
|
275
|
+
<button type="button" className="ob-create" aria-label="Start a new project" onClick={() => setCreating(true)}>
|
|
276
|
+
<span className="ob-create-icon"><Icon name="plus" size={20} /></span>
|
|
277
|
+
<span className="ob-create-tx">
|
|
278
|
+
<span className="ob-create-title">Start a new project</span>
|
|
279
|
+
<span className="ob-create-sub">A blank, private project — you choose where it lives.</span>
|
|
280
|
+
</span>
|
|
281
|
+
<span className="btn btn--primary ob-create-btn"><Icon name="arrow-right" size={15} /> Create</span>
|
|
282
|
+
</button>
|
|
283
|
+
<div className="ob-section-label">Shared with you</div>
|
|
284
|
+
{err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
|
|
285
|
+
{repos === null && <div className="ob-foot-note">Loading your projects…</div>}
|
|
286
|
+
{repos && repos.length === 0 && !err && <div className="ob-foot-note">No shared projects yet — create one to get started.</div>}
|
|
287
|
+
{repos && repos.length > 0 && (
|
|
288
|
+
<div className="ob-repolist" role="group" aria-label="Shared projects you can open">
|
|
289
|
+
{repos.map((r) => {
|
|
290
|
+
const isBusy = busy === r.full_name;
|
|
291
|
+
return (
|
|
292
|
+
<button type="button" className="ob-repo" key={r.full_name} disabled={!!busy} onClick={() => open(r)}>
|
|
293
|
+
<span className="ob-repo-icon"><Icon name={r.private ? 'lock' : 'globe'} size={15} /></span>
|
|
294
|
+
<span className="ob-repo-tx">
|
|
295
|
+
<span className="ob-repo-name">{r.name}</span>
|
|
296
|
+
<span className="ob-repo-meta">{isBusy ? 'Opening…' : `${r.owner} · updated ${new Date(r.updated_at).toLocaleDateString()}`}</span>
|
|
297
|
+
</span>
|
|
298
|
+
<span className="ob-repo-go">{isBusy ? <Icon name="spinner" size={16} className="ob-spin" /> : <><Icon name="download" size={16} /> Open</>}</span>
|
|
299
|
+
</button>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
<p className="ob-foot-note">Opening a project saves a copy on this computer and keeps it in sync.</p>
|
|
305
|
+
</main>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function CreateInline({ identity, onBack }) {
|
|
310
|
+
const [name, setName] = useState('');
|
|
311
|
+
const [isPrivate, setIsPrivate] = useState(true);
|
|
312
|
+
const [busy, setBusy] = useState(false);
|
|
313
|
+
const [step, setStep] = useState('');
|
|
314
|
+
const [err, setErr] = useState('');
|
|
315
|
+
const slug = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
316
|
+
const owner = identity?.login || 'you';
|
|
317
|
+
|
|
318
|
+
async function submit() {
|
|
319
|
+
setErr(''); setBusy(true);
|
|
320
|
+
try {
|
|
321
|
+
setStep('Choose where to save it…');
|
|
322
|
+
const parentDir = await pickDirectory();
|
|
323
|
+
if (!parentDir) { setBusy(false); setStep(''); return; }
|
|
324
|
+
setStep('Creating your project on GitHub…');
|
|
325
|
+
const r = await createProject({ name, private: isPrivate, parentDir });
|
|
326
|
+
if (!(r.ok && r.json?.ok)) { setErr(r.json?.error || "Couldn't create the project."); setBusy(false); setStep(''); return; }
|
|
327
|
+
setStep('Opening it in Maude…');
|
|
328
|
+
await openLocalProject(r.json.path);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
setErr(String(e?.message || e || 'Something went wrong creating the project.'));
|
|
331
|
+
setBusy(false); setStep('');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<main className="ob-main">
|
|
337
|
+
<BackBar onBack={onBack} />
|
|
338
|
+
<header className="ob-head">
|
|
339
|
+
<span className="ob-eyebrow">New project</span>
|
|
340
|
+
<h1>Start a new project</h1>
|
|
341
|
+
<p>A private project, set up for you. You can invite people whenever you're ready.</p>
|
|
342
|
+
</header>
|
|
343
|
+
<div className="ob-form">
|
|
344
|
+
<label className="ob-field">
|
|
345
|
+
<span className="ob-field-label">Project name</span>
|
|
346
|
+
<input className="input ob-input" type="text" value={name} placeholder="Acme Rebrand" aria-label="Project name" onChange={(e) => setName(e.target.value)} />
|
|
347
|
+
{slug && <span className="ob-foot-note">Creates <b>github.com/{owner}/{slug}</b></span>}
|
|
348
|
+
</label>
|
|
349
|
+
<div className="ob-field">
|
|
350
|
+
<span className="ob-field-label">Who can see it</span>
|
|
351
|
+
<div className="seg" role="group" aria-label="Project visibility">
|
|
352
|
+
<button type="button" aria-pressed={isPrivate} onClick={() => setIsPrivate(true)}><Icon name="lock" size={14} /> Private</button>
|
|
353
|
+
<button type="button" aria-pressed={!isPrivate} onClick={() => setIsPrivate(false)}><Icon name="globe" size={14} /> Public</button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
{err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
|
|
357
|
+
<div className="ob-form-actions">
|
|
358
|
+
<button type="button" className="btn btn--primary" onClick={submit} disabled={busy || !slug}>
|
|
359
|
+
<Icon name="plus" size={15} /> {busy ? step || 'Creating…' : 'Create project'}
|
|
360
|
+
</button>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</main>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── C · Local folder door ────────────────────────────────────────────────────
|
|
368
|
+
function LocalDoor({ onBack }) {
|
|
369
|
+
const [busy, setBusy] = useState(false);
|
|
370
|
+
const [err, setErr] = useState('');
|
|
371
|
+
const [needsSetup, setNeedsSetup] = useState(null);
|
|
372
|
+
|
|
373
|
+
async function choose() {
|
|
374
|
+
setErr(''); setBusy(true);
|
|
375
|
+
try {
|
|
376
|
+
const dir = await pickDirectory();
|
|
377
|
+
if (!dir) { setBusy(false); return; }
|
|
378
|
+
try {
|
|
379
|
+
await openLocalProject(dir); // switches if it has a .design/
|
|
380
|
+
} catch (e) {
|
|
381
|
+
// open_local_project rejects a non-Maude folder — offer to set it up.
|
|
382
|
+
setNeedsSetup(dir);
|
|
383
|
+
setBusy(false);
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {
|
|
386
|
+
setErr(String(e?.message || e || "Couldn't open that folder."));
|
|
387
|
+
setBusy(false);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function setupHere() {
|
|
392
|
+
setErr(''); setBusy(true);
|
|
393
|
+
try {
|
|
394
|
+
const r = await initDesign(needsSetup);
|
|
395
|
+
if (!(r.ok && r.json?.ok)) { setErr(r.json?.error || "Couldn't set it up."); setBusy(false); return; }
|
|
396
|
+
await openLocalProject(needsSetup);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
setErr(String(e?.message || e || "Couldn't set it up."));
|
|
399
|
+
setBusy(false);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<main className="ob-main">
|
|
405
|
+
<BackBar onBack={onBack} />
|
|
406
|
+
<header className="ob-head">
|
|
407
|
+
<span className="ob-eyebrow">From this computer</span>
|
|
408
|
+
<h1>Open a project folder</h1>
|
|
409
|
+
<p>Choose a folder on your computer. We'll check it's a Maude project before opening.</p>
|
|
410
|
+
</header>
|
|
411
|
+
{needsSetup ? (
|
|
412
|
+
<>
|
|
413
|
+
<div className="callout callout--info ob-callout">
|
|
414
|
+
<span className="ob-callout-glyph" style={{ color: 'var(--status-info)' }}><Icon name="folder-open" size={15} /></span>
|
|
415
|
+
<span>That folder isn't a Maude project yet. Set up Maude in it to start designing (you can build a design system after).</span>
|
|
416
|
+
</div>
|
|
417
|
+
{err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
|
|
418
|
+
<div className="ob-form-actions">
|
|
419
|
+
<button type="button" className="btn btn--ghost" onClick={() => setNeedsSetup(null)} disabled={busy}>Pick a different folder</button>
|
|
420
|
+
<button type="button" className="btn btn--primary" onClick={setupHere} disabled={busy}><Icon name="folder" size={15} /> {busy ? 'Setting up…' : 'Set up Maude here'}</button>
|
|
421
|
+
</div>
|
|
422
|
+
</>
|
|
423
|
+
) : (
|
|
424
|
+
<>
|
|
425
|
+
<button type="button" className="ob-drop" onClick={choose} disabled={busy} aria-label="Choose a project folder">
|
|
426
|
+
<span className="ob-drop-glyph"><Icon name="folder-open" size={34} /></span>
|
|
427
|
+
<span className="ob-drop-title">{busy ? 'Opening…' : 'Choose a project folder'}</span>
|
|
428
|
+
<span className="ob-drop-or">click to browse</span>
|
|
429
|
+
</button>
|
|
430
|
+
{err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
|
|
431
|
+
<p className="ob-foot-note">To open a project a teammate shared, use <b>Continue with GitHub</b> instead.</p>
|
|
432
|
+
</>
|
|
433
|
+
)}
|
|
434
|
+
</main>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── D · Hub door (advanced) ──────────────────────────────────────────────────
|
|
439
|
+
function HubDoor({ onBack }) {
|
|
440
|
+
const [url, setUrl] = useState('');
|
|
441
|
+
const [token, setToken] = useState('');
|
|
442
|
+
const [busy, setBusy] = useState(false);
|
|
443
|
+
const [err, setErr] = useState('');
|
|
444
|
+
const [linked, setLinked] = useState(null);
|
|
445
|
+
|
|
446
|
+
async function connect() {
|
|
447
|
+
setErr(''); setBusy(true);
|
|
448
|
+
try {
|
|
449
|
+
const r = await hubLink({ url: url.trim(), token: token.trim() });
|
|
450
|
+
setBusy(false);
|
|
451
|
+
if (r.ok && r.json?.ok) setLinked(r.json);
|
|
452
|
+
else setErr(r.json?.error || "Couldn't connect to the hub.");
|
|
453
|
+
} catch (e) {
|
|
454
|
+
setBusy(false);
|
|
455
|
+
setErr(String(e?.message || e || "Couldn't connect to the hub."));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<main className="ob-main">
|
|
461
|
+
<BackBar onBack={onBack} />
|
|
462
|
+
<header className="ob-head">
|
|
463
|
+
<span className="ob-eyebrow">Advanced</span>
|
|
464
|
+
<h1>Connect to a team hub</h1>
|
|
465
|
+
<p>For teams running their own Maude hub. Paste the address and the access token your team gave you.</p>
|
|
466
|
+
</header>
|
|
467
|
+
<div className="ob-form">
|
|
468
|
+
<label className="ob-field">
|
|
469
|
+
<span className="ob-field-label">Hub address</span>
|
|
470
|
+
<span className="ob-field-wrap"><Icon name="server" size={14} className="ob-field-pre" />
|
|
471
|
+
<input className="input ob-input" type="text" value={url} placeholder="https://hub.yourteam.dev" aria-label="Hub address" onChange={(e) => setUrl(e.target.value)} />
|
|
472
|
+
</span>
|
|
473
|
+
</label>
|
|
474
|
+
<label className="ob-field">
|
|
475
|
+
<span className="ob-field-label">Access token</span>
|
|
476
|
+
<span className="ob-field-wrap"><Icon name="key" size={14} className="ob-field-pre" />
|
|
477
|
+
<input className="input ob-input" type="text" value={token} placeholder="paste the access token your team gave you" aria-label="Access token" onChange={(e) => setToken(e.target.value)} />
|
|
478
|
+
</span>
|
|
479
|
+
</label>
|
|
480
|
+
{err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
|
|
481
|
+
{linked ? (
|
|
482
|
+
<div className="callout callout--success ob-callout">
|
|
483
|
+
<span className="ob-callout-glyph" style={{ color: 'var(--status-success)' }}><Icon name="check" size={15} /></span>
|
|
484
|
+
<span><b style={{ color: 'var(--fg-0)' }}>Connected to {linked.url.replace(/^https?:\/\//, '')}.</b> {linked.healthy ? 'Your hub is reachable.' : 'Saved — we couldn’t reach it just now, but it’ll sync when it’s up.'} Now open your team’s project with <b>Continue with GitHub</b> or <b>Open a folder</b> — it’ll sync to this hub automatically.</span>
|
|
485
|
+
</div>
|
|
486
|
+
) : (
|
|
487
|
+
<div className="ob-form-actions">
|
|
488
|
+
<button type="button" className="btn btn--primary" onClick={connect} disabled={busy || !url.trim() || !token.trim()}>
|
|
489
|
+
<Icon name="link" size={15} /> {busy ? 'Connecting…' : 'Connect'}
|
|
490
|
+
</button>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
<p className="ob-foot-note">Most people use <b>Continue with GitHub</b> — you only need a hub if your team runs one.</p>
|
|
495
|
+
</main>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── device-code modal (reused from IdentityBar) ──────────────────────────────
|
|
500
|
+
function DeviceCodeModal({ device, onClose }) {
|
|
501
|
+
const [copied, setCopied] = useState(false);
|
|
502
|
+
function copyCode() {
|
|
503
|
+
if (!device?.user_code) return;
|
|
504
|
+
navigator.clipboard?.writeText(device.user_code).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }, () => {});
|
|
505
|
+
}
|
|
506
|
+
return (
|
|
507
|
+
<div className="gi-modal" role="dialog" aria-modal="true" aria-label="Sign in with GitHub" onKeyDown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
|
508
|
+
<div className="gi-scrim" aria-hidden="true" onClick={onClose} />
|
|
509
|
+
<div className="gi-dialog gi-dialog--code">
|
|
510
|
+
<div className="gi-dc-head">
|
|
511
|
+
<span className="gi-dc-marks"><GitHubMark size={26} /></span>
|
|
512
|
+
<h2>Sign in with GitHub</h2>
|
|
513
|
+
<p>Maude opened GitHub in your browser. Enter this code to connect your account.</p>
|
|
514
|
+
</div>
|
|
515
|
+
<ol className="gi-dc-steps">
|
|
516
|
+
<li>
|
|
517
|
+
<span className="gi-dc-step-n">1</span>
|
|
518
|
+
<span className="gi-dc-step-tx">
|
|
519
|
+
Go to <span className="gi-dc-url">{(device.verification_uri || 'github.com/login/device').replace(/^https?:\/\//, '')}</span>
|
|
520
|
+
<button type="button" className="btn btn--ghost btn--sm gi-dc-open" onClick={() => openVerification().catch(() => {})}><Icon name="external" size={14} /> Open it again</button>
|
|
521
|
+
</span>
|
|
522
|
+
</li>
|
|
523
|
+
<li><span className="gi-dc-step-n">2</span><span className="gi-dc-step-tx">Enter this code to connect Maude</span></li>
|
|
524
|
+
</ol>
|
|
525
|
+
<div className="gi-code">
|
|
526
|
+
<span className="gi-code-val">{device.user_code}</span>
|
|
527
|
+
<button type="button" className="btn btn--ghost gi-code-copy" onClick={copyCode} aria-label="Copy the code"><Icon name="copy" size={15} /> {copied ? 'Copied' : 'Copy'}</button>
|
|
528
|
+
</div>
|
|
529
|
+
<div className="gi-dc-status" aria-live="polite"><span className="gi-pulse" aria-hidden="true" /><span>Waiting for you to authorize in your browser…</span></div>
|
|
530
|
+
<div className="gi-dc-foot"><button type="button" className="btn btn--ghost" onClick={onClose}>Cancel</button><span className="gi-dc-foot-note">Nothing is stored until you authorize.</span></div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export default function OnboardingWizard() {
|
|
537
|
+
const native = isNativeApp();
|
|
538
|
+
const [door, setDoor] = useState('welcome'); // welcome | github | local | hub
|
|
539
|
+
const [identity, setIdentity] = useState(null);
|
|
540
|
+
const [signedIn, setSignedIn] = useState(false);
|
|
541
|
+
const [signing, setSigning] = useState(false);
|
|
542
|
+
const [device, setDevice] = useState(null);
|
|
543
|
+
const [err, setErr] = useState('');
|
|
544
|
+
const unlistenRef = useRef(null);
|
|
545
|
+
|
|
546
|
+
// If already signed in (re-onboarding), skip the sign-in step on the GitHub door.
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
let alive = true;
|
|
549
|
+
if (!native) return undefined;
|
|
550
|
+
(async () => {
|
|
551
|
+
try {
|
|
552
|
+
if (await isSignedIn()) {
|
|
553
|
+
const r = await fetchIdentity();
|
|
554
|
+
if (alive && r.ok && r.json?.ok) { setIdentity({ login: r.json.login, name: r.json.name }); setSignedIn(true); }
|
|
555
|
+
}
|
|
556
|
+
} catch { /* not signed in */ }
|
|
557
|
+
})();
|
|
558
|
+
return () => { alive = false; unlistenRef.current?.then?.((fn) => fn?.()); };
|
|
559
|
+
}, [native]);
|
|
560
|
+
|
|
561
|
+
async function handleGithub() {
|
|
562
|
+
if (signedIn) { setDoor('github'); return; }
|
|
563
|
+
setErr(''); setSigning(true); setDevice(null);
|
|
564
|
+
try {
|
|
565
|
+
unlistenRef.current = onDeviceCode((p) => setDevice(p));
|
|
566
|
+
const login = await signIn();
|
|
567
|
+
const r = await fetchIdentity();
|
|
568
|
+
setIdentity(r.ok && r.json?.ok ? { login: r.json.login, name: r.json.name } : { login });
|
|
569
|
+
setSignedIn(true);
|
|
570
|
+
setDoor('github');
|
|
571
|
+
} catch (e) {
|
|
572
|
+
setErr(String(e?.message || e || "Sign-in didn't finish. Please try again."));
|
|
573
|
+
} finally {
|
|
574
|
+
setSigning(false);
|
|
575
|
+
setDevice(null);
|
|
576
|
+
unlistenRef.current?.then?.((fn) => fn?.());
|
|
577
|
+
unlistenRef.current = null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!native) return null;
|
|
582
|
+
|
|
583
|
+
return (
|
|
584
|
+
<div className="ob-overlay" role="dialog" aria-modal="true" aria-label="Welcome to Maude">
|
|
585
|
+
<div className="ob-shell">
|
|
586
|
+
<Rail signedInAs={signedIn ? identity?.login : null} />
|
|
587
|
+
{door === 'welcome' && <Welcome signing={signing} onGithub={handleGithub} onLocal={() => setDoor('local')} onHub={() => setDoor('hub')} />}
|
|
588
|
+
{door === 'github' && <GitHubDoor identity={identity} onBack={() => setDoor('welcome')} />}
|
|
589
|
+
{door === 'local' && <LocalDoor onBack={() => setDoor('welcome')} />}
|
|
590
|
+
{door === 'hub' && <HubDoor onBack={() => setDoor('welcome')} />}
|
|
591
|
+
</div>
|
|
592
|
+
{err && door === 'welcome' && (
|
|
593
|
+
<div className="ob-toast callout callout--error" role="alert"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>
|
|
594
|
+
)}
|
|
595
|
+
{device && <DeviceCodeModal device={device} onClose={() => setDevice(null)} />}
|
|
596
|
+
</div>
|
|
597
|
+
);
|
|
598
|
+
}
|