eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1<meta charset="utf-8" />
2<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
3<meta name="theme-color" content="#5b6cff" />
4<title>__GROUP_NAME__</title>
5
6<!-- Link-preview card (iMessage/Slack/WhatsApp/…). Crawlers fetch this URL with no
7 JS and no #fragment, so the card can only show what the server knows in the
8 clear: the group name (plaintext branding) and the server avatar. The inviter's
9 screen name is E2E — never sent to the server — so it deliberately can't appear
10 here. __OG_IMAGE__ is the server avatar when set, else the app logo. -->
11<meta property="og:type" content="website" />
12<meta property="og:site_name" content="__GROUP_NAME__" />
13<meta property="og:url" content="__OG_URL__" />
14<meta property="og:title" content="You're invited to __GROUP_NAME__" />
15<meta property="og:description" content="Private, end-to-end encrypted chat." />
16<meta property="og:image" content="__OG_IMAGE__" />
17<meta name="twitter:card" content="summary" />
18<meta name="twitter:title" content="You're invited to __GROUP_NAME__" />
19<meta name="twitter:description" content="Private, end-to-end encrypted chat." />
20<meta name="twitter:image" content="__OG_IMAGE__" />
21<!-- Native wrapper detection: the Mac app's user agent carries "lchatMac". Tag the
22 document so CSS can adopt native conventions (e.g. drop web hand cursors). -->
23<script nonce="__CSP_NONCE__">
24 if (navigator.userAgent.includes("lchatMac"))
25 document.documentElement.classList.add("native");
26</script>
27<!-- The home server (sign-in / passkey backup; server-configured via
28 LCHAT_HOME_ORIGIN — same substitution as index.html). The error card's
29 "Sign in" door reads it; the origin is public, so exposing it is fine. -->
30<script nonce="__CSP_NONCE__">
31 window.__homeOrigin = "__HOME_ORIGIN__";
32</script>
33<link rel="stylesheet" href="/static/style.css" />
34<!-- Same identity as the chat root, so "Add to Home Screen" from here installs as
35 __GROUP_NAME__ (not a generic "Join") and launches at the app's start_url ("/"). -->
36<link rel="manifest" href="/manifest.webmanifest" />
37<link rel="icon" href="/static/icon-192.png" />
38<link rel="apple-touch-icon" href="/static/icon-180.png" />
39<meta name="apple-mobile-web-app-capable" content="yes" />
40<meta name="apple-mobile-web-app-title" content="__GROUP_NAME__" />
41
42<!-- Apply the saved theme before first paint (see web/static/ui.js — the canonical
43 list; this copy must stay in sync). A local preference, never sent to the server. -->
44<script nonce="__CSP_NONCE__">
45 (function () {
46 try {
47 var THEMES = ["litroom", "lagoon", "sunny", "plum", "linen", "classic"];
48 var PANEL = {
49 litroom: ["#fff8f0", "#241a1e"], lagoon: ["#f3fbfa", "#0c2a29"],
50 sunny: ["#fffaf0", "#241d12"], plum: ["#fdf8fd", "#221829"],
51 linen: ["#f5f1ea", "#211f1b"], classic: ["#ffffff", "#1d2026"],
52 };
53 var t = localStorage.getItem("lchat_theme");
54 if (THEMES.indexOf(t) < 0) t = "classic";
55 var a = localStorage.getItem("lchat_appearance");
56 var root = document.documentElement;
57 root.dataset.theme = t;
58 root.style.colorScheme = a === "light" ? "light" : a === "dark" ? "dark" : "light dark";
59 var dark = a === "dark" || (a !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
60 var m = document.querySelector('meta[name="theme-color"]');
61 if (m) m.setAttribute("content", PANEL[t][dark ? 1 : 0]);
62 } catch (e) {}
63 })();
64</script>
65
66<!-- The one loading state: the same full-screen loader the app itself uses
67 (index.html #app-loading). It covers the page from load until the welcome
68 card is ready, and again while a claim is in flight — no per-card
69 "Loading…" / spinner-card variants. -->
70<div id="app-loading" class="app-loading"><div class="spinner" aria-label="Loading"></div></div>
71
72<div class="center">
73 <div class="card hidden" id="card"></div>
74</div>
75
76<template id="homescreen-tpl">
77 <div class="onboard-icon">📲</div>
78 <h1>You're in! Add it to your Home Screen</h1>
79 <p>
80 You're signed in here in Safari. On iPhone &amp; iPad, add the chat to your Home
81 Screen too, so it opens like an app and can send you notifications. You'll sign in
82 there once, with the link below.
83 </p>
84 <ol class="steps">
85 <li>Tap <strong>Copy sign-in link</strong> below.</li>
86 <li>Tap the <strong>Share</strong> button (the square with an up arrow) in Safari, then choose <strong>Add to Home Screen</strong>.</li>
87 <li>Open the app from your Home Screen and <strong>paste the link</strong> to sign in.</li>
88 </ol>
89 <div class="row"><button id="hs-copy">Copy sign-in link</button></div>
90 <div class="row"><button id="hs-open" class="secondary">Just use Safari for now</button></div>
91</template>
92
93<template id="form-tpl">
94 <img id="form-photo" class="onboard-photo hidden" alt="" />
95 <h1>Pick a screen name</h1>
96 <p>This is what everyone in the chat will see. You can use your first name or a nickname.</p>
97 <form id="join-form" autocomplete="off">
98 <input id="name" placeholder="e.g. Sophie" maxlength="32" autocomplete="off" />
99 <div class="row">
100 <button type="submit">Join the chat</button>
101 </div>
102 </form>
103</template>
104
105<template id="error-tpl">
106 <h1>This link doesn't work</h1>
107 <p id="error-msg">This invite link is invalid or has been turned off. Ask for a new one.</p>
108 <!-- Revealed by showError({signin}) for dead-invite errors on the web: a
109 returning member whose session is gone doesn't need a new invite — their
110 account gets them back in via the home sign-in page. Stays hidden in the
111 native add-chat webview (it owns navigation and expects a join) and on
112 self-hosts with no home server (no sign-in to offer). -->
113 <div class="row"><button id="error-signin" class="secondary hidden">Already a member? Sign in</button></div>
114</template>
115
116<!-- The signup-first lead card (docs/superpowers/specs/2026-07-31-signup-first-
117 join-design.md): fresh joiners see the sign-in door BEFORE the app/web
118 choice, so their access is saved to their account from the start. ONE path
119 — sign in — not a choice: the skip is deliberately small print (a quiet
120 link revealing the classic welcome card), never a competing button. -->
121<template id="prejoin-tpl">
122 <img id="prejoin-photo" class="onboard-photo hidden" alt="" />
123 <div class="onboard-icon" id="prejoin-icon">💬</div>
124 <h1 id="prejoin-title">You're invited to __GROUP_NAME__</h1>
125 <p id="prejoin-sub">Private, end-to-end encrypted chat.</p>
126 <p id="prejoin-desc" class="welcome-desc hidden"></p>
127 <div class="row"><button id="prejoin-signin">Sign in with Eleven</button></div>
128 <p class="welcome-caveat">
129 Signing in first keeps your access safe — get back into this space from any
130 device, with nothing to remember.
131 </p>
132 <div class="row"><button id="prejoin-skip" class="link-quiet">Continue without signing in</button></div>
133</template>
134
135<!-- The welcome card (deferred claim — see docs/onboarding-and-home-server.md).
136 Nothing is claimed until a button is pressed; the one-time secret stays in
137 the URL fragment. data-store-url is per-instance config (LCHAT_APP_STORE_URL),
138 empty when no app link is configured. -->
139<template id="welcome-tpl" data-store-url="__APP_STORE_URL__">
140 <img id="welcome-photo" class="onboard-photo hidden" alt="" />
141 <div class="onboard-icon" id="welcome-icon">💬</div>
142 <h1 id="welcome-title">You're invited to __GROUP_NAME__</h1>
143 <p id="welcome-sub">Private, end-to-end encrypted chat.</p>
144 <!-- The space's admin-set description, when one is set: served on the invite
145 lookup (valid link-holders only, never the public read — see getKeyInvite),
146 so it can render here before the joiner holds any key. -->
147 <p id="welcome-desc" class="welcome-desc hidden"></p>
148 <div class="row"><button id="welcome-app">Open in the app</button></div>
149 <div class="row"><button id="welcome-store" class="secondary">Get the app</button></div>
150 <div class="row"><button id="welcome-web" class="secondary">Continue in the browser</button></div>
151 <p id="welcome-caveat" class="welcome-caveat">
152 Joining in the browser keeps your access in this browser only — if it's
153 cleared or lost, you'll need a new invite.
154 </p>
155</template>
156
157<script type="module" src="/static/join.js"></script>
158