nuxt-telegram-mini-app 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/.vscode/settings.json +55 -0
- package/.vscode/tailwind.json +55 -0
- package/CONTRIBUTING.md +406 -0
- package/LICENSE +21 -0
- package/README.md +640 -0
- package/app/app.vue +87 -0
- package/app/assets/css/main.css +53 -0
- package/app/assets/css/tailwind.css +3 -0
- package/app/components/ErrorBoundary.vue +81 -0
- package/app/components/Hero.vue +61 -0
- package/app/components/tg/Button.vue +128 -0
- package/app/components/tg/Cell.vue +91 -0
- package/app/components/tg/Content.vue +42 -0
- package/app/components/tg/Nav.vue +107 -0
- package/app/components/tg/Section.vue +50 -0
- package/app/composables/telegram.ts +342 -0
- package/app/error.vue +161 -0
- package/app/pages/components.vue +279 -0
- package/app/pages/functions.vue +107 -0
- package/app/pages/index.vue +211 -0
- package/app/pages/utilities.vue +402 -0
- package/app/types/telegram-webapp.ts +160 -0
- package/app/utils/color.ts +37 -0
- package/eslint.config.mjs +6 -0
- package/nuxt.config.ts +55 -0
- package/package.json +46 -0
- package/public/_redirects +2 -0
- package/public/favicon.ico +0 -0
- package/public/img/hero-user.svg +8 -0
- package/public/img/nuxt-logo.svg +11 -0
- package/public/robots.txt +2 -0
- package/server/api/verify-telegram-data.post.ts +150 -0
- package/tailwind.config.ts +39 -0
- package/tests/components.spec.ts +311 -0
- package/tests/pages.spec.ts +426 -0
- package/tests/telegram.spec.ts +105 -0
- package/tests/utils.spec.ts +47 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TgContent>
|
|
3
|
+
<Hero
|
|
4
|
+
title="Components Demo"
|
|
5
|
+
subtitle="Interactive showcase of all Telegram Mini App components"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<TgSection title="Buttons" inset>
|
|
9
|
+
<div class="p-4 space-y-3">
|
|
10
|
+
<div class="space-y-2">
|
|
11
|
+
<h3 class="text-sm font-medium text-subtitle">Primary Buttons</h3>
|
|
12
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
13
|
+
<TgButton title="Default Primary" status="primary" haptic />
|
|
14
|
+
<TgButton title="Disabled Primary" status="primary" :disabled="true" />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="space-y-2">
|
|
19
|
+
<h3 class="text-sm font-medium text-subtitle">Secondary Buttons</h3>
|
|
20
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
21
|
+
<TgButton title="Secondary" status="secondary" haptic="impact-light" />
|
|
22
|
+
<TgButton title="Outline" status="outline" haptic="selection" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="space-y-2">
|
|
27
|
+
<h3 class="text-sm font-medium text-subtitle">Special States</h3>
|
|
28
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
29
|
+
<TgButton title="Loading..." status="primary" :loading="loadingButton" @click="toggleLoading" />
|
|
30
|
+
<TgButton title="Destructive" status="destructive" haptic="notification-warning" />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</TgSection>
|
|
35
|
+
|
|
36
|
+
<TgSection title="Cells & Lists" inset>
|
|
37
|
+
<TgCell
|
|
38
|
+
title="Basic Cell"
|
|
39
|
+
subtitle="Simple cell with subtitle"
|
|
40
|
+
icon="i-heroicons-star-20-solid"
|
|
41
|
+
/>
|
|
42
|
+
<TgCell
|
|
43
|
+
title="Cell with Description"
|
|
44
|
+
:description="cellDescription"
|
|
45
|
+
icon="i-heroicons-information-circle-20-solid"
|
|
46
|
+
/>
|
|
47
|
+
<TgCell
|
|
48
|
+
title="Interactive Cell"
|
|
49
|
+
subtitle="Demo cell (no navigation)"
|
|
50
|
+
icon="i-heroicons-arrow-right-circle-20-solid"
|
|
51
|
+
@click="onCellClick"
|
|
52
|
+
/>
|
|
53
|
+
<TgCell
|
|
54
|
+
title="Cell with Action"
|
|
55
|
+
subtitle="Custom click handler"
|
|
56
|
+
icon="i-heroicons-bolt-20-solid"
|
|
57
|
+
@click="onCellClick"
|
|
58
|
+
:border="false"
|
|
59
|
+
/>
|
|
60
|
+
<TgCell
|
|
61
|
+
title="Router Link Cell"
|
|
62
|
+
subtitle="Navigates using NuxtLink"
|
|
63
|
+
icon="i-heroicons-chevron-right-20-solid"
|
|
64
|
+
to="/"
|
|
65
|
+
/>
|
|
66
|
+
<TgCell
|
|
67
|
+
title="External Link Cell"
|
|
68
|
+
subtitle="Opens external URL"
|
|
69
|
+
icon="i-heroicons-arrow-top-right-on-square-20-solid"
|
|
70
|
+
href="https://nuxt.com"
|
|
71
|
+
/>
|
|
72
|
+
</TgSection>
|
|
73
|
+
|
|
74
|
+
<TgSection title="Color Showcase" inset>
|
|
75
|
+
<div class="p-4 space-y-4">
|
|
76
|
+
<div class="space-y-2">
|
|
77
|
+
<h3 class="text-sm font-medium text-subtitle">Theme Colors</h3>
|
|
78
|
+
<div class="grid grid-cols-2 gap-3">
|
|
79
|
+
<div v-for="color in themeColors" :key="color.name" class="flex items-center gap-2">
|
|
80
|
+
<div
|
|
81
|
+
class="w-4 h-4 rounded border border-sectionSeparator"
|
|
82
|
+
:style="{ backgroundColor: color.value }"
|
|
83
|
+
/>
|
|
84
|
+
<span class="text-xs">{{ color.name }}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="space-y-2">
|
|
90
|
+
<h3 class="text-sm font-medium text-subtitle">Dynamic Colors</h3>
|
|
91
|
+
<div class="flex flex-wrap gap-2">
|
|
92
|
+
<button
|
|
93
|
+
v-for="colorOption in backgroundColors"
|
|
94
|
+
:key="colorOption.name"
|
|
95
|
+
class="px-3 py-1 text-xs rounded border border-sectionSeparator"
|
|
96
|
+
:style="{ backgroundColor: colorOption.value, color: colorOption.textColor }"
|
|
97
|
+
@click="changeBackgroundColor(colorOption.value)"
|
|
98
|
+
>
|
|
99
|
+
{{ colorOption.name }}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</TgSection>
|
|
105
|
+
|
|
106
|
+
<TgSection title="Interactive Elements" inset>
|
|
107
|
+
<div class="p-4 space-y-4">
|
|
108
|
+
<div class="space-y-2">
|
|
109
|
+
<h3 class="text-sm font-medium text-subtitle">Form Elements</h3>
|
|
110
|
+
<div class="space-y-3">
|
|
111
|
+
<input
|
|
112
|
+
v-model="textInput"
|
|
113
|
+
type="text"
|
|
114
|
+
placeholder="Enter text here..."
|
|
115
|
+
class="w-full px-3 py-2 bg-secondaryBg border border-sectionSeparator rounded-lg text-text placeholder-hint"
|
|
116
|
+
/>
|
|
117
|
+
<textarea
|
|
118
|
+
v-model="textareaInput"
|
|
119
|
+
rows="3"
|
|
120
|
+
placeholder="Multi-line input..."
|
|
121
|
+
class="w-full px-3 py-2 bg-secondaryBg border border-sectionSeparator rounded-lg text-text placeholder-hint resize-none"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="space-y-2">
|
|
127
|
+
<h3 class="text-sm font-medium text-subtitle">Counters & Progress</h3>
|
|
128
|
+
<div class="space-y-3">
|
|
129
|
+
<div class="flex items-center justify-between">
|
|
130
|
+
<span class="text-sm">Counter: {{ counter }}</span>
|
|
131
|
+
<div class="flex gap-2">
|
|
132
|
+
<button
|
|
133
|
+
@click="counter--"
|
|
134
|
+
class="w-8 h-8 bg-button-color text-button-text rounded-full flex items-center justify-center text-sm font-medium"
|
|
135
|
+
>
|
|
136
|
+
-
|
|
137
|
+
</button>
|
|
138
|
+
<button
|
|
139
|
+
@click="counter++"
|
|
140
|
+
class="w-8 h-8 bg-button-color text-button-text rounded-full flex items-center justify-center text-sm font-medium"
|
|
141
|
+
>
|
|
142
|
+
+
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="space-y-1">
|
|
147
|
+
<div class="flex justify-between text-sm">
|
|
148
|
+
<span>Progress</span>
|
|
149
|
+
<span>{{ progress }}%</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="w-full h-2 bg-secondaryBg rounded-full overflow-hidden">
|
|
152
|
+
<div
|
|
153
|
+
class="h-full bg-link transition-all duration-300"
|
|
154
|
+
:style="{ width: `${progress}%` }"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</TgSection>
|
|
162
|
+
|
|
163
|
+
<TgSection title="Haptic Feedback Demo" inset>
|
|
164
|
+
<div class="p-4 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
|
165
|
+
<TgButton
|
|
166
|
+
title="Light Impact"
|
|
167
|
+
haptic="impact-light"
|
|
168
|
+
status="outline"
|
|
169
|
+
@click="() => {}"
|
|
170
|
+
/>
|
|
171
|
+
<TgButton
|
|
172
|
+
title="Medium Impact"
|
|
173
|
+
haptic="impact-medium"
|
|
174
|
+
status="outline"
|
|
175
|
+
@click="() => {}"
|
|
176
|
+
/>
|
|
177
|
+
<TgButton
|
|
178
|
+
title="Heavy Impact"
|
|
179
|
+
haptic="impact-heavy"
|
|
180
|
+
status="outline"
|
|
181
|
+
@click="() => {}"
|
|
182
|
+
/>
|
|
183
|
+
<TgButton
|
|
184
|
+
title="Success"
|
|
185
|
+
haptic="notification-success"
|
|
186
|
+
status="outline"
|
|
187
|
+
@click="() => {}"
|
|
188
|
+
/>
|
|
189
|
+
<TgButton
|
|
190
|
+
title="Warning"
|
|
191
|
+
haptic="notification-warning"
|
|
192
|
+
status="outline"
|
|
193
|
+
@click="() => {}"
|
|
194
|
+
/>
|
|
195
|
+
<TgButton
|
|
196
|
+
title="Error"
|
|
197
|
+
haptic="notification-error"
|
|
198
|
+
status="outline"
|
|
199
|
+
@click="() => {}"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
</TgSection>
|
|
203
|
+
|
|
204
|
+
<div class="h-2" />
|
|
205
|
+
</TgContent>
|
|
206
|
+
|
|
207
|
+
</template>
|
|
208
|
+
|
|
209
|
+
<script setup lang="ts">
|
|
210
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
211
|
+
import { useHapticFeedback, useMiniApp, useThemeParams, useMainButton } from '~/composables/telegram'
|
|
212
|
+
|
|
213
|
+
const haptic = useHapticFeedback()
|
|
214
|
+
const mini = useMiniApp()
|
|
215
|
+
const theme = useThemeParams()
|
|
216
|
+
const main = useMainButton()
|
|
217
|
+
|
|
218
|
+
// Interactive state
|
|
219
|
+
const loadingButton = ref(false)
|
|
220
|
+
const counter = ref(0)
|
|
221
|
+
const textInput = ref('')
|
|
222
|
+
const textareaInput = ref('')
|
|
223
|
+
|
|
224
|
+
const progress = computed(() => Math.min(100, Math.max(0, counter.value * 10)))
|
|
225
|
+
|
|
226
|
+
const cellDescription = computed(() => {
|
|
227
|
+
if (textInput.value) return `You typed: "${textInput.value}"`
|
|
228
|
+
return 'Type something in the input above'
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
function toggleLoading() {
|
|
232
|
+
loadingButton.value = true
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
loadingButton.value = false
|
|
235
|
+
haptic.notificationOccurred('success')
|
|
236
|
+
}, 2000)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function onCellClick() {
|
|
240
|
+
haptic.impactOccurred('medium')
|
|
241
|
+
counter.value += 5
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Theme colors demo
|
|
245
|
+
const themeColors = computed(() => [
|
|
246
|
+
{ name: 'Background', value: theme.backgroundColor.value || 'var(--tg-theme-bg-color)' },
|
|
247
|
+
{ name: 'Text', value: theme.textColor.value || 'var(--tg-theme-text-color)' },
|
|
248
|
+
{ name: 'Button', value: theme.buttonColor.value || 'var(--tg-theme-button-color)' },
|
|
249
|
+
{ name: 'Button Text', value: theme.buttonTextColor.value || 'var(--tg-theme-button-text-color)' },
|
|
250
|
+
{ name: 'Link', value: theme.linkColor.value || 'var(--tg-theme-link-color)' },
|
|
251
|
+
{ name: 'Secondary BG', value: theme.secondaryBackgroundColor.value || 'var(--tg-theme-secondary-bg-color)' },
|
|
252
|
+
])
|
|
253
|
+
|
|
254
|
+
const backgroundColors = [
|
|
255
|
+
{ name: 'Default', value: 'var(--tg-theme-bg-color)', textColor: 'var(--tg-theme-text-color)' },
|
|
256
|
+
{ name: 'Dark', value: '#1a1a1a', textColor: '#ffffff' },
|
|
257
|
+
{ name: 'Blue', value: '#0088cc', textColor: '#ffffff' },
|
|
258
|
+
{ name: 'Green', value: '#00c896', textColor: '#ffffff' },
|
|
259
|
+
{ name: 'Purple', value: '#8b5cf6', textColor: '#ffffff' },
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
function changeBackgroundColor(color: string) {
|
|
263
|
+
mini.setBackgroundColor(color)
|
|
264
|
+
haptic.selectionChanged()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Hide Main Button since we're using TgNav for navigation
|
|
268
|
+
onMounted(() => {
|
|
269
|
+
main.mount()
|
|
270
|
+
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
try {
|
|
273
|
+
main.setParams({ is_visible: false })
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.warn('Failed to hide main button:', e)
|
|
276
|
+
}
|
|
277
|
+
}, 500)
|
|
278
|
+
})
|
|
279
|
+
</script>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TgContent>
|
|
3
|
+
<Hero
|
|
4
|
+
title="Functions"
|
|
5
|
+
image-src="/img/hero-user.svg"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<TgSection title="Navigation" inset>
|
|
9
|
+
<TgCell
|
|
10
|
+
title="Use Back Button"
|
|
11
|
+
subtitle="Telegram back button is shown on this page"
|
|
12
|
+
icon="i-heroicons-arrow-left-circle-20-solid"
|
|
13
|
+
/>
|
|
14
|
+
<div class="p-4">
|
|
15
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
16
|
+
<TgButton title="Go Back (fallback)" haptic @click="goBack" />
|
|
17
|
+
<TgButton title="Go to Components" status="outline" to="/components" haptic="selection" />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</TgSection>
|
|
21
|
+
|
|
22
|
+
<TgSection title="Main Button" inset>
|
|
23
|
+
<TgCell title="Main Button is hidden" subtitle="Using bottom navigation instead" />
|
|
24
|
+
<div class="p-4">
|
|
25
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
26
|
+
<TgButton title="Demo Button" haptic @click="() => {}" />
|
|
27
|
+
<TgButton title="Toggle Main Button" status="outline" haptic="selection" @click="toggleMain" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</TgSection>
|
|
31
|
+
|
|
32
|
+
<TgSection title="Links & Sharing" inset>
|
|
33
|
+
<div class="p-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
34
|
+
<TgButton title="Open Nuxt" href="https://nuxt.com" haptic />
|
|
35
|
+
<TgButton title="Open Telegram" haptic @click="() => openTelegramLink('https://t.me/nuxt_tg_demo_bot')" />
|
|
36
|
+
<TgButton title="Share This Page" :share-url="shareUrl" haptic="notification-success" />
|
|
37
|
+
</div>
|
|
38
|
+
</TgSection>
|
|
39
|
+
|
|
40
|
+
<div class="h-2" />
|
|
41
|
+
</TgContent>
|
|
42
|
+
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { onBeforeUnmount, onMounted, computed } from 'vue'
|
|
47
|
+
import { useRouter } from 'vue-router'
|
|
48
|
+
import { useBackButton, useMainButton, openTelegramLink } from '~/composables/telegram'
|
|
49
|
+
|
|
50
|
+
const back = useBackButton()
|
|
51
|
+
const main = useMainButton()
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
|
|
54
|
+
function goBack() {
|
|
55
|
+
if (!import.meta.client) return
|
|
56
|
+
// Prefer router navigation to play nicely with Nuxt links/TgNav
|
|
57
|
+
if (window.history.length > 1) {
|
|
58
|
+
try { router.back() } catch { /* fallback below */ }
|
|
59
|
+
} else {
|
|
60
|
+
try { router.push('/') } catch { /* no-op */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toggleMain() {
|
|
65
|
+
const visible = !main.visible.value
|
|
66
|
+
main.setParams({ is_visible: visible })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// SSR-safe current URL for sharing examples
|
|
70
|
+
const shareUrl = computed(() => import.meta.client ? window.location.href : 'https://example.com')
|
|
71
|
+
|
|
72
|
+
onMounted(() => {
|
|
73
|
+
// Back Button wiring with guarded polling to avoid early store reads
|
|
74
|
+
back.mount()
|
|
75
|
+
const pollBack = setInterval(() => {
|
|
76
|
+
try {
|
|
77
|
+
// If store not ready yet, any error is swallowed
|
|
78
|
+
if (back.mounted.value) {
|
|
79
|
+
try { back.show() } catch {}
|
|
80
|
+
clearInterval(pollBack)
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}, 50)
|
|
84
|
+
// Register back handler and properly unregister on unmount
|
|
85
|
+
const backHandler = () => { goBack() }
|
|
86
|
+
back.onClick(backHandler)
|
|
87
|
+
|
|
88
|
+
// Hide Main Button since we're using TgNav for navigation
|
|
89
|
+
main.mount()
|
|
90
|
+
const pollMain = setInterval(() => {
|
|
91
|
+
try {
|
|
92
|
+
if (main.mounted.value) {
|
|
93
|
+
main.setParams({ is_visible: false })
|
|
94
|
+
clearInterval(pollMain)
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
}, 50)
|
|
98
|
+
|
|
99
|
+
onBeforeUnmount(() => {
|
|
100
|
+
try { back.offClick(backHandler) } catch {}
|
|
101
|
+
try { back.hide() } catch {}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TgContent>
|
|
3
|
+
<Hero
|
|
4
|
+
title="Telegram Mini App Demo"
|
|
5
|
+
subtitle="Showcasing components and SDK integrations"
|
|
6
|
+
image-src="/img/nuxt-logo.svg"
|
|
7
|
+
/>
|
|
8
|
+
|
|
9
|
+
<TgSection title="Navigation" inset>
|
|
10
|
+
<TgCell
|
|
11
|
+
title="Navigation Demo"
|
|
12
|
+
subtitle="Use the bottom navigation bar to navigate"
|
|
13
|
+
icon="i-heroicons-arrow-right-circle-20-solid"
|
|
14
|
+
/>
|
|
15
|
+
<TgButton title="Demo Button" status="primary" haptic @click="() => haptic.impactOccurred('medium')" />
|
|
16
|
+
<TgButton title="Toggle Main Button" status="primary" haptic="selection" @click="toggleMain" />
|
|
17
|
+
<TgCell
|
|
18
|
+
title="Example Cell without icon"
|
|
19
|
+
/>
|
|
20
|
+
<template #append>
|
|
21
|
+
Use the navigation bar at the bottom to switch between pages.
|
|
22
|
+
</template>
|
|
23
|
+
</TgSection>
|
|
24
|
+
|
|
25
|
+
<TgSection title="Haptic Feedback" inset>
|
|
26
|
+
<TgCell title="Supported" :description="hapticSupported" icon="i-heroicons-bolt-20-solid" :border="false" />
|
|
27
|
+
<div class="p-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
28
|
+
<TgButton title="Impact" :disabled="!hapticSupportedBool" @click="haptic.impactOccurred('medium')" />
|
|
29
|
+
<TgButton title="Notification" :disabled="!hapticSupportedBool" @click="haptic.notificationOccurred('success')" />
|
|
30
|
+
<TgButton title="Selection" :disabled="!hapticSupportedBool" @click="haptic.selectionChanged()" />
|
|
31
|
+
<!-- Example using TgButton's built-in haptic prop -->
|
|
32
|
+
<TgButton title="Built-in Haptic (medium)" haptic="impact-medium" />
|
|
33
|
+
</div>
|
|
34
|
+
</TgSection>
|
|
35
|
+
|
|
36
|
+
<TgSection title="Init Data" inset>
|
|
37
|
+
<TgCell title="User" :description="initUser" icon="i-heroicons-user-20-solid" />
|
|
38
|
+
<TgCell title="Query ID" :description="initQueryId" />
|
|
39
|
+
<TgCell title="Start Param" :description="initStartParam" :border="false" />
|
|
40
|
+
</TgSection>
|
|
41
|
+
|
|
42
|
+
<TgSection title="Mini App" inset>
|
|
43
|
+
<TgCell title="Supported" :description="miniSupported" icon="i-heroicons-check-badge-20-solid" />
|
|
44
|
+
<TgCell title="Dark" :description="miniDark" />
|
|
45
|
+
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
46
|
+
<TgButton title="Header = bg" :disabled="!miniSupportedBool" haptic @click="mini.setHeaderColor('bg_color')" />
|
|
47
|
+
<TgButton title="BG = bg" :disabled="!miniSupportedBool" haptic @click="mini.setBackgroundColor('bg_color')" />
|
|
48
|
+
</div>
|
|
49
|
+
</TgSection>
|
|
50
|
+
|
|
51
|
+
<ClientOnly>
|
|
52
|
+
<TgSection title="Theme Params" inset>
|
|
53
|
+
<div class="p-4 grid grid-cols-1 gap-3 text-sm">
|
|
54
|
+
<div class="flex items-center gap-2">
|
|
55
|
+
<span class="inline-block w-4 h-4 rounded" :style="{ background: themeBgHex }" />
|
|
56
|
+
<span>BG: {{ themeBgHex }}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="flex items-center gap-2">
|
|
59
|
+
<span class="inline-block w-4 h-4 rounded" :style="{ background: themeTextHex }" />
|
|
60
|
+
<span>Text: {{ themeTextHex }}</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</TgSection>
|
|
64
|
+
</ClientOnly>
|
|
65
|
+
|
|
66
|
+
<ClientOnly>
|
|
67
|
+
<TgSection title="Viewport" inset>
|
|
68
|
+
<TgCell title="Width" :description="viewportWidth" />
|
|
69
|
+
<TgCell title="Height" :description="viewportHeight" />
|
|
70
|
+
<TgCell title="Stable Height" :description="viewportStableHeight" />
|
|
71
|
+
<TgCell title="Expanded" :description="String(viewport.expanded.value ?? false)" :border="false" />
|
|
72
|
+
<div class="p-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
73
|
+
<TgButton title="Expand" haptic @click="viewport.expand" />
|
|
74
|
+
<TgButton title="Fullscreen" haptic="impact-light" @click="onRequestFullscreen" />
|
|
75
|
+
<TgButton title="Exit FS" haptic="impact-light" @click="onExitFullscreen" />
|
|
76
|
+
</div>
|
|
77
|
+
</TgSection>
|
|
78
|
+
</ClientOnly>
|
|
79
|
+
|
|
80
|
+
<TgSection title="Links" inset>
|
|
81
|
+
<div class="p-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
82
|
+
<TgButton title="Open Link" haptic @click="openLink('https://t.me/n')" />
|
|
83
|
+
<TgButton title="Open Telegram" haptic @click="openTelegramLink('https://t.me/nuxt_tg_demo_bot')" />
|
|
84
|
+
<TgButton title="Share URL" haptic="notification-success" :share-url="shareUrl" />
|
|
85
|
+
</div>
|
|
86
|
+
</TgSection>
|
|
87
|
+
|
|
88
|
+
<div class="h-2" />
|
|
89
|
+
</TgContent>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<script setup lang="ts">
|
|
93
|
+
import { onMounted, computed } from 'vue'
|
|
94
|
+
import { toHex } from '~/utils/color'
|
|
95
|
+
import { useRequestURL } from '#app'
|
|
96
|
+
import {
|
|
97
|
+
useHapticFeedback,
|
|
98
|
+
useInitData,
|
|
99
|
+
useMainButton,
|
|
100
|
+
useMiniApp,
|
|
101
|
+
useThemeParams,
|
|
102
|
+
useViewport,
|
|
103
|
+
openLink,
|
|
104
|
+
openTelegramLink,
|
|
105
|
+
} from '~/composables/telegram'
|
|
106
|
+
|
|
107
|
+
const main = useMainButton()
|
|
108
|
+
const haptic = useHapticFeedback()
|
|
109
|
+
const init = useInitData()
|
|
110
|
+
const mini = useMiniApp()
|
|
111
|
+
const theme = useThemeParams()
|
|
112
|
+
const viewport = useViewport()
|
|
113
|
+
|
|
114
|
+
// SSR-safe current URL for sharing
|
|
115
|
+
const reqURL = useRequestURL()
|
|
116
|
+
const shareUrl = computed(() => import.meta.client ? window.location.href : reqURL.href)
|
|
117
|
+
|
|
118
|
+
function toggleMain() {
|
|
119
|
+
const visible = !main.visible.value
|
|
120
|
+
main.setParams({ is_visible: visible })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Guarded access helpers to avoid early SDK store reads
|
|
124
|
+
const safe = function <T>(fn: () => T, fallback: T): T {
|
|
125
|
+
try { return fn() } catch { return fallback }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Haptic
|
|
129
|
+
const hapticSupported = computed(() => String(safe(() => haptic.supported.value, false)))
|
|
130
|
+
const hapticSupportedBool = computed(() => safe(() => !!haptic.supported.value, false))
|
|
131
|
+
|
|
132
|
+
// Mini App
|
|
133
|
+
const miniSupported = computed(() => String(safe(() => mini.supported.value, false)))
|
|
134
|
+
const miniSupportedBool = computed(() => safe(() => !!mini.supported.value, false))
|
|
135
|
+
const miniDark = computed(() => String(safe(() => mini.dark.value, false)))
|
|
136
|
+
|
|
137
|
+
// Init Data
|
|
138
|
+
const initUser = computed(() => safe(() => init.user.value?.username || init.user.value?.first_name || '—', '—'))
|
|
139
|
+
const initQueryId = computed(() => safe(() => init.queryId.value || '—', '—'))
|
|
140
|
+
const initStartParam = computed(() => safe(() => init.startParam.value || '—', '—'))
|
|
141
|
+
|
|
142
|
+
onMounted(async () => {
|
|
143
|
+
// Hide Main Button since we're using TgNav for navigation
|
|
144
|
+
main.mount()
|
|
145
|
+
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
try {
|
|
148
|
+
main.setParams({ is_visible: false })
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.warn('Failed to hide main button:', e)
|
|
151
|
+
}
|
|
152
|
+
}, 500)
|
|
153
|
+
|
|
154
|
+
// Try restore init data if not present yet (fallback for some environments)
|
|
155
|
+
try {
|
|
156
|
+
if (!init.state.value) init.restore()
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore restore errors - this is a fallback mechanism
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Fallbacks for Theme Params and Viewport displays
|
|
163
|
+
const themeBgHex = computed(() => {
|
|
164
|
+
const hex = toHex(theme.backgroundColor.value)
|
|
165
|
+
if (hex !== '—') return hex
|
|
166
|
+
if (import.meta.client) {
|
|
167
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue('--tg-theme-bg-color')?.trim()
|
|
168
|
+
return v || '—'
|
|
169
|
+
}
|
|
170
|
+
return '—'
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const themeTextHex = computed(() => {
|
|
174
|
+
const hex = toHex(theme.textColor.value)
|
|
175
|
+
if (hex !== '—') return hex
|
|
176
|
+
if (import.meta.client) {
|
|
177
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue('--tg-theme-text-color')?.trim()
|
|
178
|
+
return v || '—'
|
|
179
|
+
}
|
|
180
|
+
return '—'
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const viewportWidth = computed(() => String(safe(() => viewport.width.value, undefined) ?? (import.meta.client ? window.innerWidth : '—')))
|
|
184
|
+
const viewportHeight = computed(() => String(safe(() => viewport.height.value, undefined) ?? (import.meta.client ? window.innerHeight : '—')))
|
|
185
|
+
const viewportStableHeight = computed(() => String(safe(() => viewport.stableHeight.value, undefined) ?? (import.meta.client ? window.innerHeight : '—')))
|
|
186
|
+
|
|
187
|
+
function onRequestFullscreen() {
|
|
188
|
+
try {
|
|
189
|
+
viewport.requestFullscreen()
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore viewport fullscreen errors - fallback to browser API
|
|
192
|
+
}
|
|
193
|
+
if (import.meta.client && document.fullscreenEnabled && !document.fullscreenElement) {
|
|
194
|
+
document.documentElement.requestFullscreen?.().catch(() => {})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function onExitFullscreen() {
|
|
199
|
+
try {
|
|
200
|
+
viewport.exitFullscreen()
|
|
201
|
+
} catch {
|
|
202
|
+
// Ignore viewport fullscreen errors - fallback to browser API
|
|
203
|
+
}
|
|
204
|
+
if (import.meta.client && document.fullscreenElement) {
|
|
205
|
+
document.exitFullscreen?.().catch(() => {})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
</script>
|
|
209
|
+
|
|
210
|
+
<style scoped>
|
|
211
|
+
</style>
|