Paradox/TBLWebappBotPublic · Community Store Listing
Webapp Demo Bot
7 commands0 envUpdated 1mo agoCreated Dec 13, 2025
commands/webapp.html.js
javascript · 1732 lines · click line # to share
1/**#command2name: webapp.html3answer: 4keyboard: 5parse_mode: 6aliases: 7allow_only_group: false8need_reply: false9is_web: 010#command**/11 12<!DOCTYPE html>13<html lang="en">14<head>15<meta charset="UTF-8">16<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">17<title>Telegram Mini App Demo</title>18<script src="https://telegram.org/js/telegram-web-app.js?62"></script>19<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>20<style>21/* ─── Reset & Base ─────────────────────────────────────── */22*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }23 24:root {25 --bg: var(--tg-theme-bg-color, #18191c);26 --bg2: var(--tg-theme-secondary-bg-color, #232428);27 --section-bg: var(--tg-theme-section-bg-color, #232428);28 --header-bg: var(--tg-theme-header-bg-color, #18191c);29 --bottom-bg: var(--tg-theme-bottom-bar-bg-color, #18191c);30 --text: var(--tg-theme-text-color, #e8eaed);31 --hint: var(--tg-theme-hint-color, #7d8087);32 --link: var(--tg-theme-link_color, #5b9bd9);33 --btn: var(--tg-theme-button-color, #3390ec);34 --btn-text: var(--tg-theme-button-text-color, #ffffff);35 --accent: var(--tg-theme-accent-text-color, #3390ec);36 --subtitle: var(--tg-theme-subtitle-text-color, #7d8087);37 --section-head: var(--tg-theme-section-header-text-color, #7d8087);38 --separator: var(--tg-theme-section_separator_color, #2d2e32);39 --destructive: var(--tg-theme-destructive_text_color, #e53935);40 --success: #34c759;41 --warning: #ff9f0a;42 --radius: 14px;43 --radius-sm: 10px;44 --radius-lg: 20px;45}46 47html { height: 100%; }48 49body {50 font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;51 background: var(--bg);52 color: var(--text);53 min-height: 100%;54 padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);55 -webkit-tap-highlight-color: transparent;56 overscroll-behavior: none;57}58 59/* ─── Header ────────────────────────────────────────────── */60.header {61 position: sticky;62 top: 0;63 z-index: 200;64 background: var(--header-bg);65 border-bottom: 1px solid var(--separator);66 padding: 12px 16px;67 display: flex;68 align-items: center;69 gap: 12px;70}71 72.header-logo {73 width: 40px;74 height: 40px;75 flex-shrink: 0;76 /* NO animation - static */77}78 79.header-info { flex: 1; min-width: 0; }80.header-title {81 font-size: 17px;82 font-weight: 700;83 color: var(--text);84 white-space: nowrap;85 overflow: hidden;86 text-overflow: ellipsis;87}88.header-sub {89 font-size: 12px;90 color: var(--hint);91 margin-top: 1px;92}93 94.live-badge {95 display: inline-flex;96 align-items: center;97 gap: 5px;98 padding: 4px 9px;99 border-radius: 20px;100 font-size: 11px;101 font-weight: 700;102 letter-spacing: 0.3px;103 background: color-mix(in srgb, var(--success) 15%, transparent);104 color: var(--success);105 flex-shrink: 0;106}107.live-badge.inactive {108 background: color-mix(in srgb, var(--hint) 15%, transparent);109 color: var(--hint);110}111 112.pulse {113 width: 6px; height: 6px;114 border-radius: 50%;115 background: var(--success);116 animation: pulse-anim 1.6s ease-in-out infinite;117}118.live-badge.inactive .pulse {119 background: var(--hint);120 animation: none;121}122@keyframes pulse-anim {123 0%, 100% { opacity: 1; transform: scale(1); }124 50% { opacity: 0.5; transform: scale(0.7); }125}126 127/* ─── Tab Bar ───────────────────────────────────────────── */128.tab-bar {129 position: sticky;130 top: 65px;131 z-index: 100;132 background: var(--header-bg);133 border-bottom: 1px solid var(--separator);134 display: flex;135 overflow-x: auto;136 gap: 0;137 scrollbar-width: none;138 padding: 0 8px;139}140.tab-bar::-webkit-scrollbar { display: none; }141 142.tab-btn {143 padding: 10px 14px;144 border: none;145 background: none;146 color: var(--hint);147 font-size: 13px;148 font-weight: 600;149 cursor: pointer;150 white-space: nowrap;151 display: flex;152 align-items: center;153 gap: 6px;154 border-bottom: 2px solid transparent;155 transition: color 0.18s, border-color 0.18s;156 flex-shrink: 0;157 position: relative;158 top: 1px;159}160.tab-btn svg { width: 15px; height: 15px; flex-shrink: 0; }161.tab-btn.active {162 color: var(--btn);163 border-bottom-color: var(--btn);164}165 166/* ─── Container ─────────────────────────────────────────── */167.container {168 max-width: 800px;169 margin: 0 auto;170 padding: 12px 12px 24px;171}172 173/* ─── Sections ──────────────────────────────────────────── */174.section {175 background: var(--section-bg);176 border-radius: var(--radius-lg);177 margin-bottom: 12px;178 overflow: hidden;179}180 181.section-header {182 display: flex;183 align-items: center;184 justify-content: space-between;185 padding: 14px 16px;186 cursor: pointer;187 user-select: none;188 gap: 8px;189}190.section-header:active { opacity: 0.7; }191 192.section-label {193 display: flex;194 align-items: center;195 gap: 10px;196 font-size: 15px;197 font-weight: 700;198 color: var(--text);199}200.section-label svg { width: 18px; height: 18px; color: var(--btn); flex-shrink: 0; }201 202.chevron {203 width: 18px; height: 18px;204 color: var(--hint);205 transition: transform 0.22s cubic-bezier(.4,0,.2,1);206 flex-shrink: 0;207}208.chevron.open { transform: rotate(180deg); }209 210.section-body {211 padding: 0 16px 16px;212 border-top: 1px solid var(--separator);213}214 215/* Static section (no collapse) */216.section-static .section-body {217 border-top: none;218 padding-top: 4px;219}220 221/* ─── Info Grid ─────────────────────────────────────────── */222.info-grid {223 display: grid;224 grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));225 gap: 8px;226 padding-top: 12px;227}228 229.info-card {230 background: var(--bg);231 border-radius: var(--radius-sm);232 padding: 10px 12px;233 border: 1px solid var(--separator);234}235.info-label {236 font-size: 10px;237 font-weight: 700;238 color: var(--section-head);239 text-transform: uppercase;240 letter-spacing: 0.6px;241 margin-bottom: 5px;242}243.info-value {244 font-size: 14px;245 font-weight: 600;246 color: var(--text);247 word-break: break-word;248}249 250/* ─── Buttons ───────────────────────────────────────────── */251.btn-row {252 display: flex;253 flex-wrap: wrap;254 gap: 8px;255 padding-top: 12px;256}257 258.btn {259 padding: 9px 16px;260 border: none;261 border-radius: var(--radius-sm);262 background: var(--btn);263 color: var(--btn-text);264 font-size: 13px;265 font-weight: 600;266 cursor: pointer;267 display: inline-flex;268 align-items: center;269 gap: 6px;270 transition: opacity 0.15s, transform 0.12s;271 line-height: 1;272 white-space: nowrap;273}274.btn:active { transform: scale(0.95); opacity: 0.8; }275.btn svg { width: 14px; height: 14px; flex-shrink: 0; }276 277.btn-ghost {278 background: var(--bg);279 color: var(--text);280 border: 1px solid var(--separator);281}282.btn-danger {283 background: color-mix(in srgb, var(--destructive) 18%, transparent);284 color: var(--destructive);285 border: 1px solid color-mix(in srgb, var(--destructive) 30%, transparent);286}287.btn-success {288 background: color-mix(in srgb, var(--success) 18%, transparent);289 color: var(--success);290 border: 1px solid color-mix(in srgb, var(--success) 30%, transparent);291}292.btn-warning {293 background: color-mix(in srgb, var(--warning) 18%, transparent);294 color: var(--warning);295 border: 1px solid color-mix(in srgb, var(--warning) 30%, transparent);296}297 298/* ─── Inputs ─────────────────────────────────────────────── */299.field { padding-top: 12px; }300.field label {301 display: block;302 font-size: 11px;303 font-weight: 700;304 color: var(--section-head);305 text-transform: uppercase;306 letter-spacing: 0.5px;307 margin-bottom: 6px;308}309.field input,310.field select,311.field textarea {312 width: 100%;313 padding: 10px 12px;314 border: 1px solid var(--separator);315 border-radius: var(--radius-sm);316 background: var(--bg);317 color: var(--text);318 font-size: 14px;319 outline: none;320 font-family: inherit;321 transition: border-color 0.18s;322}323.field input:focus,324.field select:focus,325.field textarea:focus {326 border-color: var(--btn);327}328.field input[type="color"] {329 padding: 4px 6px;330 height: 40px;331 cursor: pointer;332}333.field textarea { resize: vertical; min-height: 72px; }334.field select option { background: var(--bg2); }335 336/* ─── Status Badges ─────────────────────────────────────── */337.badge {338 display: inline-flex;339 align-items: center;340 gap: 5px;341 padding: 4px 10px;342 border-radius: 20px;343 font-size: 12px;344 font-weight: 600;345 margin-top: 10px;346}347.badge-success { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); }348.badge-error { background: color-mix(in srgb, var(--destructive) 15%, transparent); color: var(--destructive); }349.badge-warn { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); }350.badge-info { background: color-mix(in srgb, var(--btn) 15%, transparent); color: var(--btn); }351 352.dot {353 width: 7px; height: 7px;354 border-radius: 50%;355 display: inline-block;356 flex-shrink: 0;357}358.dot-green { background: var(--success); }359.dot-red { background: var(--destructive); }360.dot-yellow { background: var(--warning); }361.dot-blue { background: var(--btn); }362 363/* ─── Section Group Label ──────────────────────────────── */364.group-label {365 font-size: 11px;366 font-weight: 700;367 text-transform: uppercase;368 letter-spacing: 0.7px;369 color: var(--section-head);370 padding: 16px 4px 6px;371}372 373/* ─── Sensors / Canvas ──────────────────────────────────── */374.sensor-wrap {375 background: var(--bg);376 border-radius: var(--radius-sm);377 padding: 12px;378 margin-top: 12px;379 border: 1px solid var(--separator);380}381.sensor-legend {382 display: flex;383 gap: 14px;384 margin-bottom: 8px;385 flex-wrap: wrap;386}387.legend-item {388 display: flex;389 align-items: center;390 gap: 5px;391 font-size: 11px;392 font-weight: 600;393 color: var(--hint);394}395.legend-dot {396 width: 10px; height: 3px;397 border-radius: 2px;398 display: inline-block;399}400 401canvas.accel-canvas {402 display: block;403 width: 100%;404 height: 100px;405 border-radius: 8px;406 background: var(--bg2);407}408 409/* ─── 3D Cube (orientation vis) ─────────────────────────── */410.cube-wrap {411 display: flex;412 justify-content: center;413 align-items: center;414 padding: 24px 0 8px;415 perspective: 400px;416}417.cube {418 width: 80px;419 height: 80px;420 position: relative;421 transform-style: preserve-3d;422 transition: transform 0.08s linear;423}424.cube-face {425 position: absolute;426 width: 80px; height: 80px;427 background: color-mix(in srgb, var(--btn) 30%, transparent);428 border: 1.5px solid var(--btn);429 border-radius: 6px;430 display: flex;431 align-items: center;432 justify-content: center;433 font-size: 10px;434 font-weight: 700;435 color: var(--btn);436}437.f-front { transform: translateZ(40px); }438.f-back { transform: rotateY(180deg) translateZ(40px); }439.f-right { transform: rotateY(90deg) translateZ(40px); }440.f-left { transform: rotateY(-90deg) translateZ(40px); }441.f-top { transform: rotateX(90deg) translateZ(40px); }442.f-bottom { transform: rotateX(-90deg) translateZ(40px); }443 444/* ─── Theme Color Swatch ────────────────────────────────── */445.color-swatch {446 padding: 8px 10px;447 border-radius: 8px;448 font-size: 13px;449 font-weight: 600;450 display: flex;451 align-items: center;452 gap: 8px;453 border: 1px solid var(--separator);454}455.swatch-box {456 width: 28px; height: 28px;457 border-radius: 6px;458 border: 1px solid var(--separator);459 flex-shrink: 0;460}461 462/* ─── Log ───────────────────────────────────────────────── */463.log-wrap {464 background: #0d0f12;465 border-radius: var(--radius-sm);466 max-height: 320px;467 overflow-y: auto;468 padding: 10px;469 margin-top: 12px;470 scrollbar-width: thin;471 scrollbar-color: var(--separator) transparent;472}473.log-wrap::-webkit-scrollbar { width: 4px; }474.log-wrap::-webkit-scrollbar-thumb { background: var(--separator); border-radius: 4px; }475 476.log-entry {477 display: flex;478 gap: 8px;479 padding: 6px 8px;480 border-radius: 6px;481 margin-bottom: 4px;482 border-left: 2px solid #444;483 font-family: 'Courier New', 'SF Mono', monospace;484 font-size: 11px;485 line-height: 1.4;486 background: rgba(255,255,255,0.03);487 word-break: break-word;488}489.log-entry.success { border-left-color: var(--success); background: color-mix(in srgb, var(--success) 6%, transparent); }490.log-entry.error { border-left-color: var(--destructive); background: color-mix(in srgb, var(--destructive) 6%, transparent); }491.log-entry.warning { border-left-color: var(--warning); background: color-mix(in srgb, var(--warning) 6%, transparent); }492 493.log-time { color: #555; flex-shrink: 0; }494.log-msg { color: #ccc; }495.log-data { color: #777; font-size: 10px; padding-top: 2px; display: block; }496 497.log-count {498 display: inline-flex;499 align-items: center;500 justify-content: center;501 background: var(--destructive);502 color: #fff;503 border-radius: 10px;504 font-size: 10px;505 font-weight: 700;506 padding: 1px 6px;507 min-width: 20px;508}509 510/* ─── Raw textarea ──────────────────────────────────────── */511.raw-textarea {512 width: 100%;513 height: 90px;514 background: var(--bg);515 border: 1px solid var(--separator);516 border-radius: var(--radius-sm);517 color: var(--hint);518 font-family: 'Courier New', monospace;519 font-size: 11px;520 padding: 10px;521 resize: none;522 outline: none;523 margin-top: 12px;524}525 526/* ─── Toast ─────────────────────────────────────────────── */527.toast {528 position: fixed;529 bottom: calc(env(safe-area-inset-bottom, 0px) + 24px);530 left: 50%;531 transform: translateX(-50%) translateY(80px);532 background: var(--text);533 color: var(--bg);534 padding: 10px 20px;535 border-radius: 24px;536 font-size: 13px;537 font-weight: 600;538 z-index: 9999;539 pointer-events: none;540 white-space: nowrap;541 max-width: 88vw;542 overflow: hidden;543 text-overflow: ellipsis;544 transition: transform 0.28s cubic-bezier(.34,1.56,.64,1), opacity 0.2s;545 opacity: 0;546 box-shadow: 0 4px 20px rgba(0,0,0,0.4);547}548.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }549 550/* ─── Divider ────────────────────────────────────────────── */551.divider { height: 1px; background: var(--separator); margin: 12px 0; }552 553/* ─── Responsive ─────────────────────────────────────────── */554@media (max-width: 400px) {555 .info-grid { grid-template-columns: 1fr 1fr; }556 .btn { padding: 8px 12px; font-size: 12px; }557}558</style>559</head>560<body>561<div id="app">562 563 <!-- ── Header ─────────────────────────────────────────────── -->564 <div class="header">565 <!-- Static Telegram SVG, no animation -->566 <svg class="header-logo" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">567 <defs>568 <linearGradient id="tg-g" x1="0%" y1="0%" x2="100%" y2="100%">569 <stop offset="0%" stop-color="#37AEE2"/>570 <stop offset="100%" stop-color="#1E96C8"/>571 </linearGradient>572 </defs>573 <circle cx="120" cy="120" r="120" fill="url(#tg-g)"/>574 <path fill="#C8DAEA" d="M98 168c-3.5 0-2.9-1.3-4.1-4.5L82.6 126.3 165 76.5"/>575 <path fill="#A9C9DD" d="M98 168c2.7 0 3.9-1.2 5.4-2.7l14.3-13.9-17.8-10.8"/>576 <path fill="#fff" d="M99.9 140.7l42.9 31.7c4.9 2.7 8.5 1.3 9.7-4.5l17.6-82.7c1.8-7.3-2.8-10.6-7.5-8.5L62.2 109c-7 2.8-6.9 6.6-1.3 8.3l31.6 9.9 73.2-46.1c3.5-2.1 6.7-1 4.1 1.3"/>577 </svg>578 <div class="header-info">579 <div class="header-title">Mini App Demo</div>580 <div class="header-sub">Telegram WebApp API Suite</div>581 </div>582 <div class="live-badge" :class="{inactive: !webAppInfo.isActive}">583 <span class="pulse"></span>584 {{ webAppInfo.isActive ? 'Active' : 'Inactive' }}585 </div>586 </div>587 588 <!-- ── Tab Bar ─────────────────────────────────────────────── -->589 <div class="tab-bar">590 <button591 v-for="tab in tabs"592 :key="tab.id"593 :class="['tab-btn', {active: activeTab === tab.id}]"594 @click="activeTab = tab.id"595 >596 <span v-html="tab.icon"></span>597 {{ tab.label }}598 </button>599 </div>600 601 <!-- ── Content ─────────────────────────────────────────────── -->602 <div class="container">603 604 <!-- ════════════════ OVERVIEW ════════════════ -->605 <template v-if="activeTab === 'overview'">606 607 <div class="group-label">WebApp Info</div>608 <div class="section">609 <div class="section-header" @click="toggle('waInfo')">610 <div class="section-label">611 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>612 WebApp Details613 </div>614 <svg class="chevron" :class="{open: open.waInfo}" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>615 </div>616 <div class="section-body" v-show="open.waInfo">617 <div class="info-grid">618 <div class="info-card"><div class="info-label">Version</div><div class="info-value">{{ webAppInfo.version || '—' }}</div></div>619 <div class="info-card"><div class="info-label">Platform</div><div class="info-value">{{ webAppInfo.platform || '—' }}</div></div>620 <div class="info-card"><div class="info-label">Color Scheme</div><div class="info-value">{{ webAppInfo.colorScheme || '—' }}</div></div>621 <div class="info-card"><div class="info-label">Viewport H</div><div class="info-value">{{ webAppInfo.viewportHeight }}px</div></div>622 <div class="info-card"><div class="info-label">Stable H</div><div class="info-value">{{ webAppInfo.viewportStableHeight }}px</div></div>623 <div class="info-card"><div class="info-label">Fullscreen</div><div class="info-value">{{ webAppInfo.isFullscreen ? 'Yes' : 'No' }}</div></div>624 <div class="info-card"><div class="info-label">Expanded</div><div class="info-value">{{ webAppInfo.isExpanded ? 'Yes' : 'No' }}</div></div>625 <div class="info-card"><div class="info-label">Swipes</div><div class="info-value">{{ webAppInfo.isVerticalSwipesEnabled ? 'On' : 'Off' }}</div></div>626 <div class="info-card"><div class="info-label">Close Confirm</div><div class="info-value">{{ webAppInfo.isClosingConfirmationEnabled ? 'On' : 'Off' }}</div></div>627 <div class="info-card"><div class="info-label">Orientation</div><div class="info-value">{{ webAppInfo.isOrientationLocked ? 'Locked' : 'Free' }}</div></div>628 </div>629 </div>630 </div>631 632 <div class="group-label">User Info</div>633 <div class="section">634 <div class="section-header" @click="toggle('userInfo')">635 <div class="section-label">636 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>637 User Details638 </div>639 <svg class="chevron" :class="{open: open.userInfo}" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>640 </div>641 <div class="section-body" v-show="open.userInfo">642 <div class="info-grid" v-if="Object.keys(userInfo).length">643 <div class="info-card" v-for="(v, k) in userInfo" :key="k">644 <div class="info-label">{{ fmtKey(k) }}</div>645 <div class="info-value">{{ v !== null && v !== undefined ? String(v) : 'N/A' }}</div>646 </div>647 </div>648 <div class="badge badge-warn" v-else>No user data (not opened from bot)</div>649 </div>650 </div>651 652 <div class="group-label">Raw Init Data</div>653 <div class="section" style="padding: 0 16px 16px;">654 <textarea class="raw-textarea" readonly :value="initData || '(empty — not opened from bot)'"></textarea>655 </div>656 657 </template>658 659 <!-- ════════════════ BASIC ════════════════ -->660 <template v-if="activeTab === 'basic'">661 662 <div class="group-label">Window</div>663 <div class="section" style="padding: 14px 16px;">664 <div class="btn-row" style="padding-top:0">665 <button class="btn" @click="expand">666 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>667 Expand668 </button>669 <button class="btn" @click="requestFullscreen">670 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>671 Fullscreen672 </button>673 <button class="btn-ghost btn" @click="exitFullscreen">Exit Fullscreen</button>674 <button class="btn btn-danger" @click="closeApp">675 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>676 Close App677 </button>678 </div>679 </div>680 681 <div class="group-label">Orientation</div>682 <div class="section" style="padding: 14px 16px;">683 <div class="btn-row" style="padding-top:0">684 <button class="btn" @click="lockOrientation">Lock</button>685 <button class="btn-ghost btn" @click="unlockOrientation">Unlock</button>686 </div>687 <div class="badge" :class="webAppInfo.isOrientationLocked ? 'badge-success' : 'badge-warn'">688 <span class="dot" :class="webAppInfo.isOrientationLocked ? 'dot-green' : 'dot-yellow'"></span>689 {{ webAppInfo.isOrientationLocked ? 'Orientation Locked' : 'Orientation Unlocked' }}690 </div>691 </div>692 693 <div class="group-label">Swipe & Close Confirmation</div>694 <div class="section" style="padding: 14px 16px;">695 <div class="btn-row" style="padding-top:0">696 <button class="btn btn-success" @click="enableVerticalSwipes">Enable Swipes</button>697 <button class="btn btn-danger" @click="disableVerticalSwipes">Disable Swipes</button>698 <button class="btn btn-success" @click="enableClosingConfirmation">Enable Close Confirm</button>699 <button class="btn btn-danger" @click="disableClosingConfirmation">Disable Close Confirm</button>700 </div>701 </div>702 703 <div class="group-label">Colors</div>704 <div class="section" style="padding: 14px 16px;">705 <div class="field"><label>Header Color</label><input type="color" v-model="headerColor" @change="setHeaderColor"></div>706 <div class="field"><label>Background Color</label><input type="color" v-model="backgroundColor" @change="setBackgroundColor"></div>707 <div class="field"><label>Bottom Bar Color</label><input type="color" v-model="bottomBarColor" @change="setBottomBarColor"></div>708 </div>709 710 <div class="group-label">Keyboard</div>711 <div class="section" style="padding: 14px 16px;">712 <div class="field"><label>Test Focus Input</label><input type="text" placeholder="Tap to show keyboard..."></div>713 <div class="btn-row" style="padding-top:8px">714 <button class="btn btn-ghost" @click="hideKeyboard">Hide Keyboard</button>715 </div>716 </div>717 718 </template>719 720 <!-- ════════════════ BUTTONS ════════════════ -->721 <template v-if="activeTab === 'buttons'">722 723 <div class="group-label">Main Button</div>724 <div class="section" style="padding:14px 16px;">725 <div class="field"><label>Text</label><input v-model="mainButtonText" placeholder="Button text"></div>726 <div class="field"><label>Color</label><input type="color" v-model="mainButtonColor"></div>727 <div class="field"><label>Icon Custom Emoji ID</label><input v-model="mainButtonIconId" placeholder="Custom emoji ID (Bot API 9.5+)"></div>728 <div class="btn-row">729 <button class="btn" @click="updateMainButton">Update</button>730 <button class="btn btn-success" @click="showMainButton">Show</button>731 <button class="btn btn-danger" @click="hideMainButton">Hide</button>732 <button class="btn btn-ghost" @click="enableMainButton">Enable</button>733 <button class="btn btn-ghost" @click="disableMainButton">Disable</button>734 <button class="btn btn-ghost" @click="toggleMainButtonShine">Toggle Shine</button>735 <button class="btn btn-ghost" @click="showMainProgress">Show Progress</button>736 <button class="btn btn-ghost" @click="hideMainProgress">Hide Progress</button>737 </div>738 </div>739 740 <div class="group-label">Secondary Button</div>741 <div class="section" style="padding:14px 16px;">742 <div class="field"><label>Text</label><input v-model="secondaryButtonText" placeholder="Button text"></div>743 <div class="field">744 <label>Position</label>745 <select v-model="secondaryButtonPosition">746 <option value="left">Left</option>747 <option value="right">Right</option>748 <option value="top">Top</option>749 <option value="bottom">Bottom</option>750 </select>751 </div>752 <div class="btn-row">753 <button class="btn" @click="updateSecondaryButton">Update</button>754 <button class="btn btn-success" @click="showSecondaryButton">Show</button>755 <button class="btn btn-danger" @click="hideSecondaryButton">Hide</button>756 </div>757 </div>758 759 <div class="group-label">Back Button</div>760 <div class="section" style="padding:14px 16px;">761 <div class="btn-row" style="padding-top:0">762 <button class="btn btn-success" @click="showBackButton">Show</button>763 <button class="btn btn-danger" @click="hideBackButton">Hide</button>764 </div>765 </div>766 767 <div class="group-label">Settings Button</div>768 <div class="section" style="padding:14px 16px;">769 <div class="btn-row" style="padding-top:0">770 <button class="btn btn-success" @click="showSettingsButton">Show</button>771 <button class="btn btn-danger" @click="hideSettingsButton">Hide</button>772 </div>773 </div>774 775 </template>776 777 <!-- ════════════════ POPUPS ════════════════ -->778 <template v-if="activeTab === 'popups'">779 780 <div class="group-label">Alert</div>781 <div class="section" style="padding:14px 16px;">782 <div class="field"><label>Message</label><input v-model="alertMessage"></div>783 <div class="btn-row"><button class="btn" @click="showAlert">Show Alert</button></div>784 </div>785 786 <div class="group-label">Confirm</div>787 <div class="section" style="padding:14px 16px;">788 <div class="field"><label>Message</label><input v-model="confirmMessage"></div>789 <div class="btn-row"><button class="btn" @click="showConfirm">Show Confirm</button></div>790 </div>791 792 <div class="group-label">Custom Popup</div>793 <div class="section" style="padding:14px 16px;">794 <div class="field"><label>Title</label><input v-model="popupTitle"></div>795 <div class="field"><label>Message</label><textarea v-model="popupMessage"></textarea></div>796 <div class="btn-row"><button class="btn" @click="showPopup">Show Popup</button></div>797 </div>798 799 <div class="group-label">QR Scanner</div>800 <div class="section" style="padding:14px 16px;">801 <div class="field"><label>Scanner Hint Text</label><input v-model="qrText"></div>802 <div class="btn-row">803 <button class="btn" @click="showScanQrPopup">Open Scanner</button>804 <button class="btn btn-danger" @click="closeScanQrPopup">Close Scanner</button>805 </div>806 </div>807 808 </template>809 810 <!-- ════════════════ LINKS ════════════════ -->811 <template v-if="activeTab === 'links'">812 813 <div class="group-label">Open External Link</div>814 <div class="section" style="padding:14px 16px;">815 <div class="field"><label>URL</label><input v-model="externalUrl" type="url"></div>816 <div class="btn-row">817 <button class="btn" @click="openLink(false)">Open Link</button>818 <button class="btn btn-ghost" @click="openLink(true)">Instant View</button>819 </div>820 </div>821 822 <div class="group-label">Telegram Link</div>823 <div class="section" style="padding:14px 16px;">824 <div class="field"><label>Telegram URL</label><input v-model="telegramUrl" type="url"></div>825 <div class="btn-row"><button class="btn" @click="openTelegramLink">Open Telegram Link</button></div>826 </div>827 828 <div class="group-label">Switch Inline Query</div>829 <div class="section" style="padding:14px 16px;">830 <div class="field"><label>Query</label><input v-model="inlineQuery"></div>831 <div class="field">832 <label>Chat Types</label>833 <select v-model="chatTypes" multiple>834 <option value="users">Users</option>835 <option value="bots">Bots</option>836 <option value="groups">Groups</option>837 <option value="channels">Channels</option>838 </select>839 </div>840 <div class="btn-row"><button class="btn" @click="switchInlineQuery">Switch Inline Query</button></div>841 </div>842 843 </template>844 845 <!-- ════════════════ SHARE ════════════════ -->846 <template v-if="activeTab === 'share'">847 848 <div class="group-label">Share to Story</div>849 <div class="section" style="padding:14px 16px;">850 <div class="field"><label>Media URL</label><input v-model="storyMediaUrl"></div>851 <div class="field"><label>Caption (max 200)</label><textarea v-model="storyCaption" maxlength="200"></textarea></div>852 <div class="field"><label>Widget URL (Premium)</label><input v-model="storyWidgetUrl"></div>853 <div class="field"><label>Widget Name</label><input v-model="storyWidgetName" maxlength="48"></div>854 <div class="btn-row"><button class="btn" @click="shareToStory">Share to Story</button></div>855 </div>856 857 <div class="group-label">Share Message</div>858 <div class="section" style="padding:14px 16px;">859 <div class="field"><label>Prepared Message ID</label><input v-model="shareMessageId" placeholder="from savePreparedInlineMessage"></div>860 <div class="btn-row"><button class="btn" @click="shareMessage">Share Message</button></div>861 </div>862 863 <div class="group-label">Send Data <span style="font-size:10px;font-weight:400;color:var(--hint)">(Keyboard Button only)</span></div>864 <div class="section" style="padding:14px 16px;">865 <div class="badge badge-warn" style="margin-top:0;margin-bottom:10px">⚠ Only works when launched via web_app keyboard button</div>866 <div class="field"><label>Data (max 4096 bytes)</label><textarea v-model="sendDataContent"></textarea></div>867 <div class="btn-row"><button class="btn btn-success" @click="sendData">Send to Bot</button></div>868 </div>869 870 <div class="group-label">Request Chat <span style="font-size:10px;font-weight:400;color:var(--hint)">(Bot API 9.6+)</span></div>871 <div class="section" style="padding:14px 16px;">872 <div class="field"><label>Prepared Keyboard Button ID</label><input v-model="preparedChatId" placeholder="from savePreparedKeyboardButton"></div>873 <div class="btn-row"><button class="btn" @click="requestChat">Select Chat</button></div>874 </div>875 876 </template>877 878 <!-- ════════════════ HAPTICS ════════════════ -->879 <template v-if="activeTab === 'haptics'">880 881 <div class="group-label">Impact</div>882 <div class="section" style="padding:14px 16px;">883 <div class="btn-row" style="padding-top:0">884 <button class="btn" @click="hapticImpact('light')">Light</button>885 <button class="btn" @click="hapticImpact('medium')">Medium</button>886 <button class="btn" @click="hapticImpact('heavy')">Heavy</button>887 <button class="btn" @click="hapticImpact('rigid')">Rigid</button>888 <button class="btn" @click="hapticImpact('soft')">Soft</button>889 </div>890 </div>891 892 <div class="group-label">Notification</div>893 <div class="section" style="padding:14px 16px;">894 <div class="btn-row" style="padding-top:0">895 <button class="btn btn-danger" @click="hapticNotification('error')">Error</button>896 <button class="btn btn-success" @click="hapticNotification('success')">Success</button>897 <button class="btn btn-warning" @click="hapticNotification('warning')">Warning</button>898 </div>899 </div>900 901 <div class="group-label">Selection</div>902 <div class="section" style="padding:14px 16px;">903 <div class="btn-row" style="padding-top:0">904 <button class="btn" @click="hapticSelection">Selection Changed</button>905 </div>906 </div>907 908 </template>909 910 <!-- ════════════════ STORAGE ════════════════ -->911 <template v-if="activeTab === 'storage'">912 913 <div class="group-label">Cloud Storage</div>914 <div class="section" style="padding:14px 16px;">915 <div class="field"><label>Key</label><input v-model="cloudKey"></div>916 <div class="field"><label>Value</label><input v-model="cloudValue"></div>917 <div class="btn-row">918 <button class="btn" @click="cloudSetItem">Set</button>919 <button class="btn btn-ghost" @click="cloudGetItem">Get</button>920 <button class="btn btn-danger" @click="cloudRemoveItem">Remove</button>921 <button class="btn btn-ghost" @click="cloudGetKeys">Get Keys</button>922 </div>923 </div>924 925 <div class="group-label">Device Storage <span style="font-size:10px;font-weight:400;color:var(--hint)">(Bot API 9.0+)</span></div>926 <div class="section" style="padding:14px 16px;">927 <div class="field"><label>Key</label><input v-model="deviceKey"></div>928 <div class="field"><label>Value</label><input v-model="deviceValue"></div>929 <div class="btn-row">930 <button class="btn" @click="deviceSetItem">Set</button>931 <button class="btn btn-ghost" @click="deviceGetItem">Get</button>932 <button class="btn btn-danger" @click="deviceRemoveItem">Remove</button>933 <button class="btn btn-danger" @click="deviceClear">Clear All</button>934 </div>935 </div>936 937 <div class="group-label">Secure Storage <span style="font-size:10px;font-weight:400;color:var(--hint)">(Bot API 9.0+)</span></div>938 <div class="section" style="padding:14px 16px;">939 <div class="field"><label>Key</label><input v-model="secureKey"></div>940 <div class="field"><label>Value</label><input v-model="secureValue"></div>941 <div class="btn-row">942 <button class="btn" @click="secureSetItem">Set</button>943 <button class="btn btn-ghost" @click="secureGetItem">Get</button>944 <button class="btn btn-danger" @click="secureRemoveItem">Remove</button>945 </div>946 </div>947 948 </template>949 950 <!-- ════════════════ SENSORS ════════════════ -->951 <template v-if="activeTab === 'sensors'">952 953 <!-- Accelerometer -->954 <div class="group-label">Accelerometer</div>955 <div class="section">956 <div class="section-header" @click="toggle('accel')">957 <div class="section-label">958 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>959 Accelerometer960 </div>961 <svg class="chevron" :class="{open: open.accel}" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>962 </div>963 <div class="section-body" v-show="open.accel">964 <div class="info-grid">965 <div class="info-card">966 <div class="info-label">Status</div>967 <div class="info-value" style="display:flex;align-items:center;gap:6px">968 <span class="dot" :class="accelData.isStarted ? 'dot-green' : 'dot-red'"></span>969 {{ accelData.isStarted ? 'Active' : 'Inactive' }}970 </div>971 </div>972 <div class="info-card"><div class="info-label">X (m/s²)</div><div class="info-value">{{ accelData.x }}</div></div>973 <div class="info-card"><div class="info-label">Y (m/s²)</div><div class="info-value">{{ accelData.y }}</div></div>974 <div class="info-card"><div class="info-label">Z (m/s²)</div><div class="info-value">{{ accelData.z }}</div></div>975 </div>976 <div class="sensor-wrap">977 <div class="sensor-legend">978 <div class="legend-item"><span class="legend-dot" style="background:#ff453a"></span> X-axis</div>979 <div class="legend-item"><span class="legend-dot" style="background:#32d74b"></span> Y-axis</div>980 <div class="legend-item"><span class="legend-dot" style="background:#0a84ff"></span> Z-axis</div>981 </div>982 <canvas class="accel-canvas" ref="accelCanvas"></canvas>983 </div>984 <div class="btn-row">985 <button class="btn btn-success" @click="startAccelerometer">Start</button>986 <button class="btn btn-danger" @click="stopAccelerometer">Stop</button>987 </div>988 </div>989 </div>990 991 <!-- Gyroscope -->992 <div class="group-label">Gyroscope</div>993 <div class="section">994 <div class="section-header" @click="toggle('gyro')">995 <div class="section-label">996 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></svg>997 Gyroscope998 </div>999 <svg class="chevron" :class="{open: open.gyro}" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>1000 </div>1001 <div class="section-body" v-show="open.gyro">1002 <div class="info-grid">1003 <div class="info-card">1004 <div class="info-label">Status</div>1005 <div class="info-value" style="display:flex;align-items:center;gap:6px">1006 <span class="dot" :class="gyroData.isStarted ? 'dot-green' : 'dot-red'"></span>1007 {{ gyroData.isStarted ? 'Active' : 'Inactive' }}1008 </div>1009 </div>1010 <div class="info-card"><div class="info-label">X (rad/s)</div><div class="info-value">{{ gyroData.x }}</div></div>1011 <div class="info-card"><div class="info-label">Y (rad/s)</div><div class="info-value">{{ gyroData.y }}</div></div>1012 <div class="info-card"><div class="info-label">Z (rad/s)</div><div class="info-value">{{ gyroData.z }}</div></div>1013 </div>1014 <div class="btn-row">1015 <button class="btn btn-success" @click="startGyroscope">Start</button>1016 <button class="btn btn-danger" @click="stopGyroscope">Stop</button>1017 </div>1018 </div>1019 </div>1020 1021 <!-- Device Orientation -->1022 <div class="group-label">Device Orientation</div>1023 <div class="section">1024 <div class="section-header" @click="toggle('orient')">1025 <div class="section-label">1026 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.48 2.52c3.27 1.55 5.61 4.72 5.97 8.48h1.5C23.44 4.84 18.29 0 12 0l-.66.03 3.81 3.81 1.33-1.32zm-6.25-.77c-.59-.59-1.54-.59-2.12 0L1.75 8.11c-.59.59-.59 1.54 0 2.12l12.02 12.02c.59.59 1.54.59 2.12 0l6.36-6.36c.59-.59.59-1.54 0-2.12L10.23 1.75zm4.6 19.44L2.81 9.17l6.36-6.36 12.02 12.02-6.36 6.36zm-7.29.37C4.27 19.99 1.93 16.82 1.57 13.06H.07C.56 19.16 5.71 24 12 24l.66-.03-3.81-3.81-1.31 1.32z"/></svg>1027 Device Orientation1028 </div>1029 <svg class="chevron" :class="{open: open.orient}" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>1030 </div>1031 <div class="section-body" v-show="open.orient">1032 <!-- 3D Cube visualization -->1033 <div class="cube-wrap">1034 <div class="cube" :style="cubeStyle">1035 <div class="cube-face f-front">F</div>1036 <div class="cube-face f-back">B</div>1037 <div class="cube-face f-right">R</div>1038 <div class="cube-face f-left">L</div>1039 <div class="cube-face f-top">T</div>1040 <div class="cube-face f-bottom">Bo</div>1041 </div>1042 </div>1043 <div class="info-grid">1044 <div class="info-card">1045 <div class="info-label">Status</div>1046 <div class="info-value" style="display:flex;align-items:center;gap:6px">1047 <span class="dot" :class="orientData.isStarted ? 'dot-green' : 'dot-red'"></span>1048 {{ orientData.isStarted ? 'Active' : 'Inactive' }}1049 </div>1050 </div>1051 <div class="info-card"><div class="info-label">Absolute</div><div class="info-value">{{ orientData.absolute ? 'Yes' : 'No' }}</div></div>1052 <div class="info-card"><div class="info-label">Alpha (rad)</div><div class="info-value">{{ orientData.alpha }}</div></div>1053 <div class="info-card"><div class="info-label">Beta (rad)</div><div class="info-value">{{ orientData.beta }}</div></div>1054 <div class="info-card"><div class="info-label">Gamma (rad)</div><div class="info-value">{{ orientData.gamma }}</div></div>1055 </div>1056 <div class="btn-row">1057 <button class="btn btn-success" @click="startOrientation(false)">Start (Relative)</button>1058 <button class="btn btn-success" @click="startOrientation(true)">Start (Absolute)</button>1059 <button class="btn btn-danger" @click="stopOrientation">Stop</button>1060 </div>1061 </div>1062 </div>1063 1064 </template>1065 1066 <!-- ════════════════ ADVANCED ════════════════ -->1067 <template v-if="activeTab === 'advanced'">1068 1069 <div class="group-label">Biometrics</div>1070 <div class="section" style="padding:14px 16px;">1071 <div class="info-grid">1072 <div class="info-card">1073 <div class="info-label">Available</div>1074 <div class="info-value" style="display:flex;align-items:center;gap:6px">1075 <span class="dot" :class="biometricInfo.isAvailable ? 'dot-green':'dot-red'"></span>1076 {{ biometricInfo.isAvailable ? 'Yes' : 'No' }}1077 </div>1078 </div>1079 <div class="info-card"><div class="info-label">Type</div><div class="info-value">{{ biometricInfo.type || 'N/A' }}</div></div>1080 <div class="info-card">1081 <div class="info-label">Access</div>1082 <div class="info-value" style="display:flex;align-items:center;gap:6px">1083 <span class="dot" :class="biometricInfo.isAccessGranted ? 'dot-green':'dot-red'"></span>1084 {{ biometricInfo.isAccessGranted ? 'Granted' : 'Denied' }}1085 </div>1086 </div>1087 </div>1088 <div class="btn-row">1089 <button class="btn" @click="biometricInit">Init</button>1090 <button class="btn btn-ghost" @click="biometricRequestAccess">Request Access</button>1091 <button class="btn btn-success" @click="biometricAuthenticate">Authenticate</button>1092 </div>1093 </div>1094 1095 <div class="group-label">Location</div>1096 <div class="section" style="padding:14px 16px;">1097 <div class="info-grid">1098 <div class="info-card">1099 <div class="info-label">Available</div>1100 <div class="info-value" style="display:flex;align-items:center;gap:6px">1101 <span class="dot" :class="locationInfo.isAvailable ? 'dot-green':'dot-red'"></span>1102 {{ locationInfo.isAvailable ? 'Yes' : 'No' }}1103 </div>1104 </div>1105 <div class="info-card">1106 <div class="info-label">Access</div>1107 <div class="info-value" style="display:flex;align-items:center;gap:6px">1108 <span class="dot" :class="locationInfo.isAccessGranted ? 'dot-green':'dot-red'"></span>1109 {{ locationInfo.isAccessGranted ? 'Granted' : 'Denied' }}1110 </div>1111 </div>1112 </div>1113 <div class="btn-row">1114 <button class="btn" @click="locationInit">Init</button>1115 <button class="btn btn-success" @click="getLocation">Get Location</button>1116 <button class="btn btn-ghost" @click="locationOpenSettings">Settings</button>1117 </div>1118 </div>1119 1120 <div class="group-label">Permissions</div>1121 <div class="section" style="padding:14px 16px;">1122 <div class="btn-row" style="padding-top:0">1123 <button class="btn" @click="requestWriteAccess">Write Access</button>1124 <button class="btn" @click="requestContact">Request Contact</button>1125 <button class="btn btn-ghost" @click="readClipboard">Read Clipboard</button>1126 </div>1127 </div>1128 1129 <div class="group-label">Home Screen</div>1130 <div class="section" style="padding:14px 16px;">1131 <div class="btn-row" style="padding-top:0">1132 <button class="btn" @click="addToHomeScreen">Add to Home Screen</button>1133 <button class="btn btn-ghost" @click="checkHomeScreenStatus">Check Status</button>1134 </div>1135 <div v-if="homeScreenStatus" class="badge" :class="getHomeScreenStatusClass()" style="margin-top:10px">1136 Status: {{ homeScreenStatus }}1137 </div>1138 </div>1139 1140 <div class="group-label">Download File</div>1141 <div class="section" style="padding:14px 16px;">1142 <div class="field"><label>URL</label><input v-model="downloadUrl"></div>1143 <div class="field"><label>File Name</label><input v-model="downloadFileName"></div>1144 <div class="btn-row"><button class="btn" @click="downloadFile">Download</button></div>1145 </div>1146 1147 <div class="group-label">Emoji Status</div>1148 <div class="section" style="padding:14px 16px;">1149 <div class="field"><label>Custom Emoji ID</label><input v-model="emojiId"></div>1150 <div class="field"><label>Duration (seconds)</label><input v-model="emojiDuration" type="number"></div>1151 <div class="btn-row">1152 <button class="btn btn-success" @click="setEmojiStatus">Set Emoji Status</button>1153 <button class="btn btn-ghost" @click="requestEmojiStatusAccess">Request Access</button>1154 </div>1155 </div>1156 1157 </template>1158 1159 <!-- ════════════════ THEME ════════════════ -->1160 <template v-if="activeTab === 'theme'">1161 1162 <div class="group-label">Theme Colors</div>1163 <div class="section" style="padding:14px 16px;">1164 <div v-for="(val, key) in themeParams" :key="key" class="color-swatch" style="margin-bottom:6px">1165 <div class="swatch-box" :style="{background: val}"></div>1166 <div style="flex:1">1167 <div style="font-size:11px;color:var(--hint);margin-bottom:2px">{{ fmtKey(key) }}</div>1168 <div style="font-size:13px;font-weight:600;color:var(--text)">{{ val }}</div>1169 </div>1170 </div>1171 <div v-if="!Object.keys(themeParams).length" class="badge badge-warn">Theme params not available</div>1172 </div>1173 1174 <div class="group-label">Safe Area Insets</div>1175 <div class="section" style="padding:14px 16px;">1176 <div class="info-grid">1177 <div class="info-card"><div class="info-label">Top</div><div class="info-value">{{ safeAreaInset.top || 0 }}px</div></div>1178 <div class="info-card"><div class="info-label">Bottom</div><div class="info-value">{{ safeAreaInset.bottom || 0 }}px</div></div>1179 <div class="info-card"><div class="info-label">Left</div><div class="info-value">{{ safeAreaInset.left || 0 }}px</div></div>1180 <div class="info-card"><div class="info-label">Right</div><div class="info-value">{{ safeAreaInset.right || 0 }}px</div></div>1181 </div>1182 </div>1183 1184 <div class="group-label">Content Safe Area Insets</div>1185 <div class="section" style="padding:14px 16px;">1186 <div class="info-grid">1187 <div class="info-card"><div class="info-label">Top</div><div class="info-value">{{ contentSafeAreaInset.top || 0 }}px</div></div>1188 <div class="info-card"><div class="info-label">Bottom</div><div class="info-value">{{ contentSafeAreaInset.bottom || 0 }}px</div></div>1189 <div class="info-card"><div class="info-label">Left</div><div class="info-value">{{ contentSafeAreaInset.left || 0 }}px</div></div>1190 <div class="info-card"><div class="info-label">Right</div><div class="info-value">{{ contentSafeAreaInset.right || 0 }}px</div></div>1191 </div>1192 </div>1193 1194 </template>1195 1196 <!-- ════════════════ DEBUG LOG (always shown) ════════════════ -->1197 <div class="group-label" style="margin-top:8px">1198 Debug Log1199 <span class="log-count" v-if="logs.length" style="margin-left:8px">{{ logs.length }}</span>1200 </div>1201 <div class="section">1202 <div class="section-header" @click="toggle('log')">1203 <div class="section-label">1204 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09C6.04 10.33 6 10.66 6 11v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>1205 Event Log1206 </div>1207 <svg class="chevron" :class="{open: open.log}" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>1208 </div>1209 <div class="section-body" v-show="open.log">1210 <button class="btn btn-ghost" style="margin-bottom:0" @click="clearLog">Clear</button>1211 <div class="log-wrap" ref="logContainer">1212 <div v-if="!logs.length" style="color:var(--hint);font-size:12px;text-align:center;padding:20px 0">No events yet</div>1213 <div v-for="(l, i) in logs" :key="i" :class="['log-entry', l.type]">1214 <span class="log-time">{{ l.time }}</span>1215 <span>1216 <span class="log-msg">{{ l.message }}</span>1217 <span v-if="l.data" class="log-data">{{ JSON.stringify(l.data) }}</span>1218 </span>1219 </div>1220 </div>1221 </div>1222 </div>1223 1224 </div><!-- /container -->1225 1226 <!-- Toast -->1227 <div class="toast" :class="{show: toastMsg}">{{ toastMsg }}</div>1228 1229</div><!-- /app -->1230 1231<script>1232const { createApp } = Vue;1233 1234createApp({1235 data() {1236 return {1237 activeTab: 'overview',1238 toastMsg: '',1239 toastTimer: null,1240 open: {1241 waInfo: true,1242 userInfo: true,1243 accel: true,1244 gyro: false,1245 orient: false,1246 log: true1247 },1248 tabs: [1249 { id:'overview', label:'Overview', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>' },1250 { id:'basic', label:'Basic', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>' },1251 { id:'buttons', label:'Buttons', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/></svg>' },1252 { id:'popups', label:'Popups', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>' },1253 { id:'links', label:'Links', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>' },1254 { id:'share', label:'Share', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>' },1255 { id:'haptics', label:'Haptics', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11 2v20c-5.07-.5-9-4.79-9-10s3.93-9.5 9-10zm2.03 0v8.99H22c-.47-4.74-4.24-8.52-8.97-8.99zm0 11.01V22c4.74-.47 8.5-4.25 8.97-8.99h-8.97z"/></svg>' },1256 { id:'storage', label:'Storage', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/></svg>' },1257 { id:'sensors', label:'Sensors', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>' },1258 { id:'advanced', label:'Advanced', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/></svg>' },1259 { id:'theme', label:'Theme', icon:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8z"/></svg>' },1260 ],1261 logs: [],1262 webAppInfo: { version:'', platform:'', colorScheme:'', isActive:true, isExpanded:false, isFullscreen:false, viewportHeight:0, viewportStableHeight:0, isOrientationLocked:false, isClosingConfirmationEnabled:false, isVerticalSwipesEnabled:true },1263 userInfo: {},1264 initData: '',1265 themeParams: {},1266 safeAreaInset: { top:0, bottom:0, left:0, right:0 },1267 contentSafeAreaInset: { top:0, bottom:0, left:0, right:0 },1268 1269 // Basic1270 headerColor: '#3390ec',1271 backgroundColor: '#18191c',1272 bottomBarColor: '#18191c',1273 1274 // Buttons1275 mainButtonText: 'Continue',1276 mainButtonColor: '#3390ec',1277 mainButtonIconId: '',1278 mainButtonShineEffect: false,1279 secondaryButtonText: 'Cancel',1280 secondaryButtonPosition: 'left',1281 1282 // Popups1283 alertMessage: 'Hello from Mini App!',1284 confirmMessage: 'Are you sure?',1285 popupTitle: 'Custom Popup',1286 popupMessage: 'This is a test popup message.',1287 qrText: 'Scan a QR code',1288 1289 // Links1290 externalUrl: 'https://telegram.org',1291 telegramUrl: 'https://t.me/telegram',1292 inlineQuery: 'test',1293 chatTypes: [],1294 1295 // Share1296 storyMediaUrl: 'https://telegram.org/img/t_logo.png',1297 storyCaption: '',1298 storyWidgetUrl: '',1299 storyWidgetName: '',1300 shareMessageId: '',1301 sendDataContent: 'Hello from Mini App!',1302 preparedChatId: '',1303 1304 // Storage1305 cloudKey: 'my_key', cloudValue: 'my_value',1306 deviceKey: 'dev_key', deviceValue: 'dev_value',1307 secureKey: 'sec_key', secureValue: 'sec_value',1308 1309 // Sensors1310 accelData: { isStarted: false, x: '0.00', y: '0.00', z: '0.00' },1311 gyroData: { isStarted: false, x: '0.00', y: '0.00', z: '0.00' },1312 orientData:{ isStarted: false, absolute: false, alpha: '0.00', beta: '0.00', gamma: '0.00' },1313 1314 // Accel graph history — only push when started1315 accelHistory: [],1316 graphRunning: false,1317 graphRafId: null,1318 1319 // Advanced1320 biometricInfo: { isAvailable: false, type: 'unknown', isAccessGranted: false },1321 locationInfo: { isAvailable: false, isAccessGranted: false },1322 homeScreenStatus: '',1323 downloadUrl: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',1324 downloadFileName: 'sample.pdf',1325 emojiId: '5368324170671202286',1326 emojiDuration: 3600,1327 };1328 },1329 1330 computed: {1331 cubeStyle() {1332 const a = parseFloat(this.orientData.alpha) || 0;1333 const b = parseFloat(this.orientData.beta) || 0;1334 const g = parseFloat(this.orientData.gamma) || 0;1335 const toDeg = r => (r * 180 / Math.PI).toFixed(1);1336 return { transform: `rotateX(${toDeg(b)}deg) rotateY(${toDeg(g)}deg) rotateZ(${toDeg(a)}deg)` };1337 }1338 },1339 1340 mounted() {1341 this.initTG();1342 this.setupEvents();1343 this.updateInfo();1344 1345 // Poll sensor data at 100ms1346 setInterval(() => {1347 this.updateSensors();1348 }, 100);1349 1350 // Update webAppInfo every second1351 setInterval(() => { this.updateInfo(); }, 1000);1352 1353 // Start graph loop1354 this.startGraphLoop();1355 },1356 1357 beforeUnmount() {1358 if (this.graphRafId) cancelAnimationFrame(this.graphRafId);1359 },1360 1361 methods: {1362 // ── Utils ──────────────────────────────────────────────────1363 tg() { return window.Telegram?.WebApp; },1364 1365 toast(msg) {1366 this.toastMsg = msg;1367 if (this.toastTimer) clearTimeout(this.toastTimer);1368 this.toastTimer = setTimeout(() => { this.toastMsg = ''; }, 2200);1369 },1370 1371 log(msg, type = '', data = null) {1372 const t = new Date().toLocaleTimeString('en', { hour12: false, hour:'2-digit', minute:'2-digit', second:'2-digit' });1373 this.logs.unshift({ time: t, message: msg, type, data });1374 if (this.logs.length > 300) this.logs.pop();1375 this.$nextTick(() => { if (this.$refs.logContainer) this.$refs.logContainer.scrollTop = 0; });1376 },1377 1378 clearLog() { this.logs = []; this.log('Log cleared', 'success'); },1379 1380 toggle(key) { this.open[key] = !this.open[key]; },1381 1382 fmtKey(k) { return k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); },1383 1384 getHomeScreenStatusClass() {1385 return { added: 'badge-success', missed: 'badge-warn' }[this.homeScreenStatus] || 'badge-info';1386 },1387 1388 // ── Init ───────────────────────────────────────────────────1389 initTG() {1390 const tg = this.tg();1391 if (!tg) { this.log('Telegram WebApp not available', 'error'); return; }1392 tg.ready();1393 this.initData = tg.initData || '';1394 this.log('WebApp initialized', 'success', { version: tg.version, platform: tg.platform });1395 },1396 1397 setupEvents() {1398 const tg = this.tg();1399 if (!tg) return;1400 const events = [1401 'themeChanged','viewportChanged','mainButtonClicked','secondaryButtonClicked',1402 'backButtonClicked','settingsButtonClicked','invoiceClosed','popupClosed',1403 'qrTextReceived','scanQrPopupClosed','clipboardTextReceived','writeAccessRequested',1404 'contactRequested','biometricManagerUpdated','biometricAuthRequested','biometricTokenUpdated',1405 'activated','deactivated','safeAreaChanged','contentSafeAreaChanged',1406 'fullscreenChanged','fullscreenFailed','homeScreenAdded','homeScreenChecked',1407 'accelerometerStarted','accelerometerStopped','accelerometerChanged','accelerometerFailed',1408 'deviceOrientationStarted','deviceOrientationStopped','deviceOrientationChanged','deviceOrientationFailed',1409 'gyroscopeStarted','gyroscopeStopped','gyroscopeChanged','gyroscopeFailed',1410 'locationManagerUpdated','locationRequested','shareMessageSent','shareMessageFailed',1411 'emojiStatusSet','emojiStatusFailed','emojiStatusAccessRequested','fileDownloadRequested'1412 ];1413 events.forEach(ev => {1414 tg.onEvent(ev, data => {1415 this.log(`event: ${ev}`, 'success', data || null);1416 this.updateInfo();1417 });1418 });1419 },1420 1421 updateInfo() {1422 const tg = this.tg();1423 if (!tg) return;1424 this.webAppInfo = {1425 version: tg.version,1426 platform: tg.platform,1427 colorScheme: tg.colorScheme,1428 isActive: tg.isActive !== false,1429 isExpanded: tg.isExpanded,1430 isFullscreen: tg.isFullscreen,1431 viewportHeight: Math.round(tg.viewportHeight || 0),1432 viewportStableHeight: Math.round(tg.viewportStableHeight || 0),1433 isOrientationLocked: tg.isOrientationLocked,1434 isClosingConfirmationEnabled: tg.isClosingConfirmationEnabled,1435 isVerticalSwipesEnabled: tg.isVerticalSwipesEnabled,1436 };1437 if (tg.initDataUnsafe?.user) {1438 const u = tg.initDataUnsafe.user;1439 this.userInfo = { id: u.id, first_name: u.first_name, last_name: u.last_name, username: u.username, language_code: u.language_code, is_premium: u.is_premium, photo_url: u.photo_url };1440 }1441 this.themeParams = tg.themeParams || {};1442 this.safeAreaInset = tg.safeAreaInset || { top:0, bottom:0, left:0, right:0 };1443 this.contentSafeAreaInset = tg.contentSafeAreaInset || { top:0, bottom:0, left:0, right:0 };1444 },1445 1446 updateSensors() {1447 const tg = this.tg();1448 if (!tg) return;1449 if (tg.Accelerometer) {1450 this.accelData = { isStarted: tg.Accelerometer.isStarted, x: (tg.Accelerometer.x ?? 0).toFixed(2), y: (tg.Accelerometer.y ?? 0).toFixed(2), z: (tg.Accelerometer.z ?? 0).toFixed(2) };1451 // Only push to history when sensor is running1452 if (tg.Accelerometer.isStarted) {1453 this.accelHistory.push({ x: tg.Accelerometer.x ?? 0, y: tg.Accelerometer.y ?? 0, z: tg.Accelerometer.z ?? 0 });1454 if (this.accelHistory.length > 120) this.accelHistory.shift();1455 }1456 }1457 if (tg.Gyroscope) {1458 this.gyroData = { isStarted: tg.Gyroscope.isStarted, x: (tg.Gyroscope.x ?? 0).toFixed(2), y: (tg.Gyroscope.y ?? 0).toFixed(2), z: (tg.Gyroscope.z ?? 0).toFixed(2) };1459 }1460 if (tg.DeviceOrientation) {1461 this.orientData = { isStarted: tg.DeviceOrientation.isStarted, absolute: tg.DeviceOrientation.absolute, alpha: (tg.DeviceOrientation.alpha ?? 0).toFixed(3), beta: (tg.DeviceOrientation.beta ?? 0).toFixed(3), gamma: (tg.DeviceOrientation.gamma ?? 0).toFixed(3) };1462 }1463 },1464 1465 // ── Accel Graph ────────────────────────────────────────────1466 startGraphLoop() {1467 const draw = () => {1468 this.drawAccelGraph();1469 this.graphRafId = requestAnimationFrame(draw);1470 };1471 this.graphRafId = requestAnimationFrame(draw);1472 },1473 1474 drawAccelGraph() {1475 const canvas = this.$refs.accelCanvas;1476 if (!canvas) return;1477 1478 // Resize canvas to actual pixel dimensions1479 const rect = canvas.getBoundingClientRect();1480 const W = Math.round(rect.width * (window.devicePixelRatio || 1));1481 const H = Math.round(rect.height * (window.devicePixelRatio || 1));1482 if (canvas.width !== W || canvas.height !== H) {1483 canvas.width = W;1484 canvas.height = H;1485 }1486 1487 const ctx = canvas.getContext('2d');1488 ctx.clearRect(0, 0, W, H);1489 1490 // Background1491 const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--tg-theme-secondary-bg-color') || '#232428';1492 ctx.fillStyle = bgColor.trim() || '#232428';1493 ctx.fillRect(0, 0, W, H);1494 1495 // If no history, show placeholder grid1496 if (!this.accelHistory.length) {1497 ctx.strokeStyle = 'rgba(255,255,255,0.06)';1498 ctx.lineWidth = 1;1499 for (let i = 1; i < 4; i++) {1500 const y = (H / 4) * i;1501 ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();1502 }1503 ctx.fillStyle = 'rgba(255,255,255,0.2)';1504 ctx.font = `${11 * (window.devicePixelRatio || 1)}px system-ui`;1505 ctx.textAlign = 'center';1506 ctx.fillText('Start accelerometer to see data', W / 2, H / 2);1507 return;1508 }1509 1510 // Center line1511 ctx.strokeStyle = 'rgba(255,255,255,0.08)';1512 ctx.lineWidth = 1;1513 ctx.beginPath(); ctx.moveTo(0, H/2); ctx.lineTo(W, H/2); ctx.stroke();1514 1515 // Scale: map value range -20..20 to 0..H1516 const scale = (val) => H/2 - (val / 20) * (H/2 * 0.85);1517 1518 const drawLine = (vals, color) => {1519 if (!vals.length) return;1520 ctx.beginPath();1521 ctx.strokeStyle = color;1522 ctx.lineWidth = 1.5 * (window.devicePixelRatio || 1);1523 ctx.lineJoin = 'round';1524 vals.forEach((v, i) => {1525 const x = (i / (vals.length - 1 || 1)) * W;1526 const y = scale(v);1527 if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);1528 });1529 ctx.stroke();1530 };1531 1532 drawLine(this.accelHistory.map(h => h.x), '#ff453a');1533 drawLine(this.accelHistory.map(h => h.y), '#32d74b');1534 drawLine(this.accelHistory.map(h => h.z), '#0a84ff');1535 },1536 1537 // ── Basic ──────────────────────────────────────────────────1538 expand() { this.tg()?.expand(); this.log('expand()', 'success'); },1539 closeApp() { this.tg()?.close(); this.log('close()', 'success'); },1540 requestFullscreen() { this.tg()?.requestFullscreen(); this.log('requestFullscreen()', 'success'); },1541 exitFullscreen() { this.tg()?.exitFullscreen(); this.log('exitFullscreen()', 'success'); },1542 lockOrientation() { this.tg()?.lockOrientation(); this.log('lockOrientation()', 'success'); },1543 unlockOrientation() { this.tg()?.unlockOrientation(); this.log('unlockOrientation()', 'success'); },1544 enableVerticalSwipes() { this.tg()?.enableVerticalSwipes(); this.log('enableVerticalSwipes()', 'success'); },1545 disableVerticalSwipes() { this.tg()?.disableVerticalSwipes(); this.log('disableVerticalSwipes()', 'success'); },1546 enableClosingConfirmation() { this.tg()?.enableClosingConfirmation(); this.log('enableClosingConfirmation()', 'success'); },1547 disableClosingConfirmation() { this.tg()?.disableClosingConfirmation(); this.log('disableClosingConfirmation()', 'success'); },1548 setHeaderColor() { this.tg()?.setHeaderColor(this.headerColor); this.log(`setHeaderColor(${this.headerColor})`); },1549 setBackgroundColor() { this.tg()?.setBackgroundColor(this.backgroundColor); this.log(`setBackgroundColor(${this.backgroundColor})`); },1550 setBottomBarColor() { this.tg()?.setBottomBarColor(this.bottomBarColor); this.log(`setBottomBarColor(${this.bottomBarColor})`); },1551 hideKeyboard() { if (this.tg()?.hideKeyboard) { this.tg().hideKeyboard(); this.log('hideKeyboard()', 'success'); } else { this.log('hideKeyboard() not available (Bot API 9.1+)', 'error'); } },1552 1553 // ── Main Button ────────────────────────────────────────────1554 updateMainButton() {1555 const tg = this.tg(); if (!tg) return;1556 tg.MainButton.setText(this.mainButtonText);1557 tg.MainButton.color = this.mainButtonColor;1558 if (this.mainButtonIconId) tg.MainButton.setParams({ icon_custom_emoji_id: this.mainButtonIconId });1559 this.log('MainButton updated', 'success');1560 },1561 showMainButton() { this.tg()?.MainButton.show(); this.log('MainButton.show()', 'success'); },1562 hideMainButton() { this.tg()?.MainButton.hide(); this.log('MainButton.hide()', 'success'); },1563 enableMainButton() { this.tg()?.MainButton.enable(); this.log('MainButton.enable()', 'success'); },1564 disableMainButton(){ this.tg()?.MainButton.disable(); this.log('MainButton.disable()', 'success'); },1565 toggleMainButtonShine() {1566 this.mainButtonShineEffect = !this.mainButtonShineEffect;1567 this.tg()?.MainButton.setParams({ has_shine_effect: this.mainButtonShineEffect });1568 this.log(`MainButton shine: ${this.mainButtonShineEffect}`, 'success');1569 },1570 showMainProgress() { this.tg()?.MainButton.showProgress(); this.log('MainButton.showProgress()', 'success'); },1571 hideMainProgress() { this.tg()?.MainButton.hideProgress(); this.log('MainButton.hideProgress()', 'success'); },1572 1573 // ── Secondary Button ───────────────────────────────────────1574 updateSecondaryButton() {1575 this.tg()?.SecondaryButton?.setParams({ text: this.secondaryButtonText, position: this.secondaryButtonPosition });1576 this.log('SecondaryButton updated', 'success');1577 },1578 showSecondaryButton() { this.tg()?.SecondaryButton?.show(); this.log('SecondaryButton.show()', 'success'); },1579 hideSecondaryButton() { this.tg()?.SecondaryButton?.hide(); this.log('SecondaryButton.hide()', 'success'); },1580 1581 // ── Back/Settings ──────────────────────────────────────────1582 showBackButton() { this.tg()?.BackButton.show(); this.log('BackButton.show()', 'success'); },1583 hideBackButton() { this.tg()?.BackButton.hide(); this.log('BackButton.hide()', 'success'); },1584 showSettingsButton() { this.tg()?.SettingsButton?.show(); this.log('SettingsButton.show()', 'success'); },1585 hideSettingsButton() { this.tg()?.SettingsButton?.hide(); this.log('SettingsButton.hide()', 'success'); },1586 1587 // ── Popups ─────────────────────────────────────────────────1588 showAlert() {1589 this.tg()?.showAlert(this.alertMessage, () => { this.log('Alert closed', 'success'); });1590 },1591 showConfirm() {1592 this.tg()?.showConfirm(this.confirmMessage, (ok) => { this.log('Confirm result', ok ? 'success' : 'warning', { result: ok }); this.toast(ok ? 'Confirmed!' : 'Cancelled'); });1593 },1594 showPopup() {1595 this.tg()?.showPopup({ title: this.popupTitle, message: this.popupMessage, buttons: [{ id:'ok', type:'ok' }, { id:'cancel', type:'cancel' }] }, (id) => { this.log('Popup closed', 'success', { button: id }); });1596 },1597 showScanQrPopup() {1598 this.tg()?.showScanQrPopup({ text: this.qrText }, (text) => { this.log('QR scanned', 'success', { text }); this.toast(`QR: ${text?.substring(0,40)}`); return true; });1599 },1600 closeScanQrPopup() { this.tg()?.closeScanQrPopup(); this.log('QR scanner closed'); },1601 1602 // ── Links ──────────────────────────────────────────────────1603 openLink(instantView) {1604 this.tg()?.openLink(this.externalUrl, { try_instant_view: instantView });1605 this.log(`openLink(${this.externalUrl}, instant:${instantView})`);1606 },1607 openTelegramLink() { this.tg()?.openTelegramLink(this.telegramUrl); this.log(`openTelegramLink(${this.telegramUrl})`); },1608 switchInlineQuery() {1609 this.tg()?.switchInlineQuery(this.inlineQuery, this.chatTypes.length ? this.chatTypes : undefined);1610 this.log('switchInlineQuery()', 'success', { query: this.inlineQuery, chatTypes: this.chatTypes });1611 },1612 1613 // ── Share ──────────────────────────────────────────────────1614 shareToStory() {1615 const params = {};1616 if (this.storyCaption) params.text = this.storyCaption;1617 if (this.storyWidgetUrl) params.widget_link = { url: this.storyWidgetUrl, name: this.storyWidgetName };1618 this.tg()?.shareToStory(this.storyMediaUrl, params);1619 this.log('shareToStory()', 'success');1620 },1621 shareMessage() {1622 if (!this.shareMessageId) { this.log('Message ID required', 'error'); return; }1623 if (this.tg()?.shareMessage) {1624 this.tg().shareMessage(this.shareMessageId, (ok) => { this.log('shareMessage', ok ? 'success' : 'error', { ok }); });1625 } else { this.log('shareMessage() not available', 'error'); }1626 },1627 sendData() {1628 if (!this.sendDataContent) { this.log('Data required', 'error'); return; }1629 if (this.sendDataContent.length > 4096) { this.log('Exceeds 4096 bytes', 'error'); return; }1630 try {1631 this.tg()?.sendData(this.sendDataContent);1632 this.log('sendData()', 'success', { len: this.sendDataContent.length });1633 this.toast('Data sent to bot!');1634 } catch(e) { this.log('sendData() failed', 'error', { err: e.message }); }1635 },1636 requestChat() {1637 if (!this.preparedChatId) { this.log('Prepared ID required', 'error'); return; }1638 if (this.tg()?.requestChat) {1639 this.tg().requestChat(this.preparedChatId, (ok) => { this.log('requestChat', ok ? 'success' : 'error', { ok }); this.toast(ok ? 'Chat selected!' : 'Cancelled'); });1640 } else { this.log('requestChat() not available (Bot API 9.6+)', 'error'); }1641 },1642 1643 // ── Haptics ────────────────────────────────────────────────1644 hapticImpact(s) { this.tg()?.HapticFeedback.impactOccurred(s); this.log(`impact: ${s}`, 'success'); },1645 hapticNotification(t) { this.tg()?.HapticFeedback.notificationOccurred(t); this.log(`notification: ${t}`, 'success'); },1646 hapticSelection() { this.tg()?.HapticFeedback.selectionChanged(); this.log('selectionChanged()', 'success'); },1647 1648 // ── Cloud Storage ──────────────────────────────────────────1649 cloudSetItem() { this.tg()?.CloudStorage.setItem(this.cloudKey, this.cloudValue, (e) => { if (e) this.log('cloud set failed', 'error', {e}); else this.log('cloud set', 'success', { key: this.cloudKey }); }); },1650 cloudGetItem() { this.tg()?.CloudStorage.getItem(this.cloudKey, (e, v) => { if (e) this.log('cloud get failed', 'error', {e}); else { this.cloudValue = v || ''; this.log('cloud get', 'success', { value: v }); } }); },1651 cloudRemoveItem() { this.tg()?.CloudStorage.removeItem(this.cloudKey, (e) => { if (e) this.log('cloud remove failed', 'error', {e}); else this.log('cloud remove', 'success'); }); },1652 cloudGetKeys() { this.tg()?.CloudStorage.getKeys((e, k) => { if (e) this.log('cloud keys failed', 'error', {e}); else this.log('cloud keys', 'success', { keys: k }); }); },1653 1654 // ── Device Storage ─────────────────────────────────────────1655 deviceSetItem() { if (this.tg()?.DeviceStorage) { this.tg().DeviceStorage.setItem(this.deviceKey, this.deviceValue, (e) => { if (e) this.log('device set failed', 'error'); else this.log('device set', 'success'); }); } else { this.log('DeviceStorage N/A', 'error'); } },1656 deviceGetItem() { if (this.tg()?.DeviceStorage) { this.tg().DeviceStorage.getItem(this.deviceKey, (e, v) => { if (e) this.log('device get failed', 'error'); else { this.deviceValue = v || ''; this.log('device get', 'success', { value: v }); } }); } else { this.log('DeviceStorage N/A', 'error'); } },1657 deviceRemoveItem() { if (this.tg()?.DeviceStorage) { this.tg().DeviceStorage.removeItem(this.deviceKey, (e) => { if (e) this.log('device remove failed', 'error'); else this.log('device remove', 'success'); }); } else { this.log('DeviceStorage N/A', 'error'); } },1658 deviceClear() { if (this.tg()?.DeviceStorage) { this.tg().DeviceStorage.clear((e) => { if (e) this.log('device clear failed', 'error'); else this.log('device cleared', 'success'); }); } else { this.log('DeviceStorage N/A', 'error'); } },1659 1660 // ── Secure Storage ─────────────────────────────────────────1661 secureSetItem() { if (this.tg()?.SecureStorage) { this.tg().SecureStorage.setItem(this.secureKey, this.secureValue, (e) => { if (e) this.log('secure set failed', 'error'); else this.log('secure set', 'success'); }); } else { this.log('SecureStorage N/A', 'error'); } },1662 secureGetItem() { if (this.tg()?.SecureStorage) { this.tg().SecureStorage.getItem(this.secureKey, (e, v) => { if (e) this.log('secure get failed', 'error'); else { this.secureValue = v || ''; this.log('secure get', 'success', { value: v }); } }); } else { this.log('SecureStorage N/A', 'error'); } },1663 secureRemoveItem() { if (this.tg()?.SecureStorage) { this.tg().SecureStorage.removeItem(this.secureKey, (e) => { if (e) this.log('secure remove failed', 'error'); else this.log('secure remove', 'success'); }); } else { this.log('SecureStorage N/A', 'error'); } },1664 1665 // ── Sensors ────────────────────────────────────────────────1666 startAccelerometer() {1667 if (this.tg()?.Accelerometer) {1668 this.tg().Accelerometer.start({ refresh_rate: 100 }, (ok) => {1669 this.log('Accelerometer start', ok ? 'success' : 'error', { ok });1670 this.toast(ok ? 'Accelerometer started' : 'Failed to start');1671 if (ok) this.accelHistory = []; // clear old data on fresh start1672 });1673 } else { this.log('Accelerometer N/A', 'error'); }1674 },1675 stopAccelerometer() {1676 if (this.tg()?.Accelerometer) {1677 this.tg().Accelerometer.stop((ok) => {1678 this.log('Accelerometer stop', ok ? 'success' : 'error', { ok });1679 this.toast('Accelerometer stopped');1680 // Do NOT clear history on stop so graph stays visible1681 });1682 }1683 },1684 startGyroscope() {1685 if (this.tg()?.Gyroscope) { this.tg().Gyroscope.start({ refresh_rate: 100 }, (ok) => { this.log('Gyroscope start', ok ? 'success':'error', {ok}); this.toast(ok ? 'Gyroscope started':'Failed'); }); } else { this.log('Gyroscope N/A', 'error'); }1686 },1687 stopGyroscope() {1688 if (this.tg()?.Gyroscope) { this.tg().Gyroscope.stop((ok) => { this.log('Gyroscope stop', ok ? 'success':'error', {ok}); this.toast('Gyroscope stopped'); }); }1689 },1690 startOrientation(absolute) {1691 if (this.tg()?.DeviceOrientation) { this.tg().DeviceOrientation.start({ refresh_rate: 100, need_absolute: absolute }, (ok) => { this.log(`DeviceOrientation start (${absolute?'absolute':'relative'})`, ok?'success':'error', {ok}); this.toast(ok ? 'Orientation started':'Failed'); }); } else { this.log('DeviceOrientation N/A', 'error'); }1692 },1693 stopOrientation() {1694 if (this.tg()?.DeviceOrientation) { this.tg().DeviceOrientation.stop((ok) => { this.log('DeviceOrientation stop', ok?'success':'error', {ok}); this.toast('Orientation stopped'); }); }1695 },1696 1697 // ── Advanced ───────────────────────────────────────────────1698 biometricInit() {1699 if (this.tg()?.BiometricManager) { this.tg().BiometricManager.init(() => { const bm = this.tg().BiometricManager; this.biometricInfo = { isAvailable: bm.isBiometricAvailable, type: bm.biometricType, isAccessGranted: bm.isAccessGranted }; this.log('BiometricManager init', 'success', this.biometricInfo); this.toast(bm.isBiometricAvailable ? 'Biometrics available' : 'Not available'); }); } else { this.log('BiometricManager N/A', 'error'); }1700 },1701 biometricRequestAccess() {1702 if (this.tg()?.BiometricManager) { this.tg().BiometricManager.requestAccess({ reason: 'Test biometric access' }, (ok) => { this.log('Biometric access', ok?'success':'error', {ok}); this.toast(ok ? 'Access granted':'Denied'); }); }1703 },1704 biometricAuthenticate() {1705 if (this.tg()?.BiometricManager) { this.tg().BiometricManager.authenticate({ reason: 'Test authentication' }, (ok, token) => { this.log('Biometric auth', ok?'success':'error', {ok, token}); this.toast(ok ? 'Authenticated!':'Failed'); }); }1706 },1707 locationInit() {1708 if (this.tg()?.LocationManager) { this.tg().LocationManager.init(() => { const lm = this.tg().LocationManager; this.locationInfo = { isAvailable: lm.isLocationAvailable, isAccessGranted: lm.isAccessGranted }; this.log('LocationManager init', 'success', this.locationInfo); }); } else { this.log('LocationManager N/A', 'error'); }1709 },1710 getLocation() {1711 if (this.tg()?.LocationManager) { this.tg().LocationManager.getLocation((l) => { if (l) { this.log('Location', 'success', l); this.toast(`${l.latitude.toFixed(4)}, ${l.longitude.toFixed(4)}`); } else { this.log('Location denied', 'error'); } }); }1712 },1713 locationOpenSettings() { if (this.tg()?.LocationManager) { this.tg().LocationManager.openSettings(); this.log('Location settings opened'); } },1714 requestWriteAccess() { this.tg()?.requestWriteAccess((ok) => { this.log('writeAccess', ok?'success':'error', {ok}); this.toast(ok ? 'Write access granted':'Denied'); }); },1715 requestContact() { this.tg()?.requestContact((ok) => { this.log('requestContact', ok?'success':'error', {ok}); this.toast(ok ? 'Contact shared':'Not shared'); }); },1716 readClipboard() { this.tg()?.readTextFromClipboard((t) => { this.log('Clipboard', 'success', {text: t}); this.toast(`Clipboard: ${(t||'').substring(0,40)}`); }); },1717 addToHomeScreen() { if (this.tg()?.addToHomeScreen) { this.tg().addToHomeScreen(); this.log('addToHomeScreen()', 'success'); } else { this.log('addToHomeScreen() N/A', 'error'); } },1718 checkHomeScreenStatus() { if (this.tg()?.checkHomeScreenStatus) { this.tg().checkHomeScreenStatus((s) => { this.homeScreenStatus = s; this.log('homeScreen status', 'success', {s}); this.toast(`Home screen: ${s}`); }); } else { this.log('checkHomeScreenStatus() N/A', 'error'); } },1719 downloadFile() {1720 if (this.tg()?.downloadFile) { this.tg().downloadFile({ url: this.downloadUrl, file_name: this.downloadFileName }, (status) => { this.log('downloadFile', status==='downloading'?'success':'error', {status}); this.toast(status==='downloading'?'Download started':'Failed'); }); } else { this.log('downloadFile() N/A', 'error'); }1721 },1722 setEmojiStatus() {1723 if (this.tg()?.setEmojiStatus) { const p = this.emojiDuration ? { duration: parseInt(this.emojiDuration) } : {}; this.tg().setEmojiStatus(this.emojiId, p, (ok) => { this.log('setEmojiStatus', ok?'success':'error', {ok}); this.toast(ok?'Emoji status set!':'Failed'); }); } else { this.log('setEmojiStatus() N/A', 'error'); }1724 },1725 requestEmojiStatusAccess() {1726 if (this.tg()?.requestEmojiStatusAccess) { this.tg().requestEmojiStatusAccess((ok) => { this.log('emojiStatusAccess', ok?'success':'error', {ok}); this.toast(ok?'Access granted':'Denied'); }); } else { this.log('requestEmojiStatusAccess() N/A', 'error'); }1727 },1728 }1729}).mount('#app');1730</script>1731</body>1732</html>