create-bridgething 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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/index.mjs +147 -0
- package/package.json +44 -0
- package/template/CLAUDE.md +87 -0
- package/template/README.md +54 -0
- package/template/_claude/skills/bridgething/SKILL.md +24 -0
- package/template/_claude/skills/bridgething/reference/develop.md +142 -0
- package/template/_claude/skills/bridgething/reference/sdk.md +180 -0
- package/template/_claude/skills/bridgething/reference/ship.md +37 -0
- package/template/_gitignore +5 -0
- package/template/index.html +13 -0
- package/template/package.json +29 -0
- package/template/public/manifest.json +9 -0
- package/template/scripts/push.ts +289 -0
- package/template/scripts/share.ts +32 -0
- package/template/src/App.tsx +98 -0
- package/template/src/fonts/Inter-Variable.ttf +0 -0
- package/template/src/fonts/Outfit-Variable.ttf +0 -0
- package/template/src/index.css +52 -0
- package/template/src/main.tsx +10 -0
- package/template/src/vite-env.d.ts +9 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.config.ts +14 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { BridgethingClient, type ConnectionState, type PlayerState } from '@bridgething/client';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
// Production: the webapp is served by the daemon on the device, so a
|
|
5
|
+
// relative `ws://<location.host>/` reaches it. In dev (vite on your
|
|
6
|
+
// laptop) point at the device explicitly with VITE_BRIDGETHING_URL.
|
|
7
|
+
const wsUrl =
|
|
8
|
+
import.meta.env.VITE_BRIDGETHING_URL ??
|
|
9
|
+
(typeof window !== 'undefined' ? `ws://${window.location.host}/` : 'ws://127.0.0.1:8891/');
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
const client = useMemo(() => new BridgethingClient({ url: wsUrl }), []);
|
|
13
|
+
const [conn, setConn] = useState<ConnectionState>(client.connectionState);
|
|
14
|
+
const [state, setState] = useState<PlayerState | null>(null);
|
|
15
|
+
const [artUrl, setArtUrl] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const offConn = client.on(event => {
|
|
19
|
+
if (event.type === 'open' || event.type === 'close' || event.type === 'connecting') {
|
|
20
|
+
setConn(client.connectionState);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const offSnapshot = client.player.onSnapshot(reply => setState(reply.state));
|
|
24
|
+
client.player.stateGet().then(r => r.ok && setState(r.response.state));
|
|
25
|
+
return () => {
|
|
26
|
+
offConn();
|
|
27
|
+
offSnapshot();
|
|
28
|
+
};
|
|
29
|
+
}, [client]);
|
|
30
|
+
|
|
31
|
+
const track = state?.track ?? null;
|
|
32
|
+
const artworkId = track?.artworkId ?? null;
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!artworkId) {
|
|
36
|
+
setArtUrl(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let revoked = false;
|
|
40
|
+
let blobUrl: string | null = null;
|
|
41
|
+
(async () => {
|
|
42
|
+
const result = await client.asset.get({ id: artworkId, requestId: crypto.randomUUID() });
|
|
43
|
+
if (revoked) return;
|
|
44
|
+
if (result.ok) {
|
|
45
|
+
const bytes = new Uint8Array(result.response.bytes as unknown as number[]);
|
|
46
|
+
blobUrl = URL.createObjectURL(new Blob([bytes], { type: result.response.mime ?? 'image/jpeg' }));
|
|
47
|
+
setArtUrl(blobUrl);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
return () => {
|
|
51
|
+
revoked = true;
|
|
52
|
+
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
53
|
+
};
|
|
54
|
+
}, [client, artworkId]);
|
|
55
|
+
|
|
56
|
+
const playing = state?.playback.state === 'playing';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex h-full w-full items-center justify-center gap-12 bg-bt-charcoal p-12 text-bt-off-white">
|
|
60
|
+
{artUrl ? (
|
|
61
|
+
<img src={artUrl} alt="" className="h-full max-h-96 w-auto rounded-2xl shadow-2xl" />
|
|
62
|
+
) : (
|
|
63
|
+
<div className="grid h-96 w-96 place-items-center rounded-2xl bg-black/40 text-sm text-bt-soft-gray">
|
|
64
|
+
{conn === 'open' ? 'no track' : conn}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
<div className="flex flex-col gap-3">
|
|
68
|
+
<div className="text-xs uppercase tracking-widest text-bt-soft-gray">{conn}</div>
|
|
69
|
+
{track ? (
|
|
70
|
+
<>
|
|
71
|
+
<div className="text-3xl font-semibold leading-tight">{track.title ?? 'unknown'}</div>
|
|
72
|
+
<div className="text-xl text-bt-soft-gray">{track.artist ?? ''}</div>
|
|
73
|
+
<div className="text-sm text-bt-soft-gray/70">{track.album ?? ''}</div>
|
|
74
|
+
<div className="mt-6 flex gap-4">
|
|
75
|
+
<button
|
|
76
|
+
className="rounded-full bg-white/10 px-6 py-3 text-sm font-medium transition active:bg-white/20"
|
|
77
|
+
onClick={() => client.player.skipPrev()}>
|
|
78
|
+
◀◀
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
className="rounded-full bg-bt-blue px-6 py-3 text-sm font-medium text-bt-charcoal transition active:scale-95"
|
|
82
|
+
onClick={() => (playing ? client.player.pause() : client.player.resume())}>
|
|
83
|
+
{playing ? '❚❚' : '▶'}
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
className="rounded-full bg-white/10 px-6 py-3 text-sm font-medium transition active:bg-white/20"
|
|
87
|
+
onClick={() => client.player.skipNext()}>
|
|
88
|
+
▶▶
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</>
|
|
92
|
+
) : (
|
|
93
|
+
<div className="text-2xl text-bt-soft-gray">connect a phone to see now playing</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@font-face {
|
|
4
|
+
font-family: "Outfit";
|
|
5
|
+
font-style: normal;
|
|
6
|
+
font-weight: 100 900;
|
|
7
|
+
font-display: block;
|
|
8
|
+
src: url("./fonts/Outfit-Variable.ttf") format("truetype-variations");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@font-face {
|
|
12
|
+
font-family: "Inter";
|
|
13
|
+
font-style: normal;
|
|
14
|
+
font-weight: 100 900;
|
|
15
|
+
font-display: block;
|
|
16
|
+
src: url("./fonts/Inter-Variable.ttf") format("truetype-variations");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@theme {
|
|
20
|
+
--color-bt-charcoal: #1b1f23;
|
|
21
|
+
--color-bt-off-white: #efefef;
|
|
22
|
+
--color-bt-blue: #00a8e8;
|
|
23
|
+
--color-bt-soft-gray: #a7adb5;
|
|
24
|
+
|
|
25
|
+
--font-display: "Outfit", system-ui, sans-serif;
|
|
26
|
+
--font-sans: "Inter", system-ui, sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@layer base {
|
|
30
|
+
:root {
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
}
|
|
33
|
+
* {
|
|
34
|
+
-webkit-tap-highlight-color: transparent;
|
|
35
|
+
}
|
|
36
|
+
html,
|
|
37
|
+
body {
|
|
38
|
+
width: 800px;
|
|
39
|
+
height: 480px;
|
|
40
|
+
margin: 0;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
overscroll-behavior: none;
|
|
43
|
+
font-family: var(--font-sans);
|
|
44
|
+
background: var(--color-bt-charcoal);
|
|
45
|
+
color: var(--color-bt-off-white);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@utility bt-wordmark {
|
|
50
|
+
font-family: var(--font-display);
|
|
51
|
+
letter-spacing: -0.02em;
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"noUnusedParameters": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"allowImportingTsExtensions": true,
|
|
18
|
+
"verbatimModuleSyntax": true,
|
|
19
|
+
"useDefineForClassFields": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react(), tailwindcss()],
|
|
7
|
+
build: {
|
|
8
|
+
target: 'es2022',
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
},
|
|
11
|
+
server: {
|
|
12
|
+
host: true,
|
|
13
|
+
},
|
|
14
|
+
});
|