@www.hyperlinks.space/program-kit 1.2.91881 → 7.8.3
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/.eas/workflows/create-development-builds.yml +21 -0
- package/.eas/workflows/create-draft.yml +15 -0
- package/.eas/workflows/deploy-to-production.yml +68 -0
- package/.env.example +19 -0
- package/.gitattributes +48 -0
- package/.gitignore +52 -0
- package/.nvmrc +1 -0
- package/.vercelignore +6 -0
- package/README.md +10 -5
- package/ai/openai.ts +202 -0
- package/ai/transmitter.ts +367 -0
- package/api/{base.ts → _base.ts} +1 -1
- package/api/wallet/_auth.ts +143 -0
- package/api/wallet/register.ts +151 -0
- package/api/wallet/status.ts +89 -0
- package/app/index.tsx +319 -5
- package/assets/images/PreviewImage.png +0 -0
- package/backlogs/medium_term_backlog.md +26 -0
- package/backlogs/short_term_backlog.md +42 -0
- package/database/start.ts +0 -1
- package/database/wallets.ts +266 -0
- package/eslint.config.cjs +10 -0
- package/fullREADME.md +142 -71
- package/index.js +3 -0
- package/npmReadMe.md +10 -5
- package/npmrc.example +1 -0
- package/package.json +7 -27
- package/polyfills/buffer.ts +9 -0
- package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
- package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
- package/research & docs/keys-retrieval-console-scripts.js +131 -0
- package/{docs/security_plan_raw.md → research & docs/security_plan_raw.md } +1 -1
- package/{docs/security_raw.md → research & docs/security_raw.md } +22 -13
- package/research & docs/storage-availability-console-script.js +152 -0
- package/research & docs/storage-lifetime.md +33 -0
- package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
- package/{docs/wallets_hosting_architecture.md → research & docs/wallets_hosting_architecture.md } +147 -1
- package/scripts/test-api-base.ts +2 -2
- package/services/wallet/tonWallet.ts +73 -0
- package/telegram/post.ts +44 -8
- package/ui/components/GlobalBottomBar.tsx +447 -0
- package/ui/components/GlobalBottomBarWeb.tsx +362 -0
- package/ui/components/GlobalLogoBar.tsx +108 -0
- package/ui/components/GlobalLogoBarFallback.tsx +66 -0
- package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
- package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
- package/ui/components/Telegram.tsx +677 -0
- package/ui/components/telegramWebApp.ts +359 -0
- package/ui/fonts.ts +12 -0
- package/ui/theme.ts +117 -0
- /package/{docs → research & docs}/ai_and_search_bar_input.md +0 -0
- /package/{docs → research & docs}/ai_bot_messages.md +0 -0
- /package/{docs → research & docs}/blue_bar_tackling.md +0 -0
- /package/{docs → research & docs}/bot_async_streaming.md +0 -0
- /package/{docs → research & docs}/build_and_install.md +0 -0
- /package/{docs → research & docs}/database_messages.md +0 -0
- /package/{docs → research & docs}/fonts.md +0 -0
- /package/{docs → research & docs}/npm-release.md +0 -0
- /package/{docs → research & docs}/releases.md +0 -0
- /package/{docs → research & docs}/releases_github_actions.md +0 -0
- /package/{docs → research & docs}/scalability.md +0 -0
- /package/{docs → research & docs}/timing_raw.md +0 -0
- /package/{docs → research & docs}/tma_logo_bar_jump_investigation.md +0 -0
- /package/{docs → research & docs}/update.md +0 -0
- /package/{docs → research & docs}/wallet_telegram_standalone_multichain_proposal.md +0 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-only Telegram WebApp helpers (single source of truth for TMA data).
|
|
3
|
+
* Aligned with Telegram Launch Parameters, Init Data, and Start Parameter docs:
|
|
4
|
+
* - Launch params: in window.location.hash (tgWebAppData, tgWebAppVersion, etc.). Cached at launch.
|
|
5
|
+
* - Start parameter: in URL query params (startattach or startapp), NOT in hash. Also in init data start_param.
|
|
6
|
+
* - Allowed start param: A-Z, a-z, 0-9, _, -; max 512 chars. Validate with /^[\w-]{0,512}$/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type TelegramWebApp = {
|
|
10
|
+
initData?: string;
|
|
11
|
+
initDataUnsafe?: { user?: { id?: number; username?: string; [k: string]: unknown } };
|
|
12
|
+
platform?: string;
|
|
13
|
+
themeParams?: Record<string, string>;
|
|
14
|
+
ready?: () => void;
|
|
15
|
+
expand?: () => void;
|
|
16
|
+
setHeaderColor?: (color: string) => void;
|
|
17
|
+
setupSwipeBehavior?: (opts: { allow_vertical_swipe?: boolean }) => void;
|
|
18
|
+
disableVerticalSwipes?: () => void;
|
|
19
|
+
isFullscreen?: boolean;
|
|
20
|
+
HapticFeedback?: { impactOccurred?: (style: string) => void };
|
|
21
|
+
[k: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
interface Window {
|
|
26
|
+
Telegram?: { WebApp?: TelegramWebApp };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let cachedHashParams: URLSearchParams | null = null;
|
|
31
|
+
let cachedInitDataFromHash: string | null | undefined = undefined;
|
|
32
|
+
|
|
33
|
+
/** Clear cached launch params (e.g. hash was empty on first read, then populated). */
|
|
34
|
+
export function resetTelegramLaunchCache(): void {
|
|
35
|
+
cachedHashParams = null;
|
|
36
|
+
cachedInitDataFromHash = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Get launch params from URL hash. Cached on first read so hash routing doesn't lose them. */
|
|
40
|
+
function getLaunchParamsFromHash(): URLSearchParams | null {
|
|
41
|
+
if (typeof window === "undefined") return null;
|
|
42
|
+
if (cachedHashParams) return cachedHashParams;
|
|
43
|
+
const hash = window.location.hash.slice(1);
|
|
44
|
+
if (!hash) return null;
|
|
45
|
+
cachedHashParams = new URLSearchParams(hash);
|
|
46
|
+
return cachedHashParams;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Init data (tgWebAppData) from hash. Cached at launch per Telegram guide. */
|
|
50
|
+
function getInitDataFromHash(): string | null {
|
|
51
|
+
if (cachedInitDataFromHash !== undefined) return cachedInitDataFromHash ?? null;
|
|
52
|
+
const params = getLaunchParamsFromHash();
|
|
53
|
+
// Do not cache "no hash yet" — first read can be before Mini App hash is present.
|
|
54
|
+
if (params == null) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const raw = params.get("tgWebAppData") ?? null;
|
|
58
|
+
const s = raw?.trim();
|
|
59
|
+
cachedInitDataFromHash = s && s.length > 0 ? s : null;
|
|
60
|
+
return cachedInitDataFromHash;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getWebApp(): TelegramWebApp | null {
|
|
64
|
+
if (typeof window === "undefined") return null;
|
|
65
|
+
const tg = (window as Window).Telegram;
|
|
66
|
+
const app = tg?.WebApp ?? null;
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True if we have WebApp object or init data in hash (launch params). */
|
|
71
|
+
export function isAvailable(): boolean {
|
|
72
|
+
if (getWebApp() != null) return true;
|
|
73
|
+
return getInitDataFromHash() != null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Load Telegram Web App script if missing (script is not auto-injected; page must include or load it). */
|
|
77
|
+
export function ensureTelegramScript(onLoad?: () => void): void {
|
|
78
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
79
|
+
if ((window as Window).Telegram?.WebApp) {
|
|
80
|
+
onLoad?.();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existing = document.querySelector<HTMLScriptElement>(
|
|
85
|
+
'script[src*="telegram.org/js/telegram-web-app"]',
|
|
86
|
+
);
|
|
87
|
+
if (existing) {
|
|
88
|
+
if (existing.dataset.telegramWebAppLoaded === "1") {
|
|
89
|
+
onLoad?.();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
existing.addEventListener(
|
|
93
|
+
"load",
|
|
94
|
+
() => {
|
|
95
|
+
existing.dataset.telegramWebAppLoaded = "1";
|
|
96
|
+
onLoad?.();
|
|
97
|
+
},
|
|
98
|
+
{ once: true },
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const script = document.createElement("script");
|
|
104
|
+
script.src = "https://telegram.org/js/telegram-web-app.js";
|
|
105
|
+
script.async = true;
|
|
106
|
+
script.onload = () => {
|
|
107
|
+
script.dataset.telegramWebAppLoaded = "1";
|
|
108
|
+
onLoad?.();
|
|
109
|
+
};
|
|
110
|
+
document.head.appendChild(script);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** In Telegram: platform is not "unknown" and user with id exists. In browser: otherwise. */
|
|
114
|
+
export function isActuallyInTelegram(): boolean {
|
|
115
|
+
const app = getWebApp();
|
|
116
|
+
if (!app) return false;
|
|
117
|
+
try {
|
|
118
|
+
const platform = app.platform;
|
|
119
|
+
const unsafe = app.initDataUnsafe;
|
|
120
|
+
const user = unsafe?.user;
|
|
121
|
+
const hasValidUser =
|
|
122
|
+
user != null &&
|
|
123
|
+
typeof user === "object" &&
|
|
124
|
+
"id" in user &&
|
|
125
|
+
(user as { id?: unknown }).id != null;
|
|
126
|
+
const ok =
|
|
127
|
+
platform != null &&
|
|
128
|
+
platform !== "unknown" &&
|
|
129
|
+
hasValidUser;
|
|
130
|
+
return !!ok;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Init data: WebApp.initData first, then tgWebAppData from hash (per Launch Parameters guide). */
|
|
137
|
+
export function getInitDataString(): string | null {
|
|
138
|
+
const app = getWebApp();
|
|
139
|
+
if (app?.initData && typeof app.initData === "string") {
|
|
140
|
+
const s = app.initData.trim();
|
|
141
|
+
if (s.length > 0) return s;
|
|
142
|
+
}
|
|
143
|
+
return getInitDataFromHash();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getUser(): { id: number; username?: string; [k: string]: unknown } | null {
|
|
147
|
+
const app = getWebApp();
|
|
148
|
+
const user = app?.initDataUnsafe?.user;
|
|
149
|
+
if (user == null || typeof user !== "object" || typeof (user as { id?: number }).id !== "number") {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return user as { id: number; username?: string; [k: string]: unknown };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getPlatform(): string | null {
|
|
156
|
+
const app = getWebApp();
|
|
157
|
+
const p = app?.platform;
|
|
158
|
+
return typeof p === "string" ? p : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type ThemeParams = Record<string, string>;
|
|
162
|
+
|
|
163
|
+
export function getThemeParamsFromLaunch(): ThemeParams | null {
|
|
164
|
+
const params = getLaunchParamsFromHash();
|
|
165
|
+
const raw = params?.get("tgWebAppThemeParams");
|
|
166
|
+
if (!raw) return null;
|
|
167
|
+
const trimmed = raw.trim();
|
|
168
|
+
const candidates = [trimmed];
|
|
169
|
+
try {
|
|
170
|
+
const dec = decodeURIComponent(trimmed);
|
|
171
|
+
if (dec !== trimmed) candidates.push(dec.trim());
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
for (const s of candidates) {
|
|
176
|
+
if (!s) continue;
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(s);
|
|
179
|
+
if (parsed && typeof parsed === "object") return parsed as ThemeParams;
|
|
180
|
+
} catch {
|
|
181
|
+
// try next candidate
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getThemeParamsFromWebApp(): ThemeParams | null {
|
|
188
|
+
const app = getWebApp();
|
|
189
|
+
const tp = app?.themeParams;
|
|
190
|
+
return tp && typeof tp === "object" ? (tp as ThemeParams) : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* WebApp.themeParams first, then hash. Use after Mini App is ready (runTmaFlow, theme listeners).
|
|
195
|
+
* Do not use for first-paint bootstrap: Telegram can briefly expose stale/default **dark**
|
|
196
|
+
* `themeParams` before syncing the real client theme (e.g. light → dark flash).
|
|
197
|
+
*/
|
|
198
|
+
export function getInitialThemeParams(): ThemeParams | null {
|
|
199
|
+
return getThemeParamsFromWebApp() ?? getThemeParamsFromLaunch();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Matches theme.ts ThemeColors shape; values are whatever Telegram sets (hex or rgb()). */
|
|
203
|
+
export type TelegramCssThemeColors = {
|
|
204
|
+
background: string;
|
|
205
|
+
primary: string;
|
|
206
|
+
secondary: string;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Telegram WebApp sets --tg-theme-* on documentElement (and sometimes body) before/while JS runs.
|
|
211
|
+
* Use while React theme is not ready so we never paint our hard-coded "dark" app palette first.
|
|
212
|
+
*/
|
|
213
|
+
export function getThemeColorsFromTelegramCssVars(): TelegramCssThemeColors | null {
|
|
214
|
+
if (typeof document === "undefined") return null;
|
|
215
|
+
const roots = [document.documentElement, document.body];
|
|
216
|
+
for (const el of roots) {
|
|
217
|
+
const cs = getComputedStyle(el);
|
|
218
|
+
const bg = cs.getPropertyValue("--tg-theme-bg-color").trim();
|
|
219
|
+
const text = cs.getPropertyValue("--tg-theme-text-color").trim();
|
|
220
|
+
const hint = cs.getPropertyValue("--tg-theme-hint-color").trim();
|
|
221
|
+
if (bg) {
|
|
222
|
+
return {
|
|
223
|
+
background: bg,
|
|
224
|
+
primary: text || "#000000",
|
|
225
|
+
secondary: hint || "#818181",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function pickThemeParam(tp: ThemeParams | null | undefined, keys: string[]): string | null {
|
|
233
|
+
if (!tp) return null;
|
|
234
|
+
for (const k of keys) {
|
|
235
|
+
const v = tp[k];
|
|
236
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Sync colors from a theme_params object (WebApp.themeParams or parsed launch JSON). */
|
|
242
|
+
export function getThemeColorsFromThemeParamsObject(
|
|
243
|
+
tp: ThemeParams | null | undefined,
|
|
244
|
+
): TelegramCssThemeColors | null {
|
|
245
|
+
const bg = pickThemeParam(tp, ["bg_color"]);
|
|
246
|
+
if (!bg) return null;
|
|
247
|
+
const text = pickThemeParam(tp, ["text_color", "link_color"]);
|
|
248
|
+
const hint = pickThemeParam(tp, ["hint_color", "subtitle_text_color"]);
|
|
249
|
+
return {
|
|
250
|
+
background: bg,
|
|
251
|
+
primary: text || "#000000",
|
|
252
|
+
secondary: hint || "#818181",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Same as theme_params on window.Telegram.WebApp — often populated before CSS vars resolve. */
|
|
257
|
+
export function getThemeColorsFromWebAppThemeParams(): TelegramCssThemeColors | null {
|
|
258
|
+
return getThemeColorsFromThemeParamsObject(getThemeParamsFromWebApp());
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Launch hash tgWebAppThemeParams — for pre-ready paint only (not for themeBgReady / scheme). */
|
|
262
|
+
export function getThemeColorsFromLaunchThemeParams(): TelegramCssThemeColors | null {
|
|
263
|
+
return getThemeColorsFromThemeParamsObject(getThemeParamsFromLaunch());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Luminance of #RRGGBB (same threshold as Telegram.tsx theme classification). */
|
|
267
|
+
function luminanceFromHex(hex: string): number {
|
|
268
|
+
const m = hex.trim().match(/^#([0-9a-fA-F]{6})$/);
|
|
269
|
+
if (!m) return 0;
|
|
270
|
+
const h = m[1];
|
|
271
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
272
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
273
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
274
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Text/placeholder color from launch theme params only (sync, first paint).
|
|
279
|
+
* Matches app theme.ts light/dark primary when explicit colors are missing.
|
|
280
|
+
*/
|
|
281
|
+
export function getPrimaryTextColorFromLaunch(): string | null {
|
|
282
|
+
const tp = getThemeParamsFromLaunch();
|
|
283
|
+
if (!tp) return null;
|
|
284
|
+
for (const key of ["text_color", "hint_color"] as const) {
|
|
285
|
+
const v = tp[key];
|
|
286
|
+
if (typeof v === "string" && /^#([0-9a-fA-F]{6})$/.test(v.trim())) {
|
|
287
|
+
return v.trim();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const bgRaw = tp.bg_color ?? tp.secondary_bg_color ?? tp.section_bg_color;
|
|
291
|
+
if (typeof bgRaw === "string" && /^#([0-9a-fA-F]{6})$/.test(bgRaw.trim())) {
|
|
292
|
+
const bg = bgRaw.trim();
|
|
293
|
+
return luminanceFromHex(bg) < 128 ? "#FAFAFA" : "#111111";
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getIsFullscreen(): boolean {
|
|
299
|
+
const app = getWebApp();
|
|
300
|
+
const v = app?.isFullscreen;
|
|
301
|
+
return typeof v === "boolean" ? v : true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function triggerHaptic(style: string): void {
|
|
305
|
+
if (!isActuallyInTelegram()) return;
|
|
306
|
+
try {
|
|
307
|
+
const app = getWebApp();
|
|
308
|
+
app?.HapticFeedback?.impactOccurred?.(style);
|
|
309
|
+
} catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Call ready(), expand(), set header color, disable vertical swipe. No-op if WebApp not loaded. */
|
|
315
|
+
export function readyAndExpand(): void {
|
|
316
|
+
const app = getWebApp();
|
|
317
|
+
if (!app) return;
|
|
318
|
+
try {
|
|
319
|
+
app.ready?.();
|
|
320
|
+
app.expand?.();
|
|
321
|
+
app.setHeaderColor?.("#000000");
|
|
322
|
+
const opts = { allow_vertical_swipe: false };
|
|
323
|
+
app.setupSwipeBehavior?.(opts);
|
|
324
|
+
app.disableVerticalSwipes?.(); // legacy API; setupSwipeBehavior is preferred
|
|
325
|
+
} catch {
|
|
326
|
+
// ignore
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** tgWebAppVersion from hash (e.g. "6.2"). Use to check method support. */
|
|
331
|
+
export function getWebAppVersionFromHash(): string | null {
|
|
332
|
+
const params = getLaunchParamsFromHash();
|
|
333
|
+
return params?.get("tgWebAppVersion") ?? null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** tgWebAppPlatform from hash. */
|
|
337
|
+
export function getPlatformFromHash(): string | null {
|
|
338
|
+
const params = getLaunchParamsFromHash();
|
|
339
|
+
return params?.get("tgWebAppPlatform") ?? null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Start parameter: query (startattach/startapp) or launch param tgWebAppStartParam (query then hash; Telegram puts launch params in hash). Valid: A-Za-z0-9_- up to 512 chars. */
|
|
343
|
+
const START_PARAM_REGEX = /^[\w-]{0,512}$/;
|
|
344
|
+
|
|
345
|
+
export function getStartParam(): string | null {
|
|
346
|
+
if (typeof window === "undefined") return null;
|
|
347
|
+
const fromQuery = new URLSearchParams(window.location.search);
|
|
348
|
+
const fromHash = getLaunchParamsFromHash();
|
|
349
|
+
const raw =
|
|
350
|
+
fromQuery.get("startattach") ??
|
|
351
|
+
fromQuery.get("startapp") ??
|
|
352
|
+
fromQuery.get("tgWebAppStartParam") ??
|
|
353
|
+
fromHash?.get("tgWebAppStartParam") ??
|
|
354
|
+
null;
|
|
355
|
+
if (raw == null || typeof raw !== "string") return null;
|
|
356
|
+
const s = raw.trim();
|
|
357
|
+
return s.length > 0 && START_PARAM_REGEX.test(s) ? s : null;
|
|
358
|
+
}
|
|
359
|
+
|
package/ui/fonts.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web typography — used by native DOM in GlobalBottomBarWeb (textarea) + global.css.
|
|
3
|
+
*
|
|
4
|
+
* **Brand intent:** Aeroport (geometric / grotesk-style sans). There are **no** `.otf`/`.ttf`
|
|
5
|
+
* files in this Expo app repo yet, so `"Aeroport"` alone resolves to **nothing** and the
|
|
6
|
+
* browser picks an unpredictable fallback (often a **serif** / “antiqua” look in WebView).
|
|
7
|
+
*
|
|
8
|
+
* Always append a **system UI sans** stack so you get a neutral grotesk-like face until
|
|
9
|
+
* real files are added (see `docs/fonts.md`).
|
|
10
|
+
*/
|
|
11
|
+
export const WEB_UI_SANS_STACK =
|
|
12
|
+
'Aeroport, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
package/ui/theme.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getThemeColorsFromLaunchThemeParams,
|
|
3
|
+
getThemeColorsFromTelegramCssVars,
|
|
4
|
+
getThemeColorsFromWebAppThemeParams,
|
|
5
|
+
} from "./components/telegramWebApp";
|
|
6
|
+
|
|
7
|
+
const sharedColors = {
|
|
8
|
+
secondary: "#818181",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const dark = {
|
|
12
|
+
...sharedColors,
|
|
13
|
+
background: "#111111",
|
|
14
|
+
primary: "#FAFAFA",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const light = {
|
|
18
|
+
...sharedColors,
|
|
19
|
+
background: "#FAFAFA",
|
|
20
|
+
primary: "#111111",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type ThemeName = "dark" | "light";
|
|
24
|
+
export type ThemeColors = {
|
|
25
|
+
background: string;
|
|
26
|
+
primary: string;
|
|
27
|
+
secondary: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function getColorsForTheme(name: ThemeName | undefined | null): ThemeColors {
|
|
31
|
+
if (name === "light") return light;
|
|
32
|
+
// Default + fallback: dark
|
|
33
|
+
return dark;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Same on SSR and first client paint — never app dark (#111); Telegram bg shows through CSS vars. */
|
|
37
|
+
const TELEGRAM_PRE_READY_FALLBACK: ThemeColors = {
|
|
38
|
+
background: "transparent",
|
|
39
|
+
primary: "rgba(0,0,0,0.35)",
|
|
40
|
+
secondary: "#818181",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Convenience hook: derive palette when used in React, using Telegram theme in TMA
|
|
44
|
+
// and dark theme as default/fallback elsewhere.
|
|
45
|
+
export function useColors(): ThemeColors {
|
|
46
|
+
// Lazy import to avoid a hard dependency when this is used outside React.
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
48
|
+
const { useTelegram } = require("./components/Telegram") as {
|
|
49
|
+
useTelegram: () => {
|
|
50
|
+
colorScheme: ThemeName;
|
|
51
|
+
isInTelegram: boolean;
|
|
52
|
+
useTelegramTheme: boolean;
|
|
53
|
+
themeBgReady: boolean;
|
|
54
|
+
clientHydrated: boolean;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const { colorScheme, isInTelegram, useTelegramTheme, themeBgReady, clientHydrated } =
|
|
58
|
+
useTelegram();
|
|
59
|
+
|
|
60
|
+
// TMA before themeBgReady: never use app dark (#111). Until clientHydrated, match SSR exactly
|
|
61
|
+
// (React #418 if server HTML used different colors / themeBgReady than first client render).
|
|
62
|
+
if (useTelegramTheme && !themeBgReady) {
|
|
63
|
+
if (!clientHydrated) {
|
|
64
|
+
return TELEGRAM_PRE_READY_FALLBACK;
|
|
65
|
+
}
|
|
66
|
+
const preReady =
|
|
67
|
+
getThemeColorsFromTelegramCssVars() ??
|
|
68
|
+
getThemeColorsFromWebAppThemeParams() ??
|
|
69
|
+
getThemeColorsFromLaunchThemeParams();
|
|
70
|
+
if (preReady) {
|
|
71
|
+
if (__DEV__) {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.log("[useColors] telegram pre-ready palette", preReady);
|
|
74
|
+
}
|
|
75
|
+
return preReady;
|
|
76
|
+
}
|
|
77
|
+
return TELEGRAM_PRE_READY_FALLBACK;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Plain web: default dark. TMA after themeBgReady: colorScheme from WebApp.
|
|
81
|
+
const themeName: ThemeName =
|
|
82
|
+
!useTelegramTheme ? "dark" : themeBgReady ? colorScheme : "dark";
|
|
83
|
+
|
|
84
|
+
if (__DEV__) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log("[useColors] resolved", {
|
|
87
|
+
isInTelegram,
|
|
88
|
+
useTelegramTheme,
|
|
89
|
+
themeBgReady,
|
|
90
|
+
colorScheme,
|
|
91
|
+
themeName,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return getColorsForTheme(themeName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const layout = {
|
|
99
|
+
maxContentWidth: 600,
|
|
100
|
+
bottomBar: {
|
|
101
|
+
barMinHeight: 59,
|
|
102
|
+
lineHeight: 20,
|
|
103
|
+
verticalPadding: 20,
|
|
104
|
+
applyIconBottom: 25,
|
|
105
|
+
maxLinesBeforeScroll: 7,
|
|
106
|
+
maxBarHeight: 190,
|
|
107
|
+
horizontalPadding: 15,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const icons = {
|
|
112
|
+
apply: {
|
|
113
|
+
width: 15,
|
|
114
|
+
height: 10,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|