main
svelte 297 lines 8.7 KB
Raw
1 <script lang="ts">
2 import { onMount } from 'svelte';
3 import {
4 buildTunnelDisplayCommand,
5 buildTunnelPreviewURL,
6 RELAY_ORIGIN,
7 type TunnelCommandOS
8 } from '$lib/tunnel-command';
9 import { buildDefaultExposeName, resolveExposeName } from '$lib/expose-name';
10
11 const DEFAULT_HOST = '3000';
12
13 let target = $state('3000');
14 let os: TunnelCommandOS = $state('unix');
15 let name = $state('');
16 let nameSeed = $state('');
17 let copied = $state(false);
18
19 onMount(() => {
20 nameSeed = crypto.randomUUID();
21 });
22
23 const generatedName = $derived(buildDefaultExposeName(target, nameSeed));
24 const effectiveName = $derived(name.trim() || generatedName);
25
26 const installBlock = $derived.by(() => {
27 const cmd = buildTunnelDisplayCommand({
28 currentOrigin: RELAY_ORIGIN,
29 target,
30 name: effectiveName,
31 nameSeed,
32 relayUrls: [RELAY_ORIGIN],
33 discovery: true,
34 thumbnailURL: '',
35 os
36 });
37 const lines = cmd.split('\n');
38 // Install is first line(s), expose is the rest
39 if (os === 'windows') {
40 // Windows: first two lines are install
41 return lines.slice(0, 2).join('\n');
42 }
43 return lines[0] ?? '';
44 });
45
46 const runBlock = $derived.by(() => {
47 const cmd = buildTunnelDisplayCommand({
48 currentOrigin: RELAY_ORIGIN,
49 target,
50 name: effectiveName,
51 nameSeed,
52 relayUrls: [RELAY_ORIGIN],
53 discovery: true,
54 thumbnailURL: '',
55 os
56 });
57 const lines = cmd.split('\n');
58 if (os === 'windows') {
59 return lines.slice(2).join('\n');
60 }
61 return lines.slice(1).join('\n');
62 });
63
64 const previewURL = $derived(
65 buildTunnelPreviewURL(RELAY_ORIGIN, effectiveName, target, nameSeed)
66 );
67
68 function handleCopy() {
69 const fullCommand = installBlock + '\n' + runBlock;
70 navigator.clipboard.writeText(fullCommand).then(() => {
71 copied = true;
72 setTimeout(() => {
73 copied = false;
74 }, 2000);
75 });
76 }
77
78 function handleShuffleName() {
79 nameSeed = crypto.randomUUID();
80 name = '';
81 }
82
83 function handleNameChange(event: Event) {
84 name = (event.target as HTMLInputElement).value;
85 }
86 </script>
87
88 <div id="quick-start" class="relative mt-8 scroll-mt-24 sm:mt-10">
89 <div class="mx-auto w-full max-w-6xl text-left">
90 <div class="space-y-2">
91 <p
92 class="text-sm font-semibold uppercase tracking-[0.3em]"
93 style="color: var(--neon-cyan);"
94 >
95 Quick Start
96 </p>
97 <h2
98 class="text-3xl font-semibold tracking-tight"
99 style="color: var(--foreground);"
100 >
101 Expose service
102 </h2>
103 </div>
104
105 <div
106 class="relative mx-auto mt-4 w-full max-w-[520px] rounded-xl border px-4 py-5 sm:px-5 sm:py-6"
107 style="background: var(--hero-terminal-bg); border-color: var(--hero-terminal-border); color: var(--hero-terminal-foreground); box-shadow: 0 30px 72px var(--hero-terminal-shadow);"
108 >
109 <div class="mb-5 flex min-w-0 items-center gap-3">
110 <span
111 aria-hidden="true"
112 class="shrink-0 font-mono text-lg leading-none"
113 style="color: var(--hero-terminal-accent);"
114 >
115 {'>'}
116 </span>
117 <h3
118 id="tunnel-preview"
119 class="min-w-0 text-xl font-bold tracking-tight sm:text-2xl"
120 >
121 Run this command
122 </h3>
123 </div>
124
125 <div class="space-y-5">
126 <!-- 1. Start your local app -->
127 <div class="space-y-2">
128 <div class="space-y-1.5">
129 <p class="text-[13px] font-semibold tracking-[0.04em] text-slate-100 sm:text-sm">
130 1. Start your local app
131 <span class="ml-1 normal-case tracking-normal text-slate-400">
132 (e.g.
133 <span class="mx-1 font-mono text-slate-200">localhost:3000</span>
134 )
135 </span>
136 </p>
137 </div>
138 </div>
139
140 <!-- 2. Run this command -->
141 <div class="space-y-3">
142 <div class="flex flex-wrap items-center justify-between gap-3">
143 <p class="text-[13px] font-semibold tracking-[0.04em] text-slate-100 sm:text-sm">
144 2. Run this command
145 </p>
146 <div
147 class="flex shrink-0 rounded-lg border p-0.5"
148 style="border-color: rgba(255,255,255,0.06); background: rgba(255,255,255,0.035);"
149 >
150 <button
151 type="button"
152 onclick={() => {
153 os = 'unix';
154 }}
155 class="min-w-[72px] whitespace-nowrap rounded-md px-2.5 py-1.5 text-[11px] font-semibold transition-colors {os ===
156 'unix'
157 ? 'bg-white/[0.08] text-slate-200'
158 : 'text-slate-500 hover:text-slate-300'}"
159 >
160 Linux
161 </button>
162 <button
163 type="button"
164 onclick={() => {
165 os = 'windows';
166 }}
167 class="min-w-[72px] whitespace-nowrap rounded-md px-2.5 py-1.5 text-[11px] font-semibold transition-colors {os ===
168 'windows'
169 ? 'bg-white/[0.08] text-slate-200'
170 : 'text-slate-500 hover:text-slate-300'}"
171 >
172 Windows
173 </button>
174 </div>
175 </div>
176
177 <!-- Port + Name controls -->
178 <div class="flex flex-wrap items-center gap-x-4 gap-y-2 sm:flex-nowrap">
179 <div class="flex shrink-0 items-center gap-2">
180 <span
181 class="shrink-0 text-[9px] font-semibold uppercase tracking-[0.16em] text-slate-500"
182 >
183 Port
184 </span>
185 <input
186 type="text"
187 bind:value={target}
188 placeholder={DEFAULT_HOST}
189 aria-label="Local port or address"
190 class="h-auto w-[76px] border-0 bg-transparent px-0 py-0 font-mono text-[13px] text-slate-200 shadow-none outline-none placeholder:text-slate-600"
191 />
192 </div>
193 <div class="ml-auto flex min-w-0 items-center justify-end gap-2 sm:w-88">
194 <span
195 class="shrink-0 text-[9px] font-semibold uppercase tracking-[0.16em] text-slate-500"
196 >
197 Name
198 </span>
199 <input
200 type="text"
201 oninput={handleNameChange}
202 placeholder={generatedName}
203 aria-label="Public name"
204 class="min-w-0 flex-1 border-0 bg-transparent px-0 py-0 text-[13px] text-slate-200 shadow-none outline-none placeholder:text-slate-600"
205 />
206 <button
207 type="button"
208 onclick={handleShuffleName}
209 class="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-white/[0.06] hover:text-slate-200"
210 aria-label="Shuffle public name"
211 title="Shuffle public name"
212 >
213 <svg
214 class="h-4 w-4"
215 fill="none"
216 viewBox="0 0 24 24"
217 stroke="currentColor"
218 stroke-width="2"
219 stroke-linecap="round"
220 stroke-linejoin="round"
221 >
222 <polyline points="23 4 23 10 17 10" />
223 <polyline points="1 20 1 14 7 14" />
224 <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10" />
225 <path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14" />
226 </svg>
227 </button>
228 </div>
229 </div>
230
231 <!-- Command block -->
232 <div
233 class="relative min-h-[148px] rounded-xl border px-4 py-4 pr-14 font-mono text-sm leading-7"
234 style="border-color: rgba(255,255,255,0.1); background: rgba(0,0,0,0.55); color: white; box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);"
235 >
236 <button
237 type="button"
238 onclick={handleCopy}
239 class="absolute right-4 top-4 inline-flex h-8 w-8 items-center justify-center rounded-lg transition-colors hover:bg-emerald-400/10"
240 style="color: rgba(110,231,183,0.75);"
241 aria-label="Copy command"
242 title={copied ? 'Copied' : 'Copy'}
243 >
244 {#if copied}
245 <svg
246 class="h-4 w-4"
247 fill="none"
248 viewBox="0 0 24 24"
249 stroke="currentColor"
250 stroke-width="2"
251 stroke-linecap="round"
252 stroke-linejoin="round"
253 >
254 <polyline points="20 6 9 17 4 12" />
255 </svg>
256 {:else}
257 <svg
258 class="h-4 w-4"
259 fill="none"
260 viewBox="0 0 24 24"
261 stroke="currentColor"
262 stroke-width="2"
263 stroke-linecap="round"
264 stroke-linejoin="round"
265 >
266 <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
267 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
268 </svg>
269 {/if}
270 </button>
271 <pre class="overflow-x-auto whitespace-pre-wrap break-all"><span class="block">{installBlock}</span><span class="mt-2 block">{runBlock}</span></pre>
272 </div>
273 </div>
274
275 <!-- 3. Open this public URL -->
276 <div class="space-y-2 pt-1">
277 <p class="text-[13px] font-semibold tracking-[0.04em] text-slate-100 sm:text-sm">
278 3. Open this public URL
279 </p>
280 <div
281 class="space-y-3 rounded-xl border px-3.5 py-3"
282 style="border-color: rgba(255,255,255,0.08); background: rgba(255,255,255,0.045);"
283 >
284 <a
285 href={previewURL}
286 target="_blank"
287 rel="noopener noreferrer"
288 class="block overflow-x-auto whitespace-nowrap font-mono text-[15px] font-medium text-sky-300 underline-offset-4 hover:underline sm:text-base"
289 >
290 {previewURL}
291 </a>
292 </div>
293 </div>
294 </div>
295 </div>
296 </div>
297 </div>