glashjs 0.13.5 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glashjs",
3
- "version": "0.13.5",
3
+ "version": "0.14.1",
4
4
  "description": "glashjs — The Postgres-native full-stack framework for builders who want to ship without DevOps. Framework, hosting, database, auth, and deploy in one GlashDB-native runtime.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,8 @@
21
21
  "./image": "./src/components/image.mjs",
22
22
  "./video": "./src/components/video.mjs",
23
23
  "./link": "./src/components/link.mjs",
24
- "./package.json": "./package.json"
24
+ "./package.json": "./package.json",
25
+ "./i18n": "./src/i18n.mjs"
25
26
  },
26
27
  "files": [
27
28
  "bin",
@@ -1,8 +1,7 @@
1
1
  // glashjs animated favicon
2
2
  // ---------------------------------------------------------------------------
3
- // Ships an animated favicon with every build. Two reliable modes:
4
- // - animated SVG (SMIL) -> set once; browsers that support it animate the
5
- // tab icon natively (the default glash mark).
3
+ // Ships an optional animated/favicon runtime with every build. Two reliable modes:
4
+ // - SVG -> set once; the default is the official glash mark.
6
5
  // - frame cycling -> a tiny runtime swaps the <link rel=icon> href
7
6
  // across frames on an interval. Works everywhere,
8
7
  // and pauses while the tab is hidden (no wasted CPU).
@@ -47,7 +46,7 @@ if (CONFIG.auto) startGlashFavicon();
47
46
  /**
48
47
  * Resolve + emit the animated favicon for a build.
49
48
  * cfg.animatedFavicon may be:
50
- * true -> the bundled default glash animated mark
49
+ * true -> the bundled default glash mark
51
50
  * "<path>" -> a project animated SVG/GIF
52
51
  * { frames:[...], fps, auto } -> frame-cycling animation
53
52
  */
@@ -59,14 +58,14 @@ export async function generateAnimatedFavicon(outDir, cfg, root, log = () => {})
59
58
 
60
59
  if (setting === true || typeof setting === 'string') {
61
60
  const src = setting === true
62
- ? fileURLToPath(new URL('../../templates/favicon-animated.svg', import.meta.url))
61
+ ? fileURLToPath(new URL('../../templates/glash_favicon.svg', import.meta.url))
63
62
  : path.resolve(root, setting);
64
63
  const out = 'favicon-animated' + path.extname(src);
65
64
  try {
66
65
  await fs.copyFile(src, path.join(outDir, out));
67
66
  } catch {
68
- log(` ! animated favicon source not found (${setting}) — using bundled default`);
69
- await fs.copyFile(fileURLToPath(new URL('../../templates/favicon-animated.svg', import.meta.url)), path.join(outDir, 'favicon-animated.svg'));
67
+ log(` ! animated favicon source not found (${setting}) — using bundled glash_favicon.svg`);
68
+ await fs.copyFile(fileURLToPath(new URL('../../templates/glash_favicon.svg', import.meta.url)), path.join(outDir, 'favicon-animated.svg'));
70
69
  baked.animated = '/favicon-animated.svg';
71
70
  }
72
71
  baked.animated = baked.animated || '/' + out;
package/src/build.mjs CHANGED
@@ -115,7 +115,7 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
115
115
  log(` saved ${t.savedPercent}% (${kb(t.originalBytes - t.optimizedBytes)})`);
116
116
  log(` routes ${routesBuilt.compiled} JSX route(s) precompiled (no runtime esbuild in prod)`);
117
117
  log(` offline ${cfg.offline ? `${offline.precached} files precached (works offline after first visit)` : 'disabled'}`);
118
- log(` favicon static glashdb logo${animated.enabled ? ' + animated (glash-favicon.mjs)' : ''}`);
118
+ log(` favicon glash_favicon.svg${animated.enabled ? ' + animated (glash-favicon.mjs)' : ''}`);
119
119
  log(` version ${version}`);
120
120
  log(` security strict CSP + ${Object.keys(securityHeaders(cfg.security)).length} headers\n`);
121
121
 
@@ -126,7 +126,7 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
126
126
  // framework. Try the configured path first, then fall back to that bundled logo
127
127
  // so the glashdb favicon shows out-of-the-box even before any override.
128
128
  async function copyFavicon(cfg, root, outDir, log) {
129
- const bundled = path.resolve(fileURLToPath(new URL('../templates/favicon.svg', import.meta.url)));
129
+ const bundled = path.resolve(fileURLToPath(new URL('../templates/glash_favicon.svg', import.meta.url)));
130
130
  const candidates = [path.resolve(root, cfg.favicon), bundled];
131
131
  for (const src of candidates) {
132
132
  try {
@@ -134,7 +134,7 @@ async function copyFavicon(cfg, root, outDir, log) {
134
134
  return;
135
135
  } catch { /* try next */ }
136
136
  }
137
- log(` ! favicon not found (looked in ${cfg.favicon}, bundled glashdb logo) — preview favicon will be missing`);
137
+ log(` ! favicon not found (looked in ${cfg.favicon}, bundled glash_favicon.svg) — preview favicon will be missing`);
138
138
  }
139
139
 
140
140
  function kb(bytes) {
package/src/config.mjs CHANGED
@@ -11,11 +11,11 @@ export const DEFAULT_CONFIG = {
11
11
  shortName: undefined,
12
12
  // The preview favicon defaults to the official glashdb logo shipped with the
13
13
  // framework; override with your own path relative to the project root.
14
- favicon: 'node_modules/glashjs/templates/favicon.svg',
14
+ favicon: 'node_modules/glashjs/templates/glash_favicon.svg',
15
15
  // Animated favicon: true = bundled animated glash mark, a path to your own
16
16
  // animated SVG/GIF, or { frames: ['/f0.svg', ...], fps: 8 } to cycle frames.
17
17
  // false disables it. Emits `glash-favicon.mjs` — call startGlashFavicon() once.
18
- animatedFavicon: true,
18
+ animatedFavicon: false,
19
19
  publicDir: 'public',
20
20
  // File-based routes (pages + api/) served by `glash dev` / `glash serve`.
21
21
  routesDir: 'routes',
package/src/create.mjs CHANGED
@@ -42,6 +42,8 @@ async function collectAnswers(options, rl) {
42
42
  const auth = boolAnswer(options.auth, await ask(rl, 'Add glashAuth routes?', 'yes'));
43
43
  const sqlRunner = boolAnswer(options.sqlRunner, await ask(rl, 'Add SQL runner support?', 'yes'));
44
44
  const aiPrompts = boolAnswer(options.aiPrompts, await ask(rl, 'Add AI deployment prompts?', 'yes'));
45
+ const translate = boolAnswer(options.translate, await ask(rl, 'Add built-in IP auto-translation?', 'yes'));
46
+ const animation = boolAnswer(options.animation, await ask(rl, 'Add motion + 3D (motion, three.js)?', 'no'));
45
47
  const install = boolAnswer(options.install, await ask(rl, 'Install dependencies now?', 'yes'));
46
48
  const packageManager = normalizeChoice(options.packageManager || await ask(rl, 'Package manager (npm/pnpm/yarn/bun)', detectPackageManager()), PM_CHOICES, 'npm');
47
49
  const git = boolAnswer(options.git, await ask(rl, 'Initialize git?', 'yes'));
@@ -54,6 +56,8 @@ async function collectAnswers(options, rl) {
54
56
  auth,
55
57
  sqlRunner,
56
58
  aiPrompts,
59
+ translate,
60
+ animation,
57
61
  install,
58
62
  packageManager,
59
63
  git,
@@ -85,6 +89,8 @@ function projectFiles(answers) {
85
89
  pg: '^8.16.3',
86
90
  preact: '^10.29.2',
87
91
  'preact-render-to-string': '^6.7.0',
92
+ // Motion (animation) + three.js (3D), bundled client-side via esbuild.
93
+ ...(answers.animation ? { motion: '^11.15.0', three: '^0.171.0' } : {}),
88
94
  };
89
95
  const devDependencies = answers.css === 'tailwind'
90
96
  ? { tailwindcss: '^4.1.0', '@tailwindcss/cli': '^4.1.0' }
@@ -105,7 +111,7 @@ function projectFiles(answers) {
105
111
  '.env.local': envLocal(answers),
106
112
  'glash.config.mjs': configFile(answers, hasCss),
107
113
  'README.md': projectReadme(answers),
108
- 'routes/_layout.jsx': layoutRoute(),
114
+ 'routes/_layout.jsx': layoutRoute(answers),
109
115
  'routes/index.jsx': indexRoute(answers),
110
116
  'routes/api/health.mjs': healthRoute(),
111
117
  'db/schema.sql': schemaSql(answers),
@@ -113,6 +119,8 @@ function projectFiles(answers) {
113
119
  ...(answers.css === 'tailwind' ? { 'styles/input.css': tailwindInput() } : {}),
114
120
  ...(answers.auth ? authRoutes() : {}),
115
121
  ...(answers.sqlRunner ? sqlRunnerRoutes() : {}),
122
+ ...(answers.translate ? { 'routes/api/_glash/geo.mjs': geoRoute() } : {}),
123
+ ...(answers.animation ? { 'routes/demo/motion.jsx': motionDemoRoute() } : {}),
116
124
  ...(answers.aiPrompts ? { '.glash/prompts/deploy.md': aiDeployPrompt(answers) } : {}),
117
125
  'public/favicon.svg': faviconSvg(),
118
126
  };
@@ -140,10 +148,19 @@ export default defineConfig({
140
148
  `;
141
149
  }
142
150
 
143
- function layoutRoute() {
144
- return `import { Link } from 'glashjs/link';
151
+ function layoutRoute(answers = {}) {
152
+ const t = answers.translate;
153
+ const imports = [`import { Link } from 'glashjs/link';`];
154
+ if (t) {
155
+ imports.push(`import { useEffect } from 'preact/hooks';`);
156
+ imports.push(`import { glashAutoTranslate } from 'glashjs/i18n';`);
157
+ }
158
+ return `${imports.join('\n')}
145
159
 
146
- export default function RootLayout({ children }) {
160
+ export default function RootLayout({ children }) {${t ? `
161
+ // Built-in IP auto-translation: detect the visitor's country and offer to
162
+ // translate the page into their native language. Reads /api/_glash/geo.
163
+ useEffect(() => { glashAutoTranslate(); }, []);` : ''}
147
164
  return (
148
165
  <div className="shell">
149
166
  <header className="nav">
@@ -160,6 +177,58 @@ export default function RootLayout({ children }) {
160
177
  `;
161
178
  }
162
179
 
180
+ function geoRoute() {
181
+ return `// IP -> country -> native language, read from the edge geo header
182
+ // (CF-IPCountry / X-Glash-Country on glashDB hosting). The client i18n runtime
183
+ // calls this to decide whether to offer auto-translation.
184
+ import { geoRouteHandler } from 'glashjs/i18n';
185
+
186
+ export const GET = geoRouteHandler;
187
+ `;
188
+ }
189
+
190
+ function motionDemoRoute() {
191
+ return `import { useEffect, useRef } from 'preact/hooks';
192
+ import { animate } from 'motion';
193
+ import * as THREE from 'three';
194
+
195
+ export const metadata = { title: 'Motion + 3D' };
196
+
197
+ export default function MotionDemo() {
198
+ const card = useRef(null);
199
+ const canvas = useRef(null);
200
+
201
+ useEffect(() => {
202
+ if (card.current) {
203
+ animate(card.current, { rotate: [0, 8, -8, 0], scale: [1, 1.06, 1] }, { duration: 2.4, repeat: Infinity });
204
+ }
205
+ const el = canvas.current;
206
+ if (!el) return;
207
+ const renderer = new THREE.WebGLRenderer({ canvas: el, antialias: true, alpha: true });
208
+ renderer.setSize(240, 240, false);
209
+ const scene = new THREE.Scene();
210
+ const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
211
+ camera.position.z = 3;
212
+ const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial());
213
+ scene.add(cube);
214
+ let raf;
215
+ const loop = () => { cube.rotation.x += 0.01; cube.rotation.y += 0.013; renderer.render(scene, camera); raf = requestAnimationFrame(loop); };
216
+ loop();
217
+ return () => cancelAnimationFrame(raf);
218
+ }, []);
219
+
220
+ return (
221
+ <main className="hero">
222
+ <h1>Motion + 3D</h1>
223
+ <p className="lede">motion drives the card animation; three.js renders the cube.</p>
224
+ <div ref={card} style={{ width: '120px', height: '120px', borderRadius: '16px', background: '#e8eaed' }} />
225
+ <canvas ref={canvas} width={240} height={240} />
226
+ </main>
227
+ );
228
+ }
229
+ `;
230
+ }
231
+
163
232
  function indexRoute(answers) {
164
233
  return `import { useState } from 'preact/hooks';
165
234
  import { callServerFunction } from 'glashjs/server-functions';
package/src/i18n.mjs ADDED
@@ -0,0 +1,185 @@
1
+ // glashjs/i18n — built-in IP-based auto-translation.
2
+ //
3
+ // Server:
4
+ // detectCountry(req) -> 'US' | null (reads edge/CDN geo headers)
5
+ // languageForCountry('FR') -> { code: 'fr', name: 'Français' } | null
6
+ //
7
+ // Client:
8
+ // glashAutoTranslate(opts) -> looks up the visitor's country (via a glashjs
9
+ // geo API route, falling back to a public IP service), maps it to a language,
10
+ // and — if that differs from the page language — shows a small prompt to
11
+ // translate the page into the visitor's native language. No API key: it uses
12
+ // the Google website-translate widget (cookie-driven, in-place).
13
+ //
14
+ // Wire it up in a client effect:
15
+ // import { glashAutoTranslate } from 'glashjs/i18n';
16
+ // useEffect(() => glashAutoTranslate(), []);
17
+ // and scaffold `routes/api/_glash/geo.mjs` (see geoRouteHandler) so detection
18
+ // works on glashDB hosting, where the edge sets the country header.
19
+
20
+ // country code -> [BCP-47 language, native name]
21
+ export const COUNTRY_LANGUAGE = {
22
+ US: ['en', 'English'], GB: ['en', 'English'], AU: ['en', 'English'], CA: ['en', 'English'],
23
+ IE: ['en', 'English'], NZ: ['en', 'English'], NG: ['en', 'English'], GH: ['en', 'English'],
24
+ KE: ['en', 'English'], ZA: ['en', 'English'], IN: ['hi', 'हिन्दी'], PK: ['ur', 'اردو'],
25
+ FR: ['fr', 'Français'], BE: ['fr', 'Français'], LU: ['fr', 'Français'], CI: ['fr', 'Français'],
26
+ SN: ['fr', 'Français'], CM: ['fr', 'Français'], ES: ['es', 'Español'], MX: ['es', 'Español'],
27
+ AR: ['es', 'Español'], CO: ['es', 'Español'], CL: ['es', 'Español'], PE: ['es', 'Español'],
28
+ VE: ['es', 'Español'], DE: ['de', 'Deutsch'], AT: ['de', 'Deutsch'], CH: ['de', 'Deutsch'],
29
+ IT: ['it', 'Italiano'], PT: ['pt', 'Português'], BR: ['pt', 'Português'], AO: ['pt', 'Português'],
30
+ NL: ['nl', 'Nederlands'], RU: ['ru', 'Русский'], UA: ['uk', 'Українська'], PL: ['pl', 'Polski'],
31
+ CZ: ['cs', 'Čeština'], SK: ['sk', 'Slovenčina'], RO: ['ro', 'Română'], HU: ['hu', 'Magyar'],
32
+ GR: ['el', 'Ελληνικά'], TR: ['tr', 'Türkçe'], SE: ['sv', 'Svenska'], NO: ['no', 'Norsk'],
33
+ DK: ['da', 'Dansk'], FI: ['fi', 'Suomi'], IS: ['is', 'Íslenska'], CN: ['zh-CN', '中文'],
34
+ TW: ['zh-TW', '繁體中文'], HK: ['zh-TW', '繁體中文'], JP: ['ja', '日本語'], KR: ['ko', '한국어'],
35
+ TH: ['th', 'ไทย'], VN: ['vi', 'Tiếng Việt'], ID: ['id', 'Bahasa Indonesia'], MY: ['ms', 'Bahasa Melayu'],
36
+ PH: ['tl', 'Filipino'], SA: ['ar', 'العربية'], AE: ['ar', 'العربية'], EG: ['ar', 'العربية'],
37
+ MA: ['ar', 'العربية'], DZ: ['ar', 'العربية'], IQ: ['ar', 'العربية'], IL: ['he', 'עברית'],
38
+ IR: ['fa', 'فارسی'], BD: ['bn', 'বাংলা'], ET: ['am', 'አማርኛ'],
39
+ };
40
+
41
+ export function detectCountry(req) {
42
+ const h = (req && req.headers) || {};
43
+ const get = (k) => (typeof h.get === 'function' ? h.get(k) : (h[k] ?? h[k.toLowerCase()]));
44
+ const cc = String(
45
+ get('cf-ipcountry') || get('x-glash-country') || get('x-vercel-ip-country') || get('x-country-code') || '',
46
+ ).split(',')[0].trim().toUpperCase();
47
+ return /^[A-Z]{2}$/.test(cc) && cc !== 'XX' ? cc : null;
48
+ }
49
+
50
+ export function languageForCountry(country) {
51
+ const entry = COUNTRY_LANGUAGE[String(country || '').toUpperCase()];
52
+ return entry ? { code: entry[0], name: entry[1] } : null;
53
+ }
54
+
55
+ /** Drop-in handler for `routes/api/_glash/geo.mjs` (export const GET = geoRouteHandler). */
56
+ export function geoRouteHandler(ctx) {
57
+ const country = detectCountry({ headers: (ctx && ctx.headers) || {} });
58
+ const language = country ? languageForCountry(country) : null;
59
+ return { country, language };
60
+ }
61
+
62
+ // ---- client ---------------------------------------------------------------
63
+
64
+ function baseLang(code) { return String(code || '').toLowerCase().split('-')[0]; }
65
+
66
+ async function lookupLanguage(geoEndpoint) {
67
+ // 1) glashjs geo route (works on glashDB hosting via the edge country header).
68
+ try {
69
+ const r = await fetch(geoEndpoint, { headers: { accept: 'application/json' } });
70
+ if (r.ok) {
71
+ const d = await r.json();
72
+ if (d && d.language && d.language.code) return d.language;
73
+ if (d && d.country) { const l = languageForCountry(d.country); if (l) return l; }
74
+ }
75
+ } catch { /* fall through */ }
76
+ // 2) Public IP geolocation fallback (no key, CORS-enabled).
77
+ try {
78
+ const r = await fetch('https://ipapi.co/json/');
79
+ if (r.ok) {
80
+ const d = await r.json();
81
+ const l = d && d.country_code ? languageForCountry(d.country_code) : null;
82
+ if (l) return l;
83
+ }
84
+ } catch { /* fall through */ }
85
+ return null;
86
+ }
87
+
88
+ function setGoogTransCookie(from, to) {
89
+ const val = `/${from}/${to}`;
90
+ const host = location.hostname;
91
+ document.cookie = `googtrans=${val};path=/`;
92
+ document.cookie = `googtrans=${val};path=/;domain=${host}`;
93
+ if (host.split('.').length > 1) document.cookie = `googtrans=${val};path=/;domain=.${host}`;
94
+ }
95
+
96
+ function loadGoogleWidget(pageLanguage) {
97
+ if (window.__glashGTLoaded) return;
98
+ window.__glashGTLoaded = true;
99
+ if (!document.getElementById('glash-gt')) {
100
+ const el = document.createElement('div');
101
+ el.id = 'glash-gt';
102
+ el.style.display = 'none';
103
+ document.body.appendChild(el);
104
+ }
105
+ window.googleTranslateElementInit = function () {
106
+ try { new window.google.translate.TranslateElement({ pageLanguage, autoDisplay: false }, 'glash-gt'); } catch { /* ignore */ }
107
+ };
108
+ const s = document.createElement('script');
109
+ s.src = 'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
110
+ document.head.appendChild(s);
111
+ }
112
+
113
+ function showPrompt(language, onAccept, onDismiss) {
114
+ if (document.getElementById('glash-translate-prompt')) return;
115
+ const box = document.createElement('div');
116
+ box.id = 'glash-translate-prompt';
117
+ box.setAttribute('role', 'dialog');
118
+ box.style.cssText = [
119
+ 'position:fixed', 'z-index:2147483646', 'left:50%', 'bottom:20px', 'transform:translateX(-50%)',
120
+ 'max-width:min(92vw,460px)', 'display:flex', 'gap:12px', 'align-items:center',
121
+ 'padding:12px 14px', 'border-radius:12px', 'font:14px/1.4 system-ui,sans-serif',
122
+ 'background:#0b0d12', 'color:#e8eaed', 'border:1px solid rgba(255,255,255,0.12)',
123
+ 'box-shadow:0 8px 30px rgba(0,0,0,0.45)',
124
+ ].join(';');
125
+ const text = document.createElement('span');
126
+ text.style.cssText = 'flex:1';
127
+ text.textContent = `View this site in ${language.name}?`;
128
+ const yes = document.createElement('button');
129
+ yes.textContent = 'Translate';
130
+ yes.style.cssText = 'cursor:pointer;border:0;border-radius:8px;padding:7px 12px;font:inherit;background:#e8eaed;color:#0b0d12;font-weight:600';
131
+ const no = document.createElement('button');
132
+ no.textContent = 'No thanks';
133
+ no.style.cssText = 'cursor:pointer;border:0;border-radius:8px;padding:7px 10px;font:inherit;background:transparent;color:#9aa0a6';
134
+ yes.addEventListener('click', () => { box.remove(); onAccept(); });
135
+ no.addEventListener('click', () => { box.remove(); onDismiss(); });
136
+ box.append(text, yes, no);
137
+ document.body.appendChild(box);
138
+ }
139
+
140
+ /**
141
+ * Detect the visitor's country by IP, map it to their native language, and (if it
142
+ * differs from the page language) prompt to auto-translate the page in place.
143
+ *
144
+ * Options:
145
+ * pageLanguage language the site is authored in (default: <html lang> or 'en')
146
+ * geoEndpoint glashjs geo route (default: '/api/_glash/geo')
147
+ * auto translate immediately without prompting (default: false)
148
+ * storageKey localStorage key remembering the choice
149
+ */
150
+ export function glashAutoTranslate(opts = {}) {
151
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
152
+ const {
153
+ pageLanguage = baseLang(document.documentElement.lang) || 'en',
154
+ geoEndpoint = '/api/_glash/geo',
155
+ auto = false,
156
+ storageKey = 'glash:autotranslate',
157
+ } = opts;
158
+
159
+ // If a previous choice already translated the page, re-apply the widget so the
160
+ // translation persists across navigations, then stop.
161
+ if (/(^|;\s*)googtrans=\//.test(document.cookie)) { loadGoogleWidget(pageLanguage); return; }
162
+ let saved = null;
163
+ try { saved = localStorage.getItem(storageKey); } catch { /* ignore */ }
164
+ if (saved === 'dismissed') return;
165
+
166
+ const translateTo = (code) => {
167
+ setGoogTransCookie(pageLanguage, code);
168
+ loadGoogleWidget(pageLanguage);
169
+ setTimeout(() => location.reload(), 60);
170
+ };
171
+
172
+ if (saved && saved !== pageLanguage) { translateTo(saved); return; }
173
+
174
+ lookupLanguage(geoEndpoint).then((language) => {
175
+ if (!language || baseLang(language.code) === pageLanguage) return;
176
+ if (auto) { try { localStorage.setItem(storageKey, language.code); } catch {} translateTo(language.code); return; }
177
+ showPrompt(
178
+ language,
179
+ () => { try { localStorage.setItem(storageKey, language.code); } catch {} translateTo(language.code); },
180
+ () => { try { localStorage.setItem(storageKey, 'dismissed'); } catch {} },
181
+ );
182
+ }).catch(() => {});
183
+ }
184
+
185
+ export default { detectCountry, languageForCountry, geoRouteHandler, glashAutoTranslate, COUNTRY_LANGUAGE };
@@ -49,7 +49,7 @@ ${headHtml}
49
49
  `;
50
50
  }
51
51
 
52
- function shellTail({ offline = true, animatedFavicon = true, dev = false, nonce = '' }) {
52
+ function shellTail({ offline = true, animatedFavicon = false, dev = false, nonce = '' }) {
53
53
  const n = nonce ? ` nonce="${escapeHtml(nonce)}"` : '';
54
54
  const fav = animatedFavicon
55
55
  ? `<script type="module"${n}>try{const m=await import("/glash-favicon.mjs");m.startGlashFavicon&&m.startGlashFavicon();}catch{}</script>`
@@ -0,0 +1,79 @@
1
+ <svg width="508" height="508" viewBox="0 0 508 508" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g filter="url(#filter0_ii_12189_2)">
3
+ <rect x="43.9863" y="34.4604" width="420.999" height="420.999" rx="102.477" fill="url(#paint0_linear_12189_2)"/>
4
+ </g>
5
+ <rect x="45.1251" y="35.5992" width="418.722" height="418.722" rx="101.338" stroke="#D4D4D4" stroke-width="2.27759"/>
6
+ <g opacity="0.6" filter="url(#filter1_f_12189_2)" style="mix-blend-mode:overlay">
7
+ <circle cx="253.556" cy="253.556" r="119.602" fill="url(#paint1_linear_12189_2)"/>
8
+ <circle cx="253.556" cy="253.556" r="118.463" stroke="#D4D4D4" stroke-width="2.27759"/>
9
+ </g>
10
+ <mask id="mask0_12189_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="43" y="34" width="422" height="422">
11
+ <rect x="45.1241" y="35.5997" width="418.722" height="418.722" rx="101.338" fill="url(#paint2_linear_12189_2)" stroke="#D4D4D4" stroke-width="2.27759"/>
12
+ </mask>
13
+ <g mask="url(#mask0_12189_2)">
14
+ <g filter="url(#filter2_f_12189_2)">
15
+ <ellipse cx="259.269" cy="23.0167" rx="215.284" ry="122.507" fill="url(#paint3_linear_12189_2)"/>
16
+ <path d="M259.269 -98.3521C318.557 -98.3521 372.177 -84.6747 410.935 -62.6196C449.725 -40.5457 473.414 -10.2249 473.414 23.0171C473.414 56.2589 449.725 86.579 410.935 108.653C372.177 130.708 318.557 144.385 259.269 144.385C199.98 144.385 146.361 130.708 107.604 108.653C68.813 86.579 45.1242 56.2589 45.124 23.0171C45.124 -10.2249 68.8128 -40.5457 107.604 -62.6196C146.361 -84.6747 199.98 -98.352 259.269 -98.3521Z" stroke="#D4D4D4" stroke-width="2.27759"/>
17
+ </g>
18
+ <g filter="url(#filter3_f_12189_2)" style="mix-blend-mode:plus-lighter">
19
+ <ellipse cx="254.486" cy="29.677" rx="47.8408" ry="81.3294" fill="url(#paint4_linear_12189_2)"/>
20
+ <path d="M254.486 -50.5137C267.129 -50.5137 278.78 -41.7943 287.333 -27.2539C295.871 -12.7399 301.188 7.3854 301.188 29.6768C301.188 51.9682 295.871 72.0943 287.333 86.6084C278.78 101.149 267.129 109.867 254.486 109.867C241.844 109.867 230.193 101.149 221.64 86.6084C213.102 72.0943 207.784 51.9682 207.784 29.6768C207.784 7.3854 213.102 -12.7399 221.64 -27.2539C230.193 -41.7943 241.844 -50.5137 254.486 -50.5137Z" stroke="#D4D4D4" stroke-width="2.27759"/>
21
+ </g>
22
+ </g>
23
+ <path d="M336.671 312.331L239.265 374.956L164.548 342.247L261.906 279.621L336.671 312.331Z" fill="#FEFFFF"/>
24
+ <path d="M264.17 166.655C286.185 166.655 304.057 184.575 304.057 206.591C304.057 213.479 302.371 219.983 299.047 225.956C292.014 238.626 278.67 246.478 264.17 246.478C258.148 246.478 252.367 245.178 246.972 242.624C233.146 236.025 224.234 221.91 224.234 206.591C224.234 184.575 242.155 166.655 264.17 166.655ZM264.17 128.116C220.814 128.116 185.695 163.235 185.695 206.591C185.695 237.807 203.953 264.784 230.352 277.406C240.565 282.319 252.03 285.017 264.122 285.017C293.604 285.017 319.328 268.734 332.672 244.648C338.983 233.375 342.548 220.416 342.548 206.591C342.548 163.283 307.43 128.116 264.122 128.116H264.17Z" fill="#FEFFFF"/>
25
+ <defs>
26
+ <filter id="filter0_ii_12189_2" x="43.9863" y="34.4604" width="420.999" height="449.704" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
27
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
28
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
29
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
30
+ <feOffset/>
31
+ <feGaussianBlur stdDeviation="19.1363"/>
32
+ <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
33
+ <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.24 0"/>
34
+ <feBlend mode="normal" in2="shape" result="effect1_innerShadow_12189_2"/>
35
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
36
+ <feOffset dy="28.7045"/>
37
+ <feGaussianBlur stdDeviation="19.1363"/>
38
+ <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
39
+ <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0"/>
40
+ <feBlend mode="normal" in2="effect1_innerShadow_12189_2" result="effect2_innerShadow_12189_2"/>
41
+ </filter>
42
+ <filter id="filter1_f_12189_2" x="-0.000152588" y="-0.000152588" width="507.113" height="507.113" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
43
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
44
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
45
+ <feGaussianBlur stdDeviation="66.9771" result="effect1_foregroundBlur_12189_2"/>
46
+ </filter>
47
+ <filter id="filter2_f_12189_2" x="-109.105" y="-252.581" width="736.749" height="551.196" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
48
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
49
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
50
+ <feGaussianBlur stdDeviation="76.5453" result="effect1_foregroundBlur_12189_2"/>
51
+ </filter>
52
+ <filter id="filter3_f_12189_2" x="149.237" y="-109.061" width="210.5" height="277.477" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
53
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
54
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
55
+ <feGaussianBlur stdDeviation="28.7045" result="effect1_foregroundBlur_12189_2"/>
56
+ </filter>
57
+ <linearGradient id="paint0_linear_12189_2" x1="254.486" y1="34.4604" x2="254.486" y2="455.46" gradientUnits="userSpaceOnUse">
58
+ <stop stop-opacity="0.3"/>
59
+ <stop offset="1" stop-opacity="0.6"/>
60
+ </linearGradient>
61
+ <linearGradient id="paint1_linear_12189_2" x1="253.556" y1="133.954" x2="253.556" y2="373.158" gradientUnits="userSpaceOnUse">
62
+ <stop stop-opacity="0"/>
63
+ <stop offset="1" stop-opacity="0.6"/>
64
+ </linearGradient>
65
+ <linearGradient id="paint2_linear_12189_2" x1="254.485" y1="34.4609" x2="254.485" y2="455.46" gradientUnits="userSpaceOnUse">
66
+ <stop stop-color="#4B4B4B"/>
67
+ <stop offset="1" stop-color="#AFAFAF"/>
68
+ </linearGradient>
69
+ <linearGradient id="paint3_linear_12189_2" x1="272.501" y1="142.813" x2="267.703" y2="-48.6569" gradientUnits="userSpaceOnUse">
70
+ <stop stop-color="#D8D8D8" stop-opacity="0.05"/>
71
+ <stop offset="0.61" stop-color="#B2B2B2" stop-opacity="0.05"/>
72
+ <stop offset="0.98" stop-color="#404040"/>
73
+ </linearGradient>
74
+ <linearGradient id="paint4_linear_12189_2" x1="254.486" y1="-51.6523" x2="254.486" y2="111.006" gradientUnits="userSpaceOnUse">
75
+ <stop stop-opacity="0"/>
76
+ <stop offset="1" stop-opacity="0.6"/>
77
+ </linearGradient>
78
+ </defs>
79
+ </svg>