forge
login
or
join now
juliet.paris
/
streamplace-spa
star
8
fork
atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
minimal streamplace frontend
star
8
fork
atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
overview
issues
pulls
pipelines
init
Juliet
1 month ago
3f3711be
+3261
30 changed files
expand all
collapse all
unified
split
.gitignore
.oxfmtrc.json
index.html
package.json
pnpm-lock.yaml
public
favicon.svg
scripts
generate-metadata.js
src
auth
login-modal.ts
login.ts
oauth-config.ts
session-manager.ts
state.ts
components
Chat.tsx
Header.tsx
LoginButton.tsx
StreamCard.tsx
VideoPlayer.tsx
index.css
index.tsx
layout.tsx
lib
api.ts
chat.ts
whep.ts
pages
Home.tsx
Watch.tsx
vite-env.d.ts
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
+6
.gitignore
reviewed
···
1
1
+
node_modules
2
2
+
dist
3
3
+
.env
4
4
+
.DS_Store
5
5
+
public/oauth-client-metadata.json
6
6
+
.claude
+4
.oxfmtrc.json
reviewed
···
1
1
+
{
2
2
+
"sortImports": {},
3
3
+
"sortTailwindcss": {}
4
4
+
}
+20
index.html
reviewed
···
1
1
+
<!doctype html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<meta charset="utf-8" />
5
5
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
6
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
7
7
+
<meta name="theme-color" content="#0a0a0b" />
8
8
+
<meta property="og:title" content="stream.place" />
9
9
+
<meta property="og:type" content="website" />
10
10
+
<meta property="og:description" content="Live streaming on the atmosphere" />
11
11
+
<title>stream.place</title>
12
12
+
<link rel="preconnect" href="https://fonts.bunny.net" />
13
13
+
<link href="https://fonts.bunny.net/css?family=dm-sans:400,500,600,700" rel="stylesheet" />
14
14
+
<script src="/src/index.tsx" type="module"></script>
15
15
+
</head>
16
16
+
17
17
+
<body id="root" class="bg-sp-bg text-sp-text min-h-dvh">
18
18
+
<noscript>You need to enable JavaScript to run this app.</noscript>
19
19
+
</body>
20
20
+
</html>
+35
package.json
reviewed
···
1
1
+
{
2
2
+
"name": "streamplace-frontend",
3
3
+
"private": true,
4
4
+
"license": "0BSD",
5
5
+
"type": "module",
6
6
+
"scripts": {
7
7
+
"predev": "node scripts/generate-metadata.js",
8
8
+
"dev": "vite",
9
9
+
"prebuild": "node scripts/generate-metadata.js",
10
10
+
"build": "vite build",
11
11
+
"serve": "vite preview",
12
12
+
"format": "oxfmt ."
13
13
+
},
14
14
+
"dependencies": {
15
15
+
"@atcute/atproto": "^3.1.10",
16
16
+
"@atcute/client": "^4.2.1",
17
17
+
"@atcute/identity": "^1.1.4",
18
18
+
"@atcute/identity-resolver": "^1.2.2",
19
19
+
"@atcute/lexicons": "^1.2.9",
20
20
+
"@atcute/oauth-browser-client": "^3.0.0",
21
21
+
"@solidjs/router": "^0.16.1",
22
22
+
"lucide-solid": "^1.7.0",
23
23
+
"solid-js": "^1.9.11"
24
24
+
},
25
25
+
"devDependencies": {
26
26
+
"@tailwindcss/vite": "^4.2.2",
27
27
+
"@types/node": "^25.5.0",
28
28
+
"oxfmt": "^0.42.0",
29
29
+
"tailwindcss": "^4.2.2",
30
30
+
"typescript": "^5.9.3",
31
31
+
"vite": "^8.0.1",
32
32
+
"vite-plugin-solid": "^2.11.11"
33
33
+
},
34
34
+
"packageManager": "pnpm@10.32.1"
35
35
+
}
+1650
pnpm-lock.yaml
reviewed
···
1
1
+
lockfileVersion: '9.0'
2
2
+
3
3
+
settings:
4
4
+
autoInstallPeers: true
5
5
+
excludeLinksFromLockfile: false
6
6
+
7
7
+
importers:
8
8
+
9
9
+
.:
10
10
+
dependencies:
11
11
+
'@atcute/atproto':
12
12
+
specifier: ^3.1.10
13
13
+
version: 3.1.10
14
14
+
'@atcute/client':
15
15
+
specifier: ^4.2.1
16
16
+
version: 4.2.1
17
17
+
'@atcute/identity':
18
18
+
specifier: ^1.1.4
19
19
+
version: 1.1.4
20
20
+
'@atcute/identity-resolver':
21
21
+
specifier: ^1.2.2
22
22
+
version: 1.2.2(@atcute/identity@1.1.4)
23
23
+
'@atcute/lexicons':
24
24
+
specifier: ^1.2.9
25
25
+
version: 1.2.9
26
26
+
'@atcute/oauth-browser-client':
27
27
+
specifier: ^3.0.0
28
28
+
version: 3.0.0(@atcute/identity@1.1.4)
29
29
+
'@solidjs/router':
30
30
+
specifier: ^0.16.1
31
31
+
version: 0.16.1(solid-js@1.9.12)
32
32
+
lucide-solid:
33
33
+
specifier: ^1.7.0
34
34
+
version: 1.7.0(solid-js@1.9.12)
35
35
+
solid-js:
36
36
+
specifier: ^1.9.11
37
37
+
version: 1.9.12
38
38
+
devDependencies:
39
39
+
'@tailwindcss/vite':
40
40
+
specifier: ^4.2.2
41
41
+
version: 4.2.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1))
42
42
+
'@types/node':
43
43
+
specifier: ^25.5.0
44
44
+
version: 25.5.0
45
45
+
oxfmt:
46
46
+
specifier: ^0.42.0
47
47
+
version: 0.42.0
48
48
+
tailwindcss:
49
49
+
specifier: ^4.2.2
50
50
+
version: 4.2.2
51
51
+
typescript:
52
52
+
specifier: ^5.9.3
53
53
+
version: 5.9.3
54
54
+
vite:
55
55
+
specifier: ^8.0.1
56
56
+
version: 8.0.3(@types/node@25.5.0)(jiti@2.6.1)
57
57
+
vite-plugin-solid:
58
58
+
specifier: ^2.11.11
59
59
+
version: 2.11.11(solid-js@1.9.12)(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1))
60
60
+
61
61
+
packages:
62
62
+
63
63
+
'@atcute/atproto@3.1.10':
64
64
+
resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==}
65
65
+
66
66
+
'@atcute/client@4.2.1':
67
67
+
resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==}
68
68
+
69
69
+
'@atcute/identity-resolver@1.2.2':
70
70
+
resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==}
71
71
+
peerDependencies:
72
72
+
'@atcute/identity': ^1.0.0
73
73
+
74
74
+
'@atcute/identity@1.1.4':
75
75
+
resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==}
76
76
+
77
77
+
'@atcute/lexicons@1.2.9':
78
78
+
resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==}
79
79
+
80
80
+
'@atcute/multibase@1.2.0':
81
81
+
resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==}
82
82
+
83
83
+
'@atcute/oauth-browser-client@3.0.0':
84
84
+
resolution: {integrity: sha512-7AbKV8tTe7aRJNJV7gCcWHSVEADb2nr58O1p7dQsf73HSe9pvlBkj/Vk1yjjtH691uAVYkwhHSh0bC7D8XdwJw==}
85
85
+
86
86
+
'@atcute/oauth-crypto@0.1.0':
87
87
+
resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==}
88
88
+
89
89
+
'@atcute/oauth-keyset@0.1.0':
90
90
+
resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==}
91
91
+
92
92
+
'@atcute/oauth-types@0.1.1':
93
93
+
resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==}
94
94
+
95
95
+
'@atcute/uint8array@1.1.1':
96
96
+
resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==}
97
97
+
98
98
+
'@atcute/util-fetch@1.0.5':
99
99
+
resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==}
100
100
+
101
101
+
'@atcute/util-text@1.2.0':
102
102
+
resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==}
103
103
+
104
104
+
'@babel/code-frame@7.29.0':
105
105
+
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
106
106
+
engines: {node: '>=6.9.0'}
107
107
+
108
108
+
'@babel/compat-data@7.29.0':
109
109
+
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
110
110
+
engines: {node: '>=6.9.0'}
111
111
+
112
112
+
'@babel/core@7.29.0':
113
113
+
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
114
114
+
engines: {node: '>=6.9.0'}
115
115
+
116
116
+
'@babel/generator@7.29.1':
117
117
+
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
118
118
+
engines: {node: '>=6.9.0'}
119
119
+
120
120
+
'@babel/helper-compilation-targets@7.28.6':
121
121
+
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
122
122
+
engines: {node: '>=6.9.0'}
123
123
+
124
124
+
'@babel/helper-globals@7.28.0':
125
125
+
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
126
126
+
engines: {node: '>=6.9.0'}
127
127
+
128
128
+
'@babel/helper-module-imports@7.18.6':
129
129
+
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
130
130
+
engines: {node: '>=6.9.0'}
131
131
+
132
132
+
'@babel/helper-module-imports@7.28.6':
133
133
+
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
134
134
+
engines: {node: '>=6.9.0'}
135
135
+
136
136
+
'@babel/helper-module-transforms@7.28.6':
137
137
+
resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
138
138
+
engines: {node: '>=6.9.0'}
139
139
+
peerDependencies:
140
140
+
'@babel/core': ^7.0.0
141
141
+
142
142
+
'@babel/helper-plugin-utils@7.28.6':
143
143
+
resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
144
144
+
engines: {node: '>=6.9.0'}
145
145
+
146
146
+
'@babel/helper-string-parser@7.27.1':
147
147
+
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
148
148
+
engines: {node: '>=6.9.0'}
149
149
+
150
150
+
'@babel/helper-validator-identifier@7.28.5':
151
151
+
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
152
152
+
engines: {node: '>=6.9.0'}
153
153
+
154
154
+
'@babel/helper-validator-option@7.27.1':
155
155
+
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
156
156
+
engines: {node: '>=6.9.0'}
157
157
+
158
158
+
'@babel/helpers@7.29.2':
159
159
+
resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
160
160
+
engines: {node: '>=6.9.0'}
161
161
+
162
162
+
'@babel/parser@7.29.2':
163
163
+
resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
164
164
+
engines: {node: '>=6.0.0'}
165
165
+
hasBin: true
166
166
+
167
167
+
'@babel/plugin-syntax-jsx@7.28.6':
168
168
+
resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
169
169
+
engines: {node: '>=6.9.0'}
170
170
+
peerDependencies:
171
171
+
'@babel/core': ^7.0.0-0
172
172
+
173
173
+
'@babel/template@7.28.6':
174
174
+
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
175
175
+
engines: {node: '>=6.9.0'}
176
176
+
177
177
+
'@babel/traverse@7.29.0':
178
178
+
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
179
179
+
engines: {node: '>=6.9.0'}
180
180
+
181
181
+
'@babel/types@7.29.0':
182
182
+
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
183
183
+
engines: {node: '>=6.9.0'}
184
184
+
185
185
+
'@badrap/valita@0.4.6':
186
186
+
resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==}
187
187
+
engines: {node: '>= 18'}
188
188
+
189
189
+
'@emnapi/core@1.9.1':
190
190
+
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
191
191
+
192
192
+
'@emnapi/runtime@1.9.1':
193
193
+
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
194
194
+
195
195
+
'@emnapi/wasi-threads@1.2.0':
196
196
+
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
197
197
+
198
198
+
'@jridgewell/gen-mapping@0.3.13':
199
199
+
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
200
200
+
201
201
+
'@jridgewell/remapping@2.3.5':
202
202
+
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
203
203
+
204
204
+
'@jridgewell/resolve-uri@3.1.2':
205
205
+
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
206
206
+
engines: {node: '>=6.0.0'}
207
207
+
208
208
+
'@jridgewell/sourcemap-codec@1.5.5':
209
209
+
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
210
210
+
211
211
+
'@jridgewell/trace-mapping@0.3.31':
212
212
+
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
213
213
+
214
214
+
'@napi-rs/wasm-runtime@1.1.1':
215
215
+
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
216
216
+
217
217
+
'@oxc-project/types@0.122.0':
218
218
+
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
219
219
+
220
220
+
'@oxfmt/binding-android-arm-eabi@0.42.0':
221
221
+
resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==}
222
222
+
engines: {node: ^20.19.0 || >=22.12.0}
223
223
+
cpu: [arm]
224
224
+
os: [android]
225
225
+
226
226
+
'@oxfmt/binding-android-arm64@0.42.0':
227
227
+
resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==}
228
228
+
engines: {node: ^20.19.0 || >=22.12.0}
229
229
+
cpu: [arm64]
230
230
+
os: [android]
231
231
+
232
232
+
'@oxfmt/binding-darwin-arm64@0.42.0':
233
233
+
resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==}
234
234
+
engines: {node: ^20.19.0 || >=22.12.0}
235
235
+
cpu: [arm64]
236
236
+
os: [darwin]
237
237
+
238
238
+
'@oxfmt/binding-darwin-x64@0.42.0':
239
239
+
resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==}
240
240
+
engines: {node: ^20.19.0 || >=22.12.0}
241
241
+
cpu: [x64]
242
242
+
os: [darwin]
243
243
+
244
244
+
'@oxfmt/binding-freebsd-x64@0.42.0':
245
245
+
resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==}
246
246
+
engines: {node: ^20.19.0 || >=22.12.0}
247
247
+
cpu: [x64]
248
248
+
os: [freebsd]
249
249
+
250
250
+
'@oxfmt/binding-linux-arm-gnueabihf@0.42.0':
251
251
+
resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==}
252
252
+
engines: {node: ^20.19.0 || >=22.12.0}
253
253
+
cpu: [arm]
254
254
+
os: [linux]
255
255
+
256
256
+
'@oxfmt/binding-linux-arm-musleabihf@0.42.0':
257
257
+
resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==}
258
258
+
engines: {node: ^20.19.0 || >=22.12.0}
259
259
+
cpu: [arm]
260
260
+
os: [linux]
261
261
+
262
262
+
'@oxfmt/binding-linux-arm64-gnu@0.42.0':
263
263
+
resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==}
264
264
+
engines: {node: ^20.19.0 || >=22.12.0}
265
265
+
cpu: [arm64]
266
266
+
os: [linux]
267
267
+
libc: [glibc]
268
268
+
269
269
+
'@oxfmt/binding-linux-arm64-musl@0.42.0':
270
270
+
resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==}
271
271
+
engines: {node: ^20.19.0 || >=22.12.0}
272
272
+
cpu: [arm64]
273
273
+
os: [linux]
274
274
+
libc: [musl]
275
275
+
276
276
+
'@oxfmt/binding-linux-ppc64-gnu@0.42.0':
277
277
+
resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==}
278
278
+
engines: {node: ^20.19.0 || >=22.12.0}
279
279
+
cpu: [ppc64]
280
280
+
os: [linux]
281
281
+
libc: [glibc]
282
282
+
283
283
+
'@oxfmt/binding-linux-riscv64-gnu@0.42.0':
284
284
+
resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==}
285
285
+
engines: {node: ^20.19.0 || >=22.12.0}
286
286
+
cpu: [riscv64]
287
287
+
os: [linux]
288
288
+
libc: [glibc]
289
289
+
290
290
+
'@oxfmt/binding-linux-riscv64-musl@0.42.0':
291
291
+
resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==}
292
292
+
engines: {node: ^20.19.0 || >=22.12.0}
293
293
+
cpu: [riscv64]
294
294
+
os: [linux]
295
295
+
libc: [musl]
296
296
+
297
297
+
'@oxfmt/binding-linux-s390x-gnu@0.42.0':
298
298
+
resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==}
299
299
+
engines: {node: ^20.19.0 || >=22.12.0}
300
300
+
cpu: [s390x]
301
301
+
os: [linux]
302
302
+
libc: [glibc]
303
303
+
304
304
+
'@oxfmt/binding-linux-x64-gnu@0.42.0':
305
305
+
resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==}
306
306
+
engines: {node: ^20.19.0 || >=22.12.0}
307
307
+
cpu: [x64]
308
308
+
os: [linux]
309
309
+
libc: [glibc]
310
310
+
311
311
+
'@oxfmt/binding-linux-x64-musl@0.42.0':
312
312
+
resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==}
313
313
+
engines: {node: ^20.19.0 || >=22.12.0}
314
314
+
cpu: [x64]
315
315
+
os: [linux]
316
316
+
libc: [musl]
317
317
+
318
318
+
'@oxfmt/binding-openharmony-arm64@0.42.0':
319
319
+
resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==}
320
320
+
engines: {node: ^20.19.0 || >=22.12.0}
321
321
+
cpu: [arm64]
322
322
+
os: [openharmony]
323
323
+
324
324
+
'@oxfmt/binding-win32-arm64-msvc@0.42.0':
325
325
+
resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==}
326
326
+
engines: {node: ^20.19.0 || >=22.12.0}
327
327
+
cpu: [arm64]
328
328
+
os: [win32]
329
329
+
330
330
+
'@oxfmt/binding-win32-ia32-msvc@0.42.0':
331
331
+
resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==}
332
332
+
engines: {node: ^20.19.0 || >=22.12.0}
333
333
+
cpu: [ia32]
334
334
+
os: [win32]
335
335
+
336
336
+
'@oxfmt/binding-win32-x64-msvc@0.42.0':
337
337
+
resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==}
338
338
+
engines: {node: ^20.19.0 || >=22.12.0}
339
339
+
cpu: [x64]
340
340
+
os: [win32]
341
341
+
342
342
+
'@rolldown/binding-android-arm64@1.0.0-rc.12':
343
343
+
resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
344
344
+
engines: {node: ^20.19.0 || >=22.12.0}
345
345
+
cpu: [arm64]
346
346
+
os: [android]
347
347
+
348
348
+
'@rolldown/binding-darwin-arm64@1.0.0-rc.12':
349
349
+
resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
350
350
+
engines: {node: ^20.19.0 || >=22.12.0}
351
351
+
cpu: [arm64]
352
352
+
os: [darwin]
353
353
+
354
354
+
'@rolldown/binding-darwin-x64@1.0.0-rc.12':
355
355
+
resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
356
356
+
engines: {node: ^20.19.0 || >=22.12.0}
357
357
+
cpu: [x64]
358
358
+
os: [darwin]
359
359
+
360
360
+
'@rolldown/binding-freebsd-x64@1.0.0-rc.12':
361
361
+
resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
362
362
+
engines: {node: ^20.19.0 || >=22.12.0}
363
363
+
cpu: [x64]
364
364
+
os: [freebsd]
365
365
+
366
366
+
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
367
367
+
resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
368
368
+
engines: {node: ^20.19.0 || >=22.12.0}
369
369
+
cpu: [arm]
370
370
+
os: [linux]
371
371
+
372
372
+
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
373
373
+
resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
374
374
+
engines: {node: ^20.19.0 || >=22.12.0}
375
375
+
cpu: [arm64]
376
376
+
os: [linux]
377
377
+
libc: [glibc]
378
378
+
379
379
+
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
380
380
+
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
381
381
+
engines: {node: ^20.19.0 || >=22.12.0}
382
382
+
cpu: [arm64]
383
383
+
os: [linux]
384
384
+
libc: [musl]
385
385
+
386
386
+
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
387
387
+
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
388
388
+
engines: {node: ^20.19.0 || >=22.12.0}
389
389
+
cpu: [ppc64]
390
390
+
os: [linux]
391
391
+
libc: [glibc]
392
392
+
393
393
+
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
394
394
+
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
395
395
+
engines: {node: ^20.19.0 || >=22.12.0}
396
396
+
cpu: [s390x]
397
397
+
os: [linux]
398
398
+
libc: [glibc]
399
399
+
400
400
+
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
401
401
+
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
402
402
+
engines: {node: ^20.19.0 || >=22.12.0}
403
403
+
cpu: [x64]
404
404
+
os: [linux]
405
405
+
libc: [glibc]
406
406
+
407
407
+
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
408
408
+
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
409
409
+
engines: {node: ^20.19.0 || >=22.12.0}
410
410
+
cpu: [x64]
411
411
+
os: [linux]
412
412
+
libc: [musl]
413
413
+
414
414
+
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
415
415
+
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
416
416
+
engines: {node: ^20.19.0 || >=22.12.0}
417
417
+
cpu: [arm64]
418
418
+
os: [openharmony]
419
419
+
420
420
+
'@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
421
421
+
resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
422
422
+
engines: {node: '>=14.0.0'}
423
423
+
cpu: [wasm32]
424
424
+
425
425
+
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
426
426
+
resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
427
427
+
engines: {node: ^20.19.0 || >=22.12.0}
428
428
+
cpu: [arm64]
429
429
+
os: [win32]
430
430
+
431
431
+
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
432
432
+
resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
433
433
+
engines: {node: ^20.19.0 || >=22.12.0}
434
434
+
cpu: [x64]
435
435
+
os: [win32]
436
436
+
437
437
+
'@rolldown/pluginutils@1.0.0-rc.12':
438
438
+
resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
439
439
+
440
440
+
'@solidjs/router@0.16.1':
441
441
+
resolution: {integrity: sha512-IhyjedgC6LRpw/8CPGGI89FrV+r0xTHzOl2c4CRyzYQ1bLepJxbVI1LLKvsavMWY5TRBRacV7hAeOhuTXkjiqg==}
442
442
+
peerDependencies:
443
443
+
solid-js: ^1.8.6
444
444
+
445
445
+
'@standard-schema/spec@1.1.0':
446
446
+
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
447
447
+
448
448
+
'@tailwindcss/node@4.2.2':
449
449
+
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
450
450
+
451
451
+
'@tailwindcss/oxide-android-arm64@4.2.2':
452
452
+
resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
453
453
+
engines: {node: '>= 20'}
454
454
+
cpu: [arm64]
455
455
+
os: [android]
456
456
+
457
457
+
'@tailwindcss/oxide-darwin-arm64@4.2.2':
458
458
+
resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
459
459
+
engines: {node: '>= 20'}
460
460
+
cpu: [arm64]
461
461
+
os: [darwin]
462
462
+
463
463
+
'@tailwindcss/oxide-darwin-x64@4.2.2':
464
464
+
resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
465
465
+
engines: {node: '>= 20'}
466
466
+
cpu: [x64]
467
467
+
os: [darwin]
468
468
+
469
469
+
'@tailwindcss/oxide-freebsd-x64@4.2.2':
470
470
+
resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
471
471
+
engines: {node: '>= 20'}
472
472
+
cpu: [x64]
473
473
+
os: [freebsd]
474
474
+
475
475
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
476
476
+
resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
477
477
+
engines: {node: '>= 20'}
478
478
+
cpu: [arm]
479
479
+
os: [linux]
480
480
+
481
481
+
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
482
482
+
resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
483
483
+
engines: {node: '>= 20'}
484
484
+
cpu: [arm64]
485
485
+
os: [linux]
486
486
+
libc: [glibc]
487
487
+
488
488
+
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
489
489
+
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
490
490
+
engines: {node: '>= 20'}
491
491
+
cpu: [arm64]
492
492
+
os: [linux]
493
493
+
libc: [musl]
494
494
+
495
495
+
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
496
496
+
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
497
497
+
engines: {node: '>= 20'}
498
498
+
cpu: [x64]
499
499
+
os: [linux]
500
500
+
libc: [glibc]
501
501
+
502
502
+
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
503
503
+
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
504
504
+
engines: {node: '>= 20'}
505
505
+
cpu: [x64]
506
506
+
os: [linux]
507
507
+
libc: [musl]
508
508
+
509
509
+
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
510
510
+
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
511
511
+
engines: {node: '>=14.0.0'}
512
512
+
cpu: [wasm32]
513
513
+
bundledDependencies:
514
514
+
- '@napi-rs/wasm-runtime'
515
515
+
- '@emnapi/core'
516
516
+
- '@emnapi/runtime'
517
517
+
- '@tybys/wasm-util'
518
518
+
- '@emnapi/wasi-threads'
519
519
+
- tslib
520
520
+
521
521
+
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
522
522
+
resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
523
523
+
engines: {node: '>= 20'}
524
524
+
cpu: [arm64]
525
525
+
os: [win32]
526
526
+
527
527
+
'@tailwindcss/oxide-win32-x64-msvc@4.2.2':
528
528
+
resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
529
529
+
engines: {node: '>= 20'}
530
530
+
cpu: [x64]
531
531
+
os: [win32]
532
532
+
533
533
+
'@tailwindcss/oxide@4.2.2':
534
534
+
resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
535
535
+
engines: {node: '>= 20'}
536
536
+
537
537
+
'@tailwindcss/vite@4.2.2':
538
538
+
resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
539
539
+
peerDependencies:
540
540
+
vite: ^5.2.0 || ^6 || ^7 || ^8
541
541
+
542
542
+
'@tybys/wasm-util@0.10.1':
543
543
+
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
544
544
+
545
545
+
'@types/babel__core@7.20.5':
546
546
+
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
547
547
+
548
548
+
'@types/babel__generator@7.27.0':
549
549
+
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
550
550
+
551
551
+
'@types/babel__template@7.4.4':
552
552
+
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
553
553
+
554
554
+
'@types/babel__traverse@7.28.0':
555
555
+
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
556
556
+
557
557
+
'@types/node@25.5.0':
558
558
+
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
559
559
+
560
560
+
babel-plugin-jsx-dom-expressions@0.40.6:
561
561
+
resolution: {integrity: sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==}
562
562
+
peerDependencies:
563
563
+
'@babel/core': ^7.20.12
564
564
+
565
565
+
babel-preset-solid@1.9.12:
566
566
+
resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==}
567
567
+
peerDependencies:
568
568
+
'@babel/core': ^7.0.0
569
569
+
solid-js: ^1.9.12
570
570
+
peerDependenciesMeta:
571
571
+
solid-js:
572
572
+
optional: true
573
573
+
574
574
+
baseline-browser-mapping@2.10.11:
575
575
+
resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==}
576
576
+
engines: {node: '>=6.0.0'}
577
577
+
hasBin: true
578
578
+
579
579
+
browserslist@4.28.1:
580
580
+
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
581
581
+
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
582
582
+
hasBin: true
583
583
+
584
584
+
caniuse-lite@1.0.30001781:
585
585
+
resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
586
586
+
587
587
+
convert-source-map@2.0.0:
588
588
+
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
589
589
+
590
590
+
csstype@3.2.3:
591
591
+
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
592
592
+
593
593
+
debug@4.4.3:
594
594
+
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
595
595
+
engines: {node: '>=6.0'}
596
596
+
peerDependencies:
597
597
+
supports-color: '*'
598
598
+
peerDependenciesMeta:
599
599
+
supports-color:
600
600
+
optional: true
601
601
+
602
602
+
detect-libc@2.1.2:
603
603
+
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
604
604
+
engines: {node: '>=8'}
605
605
+
606
606
+
electron-to-chromium@1.5.328:
607
607
+
resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==}
608
608
+
609
609
+
enhanced-resolve@5.20.1:
610
610
+
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
611
611
+
engines: {node: '>=10.13.0'}
612
612
+
613
613
+
entities@6.0.1:
614
614
+
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
615
615
+
engines: {node: '>=0.12'}
616
616
+
617
617
+
escalade@3.2.0:
618
618
+
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
619
619
+
engines: {node: '>=6'}
620
620
+
621
621
+
esm-env@1.2.2:
622
622
+
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
623
623
+
624
624
+
fdir@6.5.0:
625
625
+
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
626
626
+
engines: {node: '>=12.0.0'}
627
627
+
peerDependencies:
628
628
+
picomatch: ^3 || ^4
629
629
+
peerDependenciesMeta:
630
630
+
picomatch:
631
631
+
optional: true
632
632
+
633
633
+
fsevents@2.3.3:
634
634
+
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
635
635
+
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
636
636
+
os: [darwin]
637
637
+
638
638
+
gensync@1.0.0-beta.2:
639
639
+
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
640
640
+
engines: {node: '>=6.9.0'}
641
641
+
642
642
+
graceful-fs@4.2.11:
643
643
+
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
644
644
+
645
645
+
html-entities@2.3.3:
646
646
+
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
647
647
+
648
648
+
is-what@4.1.16:
649
649
+
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
650
650
+
engines: {node: '>=12.13'}
651
651
+
652
652
+
jiti@2.6.1:
653
653
+
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
654
654
+
hasBin: true
655
655
+
656
656
+
js-tokens@4.0.0:
657
657
+
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
658
658
+
659
659
+
jsesc@3.1.0:
660
660
+
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
661
661
+
engines: {node: '>=6'}
662
662
+
hasBin: true
663
663
+
664
664
+
json5@2.2.3:
665
665
+
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
666
666
+
engines: {node: '>=6'}
667
667
+
hasBin: true
668
668
+
669
669
+
lightningcss-android-arm64@1.32.0:
670
670
+
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
671
671
+
engines: {node: '>= 12.0.0'}
672
672
+
cpu: [arm64]
673
673
+
os: [android]
674
674
+
675
675
+
lightningcss-darwin-arm64@1.32.0:
676
676
+
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
677
677
+
engines: {node: '>= 12.0.0'}
678
678
+
cpu: [arm64]
679
679
+
os: [darwin]
680
680
+
681
681
+
lightningcss-darwin-x64@1.32.0:
682
682
+
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
683
683
+
engines: {node: '>= 12.0.0'}
684
684
+
cpu: [x64]
685
685
+
os: [darwin]
686
686
+
687
687
+
lightningcss-freebsd-x64@1.32.0:
688
688
+
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
689
689
+
engines: {node: '>= 12.0.0'}
690
690
+
cpu: [x64]
691
691
+
os: [freebsd]
692
692
+
693
693
+
lightningcss-linux-arm-gnueabihf@1.32.0:
694
694
+
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
695
695
+
engines: {node: '>= 12.0.0'}
696
696
+
cpu: [arm]
697
697
+
os: [linux]
698
698
+
699
699
+
lightningcss-linux-arm64-gnu@1.32.0:
700
700
+
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
701
701
+
engines: {node: '>= 12.0.0'}
702
702
+
cpu: [arm64]
703
703
+
os: [linux]
704
704
+
libc: [glibc]
705
705
+
706
706
+
lightningcss-linux-arm64-musl@1.32.0:
707
707
+
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
708
708
+
engines: {node: '>= 12.0.0'}
709
709
+
cpu: [arm64]
710
710
+
os: [linux]
711
711
+
libc: [musl]
712
712
+
713
713
+
lightningcss-linux-x64-gnu@1.32.0:
714
714
+
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
715
715
+
engines: {node: '>= 12.0.0'}
716
716
+
cpu: [x64]
717
717
+
os: [linux]
718
718
+
libc: [glibc]
719
719
+
720
720
+
lightningcss-linux-x64-musl@1.32.0:
721
721
+
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
722
722
+
engines: {node: '>= 12.0.0'}
723
723
+
cpu: [x64]
724
724
+
os: [linux]
725
725
+
libc: [musl]
726
726
+
727
727
+
lightningcss-win32-arm64-msvc@1.32.0:
728
728
+
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
729
729
+
engines: {node: '>= 12.0.0'}
730
730
+
cpu: [arm64]
731
731
+
os: [win32]
732
732
+
733
733
+
lightningcss-win32-x64-msvc@1.32.0:
734
734
+
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
735
735
+
engines: {node: '>= 12.0.0'}
736
736
+
cpu: [x64]
737
737
+
os: [win32]
738
738
+
739
739
+
lightningcss@1.32.0:
740
740
+
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
741
741
+
engines: {node: '>= 12.0.0'}
742
742
+
743
743
+
lru-cache@5.1.1:
744
744
+
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
745
745
+
746
746
+
lucide-solid@1.7.0:
747
747
+
resolution: {integrity: sha512-z+6Vmueb4W3Rf7ZQIEbcwWhQ/ci8vumLnixRzITXXs2uBjFPQvIvhotivUFAlgSj4xDvU822A1p0wioQlFyx8A==}
748
748
+
peerDependencies:
749
749
+
solid-js: ^1.4.7
750
750
+
751
751
+
magic-string@0.30.21:
752
752
+
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
753
753
+
754
754
+
merge-anything@5.1.7:
755
755
+
resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
756
756
+
engines: {node: '>=12.13'}
757
757
+
758
758
+
ms@2.1.3:
759
759
+
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
760
760
+
761
761
+
nanoid@3.3.11:
762
762
+
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
763
763
+
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
764
764
+
hasBin: true
765
765
+
766
766
+
nanoid@5.1.7:
767
767
+
resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==}
768
768
+
engines: {node: ^18 || >=20}
769
769
+
hasBin: true
770
770
+
771
771
+
node-releases@2.0.36:
772
772
+
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
773
773
+
774
774
+
oxfmt@0.42.0:
775
775
+
resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==}
776
776
+
engines: {node: ^20.19.0 || >=22.12.0}
777
777
+
hasBin: true
778
778
+
779
779
+
parse5@7.3.0:
780
780
+
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
781
781
+
782
782
+
picocolors@1.1.1:
783
783
+
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
784
784
+
785
785
+
picomatch@4.0.4:
786
786
+
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
787
787
+
engines: {node: '>=12'}
788
788
+
789
789
+
postcss@8.5.8:
790
790
+
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
791
791
+
engines: {node: ^10 || ^12 || >=14}
792
792
+
793
793
+
rolldown@1.0.0-rc.12:
794
794
+
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
795
795
+
engines: {node: ^20.19.0 || >=22.12.0}
796
796
+
hasBin: true
797
797
+
798
798
+
semver@6.3.1:
799
799
+
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
800
800
+
hasBin: true
801
801
+
802
802
+
seroval-plugins@1.5.1:
803
803
+
resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==}
804
804
+
engines: {node: '>=10'}
805
805
+
peerDependencies:
806
806
+
seroval: ^1.0
807
807
+
808
808
+
seroval@1.5.1:
809
809
+
resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==}
810
810
+
engines: {node: '>=10'}
811
811
+
812
812
+
solid-js@1.9.12:
813
813
+
resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==}
814
814
+
815
815
+
solid-refresh@0.6.3:
816
816
+
resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==}
817
817
+
peerDependencies:
818
818
+
solid-js: ^1.3
819
819
+
820
820
+
source-map-js@1.2.1:
821
821
+
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
822
822
+
engines: {node: '>=0.10.0'}
823
823
+
824
824
+
tailwindcss@4.2.2:
825
825
+
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
826
826
+
827
827
+
tapable@2.3.2:
828
828
+
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
829
829
+
engines: {node: '>=6'}
830
830
+
831
831
+
tinyglobby@0.2.15:
832
832
+
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
833
833
+
engines: {node: '>=12.0.0'}
834
834
+
835
835
+
tinypool@2.1.0:
836
836
+
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
837
837
+
engines: {node: ^20.0.0 || >=22.0.0}
838
838
+
839
839
+
tslib@2.8.1:
840
840
+
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
841
841
+
842
842
+
typescript@5.9.3:
843
843
+
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
844
844
+
engines: {node: '>=14.17'}
845
845
+
hasBin: true
846
846
+
847
847
+
undici-types@7.18.2:
848
848
+
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
849
849
+
850
850
+
unicode-segmenter@0.14.5:
851
851
+
resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==}
852
852
+
853
853
+
update-browserslist-db@1.2.3:
854
854
+
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
855
855
+
hasBin: true
856
856
+
peerDependencies:
857
857
+
browserslist: '>= 4.21.0'
858
858
+
859
859
+
vite-plugin-solid@2.11.11:
860
860
+
resolution: {integrity: sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw==}
861
861
+
peerDependencies:
862
862
+
'@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.*
863
863
+
solid-js: ^1.7.2
864
864
+
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
865
865
+
peerDependenciesMeta:
866
866
+
'@testing-library/jest-dom':
867
867
+
optional: true
868
868
+
869
869
+
vite@8.0.3:
870
870
+
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
871
871
+
engines: {node: ^20.19.0 || >=22.12.0}
872
872
+
hasBin: true
873
873
+
peerDependencies:
874
874
+
'@types/node': ^20.19.0 || >=22.12.0
875
875
+
'@vitejs/devtools': ^0.1.0
876
876
+
esbuild: ^0.27.0
877
877
+
jiti: '>=1.21.0'
878
878
+
less: ^4.0.0
879
879
+
sass: ^1.70.0
880
880
+
sass-embedded: ^1.70.0
881
881
+
stylus: '>=0.54.8'
882
882
+
sugarss: ^5.0.0
883
883
+
terser: ^5.16.0
884
884
+
tsx: ^4.8.1
885
885
+
yaml: ^2.4.2
886
886
+
peerDependenciesMeta:
887
887
+
'@types/node':
888
888
+
optional: true
889
889
+
'@vitejs/devtools':
890
890
+
optional: true
891
891
+
esbuild:
892
892
+
optional: true
893
893
+
jiti:
894
894
+
optional: true
895
895
+
less:
896
896
+
optional: true
897
897
+
sass:
898
898
+
optional: true
899
899
+
sass-embedded:
900
900
+
optional: true
901
901
+
stylus:
902
902
+
optional: true
903
903
+
sugarss:
904
904
+
optional: true
905
905
+
terser:
906
906
+
optional: true
907
907
+
tsx:
908
908
+
optional: true
909
909
+
yaml:
910
910
+
optional: true
911
911
+
912
912
+
vitefu@1.1.2:
913
913
+
resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==}
914
914
+
peerDependencies:
915
915
+
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0
916
916
+
peerDependenciesMeta:
917
917
+
vite:
918
918
+
optional: true
919
919
+
920
920
+
yallist@3.1.1:
921
921
+
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
922
922
+
923
923
+
snapshots:
924
924
+
925
925
+
'@atcute/atproto@3.1.10':
926
926
+
dependencies:
927
927
+
'@atcute/lexicons': 1.2.9
928
928
+
929
929
+
'@atcute/client@4.2.1':
930
930
+
dependencies:
931
931
+
'@atcute/identity': 1.1.4
932
932
+
'@atcute/lexicons': 1.2.9
933
933
+
934
934
+
'@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)':
935
935
+
dependencies:
936
936
+
'@atcute/identity': 1.1.4
937
937
+
'@atcute/lexicons': 1.2.9
938
938
+
'@atcute/util-fetch': 1.0.5
939
939
+
'@badrap/valita': 0.4.6
940
940
+
941
941
+
'@atcute/identity@1.1.4':
942
942
+
dependencies:
943
943
+
'@atcute/lexicons': 1.2.9
944
944
+
'@badrap/valita': 0.4.6
945
945
+
946
946
+
'@atcute/lexicons@1.2.9':
947
947
+
dependencies:
948
948
+
'@atcute/uint8array': 1.1.1
949
949
+
'@atcute/util-text': 1.2.0
950
950
+
'@standard-schema/spec': 1.1.0
951
951
+
esm-env: 1.2.2
952
952
+
953
953
+
'@atcute/multibase@1.2.0':
954
954
+
dependencies:
955
955
+
'@atcute/uint8array': 1.1.1
956
956
+
957
957
+
'@atcute/oauth-browser-client@3.0.0(@atcute/identity@1.1.4)':
958
958
+
dependencies:
959
959
+
'@atcute/client': 4.2.1
960
960
+
'@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4)
961
961
+
'@atcute/lexicons': 1.2.9
962
962
+
'@atcute/multibase': 1.2.0
963
963
+
'@atcute/oauth-crypto': 0.1.0
964
964
+
'@atcute/oauth-types': 0.1.1
965
965
+
nanoid: 5.1.7
966
966
+
transitivePeerDependencies:
967
967
+
- '@atcute/identity'
968
968
+
969
969
+
'@atcute/oauth-crypto@0.1.0':
970
970
+
dependencies:
971
971
+
'@atcute/multibase': 1.2.0
972
972
+
'@atcute/uint8array': 1.1.1
973
973
+
'@badrap/valita': 0.4.6
974
974
+
nanoid: 5.1.7
975
975
+
976
976
+
'@atcute/oauth-keyset@0.1.0':
977
977
+
dependencies:
978
978
+
'@atcute/oauth-crypto': 0.1.0
979
979
+
980
980
+
'@atcute/oauth-types@0.1.1':
981
981
+
dependencies:
982
982
+
'@atcute/identity': 1.1.4
983
983
+
'@atcute/lexicons': 1.2.9
984
984
+
'@atcute/oauth-keyset': 0.1.0
985
985
+
'@badrap/valita': 0.4.6
986
986
+
987
987
+
'@atcute/uint8array@1.1.1': {}
988
988
+
989
989
+
'@atcute/util-fetch@1.0.5':
990
990
+
dependencies:
991
991
+
'@badrap/valita': 0.4.6
992
992
+
993
993
+
'@atcute/util-text@1.2.0':
994
994
+
dependencies:
995
995
+
unicode-segmenter: 0.14.5
996
996
+
997
997
+
'@babel/code-frame@7.29.0':
998
998
+
dependencies:
999
999
+
'@babel/helper-validator-identifier': 7.28.5
1000
1000
+
js-tokens: 4.0.0
1001
1001
+
picocolors: 1.1.1
1002
1002
+
1003
1003
+
'@babel/compat-data@7.29.0': {}
1004
1004
+
1005
1005
+
'@babel/core@7.29.0':
1006
1006
+
dependencies:
1007
1007
+
'@babel/code-frame': 7.29.0
1008
1008
+
'@babel/generator': 7.29.1
1009
1009
+
'@babel/helper-compilation-targets': 7.28.6
1010
1010
+
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
1011
1011
+
'@babel/helpers': 7.29.2
1012
1012
+
'@babel/parser': 7.29.2
1013
1013
+
'@babel/template': 7.28.6
1014
1014
+
'@babel/traverse': 7.29.0
1015
1015
+
'@babel/types': 7.29.0
1016
1016
+
'@jridgewell/remapping': 2.3.5
1017
1017
+
convert-source-map: 2.0.0
1018
1018
+
debug: 4.4.3
1019
1019
+
gensync: 1.0.0-beta.2
1020
1020
+
json5: 2.2.3
1021
1021
+
semver: 6.3.1
1022
1022
+
transitivePeerDependencies:
1023
1023
+
- supports-color
1024
1024
+
1025
1025
+
'@babel/generator@7.29.1':
1026
1026
+
dependencies:
1027
1027
+
'@babel/parser': 7.29.2
1028
1028
+
'@babel/types': 7.29.0
1029
1029
+
'@jridgewell/gen-mapping': 0.3.13
1030
1030
+
'@jridgewell/trace-mapping': 0.3.31
1031
1031
+
jsesc: 3.1.0
1032
1032
+
1033
1033
+
'@babel/helper-compilation-targets@7.28.6':
1034
1034
+
dependencies:
1035
1035
+
'@babel/compat-data': 7.29.0
1036
1036
+
'@babel/helper-validator-option': 7.27.1
1037
1037
+
browserslist: 4.28.1
1038
1038
+
lru-cache: 5.1.1
1039
1039
+
semver: 6.3.1
1040
1040
+
1041
1041
+
'@babel/helper-globals@7.28.0': {}
1042
1042
+
1043
1043
+
'@babel/helper-module-imports@7.18.6':
1044
1044
+
dependencies:
1045
1045
+
'@babel/types': 7.29.0
1046
1046
+
1047
1047
+
'@babel/helper-module-imports@7.28.6':
1048
1048
+
dependencies:
1049
1049
+
'@babel/traverse': 7.29.0
1050
1050
+
'@babel/types': 7.29.0
1051
1051
+
transitivePeerDependencies:
1052
1052
+
- supports-color
1053
1053
+
1054
1054
+
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
1055
1055
+
dependencies:
1056
1056
+
'@babel/core': 7.29.0
1057
1057
+
'@babel/helper-module-imports': 7.28.6
1058
1058
+
'@babel/helper-validator-identifier': 7.28.5
1059
1059
+
'@babel/traverse': 7.29.0
1060
1060
+
transitivePeerDependencies:
1061
1061
+
- supports-color
1062
1062
+
1063
1063
+
'@babel/helper-plugin-utils@7.28.6': {}
1064
1064
+
1065
1065
+
'@babel/helper-string-parser@7.27.1': {}
1066
1066
+
1067
1067
+
'@babel/helper-validator-identifier@7.28.5': {}
1068
1068
+
1069
1069
+
'@babel/helper-validator-option@7.27.1': {}
1070
1070
+
1071
1071
+
'@babel/helpers@7.29.2':
1072
1072
+
dependencies:
1073
1073
+
'@babel/template': 7.28.6
1074
1074
+
'@babel/types': 7.29.0
1075
1075
+
1076
1076
+
'@babel/parser@7.29.2':
1077
1077
+
dependencies:
1078
1078
+
'@babel/types': 7.29.0
1079
1079
+
1080
1080
+
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
1081
1081
+
dependencies:
1082
1082
+
'@babel/core': 7.29.0
1083
1083
+
'@babel/helper-plugin-utils': 7.28.6
1084
1084
+
1085
1085
+
'@babel/template@7.28.6':
1086
1086
+
dependencies:
1087
1087
+
'@babel/code-frame': 7.29.0
1088
1088
+
'@babel/parser': 7.29.2
1089
1089
+
'@babel/types': 7.29.0
1090
1090
+
1091
1091
+
'@babel/traverse@7.29.0':
1092
1092
+
dependencies:
1093
1093
+
'@babel/code-frame': 7.29.0
1094
1094
+
'@babel/generator': 7.29.1
1095
1095
+
'@babel/helper-globals': 7.28.0
1096
1096
+
'@babel/parser': 7.29.2
1097
1097
+
'@babel/template': 7.28.6
1098
1098
+
'@babel/types': 7.29.0
1099
1099
+
debug: 4.4.3
1100
1100
+
transitivePeerDependencies:
1101
1101
+
- supports-color
1102
1102
+
1103
1103
+
'@babel/types@7.29.0':
1104
1104
+
dependencies:
1105
1105
+
'@babel/helper-string-parser': 7.27.1
1106
1106
+
'@babel/helper-validator-identifier': 7.28.5
1107
1107
+
1108
1108
+
'@badrap/valita@0.4.6': {}
1109
1109
+
1110
1110
+
'@emnapi/core@1.9.1':
1111
1111
+
dependencies:
1112
1112
+
'@emnapi/wasi-threads': 1.2.0
1113
1113
+
tslib: 2.8.1
1114
1114
+
optional: true
1115
1115
+
1116
1116
+
'@emnapi/runtime@1.9.1':
1117
1117
+
dependencies:
1118
1118
+
tslib: 2.8.1
1119
1119
+
optional: true
1120
1120
+
1121
1121
+
'@emnapi/wasi-threads@1.2.0':
1122
1122
+
dependencies:
1123
1123
+
tslib: 2.8.1
1124
1124
+
optional: true
1125
1125
+
1126
1126
+
'@jridgewell/gen-mapping@0.3.13':
1127
1127
+
dependencies:
1128
1128
+
'@jridgewell/sourcemap-codec': 1.5.5
1129
1129
+
'@jridgewell/trace-mapping': 0.3.31
1130
1130
+
1131
1131
+
'@jridgewell/remapping@2.3.5':
1132
1132
+
dependencies:
1133
1133
+
'@jridgewell/gen-mapping': 0.3.13
1134
1134
+
'@jridgewell/trace-mapping': 0.3.31
1135
1135
+
1136
1136
+
'@jridgewell/resolve-uri@3.1.2': {}
1137
1137
+
1138
1138
+
'@jridgewell/sourcemap-codec@1.5.5': {}
1139
1139
+
1140
1140
+
'@jridgewell/trace-mapping@0.3.31':
1141
1141
+
dependencies:
1142
1142
+
'@jridgewell/resolve-uri': 3.1.2
1143
1143
+
'@jridgewell/sourcemap-codec': 1.5.5
1144
1144
+
1145
1145
+
'@napi-rs/wasm-runtime@1.1.1':
1146
1146
+
dependencies:
1147
1147
+
'@emnapi/core': 1.9.1
1148
1148
+
'@emnapi/runtime': 1.9.1
1149
1149
+
'@tybys/wasm-util': 0.10.1
1150
1150
+
optional: true
1151
1151
+
1152
1152
+
'@oxc-project/types@0.122.0': {}
1153
1153
+
1154
1154
+
'@oxfmt/binding-android-arm-eabi@0.42.0':
1155
1155
+
optional: true
1156
1156
+
1157
1157
+
'@oxfmt/binding-android-arm64@0.42.0':
1158
1158
+
optional: true
1159
1159
+
1160
1160
+
'@oxfmt/binding-darwin-arm64@0.42.0':
1161
1161
+
optional: true
1162
1162
+
1163
1163
+
'@oxfmt/binding-darwin-x64@0.42.0':
1164
1164
+
optional: true
1165
1165
+
1166
1166
+
'@oxfmt/binding-freebsd-x64@0.42.0':
1167
1167
+
optional: true
1168
1168
+
1169
1169
+
'@oxfmt/binding-linux-arm-gnueabihf@0.42.0':
1170
1170
+
optional: true
1171
1171
+
1172
1172
+
'@oxfmt/binding-linux-arm-musleabihf@0.42.0':
1173
1173
+
optional: true
1174
1174
+
1175
1175
+
'@oxfmt/binding-linux-arm64-gnu@0.42.0':
1176
1176
+
optional: true
1177
1177
+
1178
1178
+
'@oxfmt/binding-linux-arm64-musl@0.42.0':
1179
1179
+
optional: true
1180
1180
+
1181
1181
+
'@oxfmt/binding-linux-ppc64-gnu@0.42.0':
1182
1182
+
optional: true
1183
1183
+
1184
1184
+
'@oxfmt/binding-linux-riscv64-gnu@0.42.0':
1185
1185
+
optional: true
1186
1186
+
1187
1187
+
'@oxfmt/binding-linux-riscv64-musl@0.42.0':
1188
1188
+
optional: true
1189
1189
+
1190
1190
+
'@oxfmt/binding-linux-s390x-gnu@0.42.0':
1191
1191
+
optional: true
1192
1192
+
1193
1193
+
'@oxfmt/binding-linux-x64-gnu@0.42.0':
1194
1194
+
optional: true
1195
1195
+
1196
1196
+
'@oxfmt/binding-linux-x64-musl@0.42.0':
1197
1197
+
optional: true
1198
1198
+
1199
1199
+
'@oxfmt/binding-openharmony-arm64@0.42.0':
1200
1200
+
optional: true
1201
1201
+
1202
1202
+
'@oxfmt/binding-win32-arm64-msvc@0.42.0':
1203
1203
+
optional: true
1204
1204
+
1205
1205
+
'@oxfmt/binding-win32-ia32-msvc@0.42.0':
1206
1206
+
optional: true
1207
1207
+
1208
1208
+
'@oxfmt/binding-win32-x64-msvc@0.42.0':
1209
1209
+
optional: true
1210
1210
+
1211
1211
+
'@rolldown/binding-android-arm64@1.0.0-rc.12':
1212
1212
+
optional: true
1213
1213
+
1214
1214
+
'@rolldown/binding-darwin-arm64@1.0.0-rc.12':
1215
1215
+
optional: true
1216
1216
+
1217
1217
+
'@rolldown/binding-darwin-x64@1.0.0-rc.12':
1218
1218
+
optional: true
1219
1219
+
1220
1220
+
'@rolldown/binding-freebsd-x64@1.0.0-rc.12':
1221
1221
+
optional: true
1222
1222
+
1223
1223
+
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
1224
1224
+
optional: true
1225
1225
+
1226
1226
+
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
1227
1227
+
optional: true
1228
1228
+
1229
1229
+
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
1230
1230
+
optional: true
1231
1231
+
1232
1232
+
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
1233
1233
+
optional: true
1234
1234
+
1235
1235
+
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
1236
1236
+
optional: true
1237
1237
+
1238
1238
+
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
1239
1239
+
optional: true
1240
1240
+
1241
1241
+
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
1242
1242
+
optional: true
1243
1243
+
1244
1244
+
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
1245
1245
+
optional: true
1246
1246
+
1247
1247
+
'@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
1248
1248
+
dependencies:
1249
1249
+
'@napi-rs/wasm-runtime': 1.1.1
1250
1250
+
optional: true
1251
1251
+
1252
1252
+
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
1253
1253
+
optional: true
1254
1254
+
1255
1255
+
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
1256
1256
+
optional: true
1257
1257
+
1258
1258
+
'@rolldown/pluginutils@1.0.0-rc.12': {}
1259
1259
+
1260
1260
+
'@solidjs/router@0.16.1(solid-js@1.9.12)':
1261
1261
+
dependencies:
1262
1262
+
solid-js: 1.9.12
1263
1263
+
1264
1264
+
'@standard-schema/spec@1.1.0': {}
1265
1265
+
1266
1266
+
'@tailwindcss/node@4.2.2':
1267
1267
+
dependencies:
1268
1268
+
'@jridgewell/remapping': 2.3.5
1269
1269
+
enhanced-resolve: 5.20.1
1270
1270
+
jiti: 2.6.1
1271
1271
+
lightningcss: 1.32.0
1272
1272
+
magic-string: 0.30.21
1273
1273
+
source-map-js: 1.2.1
1274
1274
+
tailwindcss: 4.2.2
1275
1275
+
1276
1276
+
'@tailwindcss/oxide-android-arm64@4.2.2':
1277
1277
+
optional: true
1278
1278
+
1279
1279
+
'@tailwindcss/oxide-darwin-arm64@4.2.2':
1280
1280
+
optional: true
1281
1281
+
1282
1282
+
'@tailwindcss/oxide-darwin-x64@4.2.2':
1283
1283
+
optional: true
1284
1284
+
1285
1285
+
'@tailwindcss/oxide-freebsd-x64@4.2.2':
1286
1286
+
optional: true
1287
1287
+
1288
1288
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
1289
1289
+
optional: true
1290
1290
+
1291
1291
+
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
1292
1292
+
optional: true
1293
1293
+
1294
1294
+
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
1295
1295
+
optional: true
1296
1296
+
1297
1297
+
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
1298
1298
+
optional: true
1299
1299
+
1300
1300
+
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
1301
1301
+
optional: true
1302
1302
+
1303
1303
+
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
1304
1304
+
optional: true
1305
1305
+
1306
1306
+
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
1307
1307
+
optional: true
1308
1308
+
1309
1309
+
'@tailwindcss/oxide-win32-x64-msvc@4.2.2':
1310
1310
+
optional: true
1311
1311
+
1312
1312
+
'@tailwindcss/oxide@4.2.2':
1313
1313
+
optionalDependencies:
1314
1314
+
'@tailwindcss/oxide-android-arm64': 4.2.2
1315
1315
+
'@tailwindcss/oxide-darwin-arm64': 4.2.2
1316
1316
+
'@tailwindcss/oxide-darwin-x64': 4.2.2
1317
1317
+
'@tailwindcss/oxide-freebsd-x64': 4.2.2
1318
1318
+
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2
1319
1319
+
'@tailwindcss/oxide-linux-arm64-gnu': 4.2.2
1320
1320
+
'@tailwindcss/oxide-linux-arm64-musl': 4.2.2
1321
1321
+
'@tailwindcss/oxide-linux-x64-gnu': 4.2.2
1322
1322
+
'@tailwindcss/oxide-linux-x64-musl': 4.2.2
1323
1323
+
'@tailwindcss/oxide-wasm32-wasi': 4.2.2
1324
1324
+
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
1325
1325
+
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2
1326
1326
+
1327
1327
+
'@tailwindcss/vite@4.2.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1))':
1328
1328
+
dependencies:
1329
1329
+
'@tailwindcss/node': 4.2.2
1330
1330
+
'@tailwindcss/oxide': 4.2.2
1331
1331
+
tailwindcss: 4.2.2
1332
1332
+
vite: 8.0.3(@types/node@25.5.0)(jiti@2.6.1)
1333
1333
+
1334
1334
+
'@tybys/wasm-util@0.10.1':
1335
1335
+
dependencies:
1336
1336
+
tslib: 2.8.1
1337
1337
+
optional: true
1338
1338
+
1339
1339
+
'@types/babel__core@7.20.5':
1340
1340
+
dependencies:
1341
1341
+
'@babel/parser': 7.29.2
1342
1342
+
'@babel/types': 7.29.0
1343
1343
+
'@types/babel__generator': 7.27.0
1344
1344
+
'@types/babel__template': 7.4.4
1345
1345
+
'@types/babel__traverse': 7.28.0
1346
1346
+
1347
1347
+
'@types/babel__generator@7.27.0':
1348
1348
+
dependencies:
1349
1349
+
'@babel/types': 7.29.0
1350
1350
+
1351
1351
+
'@types/babel__template@7.4.4':
1352
1352
+
dependencies:
1353
1353
+
'@babel/parser': 7.29.2
1354
1354
+
'@babel/types': 7.29.0
1355
1355
+
1356
1356
+
'@types/babel__traverse@7.28.0':
1357
1357
+
dependencies:
1358
1358
+
'@babel/types': 7.29.0
1359
1359
+
1360
1360
+
'@types/node@25.5.0':
1361
1361
+
dependencies:
1362
1362
+
undici-types: 7.18.2
1363
1363
+
1364
1364
+
babel-plugin-jsx-dom-expressions@0.40.6(@babel/core@7.29.0):
1365
1365
+
dependencies:
1366
1366
+
'@babel/core': 7.29.0
1367
1367
+
'@babel/helper-module-imports': 7.18.6
1368
1368
+
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
1369
1369
+
'@babel/types': 7.29.0
1370
1370
+
html-entities: 2.3.3
1371
1371
+
parse5: 7.3.0
1372
1372
+
1373
1373
+
babel-preset-solid@1.9.12(@babel/core@7.29.0)(solid-js@1.9.12):
1374
1374
+
dependencies:
1375
1375
+
'@babel/core': 7.29.0
1376
1376
+
babel-plugin-jsx-dom-expressions: 0.40.6(@babel/core@7.29.0)
1377
1377
+
optionalDependencies:
1378
1378
+
solid-js: 1.9.12
1379
1379
+
1380
1380
+
baseline-browser-mapping@2.10.11: {}
1381
1381
+
1382
1382
+
browserslist@4.28.1:
1383
1383
+
dependencies:
1384
1384
+
baseline-browser-mapping: 2.10.11
1385
1385
+
caniuse-lite: 1.0.30001781
1386
1386
+
electron-to-chromium: 1.5.328
1387
1387
+
node-releases: 2.0.36
1388
1388
+
update-browserslist-db: 1.2.3(browserslist@4.28.1)
1389
1389
+
1390
1390
+
caniuse-lite@1.0.30001781: {}
1391
1391
+
1392
1392
+
convert-source-map@2.0.0: {}
1393
1393
+
1394
1394
+
csstype@3.2.3: {}
1395
1395
+
1396
1396
+
debug@4.4.3:
1397
1397
+
dependencies:
1398
1398
+
ms: 2.1.3
1399
1399
+
1400
1400
+
detect-libc@2.1.2: {}
1401
1401
+
1402
1402
+
electron-to-chromium@1.5.328: {}
1403
1403
+
1404
1404
+
enhanced-resolve@5.20.1:
1405
1405
+
dependencies:
1406
1406
+
graceful-fs: 4.2.11
1407
1407
+
tapable: 2.3.2
1408
1408
+
1409
1409
+
entities@6.0.1: {}
1410
1410
+
1411
1411
+
escalade@3.2.0: {}
1412
1412
+
1413
1413
+
esm-env@1.2.2: {}
1414
1414
+
1415
1415
+
fdir@6.5.0(picomatch@4.0.4):
1416
1416
+
optionalDependencies:
1417
1417
+
picomatch: 4.0.4
1418
1418
+
1419
1419
+
fsevents@2.3.3:
1420
1420
+
optional: true
1421
1421
+
1422
1422
+
gensync@1.0.0-beta.2: {}
1423
1423
+
1424
1424
+
graceful-fs@4.2.11: {}
1425
1425
+
1426
1426
+
html-entities@2.3.3: {}
1427
1427
+
1428
1428
+
is-what@4.1.16: {}
1429
1429
+
1430
1430
+
jiti@2.6.1: {}
1431
1431
+
1432
1432
+
js-tokens@4.0.0: {}
1433
1433
+
1434
1434
+
jsesc@3.1.0: {}
1435
1435
+
1436
1436
+
json5@2.2.3: {}
1437
1437
+
1438
1438
+
lightningcss-android-arm64@1.32.0:
1439
1439
+
optional: true
1440
1440
+
1441
1441
+
lightningcss-darwin-arm64@1.32.0:
1442
1442
+
optional: true
1443
1443
+
1444
1444
+
lightningcss-darwin-x64@1.32.0:
1445
1445
+
optional: true
1446
1446
+
1447
1447
+
lightningcss-freebsd-x64@1.32.0:
1448
1448
+
optional: true
1449
1449
+
1450
1450
+
lightningcss-linux-arm-gnueabihf@1.32.0:
1451
1451
+
optional: true
1452
1452
+
1453
1453
+
lightningcss-linux-arm64-gnu@1.32.0:
1454
1454
+
optional: true
1455
1455
+
1456
1456
+
lightningcss-linux-arm64-musl@1.32.0:
1457
1457
+
optional: true
1458
1458
+
1459
1459
+
lightningcss-linux-x64-gnu@1.32.0:
1460
1460
+
optional: true
1461
1461
+
1462
1462
+
lightningcss-linux-x64-musl@1.32.0:
1463
1463
+
optional: true
1464
1464
+
1465
1465
+
lightningcss-win32-arm64-msvc@1.32.0:
1466
1466
+
optional: true
1467
1467
+
1468
1468
+
lightningcss-win32-x64-msvc@1.32.0:
1469
1469
+
optional: true
1470
1470
+
1471
1471
+
lightningcss@1.32.0:
1472
1472
+
dependencies:
1473
1473
+
detect-libc: 2.1.2
1474
1474
+
optionalDependencies:
1475
1475
+
lightningcss-android-arm64: 1.32.0
1476
1476
+
lightningcss-darwin-arm64: 1.32.0
1477
1477
+
lightningcss-darwin-x64: 1.32.0
1478
1478
+
lightningcss-freebsd-x64: 1.32.0
1479
1479
+
lightningcss-linux-arm-gnueabihf: 1.32.0
1480
1480
+
lightningcss-linux-arm64-gnu: 1.32.0
1481
1481
+
lightningcss-linux-arm64-musl: 1.32.0
1482
1482
+
lightningcss-linux-x64-gnu: 1.32.0
1483
1483
+
lightningcss-linux-x64-musl: 1.32.0
1484
1484
+
lightningcss-win32-arm64-msvc: 1.32.0
1485
1485
+
lightningcss-win32-x64-msvc: 1.32.0
1486
1486
+
1487
1487
+
lru-cache@5.1.1:
1488
1488
+
dependencies:
1489
1489
+
yallist: 3.1.1
1490
1490
+
1491
1491
+
lucide-solid@1.7.0(solid-js@1.9.12):
1492
1492
+
dependencies:
1493
1493
+
solid-js: 1.9.12
1494
1494
+
1495
1495
+
magic-string@0.30.21:
1496
1496
+
dependencies:
1497
1497
+
'@jridgewell/sourcemap-codec': 1.5.5
1498
1498
+
1499
1499
+
merge-anything@5.1.7:
1500
1500
+
dependencies:
1501
1501
+
is-what: 4.1.16
1502
1502
+
1503
1503
+
ms@2.1.3: {}
1504
1504
+
1505
1505
+
nanoid@3.3.11: {}
1506
1506
+
1507
1507
+
nanoid@5.1.7: {}
1508
1508
+
1509
1509
+
node-releases@2.0.36: {}
1510
1510
+
1511
1511
+
oxfmt@0.42.0:
1512
1512
+
dependencies:
1513
1513
+
tinypool: 2.1.0
1514
1514
+
optionalDependencies:
1515
1515
+
'@oxfmt/binding-android-arm-eabi': 0.42.0
1516
1516
+
'@oxfmt/binding-android-arm64': 0.42.0
1517
1517
+
'@oxfmt/binding-darwin-arm64': 0.42.0
1518
1518
+
'@oxfmt/binding-darwin-x64': 0.42.0
1519
1519
+
'@oxfmt/binding-freebsd-x64': 0.42.0
1520
1520
+
'@oxfmt/binding-linux-arm-gnueabihf': 0.42.0
1521
1521
+
'@oxfmt/binding-linux-arm-musleabihf': 0.42.0
1522
1522
+
'@oxfmt/binding-linux-arm64-gnu': 0.42.0
1523
1523
+
'@oxfmt/binding-linux-arm64-musl': 0.42.0
1524
1524
+
'@oxfmt/binding-linux-ppc64-gnu': 0.42.0
1525
1525
+
'@oxfmt/binding-linux-riscv64-gnu': 0.42.0
1526
1526
+
'@oxfmt/binding-linux-riscv64-musl': 0.42.0
1527
1527
+
'@oxfmt/binding-linux-s390x-gnu': 0.42.0
1528
1528
+
'@oxfmt/binding-linux-x64-gnu': 0.42.0
1529
1529
+
'@oxfmt/binding-linux-x64-musl': 0.42.0
1530
1530
+
'@oxfmt/binding-openharmony-arm64': 0.42.0
1531
1531
+
'@oxfmt/binding-win32-arm64-msvc': 0.42.0
1532
1532
+
'@oxfmt/binding-win32-ia32-msvc': 0.42.0
1533
1533
+
'@oxfmt/binding-win32-x64-msvc': 0.42.0
1534
1534
+
1535
1535
+
parse5@7.3.0:
1536
1536
+
dependencies:
1537
1537
+
entities: 6.0.1
1538
1538
+
1539
1539
+
picocolors@1.1.1: {}
1540
1540
+
1541
1541
+
picomatch@4.0.4: {}
1542
1542
+
1543
1543
+
postcss@8.5.8:
1544
1544
+
dependencies:
1545
1545
+
nanoid: 3.3.11
1546
1546
+
picocolors: 1.1.1
1547
1547
+
source-map-js: 1.2.1
1548
1548
+
1549
1549
+
rolldown@1.0.0-rc.12:
1550
1550
+
dependencies:
1551
1551
+
'@oxc-project/types': 0.122.0
1552
1552
+
'@rolldown/pluginutils': 1.0.0-rc.12
1553
1553
+
optionalDependencies:
1554
1554
+
'@rolldown/binding-android-arm64': 1.0.0-rc.12
1555
1555
+
'@rolldown/binding-darwin-arm64': 1.0.0-rc.12
1556
1556
+
'@rolldown/binding-darwin-x64': 1.0.0-rc.12
1557
1557
+
'@rolldown/binding-freebsd-x64': 1.0.0-rc.12
1558
1558
+
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
1559
1559
+
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
1560
1560
+
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
1561
1561
+
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
1562
1562
+
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
1563
1563
+
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
1564
1564
+
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
1565
1565
+
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
1566
1566
+
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.12
1567
1567
+
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
1568
1568
+
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
1569
1569
+
1570
1570
+
semver@6.3.1: {}
1571
1571
+
1572
1572
+
seroval-plugins@1.5.1(seroval@1.5.1):
1573
1573
+
dependencies:
1574
1574
+
seroval: 1.5.1
1575
1575
+
1576
1576
+
seroval@1.5.1: {}
1577
1577
+
1578
1578
+
solid-js@1.9.12:
1579
1579
+
dependencies:
1580
1580
+
csstype: 3.2.3
1581
1581
+
seroval: 1.5.1
1582
1582
+
seroval-plugins: 1.5.1(seroval@1.5.1)
1583
1583
+
1584
1584
+
solid-refresh@0.6.3(solid-js@1.9.12):
1585
1585
+
dependencies:
1586
1586
+
'@babel/generator': 7.29.1
1587
1587
+
'@babel/helper-module-imports': 7.28.6
1588
1588
+
'@babel/types': 7.29.0
1589
1589
+
solid-js: 1.9.12
1590
1590
+
transitivePeerDependencies:
1591
1591
+
- supports-color
1592
1592
+
1593
1593
+
source-map-js@1.2.1: {}
1594
1594
+
1595
1595
+
tailwindcss@4.2.2: {}
1596
1596
+
1597
1597
+
tapable@2.3.2: {}
1598
1598
+
1599
1599
+
tinyglobby@0.2.15:
1600
1600
+
dependencies:
1601
1601
+
fdir: 6.5.0(picomatch@4.0.4)
1602
1602
+
picomatch: 4.0.4
1603
1603
+
1604
1604
+
tinypool@2.1.0: {}
1605
1605
+
1606
1606
+
tslib@2.8.1:
1607
1607
+
optional: true
1608
1608
+
1609
1609
+
typescript@5.9.3: {}
1610
1610
+
1611
1611
+
undici-types@7.18.2: {}
1612
1612
+
1613
1613
+
unicode-segmenter@0.14.5: {}
1614
1614
+
1615
1615
+
update-browserslist-db@1.2.3(browserslist@4.28.1):
1616
1616
+
dependencies:
1617
1617
+
browserslist: 4.28.1
1618
1618
+
escalade: 3.2.0
1619
1619
+
picocolors: 1.1.1
1620
1620
+
1621
1621
+
vite-plugin-solid@2.11.11(solid-js@1.9.12)(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)):
1622
1622
+
dependencies:
1623
1623
+
'@babel/core': 7.29.0
1624
1624
+
'@types/babel__core': 7.20.5
1625
1625
+
babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.12)
1626
1626
+
merge-anything: 5.1.7
1627
1627
+
solid-js: 1.9.12
1628
1628
+
solid-refresh: 0.6.3(solid-js@1.9.12)
1629
1629
+
vite: 8.0.3(@types/node@25.5.0)(jiti@2.6.1)
1630
1630
+
vitefu: 1.1.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1))
1631
1631
+
transitivePeerDependencies:
1632
1632
+
- supports-color
1633
1633
+
1634
1634
+
vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1):
1635
1635
+
dependencies:
1636
1636
+
lightningcss: 1.32.0
1637
1637
+
picomatch: 4.0.4
1638
1638
+
postcss: 8.5.8
1639
1639
+
rolldown: 1.0.0-rc.12
1640
1640
+
tinyglobby: 0.2.15
1641
1641
+
optionalDependencies:
1642
1642
+
'@types/node': 25.5.0
1643
1643
+
fsevents: 2.3.3
1644
1644
+
jiti: 2.6.1
1645
1645
+
1646
1646
+
vitefu@1.1.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)):
1647
1647
+
optionalDependencies:
1648
1648
+
vite: 8.0.3(@types/node@25.5.0)(jiti@2.6.1)
1649
1649
+
1650
1650
+
yallist@3.1.1: {}
+5
public/favicon.svg
reviewed
···
1
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 36 36">
2
2
+
<circle cx="16" cy="16" r="15" fill="none" stroke="#c4b5fd" stroke-opacity="0.15" stroke-width="3"/>
3
3
+
<circle cx="16" cy="16" r="11" fill="none" stroke="#c4b5fd" stroke-opacity="0.3" stroke-width="3"/>
4
4
+
<circle cx="16" cy="16" r="7.5" fill="#c4b5fd"/>
5
5
+
</svg>
+35
scripts/generate-metadata.js
reviewed
···
1
1
+
import { mkdirSync, writeFileSync } from "fs";
2
2
+
import { dirname } from "path";
3
3
+
import { fileURLToPath } from "url";
4
4
+
5
5
+
const __filename = fileURLToPath(import.meta.url);
6
6
+
const __dirname = dirname(__filename);
7
7
+
8
8
+
const domain = process.env.APP_DOMAIN || "stream.place";
9
9
+
const protocol = process.env.APP_PROTOCOL || "https";
10
10
+
const baseUrl = `${protocol}://${domain}`;
11
11
+
12
12
+
const metadata = {
13
13
+
client_id: `${baseUrl}/oauth-client-metadata.json`,
14
14
+
client_name: "Streamplace",
15
15
+
client_uri: baseUrl,
16
16
+
logo_uri: `${baseUrl}/favicon.ico`,
17
17
+
redirect_uris: [`${baseUrl}/`],
18
18
+
scope: "atproto include:place.stream.authFull",
19
19
+
grant_types: ["authorization_code", "refresh_token"],
20
20
+
response_types: ["code"],
21
21
+
token_endpoint_auth_method: "none",
22
22
+
application_type: "web",
23
23
+
dpop_bound_access_tokens: true,
24
24
+
};
25
25
+
26
26
+
const outputPath = `${__dirname}/../public/oauth-client-metadata.json`;
27
27
+
28
28
+
try {
29
29
+
mkdirSync(dirname(outputPath), { recursive: true });
30
30
+
writeFileSync(outputPath, JSON.stringify(metadata, null, 2) + "\n");
31
31
+
console.log(`Generated OAuth metadata for ${baseUrl}`);
32
32
+
} catch (error) {
33
33
+
console.error("Failed to generate metadata:", error);
34
34
+
process.exit(1);
35
35
+
}
+3
src/auth/login-modal.ts
reviewed
···
1
1
+
import { createSignal } from "solid-js";
2
2
+
3
3
+
export const [showLoginModal, setShowLoginModal] = createSignal(false);
+12
src/auth/login.ts
reviewed
···
1
1
+
import { createAuthorizationUrl } from "@atcute/oauth-browser-client";
2
2
+
3
3
+
import "./oauth-config";
4
4
+
5
5
+
export const signIn = async (handle: string): Promise<void> => {
6
6
+
const authUrl = await createAuthorizationUrl({
7
7
+
scope: import.meta.env.VITE_OAUTH_SCOPE,
8
8
+
target: { type: "account", identifier: handle as `${string}.${string}` },
9
9
+
});
10
10
+
11
11
+
location.assign(authUrl);
12
12
+
};
+15
src/auth/oauth-config.ts
reviewed
···
1
1
+
import { LocalActorResolver } from "@atcute/identity-resolver";
2
2
+
import { configureOAuth } from "@atcute/oauth-browser-client";
3
3
+
4
4
+
import { didDocumentResolver, handleResolver } from "../lib/api";
5
5
+
6
6
+
configureOAuth({
7
7
+
metadata: {
8
8
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
9
9
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
10
10
+
},
11
11
+
identityResolver: new LocalActorResolver({
12
12
+
handleResolver: handleResolver,
13
13
+
didDocumentResolver: didDocumentResolver,
14
14
+
}),
15
15
+
});
+77
src/auth/session-manager.ts
reviewed
···
1
1
+
import { Client } from "@atcute/client";
2
2
+
import {
3
3
+
finalizeAuthorization,
4
4
+
getSession,
5
5
+
OAuthUserAgent,
6
6
+
type Session,
7
7
+
} from "@atcute/oauth-browser-client";
8
8
+
9
9
+
import { resolveDidDoc } from "../lib/api";
10
10
+
import { agent, setAgent, setLoggedInDid, setLoggedInHandle } from "./state";
11
11
+
import "./oauth-config";
12
12
+
13
13
+
const resolveOwnHandle = async (did: string): Promise<string | undefined> => {
14
14
+
try {
15
15
+
const doc = await resolveDidDoc(did);
16
16
+
const alias = doc.alsoKnownAs?.find((a) => a.startsWith("at://"));
17
17
+
return alias?.replace("at://", "");
18
18
+
} catch {
19
19
+
return undefined;
20
20
+
}
21
21
+
};
22
22
+
23
23
+
export const initAuth = async (): Promise<void> => {
24
24
+
const session = await (async (): Promise<Session | undefined> => {
25
25
+
const params = new URLSearchParams(decodeURIComponent(location.hash.slice(1)));
26
26
+
27
27
+
if (params.has("state") && (params.has("code") || params.has("error"))) {
28
28
+
history.replaceState(null, "", location.pathname + location.search);
29
29
+
30
30
+
const auth = await finalizeAuthorization(params);
31
31
+
const did = auth.session.info.sub;
32
32
+
33
33
+
localStorage.setItem("atproto_did", did);
34
34
+
return auth.session;
35
35
+
} else {
36
36
+
const storedDid = localStorage.getItem("atproto_did");
37
37
+
38
38
+
if (storedDid) {
39
39
+
try {
40
40
+
const session = await getSession(storedDid as `did:${string}:${string}`);
41
41
+
const rpc = new Client({ handler: new OAuthUserAgent(session) });
42
42
+
const res = await rpc.get("com.atproto.server.getSession");
43
43
+
if (!res.ok) throw new Error("Session verification failed");
44
44
+
return session;
45
45
+
} catch (err) {
46
46
+
console.warn("Failed to restore session:", err);
47
47
+
return undefined;
48
48
+
}
49
49
+
}
50
50
+
}
51
51
+
})();
52
52
+
53
53
+
if (session) {
54
54
+
const did = session.info.sub;
55
55
+
const oauthAgent = new OAuthUserAgent(session);
56
56
+
setAgent(oauthAgent);
57
57
+
setLoggedInDid(did);
58
58
+
59
59
+
const handle = await resolveOwnHandle(did);
60
60
+
if (handle) setLoggedInHandle(handle);
61
61
+
}
62
62
+
};
63
63
+
64
64
+
export const signOut = async (): Promise<void> => {
65
65
+
const currentAgent = agent();
66
66
+
if (currentAgent) {
67
67
+
try {
68
68
+
await currentAgent.signOut();
69
69
+
} catch {
70
70
+
// ignore signout errors
71
71
+
}
72
72
+
}
73
73
+
localStorage.removeItem("atproto_did");
74
74
+
setAgent(undefined);
75
75
+
setLoggedInDid(undefined);
76
76
+
setLoggedInHandle(undefined);
77
77
+
};
+6
src/auth/state.ts
reviewed
···
1
1
+
import { OAuthUserAgent } from "@atcute/oauth-browser-client";
2
2
+
import { createSignal } from "solid-js";
3
3
+
4
4
+
export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>();
5
5
+
export const [loggedInDid, setLoggedInDid] = createSignal<string | undefined>();
6
6
+
export const [loggedInHandle, setLoggedInHandle] = createSignal<string | undefined>();
+303
src/components/Chat.tsx
reviewed
···
1
1
+
import { Camera, CornerDownRight, Reply, Sword, Star, X } from "lucide-solid";
2
2
+
import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
3
3
+
4
4
+
import { setShowLoginModal } from "../auth/login-modal";
5
5
+
import { agent, loggedInDid } from "../auth/state";
6
6
+
import { resolveHandle } from "../lib/api";
7
7
+
import {
8
8
+
connectChatWs,
9
9
+
segmentRichText,
10
10
+
sendChatMessage,
11
11
+
type ChatConnection,
12
12
+
type ChatMessage,
13
13
+
type Facet,
14
14
+
type StreamInfo,
15
15
+
} from "../lib/chat";
16
16
+
17
17
+
export interface ChatProps {
18
18
+
handle: string;
19
19
+
streamerDid?: string;
20
20
+
onStreamInfo?: (info: StreamInfo) => void;
21
21
+
onViewerCount?: (count: number) => void;
22
22
+
class?: string;
23
23
+
}
24
24
+
25
25
+
const MAX_MESSAGES = 500;
26
26
+
27
27
+
function getAuthorColor(msg: ChatMessage): string {
28
28
+
const color = msg.chatProfile?.color;
29
29
+
if (color && color.red !== undefined) {
30
30
+
return `rgb(${color.red}, ${color.green}, ${color.blue})`;
31
31
+
}
32
32
+
return "#4ade80";
33
33
+
}
34
34
+
35
35
+
function ChatBadges(props: { badges?: ChatMessage["badges"] }) {
36
36
+
if (!props.badges || props.badges.length === 0) return null;
37
37
+
38
38
+
return (
39
39
+
<>
40
40
+
<For each={props.badges}>
41
41
+
{(badge) => {
42
42
+
const type = badge.badgeType;
43
43
+
if (type === "place.stream.badge.defs#mod") {
44
44
+
return (
45
45
+
<span class="mr-0.5 inline-flex align-middle" title="Moderator">
46
46
+
<Sword size={12} class="text-blue-400" />
47
47
+
</span>
48
48
+
);
49
49
+
}
50
50
+
if (type === "place.stream.badge.defs#streamer") {
51
51
+
return (
52
52
+
<span class="mr-0.5 inline-flex align-middle" title="Streamer">
53
53
+
<Camera size={12} class="text-sp-red" />
54
54
+
</span>
55
55
+
);
56
56
+
}
57
57
+
if (type === "place.stream.badge.defs#vip") {
58
58
+
return (
59
59
+
<span class="mr-0.5 inline-flex align-middle" title="VIP">
60
60
+
<Star size={12} class="text-yellow-400" />
61
61
+
</span>
62
62
+
);
63
63
+
}
64
64
+
return null;
65
65
+
}}
66
66
+
</For>
67
67
+
</>
68
68
+
);
69
69
+
}
70
70
+
71
71
+
function FacetSegment(props: { text: string; facet?: Facet }) {
72
72
+
if (!props.facet) return <>{props.text}</>;
73
73
+
74
74
+
for (const feature of props.facet.features) {
75
75
+
if (feature.$type === "app.bsky.richtext.facet#link") {
76
76
+
return (
77
77
+
<a
78
78
+
href={feature.uri}
79
79
+
target="_blank"
80
80
+
rel="noopener noreferrer"
81
81
+
class="text-sp-accent decoration-sp-accent/40 hover:decoration-sp-accent underline"
82
82
+
>
83
83
+
{props.text}
84
84
+
</a>
85
85
+
);
86
86
+
}
87
87
+
if (feature.$type === "app.bsky.richtext.facet#mention") {
88
88
+
return (
89
89
+
<a
90
90
+
href={`https://bsky.app/profile/${feature.did}`}
91
91
+
target="_blank"
92
92
+
rel="noopener noreferrer"
93
93
+
class="text-sp-accent font-medium"
94
94
+
>
95
95
+
{props.text}
96
96
+
</a>
97
97
+
);
98
98
+
}
99
99
+
}
100
100
+
101
101
+
return <>{props.text}</>;
102
102
+
}
103
103
+
104
104
+
export function Chat(props: ChatProps) {
105
105
+
let messagesEl!: HTMLDivElement;
106
106
+
let ws: ChatConnection | undefined;
107
107
+
108
108
+
const [messages, setMessages] = createSignal<ChatMessage[]>([]);
109
109
+
const [connected, setConnected] = createSignal(false);
110
110
+
const [inputText, setInputText] = createSignal("");
111
111
+
const [sending, setSending] = createSignal(false);
112
112
+
const [replyingTo, setReplyingTo] = createSignal<ChatMessage | undefined>();
113
113
+
let inputEl!: HTMLInputElement;
114
114
+
115
115
+
const addMessage = (msg: ChatMessage) => {
116
116
+
setMessages((prev) => {
117
117
+
// Insert sorted by indexedAt to handle backfill arriving in reverse order
118
118
+
const msgTime = new Date(msg.indexedAt).getTime();
119
119
+
let i = prev.length;
120
120
+
while (i > 0 && new Date(prev[i - 1].indexedAt).getTime() > msgTime) {
121
121
+
i--;
122
122
+
}
123
123
+
const next = [...prev.slice(0, i), msg, ...prev.slice(i)];
124
124
+
if (next.length > MAX_MESSAGES) return next.slice(next.length - MAX_MESSAGES);
125
125
+
return next;
126
126
+
});
127
127
+
128
128
+
// auto-scroll to bottom
129
129
+
requestAnimationFrame(() => {
130
130
+
if (messagesEl) {
131
131
+
messagesEl.scrollTop = messagesEl.scrollHeight;
132
132
+
}
133
133
+
});
134
134
+
};
135
135
+
136
136
+
const connect = () => {
137
137
+
ws = connectChatWs(props.handle, {
138
138
+
onMessage: addMessage,
139
139
+
onStreamInfo: (info) => props.onStreamInfo?.(info),
140
140
+
onViewerCount: (count) => props.onViewerCount?.(count),
141
141
+
onOpen: () => setConnected(true),
142
142
+
onClose: () => setConnected(false),
143
143
+
});
144
144
+
};
145
145
+
146
146
+
const send = async () => {
147
147
+
const text = inputText().trim();
148
148
+
if (!text) return;
149
149
+
150
150
+
const currentAgent = agent();
151
151
+
const did = loggedInDid();
152
152
+
const streamerDid = props.streamerDid;
153
153
+
if (!currentAgent || !did || !streamerDid) return;
154
154
+
155
155
+
const replyMsg = replyingTo();
156
156
+
const reply = replyMsg
157
157
+
? {
158
158
+
root: {
159
159
+
uri: replyMsg.record.reply?.root?.uri ?? replyMsg.uri,
160
160
+
cid: replyMsg.record.reply?.root?.cid ?? replyMsg.cid,
161
161
+
},
162
162
+
parent: { uri: replyMsg.uri, cid: replyMsg.cid },
163
163
+
}
164
164
+
: undefined;
165
165
+
166
166
+
setSending(true);
167
167
+
try {
168
168
+
await sendChatMessage(currentAgent, did, streamerDid, text, resolveHandle, reply);
169
169
+
setInputText("");
170
170
+
setReplyingTo(undefined);
171
171
+
} catch (err) {
172
172
+
console.error("Failed to send chat:", err);
173
173
+
} finally {
174
174
+
setSending(false);
175
175
+
}
176
176
+
};
177
177
+
178
178
+
const handleKeyDown = (e: KeyboardEvent) => {
179
179
+
if (e.key === "Enter" && !e.shiftKey) {
180
180
+
e.preventDefault();
181
181
+
send();
182
182
+
}
183
183
+
if (e.key === "Escape") {
184
184
+
setReplyingTo(undefined);
185
185
+
}
186
186
+
};
187
187
+
188
188
+
onMount(() => {
189
189
+
connect();
190
190
+
});
191
191
+
192
192
+
onCleanup(() => {
193
193
+
if (ws) {
194
194
+
ws.close();
195
195
+
ws = undefined;
196
196
+
}
197
197
+
});
198
198
+
199
199
+
return (
200
200
+
<div
201
201
+
class={`border-sp-border bg-sp-surface flex min-h-0 flex-col border-l ${props.class ?? ""}`}
202
202
+
>
203
203
+
{/* Messages */}
204
204
+
<div ref={messagesEl} class="min-h-0 flex-1 overflow-y-auto pt-2">
205
205
+
<Show
206
206
+
when={messages().length > 0}
207
207
+
fallback={
208
208
+
<div class="text-sp-dim flex h-full items-center justify-center text-sm">
209
209
+
{connected() ? "Waiting for messages..." : "Connecting..."}
210
210
+
</div>
211
211
+
}
212
212
+
>
213
213
+
<div class="space-y-1">
214
214
+
<For each={messages()}>
215
215
+
{(msg) => (
216
216
+
<div class="group/msg hover:bg-sp-hover relative px-3 text-sm leading-relaxed">
217
217
+
<Show when={agent()}>
218
218
+
<button
219
219
+
class="text-sp-dim hover:text-sp-accent absolute top-0 right-0 hidden rounded p-0.5 transition-colors group-hover/msg:inline-flex"
220
220
+
title="Reply"
221
221
+
onClick={() => {
222
222
+
setReplyingTo(msg);
223
223
+
inputEl?.focus();
224
224
+
}}
225
225
+
>
226
226
+
<Reply size={14} />
227
227
+
</button>
228
228
+
</Show>
229
229
+
<Show when={msg.replyTo}>
230
230
+
{(parent) => (
231
231
+
<div class="text-sp-dim flex items-center gap-1 text-[11px]">
232
232
+
<CornerDownRight size={10} class="shrink-0" />
233
233
+
<span class="font-medium" style={{ color: getAuthorColor(parent()) }}>
234
234
+
{parent().author.handle}
235
235
+
</span>
236
236
+
<span class="truncate">{parent().record.text}</span>
237
237
+
</div>
238
238
+
)}
239
239
+
</Show>
240
240
+
<ChatBadges badges={msg.badges} />
241
241
+
<span class="font-medium" style={{ color: getAuthorColor(msg) }}>
242
242
+
{msg.author.handle}
243
243
+
</span>
244
244
+
<span class="text-sp-dim">: </span>
245
245
+
<span class="wrap-break-word">
246
246
+
<For each={segmentRichText(msg.record.text, msg.record.facets)}>
247
247
+
{(seg) => <FacetSegment text={seg.text} facet={seg.facet} />}
248
248
+
</For>
249
249
+
</span>
250
250
+
</div>
251
251
+
)}
252
252
+
</For>
253
253
+
</div>
254
254
+
</Show>
255
255
+
</div>
256
256
+
257
257
+
{/* Input */}
258
258
+
<Show
259
259
+
when={agent()}
260
260
+
fallback={
261
261
+
<button
262
262
+
class="text-sp-dim hover:text-sp-accent border-sp-border hover:bg-sp-hover mt-2 w-full border-t py-4 text-center text-xs italic transition-colors"
263
263
+
onClick={() => setShowLoginModal(true)}
264
264
+
>
265
265
+
Sign in to chat
266
266
+
</button>
267
267
+
}
268
268
+
>
269
269
+
<div class="px-2 py-3">
270
270
+
<Show when={replyingTo()}>
271
271
+
{(msg) => (
272
272
+
<div class="bg-sp-bg text-sp-dim mb-1.5 flex items-center gap-1.5 rounded px-2 py-1 text-xs">
273
273
+
<CornerDownRight size={10} class="shrink-0" />
274
274
+
<span class="font-medium" style={{ color: getAuthorColor(msg()) }}>
275
275
+
{msg().author.handle}
276
276
+
</span>
277
277
+
<span class="min-w-0 flex-1 truncate">{msg().record.text}</span>
278
278
+
<button
279
279
+
class="hover:text-sp-text shrink-0 rounded p-0.5 transition-colors"
280
280
+
onClick={() => setReplyingTo(undefined)}
281
281
+
>
282
282
+
<X size={12} />
283
283
+
</button>
284
284
+
</div>
285
285
+
)}
286
286
+
</Show>
287
287
+
<input
288
288
+
ref={inputEl}
289
289
+
type="text"
290
290
+
placeholder={
291
291
+
replyingTo() ? `Reply to ${replyingTo()!.author.handle}...` : "Send a message..."
292
292
+
}
293
293
+
class="border-sp-border bg-sp-bg text-sp-text placeholder:text-sp-dim focus:border-sp-accent w-full rounded-sm border px-3 py-2.5 text-sm focus:outline-none"
294
294
+
value={inputText()}
295
295
+
onInput={(e) => setInputText(e.currentTarget.value)}
296
296
+
onKeyDown={handleKeyDown}
297
297
+
disabled={sending() || !props.streamerDid}
298
298
+
/>
299
299
+
</div>
300
300
+
</Show>
301
301
+
</div>
302
302
+
);
303
303
+
}
+19
src/components/Header.tsx
reviewed
···
1
1
+
import { A } from "@solidjs/router";
2
2
+
3
3
+
import { LoginButton } from "./LoginButton";
4
4
+
5
5
+
export function Header() {
6
6
+
return (
7
7
+
<header class="border-sp-border flex items-center justify-between border-b px-4 py-3">
8
8
+
<A href="/" class="flex items-center gap-2 text-lg font-medium tracking-tight">
9
9
+
<svg viewBox="-2 -2 36 36" class="text-sp-accent h-7 w-7" aria-hidden="true">
10
10
+
<circle cx="16" cy="16" r="15" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="3" />
11
11
+
<circle cx="16" cy="16" r="11" fill="none" stroke="currentColor" stroke-opacity="0.3" stroke-width="3" />
12
12
+
<circle cx="16" cy="16" r="7.5" fill="currentColor" />
13
13
+
</svg>
14
14
+
<span>streamplace</span>
15
15
+
</A>
16
16
+
<LoginButton />
17
17
+
</header>
18
18
+
);
19
19
+
}
+98
src/components/LoginButton.tsx
reviewed
···
1
1
+
import { Show, createEffect, createSignal, onCleanup } from "solid-js";
2
2
+
3
3
+
import { signIn } from "../auth/login";
4
4
+
import { showLoginModal, setShowLoginModal } from "../auth/login-modal";
5
5
+
import { signOut } from "../auth/session-manager";
6
6
+
import { agent, loggedInHandle } from "../auth/state";
7
7
+
8
8
+
export function LoginButton() {
9
9
+
const [handle, setHandle] = createSignal("");
10
10
+
const [loading, setLoading] = createSignal(false);
11
11
+
12
12
+
const handleSignIn = async () => {
13
13
+
const h = handle().trim();
14
14
+
if (!h) return;
15
15
+
setLoading(true);
16
16
+
try {
17
17
+
await signIn(h);
18
18
+
} catch (err) {
19
19
+
console.error("Sign in failed:", err);
20
20
+
setLoading(false);
21
21
+
}
22
22
+
};
23
23
+
24
24
+
const handleKeyDown = (e: KeyboardEvent) => {
25
25
+
if (e.key === "Enter") handleSignIn();
26
26
+
};
27
27
+
28
28
+
createEffect(() => {
29
29
+
if (!showLoginModal()) return;
30
30
+
const onEsc = (e: KeyboardEvent) => {
31
31
+
if (e.key === "Escape") setShowLoginModal(false);
32
32
+
};
33
33
+
document.addEventListener("keydown", onEsc);
34
34
+
onCleanup(() => document.removeEventListener("keydown", onEsc));
35
35
+
});
36
36
+
37
37
+
return (
38
38
+
<div class="flex items-center gap-3">
39
39
+
<Show
40
40
+
when={agent()}
41
41
+
fallback={
42
42
+
<>
43
43
+
<button
44
44
+
class="bg-sp-accent text-sp-bg hover:bg-sp-accent/80 rounded-sm px-3 py-1.5 text-sm font-medium transition-colors"
45
45
+
onClick={() => setShowLoginModal(true)}
46
46
+
>
47
47
+
Sign in
48
48
+
</button>
49
49
+
<Show when={showLoginModal()}>
50
50
+
<div
51
51
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
52
52
+
onClick={(e) => {
53
53
+
if (e.target === e.currentTarget) setShowLoginModal(false);
54
54
+
}}
55
55
+
>
56
56
+
<div class="bg-sp-surface border-sp-border mx-4 flex w-full max-w-md flex-col gap-4 rounded-lg border p-5 shadow-lg">
57
57
+
<h2 class="text-sp-text text-lg font-semibold">Sign in</h2>
58
58
+
<input
59
59
+
type="text"
60
60
+
placeholder="handle.bsky.social"
61
61
+
class="border-sp-border bg-sp-bg text-sp-text placeholder:text-sp-dim focus:border-sp-accent rounded-sm border px-3 py-1.5 text-sm focus:outline-none"
62
62
+
value={handle()}
63
63
+
onInput={(e) => setHandle(e.currentTarget.value)}
64
64
+
onKeyDown={handleKeyDown}
65
65
+
ref={(el: HTMLInputElement) => setTimeout(() => el.focus())}
66
66
+
/>
67
67
+
<div class="flex justify-end gap-2">
68
68
+
<button
69
69
+
class="text-sp-dim hover:text-sp-text rounded-sm px-3 py-1.5 text-sm transition-colors"
70
70
+
onClick={() => setShowLoginModal(false)}
71
71
+
>
72
72
+
Cancel
73
73
+
</button>
74
74
+
<button
75
75
+
class="bg-sp-accent text-sp-bg hover:bg-sp-accent/80 rounded-sm px-3 py-1.5 text-sm font-medium transition-colors disabled:opacity-50"
76
76
+
onClick={handleSignIn}
77
77
+
disabled={loading() || !handle().trim()}
78
78
+
>
79
79
+
{loading() ? "..." : "Go"}
80
80
+
</button>
81
81
+
</div>
82
82
+
</div>
83
83
+
</div>
84
84
+
</Show>
85
85
+
</>
86
86
+
}
87
87
+
>
88
88
+
<span class="text-sp-dim text-sm">@{loggedInHandle() || "..."}</span>
89
89
+
<button
90
90
+
class="border-sp-border text-sp-dim hover:border-sp-red hover:text-sp-red rounded-sm border px-3 py-1.5 text-sm transition-colors"
91
91
+
onClick={() => signOut()}
92
92
+
>
93
93
+
Sign out
94
94
+
</button>
95
95
+
</Show>
96
96
+
</div>
97
97
+
);
98
98
+
}
+53
src/components/StreamCard.tsx
reviewed
···
1
1
+
import { A } from "@solidjs/router";
2
2
+
3
3
+
export interface StreamCardProps {
4
4
+
handle: string;
5
5
+
did: string;
6
6
+
title: string;
7
7
+
viewerCount: number;
8
8
+
avatarUrl?: string;
9
9
+
thumbRef?: string;
10
10
+
}
11
11
+
12
12
+
function getThumbnailUrl(handle: string): string {
13
13
+
return `https://stream.place/api/playback/${encodeURIComponent(handle)}/stream.jpg?ts=${Date.now()}`;
14
14
+
}
15
15
+
16
16
+
export function StreamCard(props: StreamCardProps) {
17
17
+
const thumbUrl = () => getThumbnailUrl(props.handle);
18
18
+
19
19
+
return (
20
20
+
<A
21
21
+
href={`/${props.handle}`}
22
22
+
class="group border-sp-border bg-sp-surface hover:border-sp-accent overflow-hidden rounded-lg border transition-colors"
23
23
+
>
24
24
+
<div class="bg-sp-bg aspect-video w-full overflow-hidden">
25
25
+
{thumbUrl() ? (
26
26
+
<img src={thumbUrl()} alt="" class="h-full w-full object-cover" loading="lazy" />
27
27
+
) : (
28
28
+
<div class="text-sp-dim flex h-full items-center justify-center">
29
29
+
<span class="text-3xl">▶</span>
30
30
+
</div>
31
31
+
)}
32
32
+
</div>
33
33
+
<div class="group-hover:bg-sp-accent/10 flex gap-3 p-3 transition-colors">
34
34
+
{props.avatarUrl ? (
35
35
+
<img src={props.avatarUrl} alt="" class="h-9 w-9 shrink-0 rounded-full" loading="lazy" />
36
36
+
) : (
37
37
+
<div class="bg-sp-border h-9 w-9 shrink-0 rounded-full" />
38
38
+
)}
39
39
+
<div class="min-w-0 flex-1">
40
40
+
<div class="truncate text-sm font-medium">{props.title}</div>
41
41
+
<div class="text-sp-dim truncate text-xs">@{props.handle}</div>
42
42
+
<div class="text-sp-dim mt-1 flex items-center gap-1.5 text-xs">
43
43
+
<span
44
44
+
class="bg-sp-accent inline-block h-2 w-2 rounded-full"
45
45
+
style={{ animation: "pulse-live 3s ease-in-out infinite" }}
46
46
+
/>
47
47
+
{props.viewerCount} watching
48
48
+
</div>
49
49
+
</div>
50
50
+
</div>
51
51
+
</A>
52
52
+
);
53
53
+
}
+160
src/components/VideoPlayer.tsx
reviewed
···
1
1
+
import { Maximize, Volume2, VolumeOff } from "lucide-solid";
2
2
+
import { createSignal, onCleanup, onMount, Show } from "solid-js";
3
3
+
4
4
+
import { connectWhep, type WhepConnection } from "../lib/whep";
5
5
+
6
6
+
export interface VideoPlayerProps {
7
7
+
handle: string;
8
8
+
}
9
9
+
10
10
+
export function VideoPlayer(props: VideoPlayerProps) {
11
11
+
let videoEl!: HTMLVideoElement;
12
12
+
let containerEl!: HTMLDivElement;
13
13
+
let connection: WhepConnection | undefined;
14
14
+
15
15
+
const [status, setStatus] = createSignal<"connecting" | "live" | "error" | "idle">("idle");
16
16
+
const [muted, setMuted] = createSignal(true);
17
17
+
const [volume, setVolume] = createSignal(1);
18
18
+
const [errorMsg, setErrorMsg] = createSignal("");
19
19
+
20
20
+
const connect = async () => {
21
21
+
setStatus("connecting");
22
22
+
setErrorMsg("");
23
23
+
24
24
+
try {
25
25
+
connection = await connectWhep(props.handle);
26
26
+
27
27
+
connection.pc.oniceconnectionstatechange = () => {
28
28
+
const state = connection!.pc.iceConnectionState;
29
29
+
if (state === "connected" || state === "completed") {
30
30
+
setStatus("live");
31
31
+
} else if (state === "failed" || state === "disconnected") {
32
32
+
setStatus("error");
33
33
+
setErrorMsg("Connection lost");
34
34
+
}
35
35
+
};
36
36
+
37
37
+
connection.pc.onconnectionstatechange = () => {
38
38
+
if (connection!.pc.connectionState === "failed") {
39
39
+
setStatus("error");
40
40
+
setErrorMsg("Connection failed");
41
41
+
}
42
42
+
};
43
43
+
44
44
+
videoEl.srcObject = connection.stream;
45
45
+
videoEl.play().catch(() => {});
46
46
+
} catch (err) {
47
47
+
setStatus("error");
48
48
+
setErrorMsg(err instanceof Error ? err.message : "Failed to connect");
49
49
+
}
50
50
+
};
51
51
+
52
52
+
const disconnect = () => {
53
53
+
if (connection) {
54
54
+
connection.pc.close();
55
55
+
connection = undefined;
56
56
+
}
57
57
+
videoEl.srcObject = null;
58
58
+
setStatus("idle");
59
59
+
};
60
60
+
61
61
+
const toggleMute = () => {
62
62
+
const next = !muted();
63
63
+
setMuted(next);
64
64
+
videoEl.muted = next;
65
65
+
};
66
66
+
67
67
+
const handleVolume = (v: number) => {
68
68
+
setVolume(v);
69
69
+
videoEl.volume = v * v;
70
70
+
if (v > 0 && muted()) {
71
71
+
setMuted(false);
72
72
+
videoEl.muted = false;
73
73
+
} else if (v === 0) {
74
74
+
setMuted(true);
75
75
+
videoEl.muted = true;
76
76
+
}
77
77
+
};
78
78
+
79
79
+
const toggleFullscreen = () => {
80
80
+
if (document.fullscreenElement) {
81
81
+
document.exitFullscreen();
82
82
+
} else {
83
83
+
containerEl.requestFullscreen().catch(() => {});
84
84
+
}
85
85
+
};
86
86
+
87
87
+
onMount(() => {
88
88
+
videoEl.muted = true;
89
89
+
connect();
90
90
+
});
91
91
+
92
92
+
onCleanup(() => {
93
93
+
disconnect();
94
94
+
});
95
95
+
96
96
+
return (
97
97
+
<div ref={containerEl} class="group/player relative w-full overflow-hidden bg-black">
98
98
+
<video ref={videoEl} class="aspect-video w-full" playsinline autoplay />
99
99
+
100
100
+
{/* Status overlay */}
101
101
+
<Show when={status() !== "live"}>
102
102
+
<div class="absolute inset-0 flex items-center justify-center bg-black/60">
103
103
+
<Show when={status() === "connecting"}>
104
104
+
<div class="text-sp-dim flex items-center gap-2">
105
105
+
<div class="border-sp-dim border-t-sp-accent h-5 w-5 animate-spin rounded-full border-2" />
106
106
+
Connecting...
107
107
+
</div>
108
108
+
</Show>
109
109
+
<Show when={status() === "error"}>
110
110
+
<div class="text-center">
111
111
+
<div class="text-sp-red">{errorMsg() || "Error"}</div>
112
112
+
<button
113
113
+
class="bg-sp-surface text-sp-text hover:bg-sp-border mt-2 rounded-sm px-3 py-1.5 text-sm transition-colors"
114
114
+
onClick={connect}
115
115
+
>
116
116
+
Retry
117
117
+
</button>
118
118
+
</div>
119
119
+
</Show>
120
120
+
<Show when={status() === "idle"}>
121
121
+
<div class="text-sp-dim">Stream offline</div>
122
122
+
</Show>
123
123
+
</div>
124
124
+
</Show>
125
125
+
126
126
+
{/* Controls */}
127
127
+
<div class="absolute right-0 bottom-0 left-0 flex items-center gap-2 bg-black/60 p-3 opacity-0 transition-opacity group-hover/player:opacity-100">
128
128
+
<Show when={status() === "live"}>
129
129
+
<span class="bg-sp-accent mr-1 flex items-center gap-1.5 rounded-sm px-1.5 py-0.5 text-xs font-medium text-black">
130
130
+
LIVE
131
131
+
</span>
132
132
+
</Show>
133
133
+
<div class="flex-1" />
134
134
+
<button
135
135
+
class="rounded-sm p-1.5 text-white/70 transition-colors hover:text-white"
136
136
+
onClick={toggleMute}
137
137
+
title={muted() ? "Unmute" : "Mute"}
138
138
+
>
139
139
+
{muted() ? <VolumeOff size={18} /> : <Volume2 size={18} />}
140
140
+
</button>
141
141
+
<input
142
142
+
type="range"
143
143
+
min="0"
144
144
+
max="1"
145
145
+
step="0.01"
146
146
+
value={muted() ? 0 : volume()}
147
147
+
onInput={(e) => handleVolume(parseFloat(e.currentTarget.value))}
148
148
+
class="h-1 w-20 cursor-pointer appearance-none rounded-full bg-white/30 accent-white"
149
149
+
/>
150
150
+
<button
151
151
+
class="rounded-sm p-1.5 text-white/70 transition-colors hover:text-white"
152
152
+
onClick={toggleFullscreen}
153
153
+
title="Fullscreen"
154
154
+
>
155
155
+
<Maximize size={18} />
156
156
+
</button>
157
157
+
</div>
158
158
+
</div>
159
159
+
);
160
160
+
}
+35
src/index.css
reviewed
···
1
1
+
@import "tailwindcss";
2
2
+
3
3
+
@theme {
4
4
+
--font-sans: "DM Sans", sans-serif;
5
5
+
6
6
+
--color-sp-bg: #131316;
7
7
+
--color-sp-surface: #1c1c20;
8
8
+
--color-sp-border: #222228;
9
9
+
--color-sp-text: #f0f0f4;
10
10
+
--color-sp-dim: #8a8a96;
11
11
+
--color-sp-accent: #c4b5fd;
12
12
+
--color-sp-accent-dim: #c4b5fd15;
13
13
+
--color-sp-red: #f87171;
14
14
+
--color-sp-red-dim: #f8717118;
15
15
+
--color-sp-hover: #242428;
16
16
+
}
17
17
+
18
18
+
html {
19
19
+
scrollbar-gutter: stable both-edges;
20
20
+
}
21
21
+
22
22
+
* {
23
23
+
scrollbar-width: thin;
24
24
+
scrollbar-color: var(--color-sp-border) transparent;
25
25
+
}
26
26
+
27
27
+
@keyframes pulse-live {
28
28
+
0%,
29
29
+
100% {
30
30
+
opacity: 1;
31
31
+
}
32
32
+
50% {
33
33
+
opacity: 0.6;
34
34
+
}
35
35
+
}
+19
src/index.tsx
reviewed
···
1
1
+
/* @refresh reload */
2
2
+
import { Route, Router } from "@solidjs/router";
3
3
+
import { render } from "solid-js/web";
4
4
+
5
5
+
import { Layout } from "./layout";
6
6
+
import { Home } from "./pages/Home";
7
7
+
import { Watch } from "./pages/Watch";
8
8
+
9
9
+
import "./index.css";
10
10
+
11
11
+
render(
12
12
+
() => (
13
13
+
<Router root={Layout}>
14
14
+
<Route path="/" component={Home} />
15
15
+
<Route path="/:handle" component={Watch} />
16
16
+
</Router>
17
17
+
),
18
18
+
document.getElementById("root")!,
19
19
+
);
+19
src/layout.tsx
reviewed
···
1
1
+
import { type ParentProps, onMount } from "solid-js";
2
2
+
3
3
+
import { initAuth } from "./auth/session-manager";
4
4
+
import { Header } from "./components/Header";
5
5
+
6
6
+
export function Layout(props: ParentProps) {
7
7
+
onMount(() => {
8
8
+
initAuth().catch((err) => {
9
9
+
console.warn("Auth init failed:", err);
10
10
+
});
11
11
+
});
12
12
+
13
13
+
return (
14
14
+
<div class="flex h-dvh flex-col">
15
15
+
<Header />
16
16
+
<main class="flex min-h-0 flex-1 flex-col">{props.children}</main>
17
17
+
</div>
18
18
+
);
19
19
+
}
+85
src/lib/api.ts
reviewed
···
1
1
+
import "@atcute/atproto";
2
2
+
import { type DidDocument, getPdsEndpoint, isAtprotoDid } from "@atcute/identity";
3
3
+
import {
4
4
+
AtprotoWebDidDocumentResolver,
5
5
+
CompositeDidDocumentResolver,
6
6
+
CompositeHandleResolver,
7
7
+
DohJsonHandleResolver,
8
8
+
PlcDidDocumentResolver,
9
9
+
WellKnownHandleResolver,
10
10
+
} from "@atcute/identity-resolver";
11
11
+
12
12
+
export const didDocumentResolver = new CompositeDidDocumentResolver({
13
13
+
methods: {
14
14
+
plc: new PlcDidDocumentResolver(),
15
15
+
web: new AtprotoWebDidDocumentResolver(),
16
16
+
},
17
17
+
});
18
18
+
19
19
+
export const handleResolver = new CompositeHandleResolver({
20
20
+
strategy: "dns-first",
21
21
+
methods: {
22
22
+
dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
23
23
+
http: new WellKnownHandleResolver(),
24
24
+
},
25
25
+
});
26
26
+
27
27
+
export const resolveHandle = async (handle: string): Promise<string> => {
28
28
+
return await handleResolver.resolve(handle as `${string}.${string}`);
29
29
+
};
30
30
+
31
31
+
export const resolveDidDoc = async (did: string): Promise<DidDocument> => {
32
32
+
if (!isAtprotoDid(did)) {
33
33
+
throw new Error("Not a valid DID identifier");
34
34
+
}
35
35
+
return await didDocumentResolver.resolve(did);
36
36
+
};
37
37
+
38
38
+
const didPDSCache: Record<string, Promise<string>> = {};
39
39
+
40
40
+
export const getPDS = (did: string): Promise<string> => {
41
41
+
if (did in didPDSCache) return didPDSCache[did];
42
42
+
43
43
+
if (!isAtprotoDid(did)) {
44
44
+
return Promise.reject(new Error("Not a valid DID identifier"));
45
45
+
}
46
46
+
47
47
+
didPDSCache[did] = (async () => {
48
48
+
const doc = await didDocumentResolver.resolve(did);
49
49
+
const pds = getPdsEndpoint(doc);
50
50
+
if (!pds) {
51
51
+
delete didPDSCache[did];
52
52
+
throw new Error("No PDS found");
53
53
+
}
54
54
+
return pds;
55
55
+
})();
56
56
+
57
57
+
return didPDSCache[did];
58
58
+
};
59
59
+
60
60
+
export interface LiveUser {
61
61
+
did: string;
62
62
+
handle: string;
63
63
+
title: string;
64
64
+
viewerCount: number;
65
65
+
thumbRef?: string;
66
66
+
}
67
67
+
68
68
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69
69
+
function mapStream(stream: any): LiveUser {
70
70
+
return {
71
71
+
did: stream.author?.did || "",
72
72
+
handle: stream.author?.handle || "unknown",
73
73
+
title: stream.record?.title || "Untitled stream",
74
74
+
viewerCount: stream.viewerCount?.count ?? 0,
75
75
+
thumbRef: stream.record?.thumb?.ref?.$link,
76
76
+
};
77
77
+
}
78
78
+
79
79
+
export const fetchLiveUsers = async (): Promise<LiveUser[]> => {
80
80
+
const res = await fetch("https://stream.place/xrpc/place.stream.live.getLiveUsers");
81
81
+
if (!res.ok) throw new Error("Failed to fetch live users");
82
82
+
const data = await res.json();
83
83
+
const streams = data.streams || [];
84
84
+
return streams.map(mapStream);
85
85
+
};
+266
src/lib/chat.ts
reviewed
···
1
1
+
import { Client } from "@atcute/client";
2
2
+
import { OAuthUserAgent } from "@atcute/oauth-browser-client";
3
3
+
4
4
+
export interface Facet {
5
5
+
index: { byteStart: number; byteEnd: number };
6
6
+
features: Array<
7
7
+
| { $type: "app.bsky.richtext.facet#mention"; did: string }
8
8
+
| { $type: "app.bsky.richtext.facet#link"; uri: string }
9
9
+
>;
10
10
+
}
11
11
+
12
12
+
export interface ChatMessage {
13
13
+
uri: string;
14
14
+
cid: string;
15
15
+
author: { handle: string; did: string };
16
16
+
record: {
17
17
+
text: string;
18
18
+
facets?: Facet[];
19
19
+
reply?: { parent?: { uri: string; cid: string }; root?: { uri: string; cid: string } };
20
20
+
};
21
21
+
chatProfile?: { color?: { red: number; green: number; blue: number } };
22
22
+
indexedAt: string;
23
23
+
badges?: Array<{ badgeType: string }>;
24
24
+
replyTo?: ChatMessage;
25
25
+
}
26
26
+
27
27
+
export interface RichTextSegment {
28
28
+
text: string;
29
29
+
facet?: Facet;
30
30
+
}
31
31
+
32
32
+
export function segmentRichText(text: string, facets?: Facet[]): RichTextSegment[] {
33
33
+
if (!facets || facets.length === 0) {
34
34
+
return [{ text }];
35
35
+
}
36
36
+
37
37
+
const encoder = new TextEncoder();
38
38
+
const decoder = new TextDecoder();
39
39
+
const bytes = encoder.encode(text);
40
40
+
41
41
+
// Sort facets by byteStart
42
42
+
const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart);
43
43
+
44
44
+
const segments: RichTextSegment[] = [];
45
45
+
let cursor = 0;
46
46
+
47
47
+
for (const facet of sorted) {
48
48
+
const { byteStart, byteEnd } = facet.index;
49
49
+
if (byteStart < cursor || byteEnd > bytes.length) continue;
50
50
+
51
51
+
// Text before this facet
52
52
+
if (byteStart > cursor) {
53
53
+
segments.push({ text: decoder.decode(bytes.slice(cursor, byteStart)) });
54
54
+
}
55
55
+
56
56
+
// The facet text
57
57
+
segments.push({
58
58
+
text: decoder.decode(bytes.slice(byteStart, byteEnd)),
59
59
+
facet,
60
60
+
});
61
61
+
62
62
+
cursor = byteEnd;
63
63
+
}
64
64
+
65
65
+
// Remaining text after last facet
66
66
+
if (cursor < bytes.length) {
67
67
+
segments.push({ text: decoder.decode(bytes.slice(cursor)) });
68
68
+
}
69
69
+
70
70
+
return segments;
71
71
+
}
72
72
+
73
73
+
export interface StreamInfo {
74
74
+
title: string;
75
75
+
handle: string;
76
76
+
}
77
77
+
78
78
+
export interface ChatCallbacks {
79
79
+
onMessage: (msg: ChatMessage) => void;
80
80
+
onStreamInfo: (info: StreamInfo) => void;
81
81
+
onViewerCount: (count: number) => void;
82
82
+
onOpen: () => void;
83
83
+
onClose: () => void;
84
84
+
}
85
85
+
86
86
+
export interface ChatConnection {
87
87
+
close(): void;
88
88
+
}
89
89
+
90
90
+
export function connectChatWs(handle: string, callbacks: ChatCallbacks): ChatConnection {
91
91
+
const wsUrl = `wss://stream.place/api/websocket/${encodeURIComponent(handle)}`;
92
92
+
let ws: WebSocket | undefined;
93
93
+
let closed = false;
94
94
+
let retryDelay = 1000;
95
95
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
96
96
+
97
97
+
function connect() {
98
98
+
if (closed) return;
99
99
+
ws = new WebSocket(wsUrl);
100
100
+
101
101
+
ws.onopen = () => {
102
102
+
retryDelay = 1000;
103
103
+
callbacks.onOpen();
104
104
+
};
105
105
+
106
106
+
ws.onclose = () => {
107
107
+
callbacks.onClose();
108
108
+
scheduleReconnect();
109
109
+
};
110
110
+
111
111
+
ws.onerror = () => {
112
112
+
ws?.close();
113
113
+
};
114
114
+
115
115
+
ws.onmessage = (event) => {
116
116
+
try {
117
117
+
const data = JSON.parse(event.data);
118
118
+
119
119
+
if (data.$type === "place.stream.chat.defs#messageView") {
120
120
+
callbacks.onMessage(data as ChatMessage);
121
121
+
} else if (data.$type === "place.stream.livestream#livestreamView") {
122
122
+
callbacks.onStreamInfo({
123
123
+
title: data.record?.title || "",
124
124
+
handle: data.author?.handle || "",
125
125
+
});
126
126
+
} else if (data.$type === "place.stream.livestream#viewerCount") {
127
127
+
callbacks.onViewerCount(data.count ?? 0);
128
128
+
}
129
129
+
} catch {
130
130
+
// ignore non-JSON messages
131
131
+
}
132
132
+
};
133
133
+
}
134
134
+
135
135
+
function scheduleReconnect() {
136
136
+
if (closed) return;
137
137
+
retryTimer = setTimeout(() => {
138
138
+
retryDelay = Math.min(retryDelay * 2, 30000);
139
139
+
connect();
140
140
+
}, retryDelay);
141
141
+
}
142
142
+
143
143
+
connect();
144
144
+
145
145
+
return {
146
146
+
close() {
147
147
+
closed = true;
148
148
+
clearTimeout(retryTimer);
149
149
+
ws?.close();
150
150
+
},
151
151
+
};
152
152
+
}
153
153
+
154
154
+
// URL regex - matches http:// and https:// URLs
155
155
+
const URL_RE = /https?:\/\/[^\s\])<>]+/g;
156
156
+
// Mention regex - matches @handle.tld patterns
157
157
+
const MENTION_RE = /(?<![.\w])@([\w.-]+\.[\w.-]+)/g;
158
158
+
159
159
+
export function detectFacets(text: string): Facet[] {
160
160
+
const encoder = new TextEncoder();
161
161
+
const bytes = encoder.encode(text);
162
162
+
const facets: Facet[] = [];
163
163
+
164
164
+
// We need byte offsets, so we convert char indices to byte indices
165
165
+
const charToByteOffset = (charIdx: number): number => {
166
166
+
return encoder.encode(text.slice(0, charIdx)).byteLength;
167
167
+
};
168
168
+
169
169
+
// Detect URLs
170
170
+
for (const match of text.matchAll(URL_RE)) {
171
171
+
const start = match.index!;
172
172
+
// Trim trailing punctuation that's likely not part of the URL
173
173
+
let uri = match[0];
174
174
+
while (uri.length > 0 && /[.,;:!?)}\]'"]+$/.test(uri)) {
175
175
+
uri = uri.slice(0, -1);
176
176
+
}
177
177
+
const byteStart = charToByteOffset(start);
178
178
+
const byteEnd = charToByteOffset(start + uri.length);
179
179
+
if (byteEnd <= bytes.byteLength) {
180
180
+
facets.push({
181
181
+
index: { byteStart, byteEnd },
182
182
+
features: [{ $type: "app.bsky.richtext.facet#link", uri }],
183
183
+
});
184
184
+
}
185
185
+
}
186
186
+
187
187
+
// Detect mentions
188
188
+
for (const match of text.matchAll(MENTION_RE)) {
189
189
+
const start = match.index!;
190
190
+
const end = start + match[0].length;
191
191
+
const byteStart = charToByteOffset(start);
192
192
+
const byteEnd = charToByteOffset(end);
193
193
+
if (byteEnd <= bytes.byteLength) {
194
194
+
// Store the handle; the DID will need to be resolved before sending
195
195
+
facets.push({
196
196
+
index: { byteStart, byteEnd },
197
197
+
features: [{ $type: "app.bsky.richtext.facet#mention", did: match[1] }],
198
198
+
});
199
199
+
}
200
200
+
}
201
201
+
202
202
+
return facets.sort((a, b) => a.index.byteStart - b.index.byteStart);
203
203
+
}
204
204
+
205
205
+
export interface ReplyRef {
206
206
+
uri: string;
207
207
+
cid: string;
208
208
+
}
209
209
+
210
210
+
export async function sendChatMessage(
211
211
+
oauthAgent: OAuthUserAgent,
212
212
+
repo: string,
213
213
+
streamerDid: string,
214
214
+
text: string,
215
215
+
resolveHandle?: (handle: string) => Promise<string>,
216
216
+
reply?: { root: ReplyRef; parent: ReplyRef },
217
217
+
): Promise<void> {
218
218
+
const rpc = new Client({ handler: oauthAgent });
219
219
+
220
220
+
let facets = detectFacets(text);
221
221
+
222
222
+
// Resolve mention handles to DIDs
223
223
+
if (resolveHandle) {
224
224
+
facets = await Promise.all(
225
225
+
facets.map(async (facet) => {
226
226
+
const feature = facet.features[0];
227
227
+
if (feature.$type === "app.bsky.richtext.facet#mention") {
228
228
+
try {
229
229
+
const did = await resolveHandle(feature.did);
230
230
+
return {
231
231
+
...facet,
232
232
+
features: [{ $type: "app.bsky.richtext.facet#mention" as const, did }],
233
233
+
};
234
234
+
} catch {
235
235
+
// If resolution fails, drop the facet
236
236
+
return null;
237
237
+
}
238
238
+
}
239
239
+
return facet;
240
240
+
}),
241
241
+
).then((results) => results.filter((f): f is Facet => f !== null));
242
242
+
}
243
243
+
244
244
+
const record: Record<string, unknown> = {
245
245
+
$type: "place.stream.chat.message",
246
246
+
text,
247
247
+
streamer: streamerDid,
248
248
+
createdAt: new Date().toISOString(),
249
249
+
};
250
250
+
251
251
+
if (facets.length > 0) {
252
252
+
record.facets = facets;
253
253
+
}
254
254
+
255
255
+
if (reply) {
256
256
+
record.reply = reply;
257
257
+
}
258
258
+
259
259
+
await rpc.post("com.atproto.repo.createRecord", {
260
260
+
input: {
261
261
+
repo: repo as `did:${string}:${string}`,
262
262
+
collection: "place.stream.chat.message",
263
263
+
record,
264
264
+
},
265
265
+
});
266
266
+
}
+65
src/lib/whep.ts
reviewed
···
1
1
+
function waitForIceGathering(pc: RTCPeerConnection, timeout: number): Promise<void> {
2
2
+
return new Promise((resolve) => {
3
3
+
if (pc.iceGatheringState === "complete") {
4
4
+
resolve();
5
5
+
return;
6
6
+
}
7
7
+
const timer = setTimeout(() => resolve(), timeout);
8
8
+
pc.onicegatheringstatechange = () => {
9
9
+
if (pc.iceGatheringState === "complete") {
10
10
+
clearTimeout(timer);
11
11
+
resolve();
12
12
+
}
13
13
+
};
14
14
+
});
15
15
+
}
16
16
+
17
17
+
export interface WhepConnection {
18
18
+
pc: RTCPeerConnection;
19
19
+
stream: MediaStream;
20
20
+
}
21
21
+
22
22
+
export async function connectWhep(handle: string): Promise<WhepConnection> {
23
23
+
const whepUrl = `https://stream.place/api/playback/${encodeURIComponent(handle)}/webrtc?rendition=source`;
24
24
+
25
25
+
const pc = new RTCPeerConnection({
26
26
+
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
27
27
+
bundlePolicy: "max-bundle",
28
28
+
});
29
29
+
30
30
+
const stream = new MediaStream();
31
31
+
32
32
+
pc.addTransceiver("video", { direction: "recvonly" });
33
33
+
pc.addTransceiver("audio", { direction: "recvonly" });
34
34
+
35
35
+
pc.ontrack = (event) => {
36
36
+
if (event.streams && event.streams[0]) {
37
37
+
for (const track of event.streams[0].getTracks()) {
38
38
+
stream.addTrack(track);
39
39
+
}
40
40
+
} else {
41
41
+
stream.addTrack(event.track);
42
42
+
}
43
43
+
};
44
44
+
45
45
+
const offer = await pc.createOffer();
46
46
+
await pc.setLocalDescription(offer);
47
47
+
await waitForIceGathering(pc, 500);
48
48
+
49
49
+
const resp = await fetch(whepUrl, {
50
50
+
method: "POST",
51
51
+
headers: { "Content-Type": "application/sdp" },
52
52
+
body: pc.localDescription!.sdp,
53
53
+
});
54
54
+
55
55
+
if (!resp.ok) {
56
56
+
pc.close();
57
57
+
const errText = await resp.text();
58
58
+
throw new Error(`WHEP ${resp.status}: ${errText}`);
59
59
+
}
60
60
+
61
61
+
const answerSdp = await resp.text();
62
62
+
await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
63
63
+
64
64
+
return { pc, stream };
65
65
+
}
+100
src/pages/Home.tsx
reviewed
···
1
1
+
import { For, onCleanup, onMount, Show } from "solid-js";
2
2
+
import { createStore, reconcile } from "solid-js/store";
3
3
+
4
4
+
import { StreamCard } from "../components/StreamCard";
5
5
+
import { fetchLiveUsers, type LiveUser } from "../lib/api";
6
6
+
7
7
+
const POLL_INTERVAL = 15_000;
8
8
+
9
9
+
const avatarCache: Record<string, string> = {};
10
10
+
11
11
+
async function fetchNewAvatars(handles: string[]): Promise<void> {
12
12
+
const missing = handles.filter((h) => !(h in avatarCache));
13
13
+
if (!missing.length) return;
14
14
+
try {
15
15
+
const params = missing.map((h) => `actors=${encodeURIComponent(h)}`).join("&");
16
16
+
const res = await fetch(
17
17
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?${params}`,
18
18
+
);
19
19
+
if (!res.ok) return;
20
20
+
const data = await res.json();
21
21
+
for (const profile of data.profiles || []) {
22
22
+
if (profile.avatar) {
23
23
+
avatarCache[profile.handle] = profile.avatar;
24
24
+
}
25
25
+
}
26
26
+
} catch {
27
27
+
// ignore
28
28
+
}
29
29
+
}
30
30
+
31
31
+
interface StreamEntry extends LiveUser {
32
32
+
avatarUrl?: string;
33
33
+
}
34
34
+
35
35
+
export function Home() {
36
36
+
const [streams, setStreams] = createStore<StreamEntry[]>([]);
37
37
+
const [state, setState] = createStore({ loading: true });
38
38
+
39
39
+
const refresh = async () => {
40
40
+
try {
41
41
+
const users = await fetchLiveUsers();
42
42
+
users.sort((a, b) => (b.viewerCount ?? 0) - (a.viewerCount ?? 0));
43
43
+
44
44
+
const handles = users.map((s) => s.handle).filter(Boolean);
45
45
+
await fetchNewAvatars(handles);
46
46
+
47
47
+
const entries: StreamEntry[] = users.map((s) => ({
48
48
+
...s,
49
49
+
avatarUrl: avatarCache[s.handle],
50
50
+
}));
51
51
+
52
52
+
setStreams(reconcile(entries, { key: "did", merge: false }));
53
53
+
} catch (err) {
54
54
+
console.error("Failed to fetch streams:", err);
55
55
+
} finally {
56
56
+
setState("loading", false);
57
57
+
}
58
58
+
};
59
59
+
60
60
+
onMount(() => {
61
61
+
refresh();
62
62
+
});
63
63
+
64
64
+
const interval = setInterval(refresh, POLL_INTERVAL);
65
65
+
onCleanup(() => clearInterval(interval));
66
66
+
67
67
+
return (
68
68
+
<div class="mx-auto w-full max-w-6xl p-6">
69
69
+
<Show
70
70
+
when={!state.loading}
71
71
+
fallback={
72
72
+
<div class="text-sp-dim flex items-center gap-2">
73
73
+
<div class="border-sp-dim border-t-sp-accent h-4 w-4 animate-spin rounded-full border-2" />
74
74
+
Loading streams...
75
75
+
</div>
76
76
+
}
77
77
+
>
78
78
+
<Show
79
79
+
when={streams.length > 0}
80
80
+
fallback={<p class="text-sp-dim">No one is live right now. Check back later!</p>}
81
81
+
>
82
82
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
83
83
+
<For each={streams}>
84
84
+
{(stream) => (
85
85
+
<StreamCard
86
86
+
handle={stream.handle}
87
87
+
did={stream.did}
88
88
+
title={stream.title}
89
89
+
viewerCount={stream.viewerCount}
90
90
+
avatarUrl={stream.avatarUrl}
91
91
+
thumbRef={stream.thumbRef}
92
92
+
/>
93
93
+
)}
94
94
+
</For>
95
95
+
</div>
96
96
+
</Show>
97
97
+
</Show>
98
98
+
</div>
99
99
+
);
100
100
+
}
+62
src/pages/Watch.tsx
reviewed
···
1
1
+
import { useParams } from "@solidjs/router";
2
2
+
import { createResource, createSignal, Show } from "solid-js";
3
3
+
4
4
+
import { Chat } from "../components/Chat";
5
5
+
import { VideoPlayer } from "../components/VideoPlayer";
6
6
+
import { resolveHandle } from "../lib/api";
7
7
+
import type { StreamInfo } from "../lib/chat";
8
8
+
9
9
+
export function Watch() {
10
10
+
const params = useParams<{ handle: string }>();
11
11
+
12
12
+
const [streamerDid] = createResource(
13
13
+
() => params.handle,
14
14
+
async (handle) => {
15
15
+
try {
16
16
+
return await resolveHandle(handle);
17
17
+
} catch (err) {
18
18
+
console.error("Failed to resolve handle:", err);
19
19
+
return undefined;
20
20
+
}
21
21
+
},
22
22
+
);
23
23
+
24
24
+
const [streamInfo, setStreamInfo] = createSignal<StreamInfo | undefined>();
25
25
+
const [viewerCount, setViewerCount] = createSignal(0);
26
26
+
27
27
+
return (
28
28
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden lg:flex-row">
29
29
+
{/* Main content - video + info */}
30
30
+
<div class="flex min-w-0 flex-1 flex-col pb-4">
31
31
+
<VideoPlayer handle={params.handle} />
32
32
+
33
33
+
{/* Stream info bar */}
34
34
+
<div class="mt-3 flex items-start justify-between gap-4 px-4">
35
35
+
<div class="min-w-0">
36
36
+
<h1 class="truncate text-lg font-semibold">
37
37
+
{streamInfo()?.title || `@${params.handle}`}
38
38
+
</h1>
39
39
+
<Show when={streamInfo()?.title}>
40
40
+
<div class="text-sp-dim text-sm">@{params.handle}</div>
41
41
+
</Show>
42
42
+
</div>
43
43
+
<div class="text-sp-dim flex shrink-0 items-center gap-2 text-base">
44
44
+
<span class="bg-sp-accent inline-block h-2.5 w-2.5 rounded-full" />
45
45
+
{viewerCount()} watching
46
46
+
</div>
47
47
+
</div>
48
48
+
</div>
49
49
+
50
50
+
{/* Chat sidebar */}
51
51
+
<div class="flex min-h-0 w-full flex-col lg:w-105">
52
52
+
<Chat
53
53
+
class="flex-1"
54
54
+
handle={params.handle}
55
55
+
streamerDid={streamerDid()}
56
56
+
onStreamInfo={setStreamInfo}
57
57
+
onViewerCount={setViewerCount}
58
58
+
/>
59
59
+
</div>
60
60
+
</div>
61
61
+
);
62
62
+
}
+12
src/vite-env.d.ts
reviewed
···
1
1
+
/// <reference types="vite/client" />
2
2
+
3
3
+
interface ImportMetaEnv {
4
4
+
readonly VITE_CLIENT_URI: string;
5
5
+
readonly VITE_OAUTH_CLIENT_ID: string;
6
6
+
readonly VITE_OAUTH_REDIRECT_URL: string;
7
7
+
readonly VITE_OAUTH_SCOPE: string;
8
8
+
}
9
9
+
10
10
+
interface ImportMeta {
11
11
+
readonly env: ImportMetaEnv;
12
12
+
}
+26
tsconfig.app.json
reviewed
···
1
1
+
{
2
2
+
"compilerOptions": {
3
3
+
"target": "ESNext",
4
4
+
"useDefineForClassFields": true,
5
5
+
"module": "ESNext",
6
6
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
7
7
+
"types": [],
8
8
+
"skipLibCheck": true,
9
9
+
10
10
+
/* Bundler mode */
11
11
+
"moduleResolution": "bundler",
12
12
+
"allowImportingTsExtensions": true,
13
13
+
"isolatedModules": true,
14
14
+
"moduleDetection": "force",
15
15
+
"noEmit": true,
16
16
+
"jsx": "preserve",
17
17
+
"jsxImportSource": "solid-js",
18
18
+
19
19
+
/* Linting */
20
20
+
"strict": true,
21
21
+
"noUnusedLocals": true,
22
22
+
"noUnusedParameters": true,
23
23
+
"noFallthroughCasesInSwitch": true
24
24
+
},
25
25
+
"include": ["src"]
26
26
+
}
+4
tsconfig.json
reviewed
···
1
1
+
{
2
2
+
"files": [],
3
3
+
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
4
4
+
}
+23
tsconfig.node.json
reviewed
···
1
1
+
{
2
2
+
"compilerOptions": {
3
3
+
"target": "ESNext",
4
4
+
"lib": ["ESNext"],
5
5
+
"types": ["node"],
6
6
+
"module": "ESNext",
7
7
+
"skipLibCheck": true,
8
8
+
9
9
+
/* Bundler mode */
10
10
+
"moduleResolution": "bundler",
11
11
+
"allowImportingTsExtensions": true,
12
12
+
"isolatedModules": true,
13
13
+
"moduleDetection": "force",
14
14
+
"noEmit": true,
15
15
+
16
16
+
/* Linting */
17
17
+
"strict": true,
18
18
+
"noUnusedLocals": true,
19
19
+
"noUnusedParameters": true,
20
20
+
"noFallthroughCasesInSwitch": true
21
21
+
},
22
22
+
"include": ["vite.config.ts"]
23
23
+
}
+44
vite.config.ts
reviewed
···
1
1
+
import tailwindcss from "@tailwindcss/vite";
2
2
+
import { defineConfig } from "vite";
3
3
+
import solidPlugin from "vite-plugin-solid";
4
4
+
5
5
+
import metadata from "./public/oauth-client-metadata.json";
6
6
+
7
7
+
const SERVER_HOST = "127.0.0.1";
8
8
+
const SERVER_PORT = 5173;
9
9
+
10
10
+
export default defineConfig({
11
11
+
plugins: [
12
12
+
tailwindcss(),
13
13
+
solidPlugin(),
14
14
+
{
15
15
+
name: "oauth",
16
16
+
config(_conf, { command }) {
17
17
+
if (command === "build") {
18
18
+
process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id;
19
19
+
process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0];
20
20
+
} else {
21
21
+
const redirectUri = `http://${SERVER_HOST}:${SERVER_PORT}/`;
22
22
+
23
23
+
const clientId =
24
24
+
`http://localhost` +
25
25
+
`?redirect_uri=${encodeURIComponent(redirectUri)}` +
26
26
+
`&scope=${encodeURIComponent(metadata.scope)}`;
27
27
+
28
28
+
process.env.VITE_OAUTH_CLIENT_ID = clientId;
29
29
+
process.env.VITE_OAUTH_REDIRECT_URL = redirectUri;
30
30
+
}
31
31
+
32
32
+
process.env.VITE_CLIENT_URI = metadata.client_uri;
33
33
+
process.env.VITE_OAUTH_SCOPE = metadata.scope;
34
34
+
},
35
35
+
},
36
36
+
],
37
37
+
server: {
38
38
+
host: SERVER_HOST,
39
39
+
port: SERVER_PORT,
40
40
+
},
41
41
+
build: {
42
42
+
target: "esnext",
43
43
+
},
44
44
+
});