Paradox/TBLWebappBotPublic · Community Store Listing

This demo bot showcases how to build and integrate Telegram Web Apps using TeleBotHost, demonstrating the core features and usage of the Telegram WebApp JavaScript library.

Developerwebappminiappdemoinlinetelegramhtml
ProfileTelegram
6 commands0 envUpdated 10d agoCreated Dec 13, 2025
Back to folder

commands/webapp.html

html · 1732 lines

Raw
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 &amp; 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>