create-eventus-app 0.1.1 → 0.1.2
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/package.json +1 -1
- package/template/README.md +6 -0
- package/template/package.json +2 -0
- package/template/src/App.tsx +136 -5
- package/template/src/dev-preview.ts +102 -0
- package/template/src/styles.css +81 -0
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -15,6 +15,12 @@ pnpm dev
|
|
|
15
15
|
- `pnpm build`: type-check and build the app
|
|
16
16
|
- `pnpm preview`: preview the production build locally
|
|
17
17
|
|
|
18
|
+
## Local Live Preview
|
|
19
|
+
|
|
20
|
+
Run `pnpm dev`, open the app in your desktop browser, and scan the QR code from
|
|
21
|
+
Eventus X using the `Developer` screen. Your phone and computer must be on the
|
|
22
|
+
same local network for the live preview URL to be reachable.
|
|
23
|
+
|
|
18
24
|
## Eventus Files
|
|
19
25
|
|
|
20
26
|
- `eventus.manifest.json`: base Eventus manifest consumed by the Vite plugin
|
package/template/package.json
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@eventusgo/sdk": "__EVENTUS_SDK_VERSION__",
|
|
13
|
+
"qrcode": "1.5.4",
|
|
13
14
|
"react": "18.3.1",
|
|
14
15
|
"react-dom": "18.3.1"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
18
|
+
"@types/qrcode": "1.5.5",
|
|
17
19
|
"@eventusgo/vite-plugin": "__EVENTUS_VITE_PLUGIN_VERSION__",
|
|
18
20
|
"@types/react": "18.3.28",
|
|
19
21
|
"@types/react-dom": "18.3.7",
|
package/template/src/App.tsx
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import manifest from "virtual:eventus-manifest";
|
|
3
|
+
import QRCode from "qrcode";
|
|
3
4
|
import {
|
|
4
5
|
eventus,
|
|
5
6
|
type EventusContext,
|
|
6
7
|
type EventusRuntimeSource,
|
|
7
8
|
} from "@eventusgo/sdk";
|
|
9
|
+
import {
|
|
10
|
+
fetchDevPreviewDescriptor,
|
|
11
|
+
isBridgeUnavailableError,
|
|
12
|
+
type EventusDevPreviewDescriptor,
|
|
13
|
+
} from "./dev-preview";
|
|
8
14
|
|
|
9
15
|
type LoadState =
|
|
10
16
|
| { status: "idle" | "loading" }
|
|
11
17
|
| { status: "ready"; context: EventusContext }
|
|
18
|
+
| {
|
|
19
|
+
status: "bridge-unavailable";
|
|
20
|
+
preview: EventusDevPreviewDescriptor | null;
|
|
21
|
+
}
|
|
12
22
|
| { status: "error"; message: string };
|
|
13
23
|
|
|
14
24
|
export default function App() {
|
|
15
25
|
const [state, setState] = useState<LoadState>({ status: "loading" });
|
|
16
|
-
const [runtimeSource, setRuntimeSource] = useState<
|
|
17
|
-
"loading"
|
|
18
|
-
);
|
|
26
|
+
const [runtimeSource, setRuntimeSource] = useState<
|
|
27
|
+
EventusRuntimeSource | "bridge-unavailable" | "loading"
|
|
28
|
+
>("loading");
|
|
19
29
|
|
|
20
30
|
useEffect(() => {
|
|
21
31
|
let cancelled = false;
|
|
@@ -41,10 +51,26 @@ export default function App() {
|
|
|
41
51
|
const context = await eventus.getContext();
|
|
42
52
|
|
|
43
53
|
if (!cancelled && !receivedReadyEvent) {
|
|
44
|
-
setRuntimeSource("mock");
|
|
54
|
+
setRuntimeSource(window.Eventus ? "native" : "mock");
|
|
45
55
|
setState({ status: "ready", context });
|
|
46
56
|
}
|
|
47
57
|
} catch (error) {
|
|
58
|
+
if (receivedReadyEvent || cancelled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isBridgeUnavailableError(error)) {
|
|
63
|
+
const preview = await fetchDevPreviewDescriptor();
|
|
64
|
+
if (!cancelled && !receivedReadyEvent) {
|
|
65
|
+
setRuntimeSource("bridge-unavailable");
|
|
66
|
+
setState({
|
|
67
|
+
status: "bridge-unavailable",
|
|
68
|
+
preview,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
if (!cancelled) {
|
|
49
75
|
setState({
|
|
50
76
|
status: "error",
|
|
@@ -68,7 +94,9 @@ export default function App() {
|
|
|
68
94
|
runtimeSource === "native"
|
|
69
95
|
? "Native bridge detected"
|
|
70
96
|
: runtimeSource === "mock"
|
|
71
|
-
? "
|
|
97
|
+
? "Explicit mock mode active"
|
|
98
|
+
: runtimeSource === "bridge-unavailable"
|
|
99
|
+
? "Open this mini app inside Eventus X"
|
|
72
100
|
: "Detecting runtime…";
|
|
73
101
|
|
|
74
102
|
return (
|
|
@@ -90,6 +118,10 @@ export default function App() {
|
|
|
90
118
|
<p className="error-text">{state.message}</p>
|
|
91
119
|
) : null}
|
|
92
120
|
|
|
121
|
+
{state.status === "bridge-unavailable" ? (
|
|
122
|
+
<DeveloperHandoff preview={state.preview} />
|
|
123
|
+
) : null}
|
|
124
|
+
|
|
93
125
|
{state.status === "ready" ? (
|
|
94
126
|
<dl className="details-grid">
|
|
95
127
|
<div>
|
|
@@ -120,3 +152,102 @@ export default function App() {
|
|
|
120
152
|
</main>
|
|
121
153
|
);
|
|
122
154
|
}
|
|
155
|
+
|
|
156
|
+
function DeveloperHandoff({
|
|
157
|
+
preview,
|
|
158
|
+
}: {
|
|
159
|
+
preview: EventusDevPreviewDescriptor | null;
|
|
160
|
+
}) {
|
|
161
|
+
if (!preview) {
|
|
162
|
+
return (
|
|
163
|
+
<div className="handoff-panel">
|
|
164
|
+
<h2>Open inside Eventus X</h2>
|
|
165
|
+
<p>
|
|
166
|
+
This mini app needs the Eventus bridge to access the real runtime.
|
|
167
|
+
Open it from Eventus X to test it with production-like behavior.
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div className="handoff-panel">
|
|
175
|
+
<div className="handoff-copy">
|
|
176
|
+
<h2>Live preview from your phone</h2>
|
|
177
|
+
<p>
|
|
178
|
+
Open Eventus X, go to <strong>Developer</strong>, and scan this QR
|
|
179
|
+
code to launch the mini app with the real Eventus bridge.
|
|
180
|
+
</p>
|
|
181
|
+
<dl className="preview-grid">
|
|
182
|
+
<div>
|
|
183
|
+
<dt>Preview URL</dt>
|
|
184
|
+
<dd>
|
|
185
|
+
<code>{preview.urls.preview}</code>
|
|
186
|
+
</dd>
|
|
187
|
+
</div>
|
|
188
|
+
<div>
|
|
189
|
+
<dt>Local URL</dt>
|
|
190
|
+
<dd>
|
|
191
|
+
<code>{preview.urls.local}</code>
|
|
192
|
+
</dd>
|
|
193
|
+
</div>
|
|
194
|
+
{preview.urls.network ? (
|
|
195
|
+
<div>
|
|
196
|
+
<dt>Network URL</dt>
|
|
197
|
+
<dd>
|
|
198
|
+
<code>{preview.urls.network}</code>
|
|
199
|
+
</dd>
|
|
200
|
+
</div>
|
|
201
|
+
) : null}
|
|
202
|
+
<div>
|
|
203
|
+
<dt>Permissions</dt>
|
|
204
|
+
<dd>{preview.manifest.permissions?.join(", ") ?? "None"}</dd>
|
|
205
|
+
</div>
|
|
206
|
+
</dl>
|
|
207
|
+
<p className="hint-text">
|
|
208
|
+
Your phone and computer must be on the same local network.
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
<QrPanel payload={preview} />
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function QrPanel({
|
|
217
|
+
payload,
|
|
218
|
+
}: {
|
|
219
|
+
payload: EventusDevPreviewDescriptor;
|
|
220
|
+
}) {
|
|
221
|
+
const [svg, setSvg] = useState("");
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
let cancelled = false;
|
|
225
|
+
|
|
226
|
+
void (async () => {
|
|
227
|
+
const nextSvg = await QRCode.toString(JSON.stringify(payload), {
|
|
228
|
+
type: "svg",
|
|
229
|
+
width: 220,
|
|
230
|
+
margin: 1,
|
|
231
|
+
});
|
|
232
|
+
if (!cancelled) {
|
|
233
|
+
setSvg(nextSvg);
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
|
|
237
|
+
return () => {
|
|
238
|
+
cancelled = true;
|
|
239
|
+
};
|
|
240
|
+
}, [payload]);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className="qr-panel">
|
|
244
|
+
<div
|
|
245
|
+
className="qr-code"
|
|
246
|
+
aria-label="Eventus live preview QR code"
|
|
247
|
+
dangerouslySetInnerHTML={{
|
|
248
|
+
__html: svg || "<div class='qr-placeholder'>Generating QR…</div>",
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type EventusDevPreviewDescriptor = {
|
|
2
|
+
type: "eventus-dev-preview";
|
|
3
|
+
version: 1;
|
|
4
|
+
manifest: {
|
|
5
|
+
name: string;
|
|
6
|
+
appId: string;
|
|
7
|
+
version: string;
|
|
8
|
+
permissions?: string[];
|
|
9
|
+
};
|
|
10
|
+
urls: {
|
|
11
|
+
local: string;
|
|
12
|
+
network: string | null;
|
|
13
|
+
preview: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function isBridgeUnavailableError(error: unknown): boolean {
|
|
18
|
+
return (
|
|
19
|
+
error instanceof Error && error.name === "EventusBridgeUnavailableError"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchDevPreviewDescriptor(): Promise<EventusDevPreviewDescriptor | null> {
|
|
24
|
+
if (typeof fetch !== "function") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch("/__eventus__/preview.json", {
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const json = (await response.json()) as unknown;
|
|
39
|
+
return parseDevPreviewDescriptor(json);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseDevPreviewDescriptor(
|
|
46
|
+
value: unknown,
|
|
47
|
+
): EventusDevPreviewDescriptor | null {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const descriptor = value as Record<string, unknown>;
|
|
53
|
+
const manifest = descriptor.manifest;
|
|
54
|
+
const urls = descriptor.urls;
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
descriptor.type !== "eventus-dev-preview" ||
|
|
58
|
+
descriptor.version !== 1 ||
|
|
59
|
+
!manifest ||
|
|
60
|
+
typeof manifest !== "object" ||
|
|
61
|
+
Array.isArray(manifest) ||
|
|
62
|
+
!urls ||
|
|
63
|
+
typeof urls !== "object" ||
|
|
64
|
+
Array.isArray(urls)
|
|
65
|
+
) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nextManifest = manifest as Record<string, unknown>;
|
|
70
|
+
const nextUrls = urls as Record<string, unknown>;
|
|
71
|
+
const permissions = nextManifest.permissions;
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
typeof nextManifest.name !== "string" ||
|
|
75
|
+
typeof nextManifest.appId !== "string" ||
|
|
76
|
+
typeof nextManifest.version !== "string" ||
|
|
77
|
+
typeof nextUrls.local !== "string" ||
|
|
78
|
+
(nextUrls.network !== null && typeof nextUrls.network !== "string") ||
|
|
79
|
+
typeof nextUrls.preview !== "string" ||
|
|
80
|
+
(permissions !== undefined &&
|
|
81
|
+
(!Array.isArray(permissions) ||
|
|
82
|
+
permissions.some((permission) => typeof permission !== "string")))
|
|
83
|
+
) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
type: "eventus-dev-preview",
|
|
89
|
+
version: 1,
|
|
90
|
+
manifest: {
|
|
91
|
+
name: nextManifest.name,
|
|
92
|
+
appId: nextManifest.appId,
|
|
93
|
+
version: nextManifest.version,
|
|
94
|
+
...(permissions ? { permissions: permissions as string[] } : {}),
|
|
95
|
+
},
|
|
96
|
+
urls: {
|
|
97
|
+
local: nextUrls.local,
|
|
98
|
+
network: nextUrls.network as string | null,
|
|
99
|
+
preview: nextUrls.preview,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
package/template/src/styles.css
CHANGED
|
@@ -67,6 +67,76 @@ h1 {
|
|
|
67
67
|
font-weight: 600;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
.handoff-panel {
|
|
71
|
+
margin: 24px 0;
|
|
72
|
+
display: grid;
|
|
73
|
+
gap: 20px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.handoff-copy h2 {
|
|
77
|
+
margin: 0 0 8px;
|
|
78
|
+
font-size: 1.5rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.handoff-copy p {
|
|
82
|
+
margin: 0;
|
|
83
|
+
color: #465d82;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.preview-grid {
|
|
87
|
+
margin: 20px 0 0;
|
|
88
|
+
display: grid;
|
|
89
|
+
gap: 12px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.preview-grid div {
|
|
93
|
+
padding: 14px 16px;
|
|
94
|
+
border-radius: 16px;
|
|
95
|
+
background: #f8fbff;
|
|
96
|
+
border: 1px solid rgba(16, 35, 71, 0.08);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.preview-grid code {
|
|
100
|
+
word-break: break-all;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.hint-text {
|
|
104
|
+
margin-top: 16px;
|
|
105
|
+
font-size: 0.95rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.qr-panel {
|
|
109
|
+
display: grid;
|
|
110
|
+
place-items: center;
|
|
111
|
+
padding: 20px;
|
|
112
|
+
border-radius: 24px;
|
|
113
|
+
background: linear-gradient(180deg, #fefefe 0%, #f3f7ff 100%);
|
|
114
|
+
border: 1px solid rgba(16, 35, 71, 0.08);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.qr-code {
|
|
118
|
+
width: min(260px, 100%);
|
|
119
|
+
aspect-ratio: 1;
|
|
120
|
+
display: grid;
|
|
121
|
+
place-items: center;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.qr-code svg {
|
|
125
|
+
width: 100%;
|
|
126
|
+
height: auto;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.qr-placeholder {
|
|
130
|
+
display: grid;
|
|
131
|
+
place-items: center;
|
|
132
|
+
width: 100%;
|
|
133
|
+
height: 100%;
|
|
134
|
+
border-radius: 20px;
|
|
135
|
+
background: #eef6ff;
|
|
136
|
+
color: #22548d;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
}
|
|
139
|
+
|
|
70
140
|
.details-grid {
|
|
71
141
|
display: grid;
|
|
72
142
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
@@ -111,4 +181,15 @@ dd {
|
|
|
111
181
|
.card {
|
|
112
182
|
padding: 24px;
|
|
113
183
|
}
|
|
184
|
+
|
|
185
|
+
.handoff-panel {
|
|
186
|
+
grid-template-columns: 1fr;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@media (min-width: 641px) {
|
|
191
|
+
.handoff-panel {
|
|
192
|
+
grid-template-columns: minmax(0, 1.3fr) minmax(220px, 0.7fr);
|
|
193
|
+
align-items: start;
|
|
194
|
+
}
|
|
114
195
|
}
|