Paradox/TBLWebappBotPublic · Community Store Listing

Webapp Demo Bot

ProfileTelegram
7 commands0 envUpdated 1mo agoCreated Dec 13, 2025
Back to folder

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 &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>