diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..736e980
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,55 @@
+name: CI
+
+on:
+ push:
+ branches: ["main"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+ # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: 'pages'
+ cancel-in-progress: true
+
+jobs:
+ # Single deploy job since we're just deploying
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ cache: 'npm'
+ - name: Install dependencies
+ run: npm install
+ - name: Build
+ # We need to pass the repository name directly to the Vite build command
+ # as the "base" parameter.
+ # This is necessary because it aligns with how GitHub Pages functions.
+ # Here's how you can achieve this:
+ run: npm run build -- -- --base /${{ github.event.repository.name }}
+ - name: Create 404.html
+ run: cp ./app/dist/index.html ./app/dist/404.html
+ - name: Setup Pages
+ uses: actions/configure-pages@v3
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v1
+ with:
+ # Upload dist repository
+ path: './app/dist'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0ea79ed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+.pnp
+.pnp.js
+.expo-shared
+
+.vscode
+
+# testing
+coverage
+
+# next.js
+.next/
+.swc/
+out/
+build
+
+# expo
+.expo
+
+# misc
+.DS_Store
+*.pem
+dist
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# turbo
+.turbo
+
+# bot
+bot/.env
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6802bc4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9faa1c5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,149 @@
+# Telegram Onboarding Kit
+
+[Telegram Onboarding Kit](https://github.com/Easterok/telegram-onboarding-kit) is a simple-to-use tool for crafting **onboardings** and **paywalls** for Telegram Bots. You can create pretty onboardings with minimal adjustments to our presets or easily create your own custom flows with our web components – thanks to **Telegram Mini Apps**.
+
+But why do you need it? Because onboarding/paywall is a **must-have attribute** of any mobile app – it offers a bright product presentation and seamless payment process. We believe Telegram Mini Apps will be used by millions of users in the future and this kit will help developers to turn their apps into real products!
+
+To start:
+
+- Have a look at live demo:
+- Watch tutorial: [![YouTube | Watch Tutorial](https://img.shields.io/badge/YouTube-Watch%20Tutorial-red?logo=youtube&style=social)](https://www.youtube.com/watch?v=q9R42T-7ykI)
+- And check out video demo:
+
+https://github.com/Easterok/telegram-onboarding-kit/assets/7571844/a3b7040d-35fa-40c5-ac36-d0595eb33457
+
+## Features
+
+- Native support for images, videos, Telegram (.tgs) animated stickers, forms, feature lists and page navigation
+- Configurable products on paywall
+- One-click 0$ **deploy** on GitHub Pages
+- Auto adaptation to Telegram color scheme
+- **Telegram Payments** & 👛 **Wallet Pay** payment methods
+- Large library of UI components for custom builds
+- Language and currency **localization**
+- Buttons with **haptic feedback**
+- Content pre-loading for smooth user experience
+- Telegram-native design
+- Many **examples/presets**
+- MIT License (free for commercial use)
+
+## Quick Start
+
+For a quick start you can watch video tutorial: [![YouTube | Watch Tutorial](https://img.shields.io/badge/YouTube-Watch%20Tutorial-red?logo=youtube&style=social)](https://www.youtube.com/watch?v=q9R42T-7ykI)
+
+But if you prefer text:
+
+1. Fork this repository and clone it to your local machine
+2. Run command `npm ci` to install all the dependencies (make sure that you have installed `pip` and `node` on your machine)
+3. And now you're ready to run the app with `npm run dev` command
+4. To run python bot put your tokens to `bot/.env` (use `bot/example.env` as an example). Then start the bot with `npm run bot` command
+
+## Configuration
+
+The heart of this project lies in the configuration. By tweaking the configuration file, you can customize the onboarding experience according to your project's requirements. The configuration file can be found at [config.ts](./app/src/config.ts).
+
+For detailed information on configuring the app, refer to the [Configuration Guide](./configuration-guide.md).
+
+## 🌈 Examples/Presets
+
+To help you understand how to create your own configuration, we provide multiple example applications in the `./examples` directory. Each example demonstrates different onboarding scenarios and includes a sample configuration file:
+
+1. Base App
+
+ - Located in: `./app`
+ - Key features: `various slide configurations` `telegram-native design` `onboarding from demo video`
+ - Run command: `npm run dev`
+
+2. Fashion AI App
+
+ - Located in: `./examples/ai`
+ - Key features: `interactive slides` `currency configuration` `vertical products on paywall` `custom preset`
+ - Run command: `npm run dev:ai`
+
+3. Meditation App
+
+ - Located in: `./examples/meditation`
+ - Key features: `localization` `custom icons` `different image styles` `custom preset`
+ - Run command: `npm run dev:meditation`
+
+4. AI Tales App
+
+ - Located in: `./examples/tales`
+ - Key features: `language/currency localization` `interactive flow on onboarding` `custom preset`
+ - Run command: `npm run dev:tales`
+
+5. VPN App
+ - Located in: `./examples/vpn`
+ - Key features: `app created during YouTube tutorial using base preset`
+ - Run command: `npm run dev:vpn`
+
+6. ChatGPT App
+ - Located in: `./examples/chatgpt`
+ - Key features: `onboarding for real bot @chatgpt_karfly_bot` `videos`
+ - Run command: `npm run dev:chagpt`
+
+## Reusable Packages
+
+### [@tok/ui](packages/ui/README.md)
+
+A collection of essential UI components. Explore the potential of these components by visiting our [Figma project](https://www.figma.com/file/ssQqPZ2vqZhD4QF2xyCTd2/Telegram-Onboarding--ToolKit), where you can see them in action and gain a better understanding of their capabilities.
+
+### [@tok/telegram-ui](packages/telegram-ui/README.md)
+
+This package offers a convenient wrapper around the [@twa-dev/sdk](https://github.com/twa-dev/SDK), providing Vue-like components and use-case solutions for Popups, MainButton, BackButton, and Theme integration.
+
+### [@tok/i18n](packages/i18n/README.md)
+
+A minimalistic package for handling localization in your applications. We refrain from using third-party solutions due to concerns about the bundle sizes they introduce. While we provide this solution, feel free to replace it with your own if it better suits your needs.
+
+### [@tok/generation](packages/generation/README.md)
+
+The primary package for generating projects via a configuration file. It offers presets that can be easily extended within the configuration file.
+
+### Tools:
+
+#### [@tok/compress](packages/compress/README.md)
+
+A Node solution for image compression. It processes PNG, JPG, and JPEG files, compressing them and converting them into WEBP, PNG, JPG, or JPEG formats.
+
+#### [@tok/eslint-config](packages/compress/README.md)
+
+A basic ESLint configuration for vue projects to maintain clean and consistent code.
+
+#### [@tok/tsconfig](packages/tsconfig/README.md)
+
+Shared `tsconfig.base.json` file for vue + vite projects
+
+## 3rd party packages:
+
+### [Turborepo](https://turbo.build/)
+
+A package for managing a monorepository. If you haven't heard of it, take a look at its [documentation](https://turbo.build/repo/docs); it's worth it
+
+### [Vite](https://vitejs.dev/)
+
+A package for building and running modern web applications. [Documentation](https://vitejs.dev/guide/)
+
+### [Vue](https://vuejs.org/)
+
+Vue is a JavaScript framework for building user interfaces. [Documentation](https://vuejs.org/)
+
+### [@twa-dev/sdk](https://github.com/twa-dev/SDK)
+
+NPM package for Telegram Mini Apps SDK. Take a look at their [GitHub repository](https://github.com/twa-dev/SDK)
+
+### [Lottie-web](https://github.com/airbnb/lottie-web)
+
+Lottie is a mobile library for Web that parses Adobe After Effects animations exported as json with Bodymovin and renders them natively!
+
+For information on how `sticker.tgs` files are rendered in this project, please refer to [this link](./packages/telegram-ui/components/Sticker/README.md)
+
+## Our team
+
+- [Konstantin Beskrovnyi](https://github.com/Easterok) – Web
+- [Karim Iskakov](https://github.com/karfly) – Python/Product
+- [Michael Browk](https://www.michaelbrowk.com) – Design
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
diff --git a/app/.eslintrc.js b/app/.eslintrc.js
new file mode 100644
index 0000000..a5e5ec9
--- /dev/null
+++ b/app/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+ root: true,
+ extends: ['@tok/eslint-config'],
+ parserOptions: { tsconfigRootDir: __dirname },
+};
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..0d9d64f
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1 @@
+# Hi!
diff --git a/app/_internal/tgs.loader.ts b/app/_internal/tgs.loader.ts
new file mode 100644
index 0000000..8bf2f0a
--- /dev/null
+++ b/app/_internal/tgs.loader.ts
@@ -0,0 +1,37 @@
+import fs from 'fs';
+import { ungzip } from 'pako';
+import type { Plugin } from 'vite';
+
+// find the way to import it as a component
+const tgsRegexp = /\.tgs$/;
+
+export function telegramStickerLoader(): Plugin {
+ return {
+ name: 'telegram-sticker-loader',
+ enforce: 'pre',
+ load(id: string) {
+ if (!id.match(tgsRegexp)) {
+ return;
+ }
+
+ const [path, importType = 'json'] = id.split('?', 2) as [string, 'json'];
+
+ let decodedTgs;
+
+ try {
+ const fileBuffer = fs.readFileSync(path);
+
+ decodedTgs = new TextDecoder('utf-8').decode(ungzip(fileBuffer));
+ } catch (e) {
+ console.error(`\n${id} couldn't be loaded by telegramStickerLoader \n`);
+
+ return;
+ }
+
+ if (importType === 'json') {
+ // https://v8.dev/blog/cost-of-javascript-2019#json
+ return `export default JSON.parse(${JSON.stringify(decodedTgs)})`;
+ }
+ },
+ };
+}
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..11a94d5
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+ Onboarding | Tok
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..ed4d776
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@tok/app",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite --port 3000 --open",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@tok/generation": "*",
+ "vue": "^3.3.4",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@tok/tsconfig": "*",
+ "@tok/eslint-config": "*"
+ }
+}
\ No newline at end of file
diff --git a/app/public/favicon.ico b/app/public/favicon.ico
new file mode 100644
index 0000000..7ea0a83
Binary files /dev/null and b/app/public/favicon.ico differ
diff --git a/app/public/robots.txt b/app/public/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/app/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/app/src/App.vue b/app/src/App.vue
new file mode 100644
index 0000000..7faf63a
--- /dev/null
+++ b/app/src/App.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/app/src/assets/icons/guide.svg b/app/src/assets/icons/guide.svg
new file mode 100644
index 0000000..df65f54
--- /dev/null
+++ b/app/src/assets/icons/guide.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/src/assets/icons/star.svg b/app/src/assets/icons/star.svg
new file mode 100644
index 0000000..8088977
--- /dev/null
+++ b/app/src/assets/icons/star.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/src/assets/icons/time.svg b/app/src/assets/icons/time.svg
new file mode 100644
index 0000000..53f3f72
--- /dev/null
+++ b/app/src/assets/icons/time.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/src/assets/icons/track.svg b/app/src/assets/icons/track.svg
new file mode 100644
index 0000000..f869dc1
--- /dev/null
+++ b/app/src/assets/icons/track.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/src/assets/img/durov.webp b/app/src/assets/img/durov.webp
new file mode 100644
index 0000000..7dd358a
Binary files /dev/null and b/app/src/assets/img/durov.webp differ
diff --git a/app/src/assets/img/spongebob_poster.webp b/app/src/assets/img/spongebob_poster.webp
new file mode 100644
index 0000000..db24f28
Binary files /dev/null and b/app/src/assets/img/spongebob_poster.webp differ
diff --git a/app/src/assets/stickers/duck_cool.tgs b/app/src/assets/stickers/duck_cool.tgs
new file mode 100644
index 0000000..6d4b41f
Binary files /dev/null and b/app/src/assets/stickers/duck_cool.tgs differ
diff --git a/app/src/assets/stickers/duck_hello.tgs b/app/src/assets/stickers/duck_hello.tgs
new file mode 100644
index 0000000..c3837d9
Binary files /dev/null and b/app/src/assets/stickers/duck_hello.tgs differ
diff --git a/app/src/assets/stickers/duck_juggling.tgs b/app/src/assets/stickers/duck_juggling.tgs
new file mode 100644
index 0000000..13e2bca
Binary files /dev/null and b/app/src/assets/stickers/duck_juggling.tgs differ
diff --git a/app/src/assets/stickers/duck_knife.tgs b/app/src/assets/stickers/duck_knife.tgs
new file mode 100644
index 0000000..37f29c9
Binary files /dev/null and b/app/src/assets/stickers/duck_knife.tgs differ
diff --git a/app/src/assets/stickers/duck_love.tgs b/app/src/assets/stickers/duck_love.tgs
new file mode 100644
index 0000000..50c740d
Binary files /dev/null and b/app/src/assets/stickers/duck_love.tgs differ
diff --git a/app/src/assets/stickers/duck_money.tgs b/app/src/assets/stickers/duck_money.tgs
new file mode 100644
index 0000000..f040c62
Binary files /dev/null and b/app/src/assets/stickers/duck_money.tgs differ
diff --git a/app/src/assets/stickers/duck_spy.tgs b/app/src/assets/stickers/duck_spy.tgs
new file mode 100644
index 0000000..c65577a
Binary files /dev/null and b/app/src/assets/stickers/duck_spy.tgs differ
diff --git a/app/src/assets/stickers/duck_xray.tgs b/app/src/assets/stickers/duck_xray.tgs
new file mode 100644
index 0000000..7265392
Binary files /dev/null and b/app/src/assets/stickers/duck_xray.tgs differ
diff --git a/app/src/assets/videos/spongebob.mp4 b/app/src/assets/videos/spongebob.mp4
new file mode 100644
index 0000000..72bf431
Binary files /dev/null and b/app/src/assets/videos/spongebob.mp4 differ
diff --git a/app/src/config.ts b/app/src/config.ts
new file mode 100644
index 0000000..506815f
--- /dev/null
+++ b/app/src/config.ts
@@ -0,0 +1,261 @@
+import { defineConfig } from '@tok/generation';
+
+export default defineConfig({
+ // If you want to add language/currency localization – see ./examples/meditation as reference
+
+ pages: [
+ {
+ slides: [
+ // intro
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_hello.tgs'),
+ size: 250,
+ },
+ shape: 'square',
+ pagination: 'count',
+ title: 'Welcome to Telegram Onboarding Kit',
+ description:
+ "Create stunning onboarding and paywall for your Telegram Bot using the full power of Mini Apps
It's simple, fast, highly customizable and open-source!",
+ button: 'Next',
+ },
+
+ // image
+ {
+ media: {
+ type: 'image',
+ src: import('./assets/img/durov.webp'),
+ },
+ shape: 'rounded',
+ pagination: 'count',
+ title: 'Onboarding supports many types of content',
+ description:
+ "Here you can see Image. But it's just the beginning...",
+ button: 'Next',
+ },
+
+ // sticker
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_love.tgs'),
+ size: 250,
+ },
+ shape: 'square',
+ pagination: 'count',
+ title: 'Telegram stickers',
+ description:
+ 'Just download any .tgs sticker from Telegram and use it in your onboardings',
+ button: 'Next',
+ },
+
+ // form
+ {
+ extends: 'form', // note, it's important to extend from 'form' here
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_spy.tgs'),
+ size: 150,
+ },
+ shape: 'square',
+ pagination: 'count',
+ title: 'Forms',
+ description: 'User fills in the form – the bot receives the data',
+ form: [
+ {
+ id: 'text_from_form',
+ placeholder: 'Text input',
+ type: 'text',
+ },
+ {
+ id: 'number_from_form',
+ placeholder: 'Number input',
+ type: 'number',
+ },
+ {
+ id: 'checkbox_from_form',
+ placeholder: 'Checkbox',
+ type: 'checkbox',
+ },
+ ],
+ button: 'Next',
+ },
+
+ // video
+ {
+ media: {
+ type: 'video',
+ src: import('./assets/videos/spongebob.mp4'),
+ poster: import('./assets/img/spongebob_poster.webp'),
+ style: 'aspect-ratio: 400/287', // here we manually set video aspect-ratio (default is 16:9)
+ },
+ shape: 'rounded',
+ pagination: 'count',
+ title: 'Videos',
+ description:
+ "Typically, video starts automatically
However, on iOS, it will only autoplay upon any prior tap on the page ('Next' button doesn't count). If video doesn't autoplay, user will see preview and pretty animation, inviting them to tap to play the video",
+ button: 'Next',
+ },
+
+ // list
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_juggling.tgs'),
+ size: 150,
+ },
+ shape: 'square',
+ pagination: 'count',
+ title: 'Lists',
+ description:
+ 'Lists can be used to showcase features of your product. Items support customizable icons',
+ list: [
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/guide.svg'),
+ size: 30,
+ },
+ text: 'Some cool feature',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/track.svg'),
+ size: 30,
+ },
+ text: 'Some very cool feature',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/time.svg'),
+ size: 30,
+ },
+ text: 'Some extremely cool feature',
+ },
+ ],
+ button: 'Next',
+ },
+
+ // "everything is customizable" slide
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_xray.tgs'),
+ size: 250,
+ },
+ shape: 'square',
+ pagination: 'count',
+ title: 'Everything is customizable',
+ description: '',
+ textAlign: 'center',
+ list: [
+ 'CSS styles: extend primary colors from Telegram or set yours',
+ 'Button text and actions (look down)',
+ 'Use our carefully crafted presets or easily create your own',
+ ],
+ button: 'Super-Duper Next',
+ },
+
+ // slide with other features
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_cool.tgs'),
+ size: 150,
+ },
+ shape: 'square',
+ pagination: 'count',
+ title: 'Some other features:',
+ description: '',
+ list: [
+ 'One-click 0$ deploy on GitHub Pages',
+ 'Language and currency localization',
+ 'Buttons with haptic feedback',
+ 'Content pre-loading for high speed',
+ 'Low-code approach to building onboardings',
+ 'Many examples/presets',
+ "And many more... (see GitHub)",
+ ],
+ button: 'Next',
+ },
+
+ // go to paywall slide
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_knife.tgs'),
+ size: 250,
+ },
+ shape: 'square',
+ pagination: 'count',
+ textAlign: 'center',
+ title: 'But onboarding slides are not enough...',
+ description: "Let's go to Paywall",
+ button: {
+ content: 'Go to Paywall',
+ to: '/paywall',
+ },
+ },
+ ],
+ },
+
+ // paywall
+ {
+ extends: 'paywall',
+ path: '/paywall',
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/duck_money.tgs'),
+ size: 150,
+ },
+ shape: 'square',
+ title: 'Your beautiful Paywall',
+ list: [
+ 'Adjustable product cards',
+ '👛 Wallet Pay and Telegram Payments ready. Add custom methods easily',
+ 'Subscriptions or One-time payments',
+ ],
+ products: [
+ {
+ id: '1_month_subscription',
+ title: '1 month subscription',
+ description: '2$/month',
+ discount: '',
+ price: 2,
+ },
+ {
+ id: '1_year_subscription',
+ title: '1 year subscription',
+ description: '1$/month',
+ discount: 'Discount 50%',
+ price: 12,
+ },
+ {
+ id: 'lifetime_access',
+ title: 'Lifetime access',
+ description: '20$ once',
+ discount: 'Best offer',
+ price: 20,
+ },
+ ],
+ mainButtonText: 'Buy for {price}',
+ popup: {
+ // popup for payment methods choice
+ type: 'web',
+ },
+ links: [
+ {
+ text: 'Privacy policy',
+ href: 'https://google.com',
+ },
+ {
+ text: 'Terms of use',
+ href: 'https://google.com',
+ },
+ ],
+ },
+ ],
+});
diff --git a/app/src/main.ts b/app/src/main.ts
new file mode 100644
index 0000000..5d02ad1
--- /dev/null
+++ b/app/src/main.ts
@@ -0,0 +1,6 @@
+import { bootstrap } from '@tok/generation';
+
+import App from './App.vue';
+import { default as config } from './config';
+
+bootstrap(App, config);
diff --git a/app/src/styles.scss b/app/src/styles.scss
new file mode 100644
index 0000000..fd45b24
--- /dev/null
+++ b/app/src/styles.scss
@@ -0,0 +1,8 @@
+// See https://github.com/Easterok/telegram-onboarding-kit/tree/main/packages/ui/styles
+// on how to override global CSS parameters
+
+:root {
+}
+
+html[data-theme='dark'] {
+}
diff --git a/app/tsconfig.json b/app/tsconfig.json
new file mode 100644
index 0000000..1618238
--- /dev/null
+++ b/app/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@tok/tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"]
+}
diff --git a/app/types/tgs.d.ts b/app/types/tgs.d.ts
new file mode 100644
index 0000000..cf5ae23
--- /dev/null
+++ b/app/types/tgs.d.ts
@@ -0,0 +1,9 @@
+declare type TelegramStickerJson = {
+ tgs: number;
+ v: string;
+ fr: number;
+};
+
+declare module '*.tgs' {
+ export default TelegramStickerJson;
+}
diff --git a/app/vite.config.ts b/app/vite.config.ts
new file mode 100644
index 0000000..6c99739
--- /dev/null
+++ b/app/vite.config.ts
@@ -0,0 +1,30 @@
+import vue from '@vitejs/plugin-vue';
+import { defineConfig } from 'vite';
+import svgLoader from 'vite-svg-loader';
+
+import { telegramStickerLoader } from './_internal/tgs.loader';
+
+export default defineConfig({
+ plugins: [
+ telegramStickerLoader(),
+ vue(),
+ svgLoader({
+ defaultImport: 'component',
+ svgoConfig: {
+ plugins: [
+ {
+ name: 'cleanupIds',
+ params: {
+ remove: false,
+ minify: false,
+ },
+ },
+ ],
+ },
+ }),
+ ],
+ build: {
+ assetsInlineLimit: 0,
+ minify: true,
+ },
+});
diff --git a/bot/bot.py b/bot/bot.py
new file mode 100644
index 0000000..7d39bec
--- /dev/null
+++ b/bot/bot.py
@@ -0,0 +1,439 @@
+# This this code for demo bot of Telegram Onboarding Kit
+#
+# Use this code as an example of how to use Telegram Onboarding Kit in your bot:
+# 1. How to add URL of your Mini App onboadring to button
+# 2. How to get data from Mini App onboadring (e.g. filled in form or product info)
+# 3. How to send ivoices and handle successful payments for Telegram Payments/👛 Wallet Pay
+#
+# Happy coding!
+
+
+import json
+import os
+import re
+import urllib.parse
+import uuid
+from typing import Any, Dict, Optional, Tuple
+import httpx
+from dotenv import load_dotenv
+
+from telegram import (
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ KeyboardButton,
+ LabeledPrice,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ Update,
+ User,
+ WebAppInfo,
+)
+from telegram.constants import ChatAction, ParseMode
+from telegram.ext import (
+ ApplicationBuilder,
+ CallbackQueryHandler,
+ CommandHandler,
+ ContextTypes,
+ MessageHandler,
+ PreCheckoutQueryHandler,
+ filters,
+)
+
+
+# region: helper functions
+def get_user_data(user: User) -> Dict[str, Any]:
+ """Get user data from user object"""
+ return {
+ "language_code": user.language_code,
+ }
+
+
+def add_get_params_to_url(url: str, user_data: Dict[str, Any]):
+ query_string = urllib.parse.urlencode(user_data)
+ return f"{url}?{query_string}"
+
+
+def remove_html_tags(text: str) -> str:
+ """Remove html tags from a string and replace tags with a space"""
+ # replace tags with a space
+ text = re.sub("", " ", text)
+
+ # remove all other HTML tags
+ return re.sub("<.*?>", "", text)
+
+
+# endregion: helper functions
+
+
+async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ user_data = get_user_data(update.effective_user)
+
+ text = (
+ f"♥️ Hi! I'm demo bot for Telegram Onboarding Kit\n"
+ f"\n"
+ f"Below you can see demo onboardings created with our kit. It's better to you watch them from 📱 mobile device\n"
+ f"\n"
+ f"Your language code: {user_data['language_code']}\n"
+ )
+
+ user_data = get_user_data(update.effective_user)
+
+ reply_markup = ReplyKeyboardMarkup.from_column(
+ [
+ KeyboardButton(
+ text="🌈 Base Onboarding",
+ web_app=WebAppInfo(
+ url=add_get_params_to_url(
+ "https://easterok.github.io/telegram-onboarding-kit", user_data
+ )
+ ),
+ ),
+ KeyboardButton(
+ text="💃 Fashion AI Bot",
+ web_app=WebAppInfo(
+ url=add_get_params_to_url("https://tok-ai.netlify.app", user_data)
+ ),
+ ),
+ KeyboardButton(
+ text="🧘 Meditation Bot",
+ web_app=WebAppInfo(
+ url=add_get_params_to_url(
+ "https://tok-meditation.netlify.app", user_data
+ )
+ ),
+ ),
+ KeyboardButton(
+ text="🧚♂️ AI Tales Bot",
+ web_app=WebAppInfo(
+ url=add_get_params_to_url(
+ "https://tok-wondertales.netlify.app", user_data
+ )
+ ),
+ ),
+ KeyboardButton(
+ text="🔐 VPN Onboarding",
+ web_app=WebAppInfo(
+ url=add_get_params_to_url(
+ "https://tok-vpn.netlify.app", user_data
+ )
+ ),
+ ),
+ KeyboardButton(
+ text="🧠 ChatGPT Onboarding",
+ web_app=WebAppInfo(
+ url=add_get_params_to_url(
+ "https://tok-chatgpt.netlify.app", user_data
+ )
+ ),
+ ),
+ ]
+ )
+
+ await update.effective_message.reply_text(
+ text=text,
+ reply_markup=reply_markup,
+ parse_mode=ParseMode.HTML,
+ disable_web_page_preview=True,
+ )
+
+
+async def get_data_from_mini_app(
+ update: Update, context: ContextTypes.DEFAULT_TYPE
+) -> None:
+ """This handler is called when user sends data from mini app"""
+
+ data = json.loads(update.effective_message.web_app_data.data)
+ payload, product = data["payload"], data["product"]
+
+ # send received payload
+ if payload:
+ payload_str = json.dumps(payload, indent=4)
+ text = f"📦 Got data from onboarding:\n" f"{payload_str}"
+
+ await update.effective_message.reply_text(
+ text=text,
+ reply_markup=ReplyKeyboardRemove(),
+ )
+
+ await send_invoice(update, context, product)
+
+
+async def send_invoice(
+ update: Update, context: ContextTypes.DEFAULT_TYPE, product: Dict
+) -> None:
+ if product["payment_method"] not in context.bot_data["payment_tokens"]:
+ await send_message_that_payment_method_is_not_supported(
+ update, context, product["payment_method"]
+ )
+ return
+
+ if product["payment_method"] == "telegram_payments":
+ await send_telegram_payment_invoice(update, context, product)
+ elif product["payment_method"] == "wallet_pay":
+ await send_wallet_pay_invoice(update, context, product)
+ else:
+ raise ValueError(f"Unknown payment method: {product['payment_method']}")
+
+
+async def send_message_that_payment_method_is_not_supported(
+ update: Update, context: ContextTypes.DEFAULT_TYPE, payment_method: str
+) -> None:
+ text = f"🥲 Sorry, {payment_method} payment method is not supported"
+ await update.effective_message.reply_text(
+ text=text,
+ parse_mode=ParseMode.HTML,
+ )
+
+
+async def send_message_about_successful_payment(
+ update: Update, context: ContextTypes.DEFAULT_TYPE
+) -> None:
+ await update.effective_message.delete() # delete message with invoice
+
+ await update.effective_message.reply_text(
+ text=f"🎉 Successful payment!",
+ reply_markup=None,
+ parse_mode=ParseMode.HTML,
+ )
+
+
+# region: telegram payments
+async def send_telegram_payment_invoice(
+ update: Update, context: ContextTypes.DEFAULT_TYPE, product: Dict
+) -> None:
+ """Send invoice with Telegram Payments. Docs: https://core.telegram.org/bots/payments"""
+
+ text = (
+ f"⚠️ It's test mode\n"
+ f"You can use test card number 4242 4242 4242 4242 with any CVC and any future expiration date for testing"
+ )
+ await update.effective_message.reply_text(
+ text=text,
+ parse_mode=ParseMode.HTML,
+ )
+
+ # telegram invoices don't support html, so let's remove html tags from title and description
+ title = remove_html_tags(product["title"])
+ description = remove_html_tags(product["description"])
+
+ await update.effective_message.reply_invoice(
+ title=title,
+ description=description,
+ currency=product["currency"],
+ prices=[LabeledPrice(title, int(product["price"] * 100))],
+ provider_token=context.bot_data["payment_tokens"]["telegram_payments"],
+ payload="some_payload_could_be_here",
+ )
+
+
+async def telegram_payment_pre_checkout(
+ update: Update, context: ContextTypes.DEFAULT_TYPE
+) -> None:
+ await update.pre_checkout_query.answer(ok=True)
+
+
+async def successful_telegram_payment(
+ update: Update, context: ContextTypes.DEFAULT_TYPE
+) -> None:
+ await send_message_about_successful_payment(update, context)
+
+
+# endregion: telegram payments
+
+
+# region: wallet pay
+async def send_wallet_pay_invoice(
+ update: Update, context: ContextTypes.DEFAULT_TYPE, product: Dict
+) -> None:
+ """Send invoice with Wallet Pay. Docs: https://docs.wallet.tg/pay"""
+
+ await update.effective_chat.send_action(
+ ChatAction.TYPING
+ ) # send typing action here, because wallet pay request could take some time
+
+ async def create_invoice() -> Tuple[str, str]:
+ url = "https://pay.wallet.tg/wpay/store-api/v1/order"
+
+ headers = {
+ "Wpay-Store-Api-Key": context.bot_data["payment_tokens"]["wallet_pay"],
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ }
+
+ bot_url = f"https://t.me/{context.bot.username}"
+ data = {
+ "amount": {
+ "amount": product["price"],
+ "currencyCode": product["currency"],
+ },
+ "externalId": str(uuid.uuid4()),
+ "customerTelegramUserId": update.effective_user.id,
+ "timeoutSeconds": 3600,
+ "description": product["title"],
+ "returnUrl": bot_url,
+ "failReturnUrl": bot_url,
+ "customData": "",
+ }
+
+ async with httpx.AsyncClient() as client:
+ result = await client.post(url, headers=headers, data=json.dumps(data))
+
+ result_json = result.json()
+ if result_json["status"] != "SUCCESS":
+ raise Exception(
+ f"Wallet Pay create invoice status != SUCCESS. Reason: {result_json['message']}"
+ )
+
+ return result_json["data"]["directPayLink"], result_json["data"]["id"]
+
+ # create invoice
+ direct_pay_link, order_id = await create_invoice()
+
+ # send invoice
+ ## remove html tags from title and description to avoid bad markup
+ title = remove_html_tags(product["title"])
+ description = remove_html_tags(product["description"])
+
+ text = (
+ f"{title} ({product['price']} {product['currency']})\n"
+ f"{description}\n"
+ f"\n"
+ f"Tap 👛 Wallet Pay button to pay. After you pay, tap Check payment status button\n"
+ f"\n"
+ f"⚠️ Note: there is no test mode, all payments are carried out in real assets!"
+ )
+
+ reply_markup = InlineKeyboardMarkup.from_column(
+ [
+ InlineKeyboardButton(
+ text="👛 Wallet Pay",
+ url=direct_pay_link,
+ ),
+ InlineKeyboardButton(
+ text="Check payment status",
+ callback_data=f"check_wallet_pay_payment_status|{order_id}",
+ ),
+ ]
+ )
+
+ await update.effective_message.reply_text(
+ text=text,
+ reply_markup=reply_markup,
+ parse_mode=ParseMode.HTML,
+ )
+
+
+async def check_wallet_pay_payment_status(
+ update: Update, context: ContextTypes.DEFAULT_TYPE
+) -> None:
+ order_id = update.callback_query.data.split("|")[1]
+
+ async def check_invoice_status() -> bool:
+ url = "https://pay.wallet.tg/wpay/store-api/v1/order/preview"
+
+ headers = {
+ "Wpay-Store-Api-Key": context.bot_data["payment_tokens"]["wallet_pay"],
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ }
+
+ params = {"id": order_id}
+
+ async with httpx.AsyncClient() as client:
+ result = await client.get(url, headers=headers, params=params)
+
+ result_json = result.json()
+ return result_json["data"]["status"] == "PAID"
+
+ # check invoice status
+ is_paid = await check_invoice_status()
+ if is_paid:
+ await update.callback_query.answer()
+ await send_message_about_successful_payment(update, context)
+ else:
+ await update.callback_query.answer("🥲 Not paid yet")
+
+
+# endregion: wallet pay
+
+
+async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ error = context.error
+
+ text = f"🥲 Some error happened...\n" f"Error: {error}"
+ await update.effective_message.reply_text(
+ text=text,
+ parse_mode=ParseMode.HTML,
+ )
+
+ raise error
+
+
+def run_bot(
+ bot_token: str,
+ telegram_payments_token: Optional[str] = None,
+ wallet_pay_token: Optional[str] = None,
+) -> None:
+ application = (
+ ApplicationBuilder()
+ .token(bot_token)
+ .concurrent_updates(True)
+ .http_version("1.1")
+ .get_updates_http_version("1.1")
+ .build()
+ )
+
+ # store payment tokens in bot data
+ application.bot_data["payment_tokens"] = {}
+
+ for payment_method, payment_token in [
+ ("telegram_payments", telegram_payments_token),
+ ("wallet_pay", wallet_pay_token),
+ ]:
+ if payment_token:
+ application.bot_data["payment_tokens"][payment_method] = payment_token
+
+ # handlers
+ ## /start
+ application.add_handler(CommandHandler("start", start))
+
+ ## get data from mini app
+ application.add_handler(
+ MessageHandler(filters.StatusUpdate.WEB_APP_DATA, get_data_from_mini_app)
+ )
+
+ ## payment
+ application.add_handler(PreCheckoutQueryHandler(telegram_payment_pre_checkout))
+ application.add_handler(
+ MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_telegram_payment)
+ )
+
+ application.add_handler(
+ CallbackQueryHandler(
+ check_wallet_pay_payment_status, pattern="^check_wallet_pay_payment_status"
+ )
+ )
+
+ # error handler
+ application.add_error_handler(error_handler)
+
+ # start the bot
+ print("Starting the bot...")
+ application.run_polling(allowed_updates=Update.ALL_TYPES)
+
+
+if __name__ == "__main__":
+ load_dotenv() # load variables from .env file (don't forget to fill it!)
+
+ bot_token = os.getenv("BOT_TOKEN")
+ if not bot_token:
+ raise ValueError("Invalid BOT_TOKEN in .env file")
+
+ telegram_payments_token = os.getenv("TELEGRAM_PAYMENTS_TOKEN")
+ wallet_pay_token = os.getenv("WALLET_PAY_TOKEN")
+
+ run_bot(
+ bot_token=bot_token,
+ telegram_payments_token=telegram_payments_token if telegram_payments_token else None,
+ wallet_pay_token=wallet_pay_token if wallet_pay_token else None,
+ )
diff --git a/bot/example.env b/bot/example.env
new file mode 100644
index 0000000..c511376
--- /dev/null
+++ b/bot/example.env
@@ -0,0 +1,3 @@
+BOT_TOKEN="" # (required) Telegram bot token from @BotFather
+TELEGRAM_PAYMENTS_TOKEN="" # (optional) Telegram Payments token from @BotFather. If not specified, Telegram Payments method will not work
+WALLET_PAY_TOKEN="" # (optional) Token for Wallet Pay from pay.wallet.tg. If not specified, Wallet Pay method will not work
\ No newline at end of file
diff --git a/bot/package.json b/bot/package.json
new file mode 100644
index 0000000..3b891bf
--- /dev/null
+++ b/bot/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@tok/bot",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "python -m pip install -r requirements.txt && python ./bot.py"
+ }
+}
\ No newline at end of file
diff --git a/bot/requirements.txt b/bot/requirements.txt
new file mode 100644
index 0000000..e9b2c6a
--- /dev/null
+++ b/bot/requirements.txt
@@ -0,0 +1,2 @@
+python-telegram-bot==20.5
+python-dotenv==0.21.0
diff --git a/configuration-guide.md b/configuration-guide.md
new file mode 100644
index 0000000..0208435
--- /dev/null
+++ b/configuration-guide.md
@@ -0,0 +1,1521 @@
+# Configuration Guide
+
+1. [DefineConfig and Bootstrap](#defineconfig-and-bootstrap)
+2. [Pressets](#pressets)
+ - [base](#base)
+ - [slide](#slide)
+ - [Standalone Page](#standalone-page)
+ - [Inside base.presset](#inside-basepresset)
+ - Parameters
+ - [Title](#title-required)
+ - [Button](#button-required)
+ - [Description](#description-optional)
+ - [Media](#media-optional)
+ - [List](#list-optional)
+ - [Pagination](#pagination-optional)
+ - [TextAlign](#textalign-optional)
+ - [Shape](#shape-optional)
+ - [Background](#background-optional)
+ - [form](#form)
+ - Parameters
+ - [Form](#form)
+ - [paywall](#paywall)
+ - Parameters
+ - [products](#products-required)
+ - [links](#links-required)
+ - [mainButtonText](#mainbuttontext-optional)
+ - [popup](#popup-optional)
+ - [Type](#type)
+ - [Title](#title)
+ - [Message](#message)
+ - [Buttons](#buttons)
+ - [paywall_single](#paywall_single)
+ - Parameters
+ - [product](#product-required)
+ - [paywall_row](#paywall_row)
+ - Parameters
+ - [products](#products-required-1)
+3. [Media](#media)
+ - [Image](#image)
+ - [Sticker](#sticker)
+ - [Video](#video)
+ - [Icon](#icon)
+ - [Emodji](#emodji)
+4. [Button Actions](#button-actions)
+5. [Theme](#theme)
+6. [Currency Config](#currency-config)
+7. [Localization](#localization)
+8. [Currency localization](#currency-localization)
+9. [Custom Pressets](#custom-pressets)
+
+# DefineConfig and Bootstrap
+
+You can check their area of responsibility [here](./packages/generation/README.md)
+
+The structure of your configuration:
+
+```ts
+export default defineConfig({
+ // [Required]. App pages configuration.
+ // More info in the "Pressets" section
+ pages: [...],
+
+ // [Optional]. Theme configuration.
+ // More information in the "Theme" section
+ theme: '',
+
+ // [Optional]. Localization configuration.
+ // More information in the "Localization" section
+ locale: {},
+
+ // [Optional]. Currency configuration.
+ // More information in the "Currency Config" section
+ currencyConfig: {},
+
+ // [Optional]. custom pressets configuration
+ // More info in the "Custom Pressets" section
+ definePressets: {},
+});
+```
+
+# Pressets
+
+In the `pages` section of your configuration file, you can extend our presets with the following code:
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'base',
+ },
+ {
+ extends: 'slide',
+ },
+ {
+ extends: 'form',
+ },
+ // ...
+ ],
+});
+```
+
+You can see all available extensions in the examples below
+
+## base
+
+```json
+{
+ "extends": "base",
+ "slides": []
+}
+```
+
+[Link to the component](./packages/generation/pressets/base/README.md)
+
+This is the main preset of your application, representing a [Carousel](./packages/ui/components/Carousel/README.md). You can use it without defining an extends keyword. We will automatically resolve this preset **only for root elements** inside the pages section
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ // optional
+ // extends: 'base',
+ slides: [],
+ },
+ ],
+});
+```
+
+The only required parameter for this `base.presset` is `slides`, which takes an array of other presets.
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ slides: [
+ {
+ extends: 'slide',
+ // ...
+ },
+ {
+ extends: 'from',
+ // ...
+ },
+ // ...
+ ],
+ },
+ ],
+});
+```
+
+By default, if there is no `extends` keyword inside the `slides`, the `Base` will use [slide presset](#slide)
+
+So, this is valid config for this case:
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ slides: [
+ {
+ title: 'Title',
+ // ...
+ },
+ // ...
+ ],
+ },
+ ],
+});
+```
+
+## slide
+
+```json
+{
+ "extends": "slide"
+}
+```
+
+[Link to the component](./packages/generation/pressets/slide/README.md)
+
+This presset can be used as a standalone page or inside [base.presset](#base)
+
+### Standalone page
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ // it's important to use a extends: 'slide' here for a standalone page
+ extends: 'slide',
+ },
+ ],
+});
+```
+
+### Inside base.presset
+
+Refer to the examples above for [base.presset](#base)
+
+### Parameters
+
+The examples below are for standalone page usage. The same configuration can be used for a slide inside [base.presset](#base).
+
+And `Button` will be shown only inside Telegram
+
+> [!IMPORTANT]
+> All text parameters support HTML inside strings. For example:
+>
+> ```
+> title: 'Hello World!'
+> ```
+>
+> Please ensure that you are writing safe HTML.
+
+#### Title [Required]
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_title.png)
+
+
+
+#### Button [Required]
+
+You can configurate some actions by clicking on the MainButton with this parameter. [More about this see here](#button-actions)
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_title_button.png)
+
+
+
+### Description [Optional]
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_title_description_button.png)
+
+
+
+### Media [Optional]
+
+All available media parameters see [here](#media)
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ media: {
+ type: 'image',
+ // you can use here src as webp, png, jpg and so on
+ src: import('./assets/img/base.webp'),
+ },
+ title: 'Title',
+ description: 'Description',
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+Keep in mind that all media pressets with `type="image"` by default will be with aspect-ratio: 1/1
+
+![preview](./docs/images/slide_media.png)
+
+
+
+### List [Optional]
+
+You can write list as array of string. We will add default icon for them
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_list.png)
+
+
+
+Our you can write list as array of objects with parameters:
+
+```ts
+{
+ // media is optional. All available media parameters see media section of this page
+ media?: {},
+ text: 'string',
+}
+```
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ list: [
+ {
+ text: 'item 1',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/star.svg'),
+ },
+ text: 'item 2',
+ },
+ ],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_list_media.png)
+
+
+
+### Pagination [Optional]
+
+If you are using the [slide.presset](#slide) inside slides in [base.presset](#base), you can use the `pagination` parameter to show the counter on the page. If it's not inside [base.presset](#base), pagination won't be displayed.
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ slides: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ pagination: 'count',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ ],
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_pagination.png)
+
+
+
+### TextAlign [Optional]
+
+You can set the text alignment of your content using the `textAlign` parameter.
+
+Available options: `'left' | 'right' | 'center'`
+Default: `'left'`
+
+> [!Note]
+> This parameter won't affect list and forms
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ textAlign: 'right',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_align.png)
+
+
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ slides: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ textAlign: 'center',
+ pagination: 'count',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ ],
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_center_pagination.png)
+
+
+
+### Shape [Optional]
+
+You can set shape of your content with help of `shape` parameter
+
+Available parameters: `'rounded' | 'stacked' | 'square'`
+default: `'square'`
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ media: {
+ type: 'image',
+ src: import('./assets/img/durov.webp'),
+ },
+ description: 'Description',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ {
+ extends: 'slide',
+ title: 'Title',
+ media: {
+ type: 'image',
+ src: import('./assets/img/durov.webp'),
+ },
+ description: 'Description',
+ shape: 'stacked',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ {
+ extends: 'slide',
+ title: 'Title',
+ media: {
+ type: 'image',
+ src: import('./assets/img/durov.webp'),
+ },
+ shape: 'rounded',
+ description: 'Description',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+shape: 'square' (default)
+![preview](./docs/images/slide_square.png)
+
+shape: 'stacked'
+![preview](./docs/images/slide_stacked.png)
+
+shape: 'rounded'
+![preview](./docs/images/slide_rounded.png)
+
+
+
+### Background [Optional]
+
+You can set a custom background for the content on the slide if want.
+
+This parameter will apply only to the content inside the slide, not the entire page
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ title: 'Title',
+ description: 'Description',
+ background: '#00ff00',
+ list: ['Item 1', 'Item 2', 'Item 3'],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/slide_background.png)
+
+
+
+## form
+
+```json
+{
+ "extends": "form"
+}
+```
+
+[Link to the component](./packages/generation/pressets/form/README.md)
+
+This preset supports all the parameters from the [slide](#slide) and adds a new one: `form`.
+
+### Parameters
+
+#### Form
+
+You can specify your form with this parameter.
+Ensure that your IDs are unique
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'form',
+ title: 'Title',
+ description: 'Description',
+ form: [
+ {
+ id: 'id1',
+ type: 'text',
+ placeholder: 'Text input',
+ },
+ {
+ id: 'id2',
+ type: 'number',
+ placeholder: 'Number input',
+ },
+ {
+ id: 'id3',
+ type: 'checkbox',
+ placeholder: 'Checkbox input',
+ },
+ ],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/form.png)
+
+
+
+If you are familiar with other input types, you can also use them, such as type='date'.
+
+All native input attributes are available.
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'form',
+ title: 'Title',
+ description: 'Description',
+ form: [
+ {
+ id: 'id1',
+ type: 'date',
+ placeholder: 'Input date',
+ },
+ ],
+ button: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/form_date.png)
+
+
+
+## paywall
+
+```json
+{
+ "extends": "paywall"
+}
+```
+
+[Link to the component](./packages/generation/pressets/paywall/README.md)
+
+The currency can be customized. [See these examples](#currency-config).
+
+This preset supports all the parameters except `button` from the [slide](#slide) and adds new ones:
+
+### Parameters
+
+#### products [Required]
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ {
+ id: 'id1',
+ title: 'Title',
+ // you can show price instead of description with this "trick"
+ description: '',
+ price: 10,
+ },
+ {
+ id: 'id2',
+ title: 'Title 2',
+ description: 'Description 2',
+ price: 20,
+ discount: 'Discount text',
+ },
+ ],
+ links: [],
+ mainButtonText: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall.png)
+
+
+
+#### links [Required]
+
+To display a link for terms of use and other necessary information for subscriptions or payments.
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [
+ {
+ text: 'Link 1',
+ href: 'https://google.com'
+ },
+ {
+ text: 'Link 2',
+ href: 'https://google.com'
+ }
+ ],
+ mainButtonText: 'Button',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_links.png)
+
+
+
+#### mainButtonText [Optional]
+
+By default will be with "Continue" text
+
+You can show price inside main button text with this config:
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [],
+ mainButtonText: 'Subscribe for {price}',
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_price.png)
+
+
+
+#### popup [Optional]
+
+By default will be show our predefinned popup
+
+##### type
+
+By default: 'telegram'
+
+You can tell that you want to show only web popup with this parameter:
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [],
+ popup: {
+ type: 'web'
+ },
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_popup_web.png)
+
+
+
+##### title
+
+By default: 'Choose the payment method'
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [],
+ popup: {
+ type: 'web',
+ title: 'Are you happy?'
+ },
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_popup_title.png)
+
+
+
+##### message
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [],
+ popup: {
+ type: 'web',
+ title: 'Are you happy?',
+ message: 'Tell me'
+ },
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_popup_message.png)
+
+
+
+##### buttons
+
+By default: ['telegram_payments', 'wallet_pay']
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [],
+ popup: {
+ type: 'web',
+ title: 'Are you happy?',
+ buttons: [
+ {
+ id: 'yes',
+ text: 'Yes!',
+ type: 'default'
+ },
+ {
+ id: 'no',
+ text: 'No!',
+ type: 'destructive'
+ }
+ ]
+ },
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_popup_buttons.png)
+
+
+
+If the popup mode is `type='web'` you can specify the media parameter inside buttons:
+
+All available parameters for [media see here](#media)
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ ...
+ ],
+ links: [],
+ popup: {
+ type: 'web',
+ title: 'Are you happy?',
+ buttons: [
+ {
+ id: 'yes',
+ text: 'Yes!',
+ type: 'default',
+ media: {
+ type: 'emodji',
+ src: '💳'
+ }
+ },
+ {
+ id: 'no',
+ text: 'No!',
+ type: 'destructive',
+ media: {
+ type: 'emodji',
+ src: '💳'
+ }
+ }
+ ]
+ },
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_popup_buttons_media.png)
+
+
+
+## paywall_single
+
+```json
+{
+ "extends": "paywall_single"
+}
+```
+
+[Link to the component](./packages/generation/pressets/paywall_single/README.md)
+
+This preset supports all the parameters except `products` from the [paywall](#paywall) and add new once:
+
+### Parameters
+
+#### product [Required]
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall_single',
+ title: 'Title',
+ description: 'Description',
+ product: {
+ id: 'id',
+ title: 'Product Title',
+ price: 99,
+ description: 'Product Description',
+ },
+ links: [],
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_single.png)
+
+
+
+And you can add some media for your product:
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall_single',
+ title: 'Title',
+ description: 'Description',
+ product: {
+ id: 'id',
+ title: 'Product Title',
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/star.svg'),
+ size: 40,
+ },
+ price: 99,
+ description: 'Product Description',
+ },
+ links: [],
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_single_media.png)
+
+
+
+## paywall_row
+
+```json
+{
+ "extends": "paywall_row"
+}
+```
+
+[Link to the component](./packages/generation/pressets/paywall_row/README.md)
+
+This preset supports all the parameters from the [paywall](#paywall) but overrides products:
+
+### Parameters
+
+#### products [Required]
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall_row',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ {
+ id: 'id1',
+ price: 4.99,
+ title: '4 credits',
+ description: 'Perfect to start with',
+ },
+ {
+ id: 'id2',
+ price: 8.99,
+ title: '20 credits',
+ description: 'Best value offer',
+ bestText: 'Best Choice',
+ },
+ {
+ id: 'id3',
+ price: 19.99,
+ title: '100 credits',
+ description: 'For true enthusiasts',
+ },
+ ],
+ links: [],
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+![preview](./docs/images/paywall_row.png)
+
+
+
+If you have a lot of products or only two, don't worry this component will take care of how to display them
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'paywall_row',
+ title: 'Title',
+ description: 'Description',
+ products: [
+ {
+ id: 'id1',
+ // ...
+ },
+ {
+ id: 'id2',
+ // ...
+ },
+ {
+ id: 'id3',
+ // ...
+ },
+ {
+ id: 'id4',
+ // ...
+ },
+ {
+ id: 'id5',
+ // ...
+ },
+ {
+ id: 'id6',
+ // ...
+ },
+ ],
+ links: [],
+ },
+ ],
+});
+```
+
+
+ With the code above you will get this result:
+
+How it looks like
+![preview](./docs/images/paywall_row_page.png)
+
+Expanded screenshot
+![preview](./docs/images/paywall_row_multiple.png)
+
+
+
+# Media
+
+[Link to the component](./packages/generation/components/Media/README.md)
+
+Media supports 5 types of content:
+
+## Image
+
+```ts
+{
+ media: {
+ type: 'image',
+ src: import('path/to/image.png'),
+ // Optional.
+ webp?: import('path/to/image.webp'),
+ // Optional. If you aren't happy with our styles you can override them with this props
+ style?: 'margin: ...; position: absolute; ...'
+ }
+}
+```
+
+Styles by default:
+
+```css
+.image {
+ aspect-ratio: 1/1;
+}
+```
+
+## Sticker
+
+```ts
+{
+ media: {
+ type: 'image',
+ src: import('path/to/sticker.tgs'),
+ // Optional.
+ // size paramter of the sticker
+ // if it's a number, like `size: 20` it will translate to:
+ // {
+ // width: 20px;
+ // height: 20px;
+ // }
+ size?: number | [number, number],
+ // if it's a [number, number], like `size: [20, 40]` it will translate to:
+ // {
+ // width: 20px;
+ // height: 40px;
+ // },
+ // Optional
+ style?: 'margin: ...; position: absolute; ...'
+ }
+}
+```
+
+Styles by default:
+
+```css
+.sticker {
+ aspect-ratio: 1/1;
+ max-height: 40vw;
+}
+```
+
+## Video
+
+```ts
+{
+ media: {
+ type: 'video',
+ src: import('path/to/video.mp4'),
+ // Optional.
+ // For a better user experience, you need to add this
+ poster?: import('path/to/poster.png'),
+ // Optional
+ style?: 'margin: ...; position: absolute; ...'
+ }
+}
+```
+
+Styles by default:
+
+```css
+.video {
+ aspect-ratio: 16/9;
+}
+```
+
+## Icon
+
+```ts
+{
+ media: {
+ type: 'icon',
+ src: import('path/to/icon.svg'),
+ // Optional.
+ size?: number | [number, number],
+ // Optional
+ style?: 'margin: ...; position: absolute; ...'
+ }
+}
+```
+
+Styles by default:
+
+```css
+.icon {
+ width: 1.5em;
+ height: 1.5em;
+}
+```
+
+## Emodji
+
+```ts
+{
+ media: {
+ type: 'emodji',
+ src: '💳',
+ // Optional
+ size?: number | [number, number],
+ // Optional
+ style?: 'margin: ...; position: absolute; ...'
+ }
+}
+```
+
+Styles by default:
+
+```css
+.emodji {
+ width: 1.625em;
+ height: 1.625em;
+}
+```
+
+# Button Actions
+
+You can specify button action behavior inside [slide.presset](#slide).
+It will be shown as text with the default behavior of navigating to the next slide if possible.
+
+> [!NOTE]
+> For the [paywalls](#paywall) you can't override button behavior
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ button: 'Text',
+ },
+ ],
+});
+```
+
+Alternatively, you can navigate to a different page.
+However, you need to specify the name of that page within your configuration.
+Here's how you can do it:
+
+```ts
+export default defineConfig({
+ pages: [
+ {
+ extends: 'slide',
+ button: {
+ content: 'Go to paywall',
+ to: '/paywall', // this
+ },
+ },
+ {
+ extends: 'paywall',
+ path: '/paywall', // and this will be connected
+ },
+ ],
+});
+```
+
+> [!NOTE]
+> If you need more actions, let us know, and we will easily add them
+
+# Theme
+
+You can specify what theme do you want to use inside your application
+
+By default it will inherit Telegram.colorScheme
+
+```ts
+export default defineConfig({
+ theme: 'light' | 'dark' | 'auto',
+});
+```
+
+# Currency Config
+
+You can specify what currency do you want to use inside your application
+
+These are the parameters you can change and their default values:
+
+```ts
+const config = {
+ // currency symbol alignment
+ // default: 'left'
+ align?: 'left' | 'right';
+
+ // currency symbol
+ // default: 'USD'
+ currency?: CurrencyVariants;
+
+ // separator for decimal 1.00 or 1,00 as you wish
+ // default '.'
+ decimalSeparator?: string;
+
+ // separator for thousand 1_000_000 or 1x000x000
+ // default ' '
+ thousandSeparator?: string;
+}
+```
+
+It's support [localization](#currency-localization)
+
+```ts
+export default defineConfig({
+ currencyConfig: {
+ currency: 'AUD',
+ align: 'right',
+ },
+});
+```
+
+# Localization
+
+For localization you need to define `json` files with your tokens and localized values.
+For example:
+
+```json
+// ru.json
+{
+ "_hello": "Привет!"
+}
+
+// en.json
+{
+ "_hello": "Hello!"
+}
+```
+
+```ts
+export default defineConfig({
+ locale: {
+ fallback: 'en',
+ ru: import('./locales/ru.json'),
+ en: import('./locales/en.json'),
+ },
+ pages: [
+ {
+ extends: 'slide',
+ // this will automaticaly translated
+ title: '_hello',
+ },
+ ],
+});
+```
+
+> [!NOTE]
+> Our i18n logic is extremely simple. You can use tokens like `slide1.product.title`.
+> To correctly resolve the token value, your config should contain this:
+
+```json
+// locales/en.json
+{
+ "slide1": {
+ "product": {
+ "title": "Product Title"
+ }
+ }
+}
+```
+
+> Do not try to trick it with values like:
+> we haven't tested it yet :(
+
+```json
+// locales/en.json
+{
+ "slide1.product.title": "Product Title"
+}
+```
+
+If your application supports the user's language, that language will be downloaded, and then the application will call the `Telegram.ready()` method to indicate that the app is ready for use.
+
+If the user's language is not supported, the application will switch to the fallback language and download the locale for it.
+
+If you haven't defined a locale, we will call the `Telegram.ready()` method right after the main page of the application is mounted.
+
+# Currency localization
+
+You can also use localization within the currency configuration:
+
+```json
+// ru.json
+{
+ "_currency": "RUB",
+ "_price": 1000,
+ "_align": "right",
+ "_decimal": ",",
+ "_thousands": "_"
+}
+
+// en.json
+{
+ "_currency": "USD",
+ "_price": 10,
+ "_align": "left",
+ "_decimal": ".",
+ "_thousands": " "
+}
+```
+
+```ts
+export default defineConfig({
+ locale: {
+ fallback: 'en',
+ ru: import('./locales/ru.json'),
+ en: import('./locales/en.json'),
+ },
+ currencyConfig: {
+ currency: '_currency',
+ align: '_align',
+ decimalSeparator: '_decimal',
+ thousandSeparator: '_thousands',
+ },
+ pages: [
+ {
+ extends: 'paywall',
+ products: [
+ {
+ price: '_price',
+ // ...
+ },
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+# Custom Pressets
+
+> [!NOTE]
+> Try to avoid using reserved preset names above.
+> You can certainly do it to override ours, but you should understand what you're doing
+
+[Example for this technique](./examples/ai/src/config.ts)
+
+You can define custom presets if you want to using the following code:
+
+```ts
+// this will be inside intial bundle
+import CustomPresset from './CustomPresset.vue';
+
+// this will be loaded asynchronously when such presset will be shown
+const AsyncCustomPresset = defineAsyncComponent(
+ () => import('./CustomPresset.vue')
+);
+
+export default defineConfig({
+ definePressets: {
+ your_custom_name: CustomPresset,
+ async_your_custom_name: AsyncCustomPresset,
+ },
+ // and use them as:
+ pages: [
+ {
+ extends: 'your_custom_name',
+ // extends: 'async_your_custom_name',
+ // some props
+ },
+ ],
+});
+```
diff --git a/docs/images/form.png b/docs/images/form.png
new file mode 100644
index 0000000..f9035c9
Binary files /dev/null and b/docs/images/form.png differ
diff --git a/docs/images/form_date.png b/docs/images/form_date.png
new file mode 100644
index 0000000..e328c5f
Binary files /dev/null and b/docs/images/form_date.png differ
diff --git a/docs/images/paywall.png b/docs/images/paywall.png
new file mode 100644
index 0000000..bfe18cd
Binary files /dev/null and b/docs/images/paywall.png differ
diff --git a/docs/images/paywall_links.png b/docs/images/paywall_links.png
new file mode 100644
index 0000000..58d8df5
Binary files /dev/null and b/docs/images/paywall_links.png differ
diff --git a/docs/images/paywall_popup_buttons.png b/docs/images/paywall_popup_buttons.png
new file mode 100644
index 0000000..474ba8c
Binary files /dev/null and b/docs/images/paywall_popup_buttons.png differ
diff --git a/docs/images/paywall_popup_buttons_media.png b/docs/images/paywall_popup_buttons_media.png
new file mode 100644
index 0000000..91719ad
Binary files /dev/null and b/docs/images/paywall_popup_buttons_media.png differ
diff --git a/docs/images/paywall_popup_message.png b/docs/images/paywall_popup_message.png
new file mode 100644
index 0000000..ba7d2a3
Binary files /dev/null and b/docs/images/paywall_popup_message.png differ
diff --git a/docs/images/paywall_popup_title.png b/docs/images/paywall_popup_title.png
new file mode 100644
index 0000000..bb3ca49
Binary files /dev/null and b/docs/images/paywall_popup_title.png differ
diff --git a/docs/images/paywall_popup_web.png b/docs/images/paywall_popup_web.png
new file mode 100644
index 0000000..832b94a
Binary files /dev/null and b/docs/images/paywall_popup_web.png differ
diff --git a/docs/images/paywall_price.png b/docs/images/paywall_price.png
new file mode 100644
index 0000000..c1c8a85
Binary files /dev/null and b/docs/images/paywall_price.png differ
diff --git a/docs/images/paywall_row.png b/docs/images/paywall_row.png
new file mode 100644
index 0000000..99e4386
Binary files /dev/null and b/docs/images/paywall_row.png differ
diff --git a/docs/images/paywall_row_multiple.png b/docs/images/paywall_row_multiple.png
new file mode 100644
index 0000000..2ad8f65
Binary files /dev/null and b/docs/images/paywall_row_multiple.png differ
diff --git a/docs/images/paywall_row_page.PNG b/docs/images/paywall_row_page.PNG
new file mode 100644
index 0000000..2a8415d
Binary files /dev/null and b/docs/images/paywall_row_page.PNG differ
diff --git a/docs/images/paywall_single.png b/docs/images/paywall_single.png
new file mode 100644
index 0000000..e67e417
Binary files /dev/null and b/docs/images/paywall_single.png differ
diff --git a/docs/images/paywall_single_media.png b/docs/images/paywall_single_media.png
new file mode 100644
index 0000000..33dcf7a
Binary files /dev/null and b/docs/images/paywall_single_media.png differ
diff --git a/docs/images/slide_align.png b/docs/images/slide_align.png
new file mode 100644
index 0000000..8648ae7
Binary files /dev/null and b/docs/images/slide_align.png differ
diff --git a/docs/images/slide_background.png b/docs/images/slide_background.png
new file mode 100644
index 0000000..c676bf9
Binary files /dev/null and b/docs/images/slide_background.png differ
diff --git a/docs/images/slide_center_pagination.png b/docs/images/slide_center_pagination.png
new file mode 100644
index 0000000..619a07a
Binary files /dev/null and b/docs/images/slide_center_pagination.png differ
diff --git a/docs/images/slide_list.png b/docs/images/slide_list.png
new file mode 100644
index 0000000..9596e0b
Binary files /dev/null and b/docs/images/slide_list.png differ
diff --git a/docs/images/slide_list_media.png b/docs/images/slide_list_media.png
new file mode 100644
index 0000000..0a5b234
Binary files /dev/null and b/docs/images/slide_list_media.png differ
diff --git a/docs/images/slide_media.png b/docs/images/slide_media.png
new file mode 100644
index 0000000..1a6101e
Binary files /dev/null and b/docs/images/slide_media.png differ
diff --git a/docs/images/slide_pagination.png b/docs/images/slide_pagination.png
new file mode 100644
index 0000000..8799286
Binary files /dev/null and b/docs/images/slide_pagination.png differ
diff --git a/docs/images/slide_rounded.png b/docs/images/slide_rounded.png
new file mode 100644
index 0000000..e688ec6
Binary files /dev/null and b/docs/images/slide_rounded.png differ
diff --git a/docs/images/slide_square.png b/docs/images/slide_square.png
new file mode 100644
index 0000000..394ab14
Binary files /dev/null and b/docs/images/slide_square.png differ
diff --git a/docs/images/slide_stacked.png b/docs/images/slide_stacked.png
new file mode 100644
index 0000000..a6d6e30
Binary files /dev/null and b/docs/images/slide_stacked.png differ
diff --git a/docs/images/slide_title.png b/docs/images/slide_title.png
new file mode 100644
index 0000000..363329c
Binary files /dev/null and b/docs/images/slide_title.png differ
diff --git a/docs/images/slide_title_button.png b/docs/images/slide_title_button.png
new file mode 100644
index 0000000..7b70d0c
Binary files /dev/null and b/docs/images/slide_title_button.png differ
diff --git a/docs/images/slide_title_description_button.png b/docs/images/slide_title_description_button.png
new file mode 100644
index 0000000..ade5383
Binary files /dev/null and b/docs/images/slide_title_description_button.png differ
diff --git a/examples/ai/.eslintrc.js b/examples/ai/.eslintrc.js
new file mode 100644
index 0000000..a5e5ec9
--- /dev/null
+++ b/examples/ai/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+ root: true,
+ extends: ['@tok/eslint-config'],
+ parserOptions: { tsconfigRootDir: __dirname },
+};
diff --git a/examples/ai/README.md b/examples/ai/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/examples/ai/_internal/tgs.loader.ts b/examples/ai/_internal/tgs.loader.ts
new file mode 100644
index 0000000..8bf2f0a
--- /dev/null
+++ b/examples/ai/_internal/tgs.loader.ts
@@ -0,0 +1,37 @@
+import fs from 'fs';
+import { ungzip } from 'pako';
+import type { Plugin } from 'vite';
+
+// find the way to import it as a component
+const tgsRegexp = /\.tgs$/;
+
+export function telegramStickerLoader(): Plugin {
+ return {
+ name: 'telegram-sticker-loader',
+ enforce: 'pre',
+ load(id: string) {
+ if (!id.match(tgsRegexp)) {
+ return;
+ }
+
+ const [path, importType = 'json'] = id.split('?', 2) as [string, 'json'];
+
+ let decodedTgs;
+
+ try {
+ const fileBuffer = fs.readFileSync(path);
+
+ decodedTgs = new TextDecoder('utf-8').decode(ungzip(fileBuffer));
+ } catch (e) {
+ console.error(`\n${id} couldn't be loaded by telegramStickerLoader \n`);
+
+ return;
+ }
+
+ if (importType === 'json') {
+ // https://v8.dev/blog/cost-of-javascript-2019#json
+ return `export default JSON.parse(${JSON.stringify(decodedTgs)})`;
+ }
+ },
+ };
+}
diff --git a/examples/ai/index.html b/examples/ai/index.html
new file mode 100644
index 0000000..11a94d5
--- /dev/null
+++ b/examples/ai/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+ Onboarding | Tok
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/ai/package.json b/examples/ai/package.json
new file mode 100644
index 0000000..8b36517
--- /dev/null
+++ b/examples/ai/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@tok/ai",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite --port 3001 --open",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@tok/generation": "*",
+ "@tok/i18n": "^0.0.0",
+ "vue": "^3.3.4",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@tok/eslint-config": "*",
+ "@tok/tsconfig": "*"
+ }
+}
\ No newline at end of file
diff --git a/examples/ai/public/_redirects b/examples/ai/public/_redirects
new file mode 100644
index 0000000..9fe800e
--- /dev/null
+++ b/examples/ai/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git a/examples/ai/public/favicon.ico b/examples/ai/public/favicon.ico
new file mode 100644
index 0000000..7ea0a83
Binary files /dev/null and b/examples/ai/public/favicon.ico differ
diff --git a/examples/ai/public/robots.txt b/examples/ai/public/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/examples/ai/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/examples/ai/src/App.vue b/examples/ai/src/App.vue
new file mode 100644
index 0000000..7faf63a
--- /dev/null
+++ b/examples/ai/src/App.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/examples/ai/src/assets/icons/check.svg b/examples/ai/src/assets/icons/check.svg
new file mode 100644
index 0000000..f44afe1
--- /dev/null
+++ b/examples/ai/src/assets/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/ai/src/assets/icons/wind.svg b/examples/ai/src/assets/icons/wind.svg
new file mode 100644
index 0000000..9300175
--- /dev/null
+++ b/examples/ai/src/assets/icons/wind.svg
@@ -0,0 +1,8 @@
+
diff --git a/examples/ai/src/assets/img/1_init.webp b/examples/ai/src/assets/img/1_init.webp
new file mode 100644
index 0000000..e66b772
Binary files /dev/null and b/examples/ai/src/assets/img/1_init.webp differ
diff --git a/examples/ai/src/assets/img/1_res.webp b/examples/ai/src/assets/img/1_res.webp
new file mode 100644
index 0000000..a3ef12e
Binary files /dev/null and b/examples/ai/src/assets/img/1_res.webp differ
diff --git a/examples/ai/src/assets/img/2_init.webp b/examples/ai/src/assets/img/2_init.webp
new file mode 100644
index 0000000..6b3277e
Binary files /dev/null and b/examples/ai/src/assets/img/2_init.webp differ
diff --git a/examples/ai/src/assets/img/2_res.webp b/examples/ai/src/assets/img/2_res.webp
new file mode 100644
index 0000000..6e508df
Binary files /dev/null and b/examples/ai/src/assets/img/2_res.webp differ
diff --git a/examples/ai/src/assets/img/3_init.webp b/examples/ai/src/assets/img/3_init.webp
new file mode 100644
index 0000000..3b8e161
Binary files /dev/null and b/examples/ai/src/assets/img/3_init.webp differ
diff --git a/examples/ai/src/assets/img/3_res.webp b/examples/ai/src/assets/img/3_res.webp
new file mode 100644
index 0000000..93b5fe7
Binary files /dev/null and b/examples/ai/src/assets/img/3_res.webp differ
diff --git a/examples/ai/src/assets/img/paywall.png b/examples/ai/src/assets/img/paywall.png
new file mode 100644
index 0000000..f3d6e64
Binary files /dev/null and b/examples/ai/src/assets/img/paywall.png differ
diff --git a/examples/ai/src/assets/img/paywall.webp b/examples/ai/src/assets/img/paywall.webp
new file mode 100644
index 0000000..14ae382
Binary files /dev/null and b/examples/ai/src/assets/img/paywall.webp differ
diff --git a/examples/ai/src/config.ts b/examples/ai/src/config.ts
new file mode 100644
index 0000000..8cf269f
--- /dev/null
+++ b/examples/ai/src/config.ts
@@ -0,0 +1,125 @@
+import { defineConfig } from '@tok/generation';
+
+import ActionSlide from './custom/ActionSlide.vue';
+
+const imageStyle =
+ 'left: 50%; top: 50%; transform: translate(-50%, -50%); max-width: initial';
+
+export default defineConfig({
+ theme: 'dark',
+ definePressets: {
+ action_slide: ActionSlide,
+ },
+ currencyConfig: {
+ align: 'right',
+ },
+ pages: [
+ {
+ slides: [
+ {
+ extends: 'action_slide',
+ title: 'Re-dress photos with AI',
+ description: 'Tap the button below to try',
+ actionButton: ['Re-dress', 'Applied'],
+ nextButton: 'Next',
+ media: [
+ {
+ type: 'image',
+ src: import('./assets/img/1_init.webp'),
+ style: imageStyle,
+ },
+ {
+ type: 'image',
+ src: import('./assets/img/1_res.webp'),
+ style: imageStyle,
+ },
+ ],
+ },
+ {
+ extends: 'action_slide',
+ title: 'Ready for an important meeting?',
+ description: 'Find perfect business suit',
+ actionButton: ['Re-dress', 'Applied'],
+ nextButton: 'Next',
+ media: [
+ {
+ type: 'image',
+ src: import('./assets/img/2_init.webp'),
+ style: imageStyle,
+ },
+ {
+ type: 'image',
+ src: import('./assets/img/2_res.webp'),
+ style: imageStyle,
+ },
+ ],
+ },
+ {
+ extends: 'action_slide',
+ title: "Let's go to rave party!",
+ description: 'Your friends will be impressed',
+ actionButton: ['Re-dress', 'Applied'],
+ nextButton: {
+ to: '/paywall',
+ content: 'Next',
+ },
+ media: [
+ {
+ type: 'image',
+ src: import('./assets/img/3_init.webp'),
+ style: imageStyle,
+ },
+ {
+ type: 'image',
+ src: import('./assets/img/3_res.webp'),
+ style: imageStyle,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ extends: 'paywall_row',
+ path: '/paywall',
+ media: {
+ type: 'image',
+ src: import('./assets/img/paywall.png'),
+ style: 'aspect-ratio: 390/251',
+ },
+ title: 'Purchase credits to re-dress your photos',
+ description: '1 credit = 1 photo re-dress',
+ mainButtonText: 'Buy credits for {price}',
+ products: [
+ {
+ id: '4_credits',
+ price: 4.99,
+ title: '4 credits',
+ description: 'Perfect to start with',
+ },
+ {
+ id: '20_credits',
+ price: 8.99,
+ title: '20 credits',
+ description: 'Best value offer',
+ bestText: 'Best Choice',
+ },
+ {
+ id: '100 credits',
+ price: 19.99,
+ title: '100 credits',
+ description: 'For true enthusiasts',
+ },
+ ],
+ links: [
+ {
+ text: 'Privacy policy',
+ href: 'http://google.com',
+ },
+ {
+ text: 'Terms of use',
+ href: 'http://google.com',
+ },
+ ],
+ },
+ ],
+});
diff --git a/examples/ai/src/custom/ActionSlide.vue b/examples/ai/src/custom/ActionSlide.vue
new file mode 100644
index 0000000..4f6169b
--- /dev/null
+++ b/examples/ai/src/custom/ActionSlide.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/tales/src/assets/icons/book.svg b/examples/tales/src/assets/icons/book.svg
new file mode 100644
index 0000000..e8b9ccc
--- /dev/null
+++ b/examples/tales/src/assets/icons/book.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/brain.svg b/examples/tales/src/assets/icons/brain.svg
new file mode 100644
index 0000000..075794b
--- /dev/null
+++ b/examples/tales/src/assets/icons/brain.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/count_1.svg b/examples/tales/src/assets/icons/count_1.svg
new file mode 100644
index 0000000..781e20f
--- /dev/null
+++ b/examples/tales/src/assets/icons/count_1.svg
@@ -0,0 +1,4 @@
+
diff --git a/examples/tales/src/assets/icons/count_2.svg b/examples/tales/src/assets/icons/count_2.svg
new file mode 100644
index 0000000..e59f3bf
--- /dev/null
+++ b/examples/tales/src/assets/icons/count_2.svg
@@ -0,0 +1,4 @@
+
diff --git a/examples/tales/src/assets/icons/count_3.svg b/examples/tales/src/assets/icons/count_3.svg
new file mode 100644
index 0000000..f8cc7e0
--- /dev/null
+++ b/examples/tales/src/assets/icons/count_3.svg
@@ -0,0 +1,4 @@
+
diff --git a/examples/tales/src/assets/icons/lang.svg b/examples/tales/src/assets/icons/lang.svg
new file mode 100644
index 0000000..7f0b2b0
--- /dev/null
+++ b/examples/tales/src/assets/icons/lang.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/learn.svg b/examples/tales/src/assets/icons/learn.svg
new file mode 100644
index 0000000..2ac7d5f
--- /dev/null
+++ b/examples/tales/src/assets/icons/learn.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/paint.svg b/examples/tales/src/assets/icons/paint.svg
new file mode 100644
index 0000000..21225b7
--- /dev/null
+++ b/examples/tales/src/assets/icons/paint.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/play.svg b/examples/tales/src/assets/icons/play.svg
new file mode 100644
index 0000000..7d7105e
--- /dev/null
+++ b/examples/tales/src/assets/icons/play.svg
@@ -0,0 +1,4 @@
+
diff --git a/examples/tales/src/assets/icons/review.svg b/examples/tales/src/assets/icons/review.svg
new file mode 100644
index 0000000..5a3e4fd
--- /dev/null
+++ b/examples/tales/src/assets/icons/review.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/smile.svg b/examples/tales/src/assets/icons/smile.svg
new file mode 100644
index 0000000..00407c5
--- /dev/null
+++ b/examples/tales/src/assets/icons/smile.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/star.svg b/examples/tales/src/assets/icons/star.svg
new file mode 100644
index 0000000..1dd2b1f
--- /dev/null
+++ b/examples/tales/src/assets/icons/star.svg
@@ -0,0 +1,10 @@
+
diff --git a/examples/tales/src/assets/icons/story.jpg b/examples/tales/src/assets/icons/story.jpg
new file mode 100644
index 0000000..a495865
Binary files /dev/null and b/examples/tales/src/assets/icons/story.jpg differ
diff --git a/examples/tales/src/assets/icons/time.svg b/examples/tales/src/assets/icons/time.svg
new file mode 100644
index 0000000..35a6597
--- /dev/null
+++ b/examples/tales/src/assets/icons/time.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/icons/wing.svg b/examples/tales/src/assets/icons/wing.svg
new file mode 100644
index 0000000..123d872
--- /dev/null
+++ b/examples/tales/src/assets/icons/wing.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/tales/src/assets/img/background.png b/examples/tales/src/assets/img/background.png
new file mode 100644
index 0000000..9378892
Binary files /dev/null and b/examples/tales/src/assets/img/background.png differ
diff --git a/examples/tales/src/assets/img/background.webp b/examples/tales/src/assets/img/background.webp
new file mode 100644
index 0000000..8c3eed0
Binary files /dev/null and b/examples/tales/src/assets/img/background.webp differ
diff --git a/examples/tales/src/assets/img/colorful.png b/examples/tales/src/assets/img/colorful.png
new file mode 100644
index 0000000..9baf6b5
Binary files /dev/null and b/examples/tales/src/assets/img/colorful.png differ
diff --git a/examples/tales/src/assets/img/colorful.webp b/examples/tales/src/assets/img/colorful.webp
new file mode 100644
index 0000000..fbad201
Binary files /dev/null and b/examples/tales/src/assets/img/colorful.webp differ
diff --git a/examples/tales/src/assets/img/main.png b/examples/tales/src/assets/img/main.png
new file mode 100644
index 0000000..2c41857
Binary files /dev/null and b/examples/tales/src/assets/img/main.png differ
diff --git a/examples/tales/src/assets/img/main.webp b/examples/tales/src/assets/img/main.webp
new file mode 100644
index 0000000..591def0
Binary files /dev/null and b/examples/tales/src/assets/img/main.webp differ
diff --git a/examples/tales/src/assets/img/unicorn.png b/examples/tales/src/assets/img/unicorn.png
new file mode 100644
index 0000000..470ed5d
Binary files /dev/null and b/examples/tales/src/assets/img/unicorn.png differ
diff --git a/examples/tales/src/assets/img/unicorn.webp b/examples/tales/src/assets/img/unicorn.webp
new file mode 100644
index 0000000..2cd1e39
Binary files /dev/null and b/examples/tales/src/assets/img/unicorn.webp differ
diff --git a/examples/tales/src/assets/stickers/shpooky_easy.tgs b/examples/tales/src/assets/stickers/shpooky_easy.tgs
new file mode 100644
index 0000000..f56a7ac
Binary files /dev/null and b/examples/tales/src/assets/stickers/shpooky_easy.tgs differ
diff --git a/examples/tales/src/assets/stickers/shpooky_love.tgs b/examples/tales/src/assets/stickers/shpooky_love.tgs
new file mode 100644
index 0000000..35daa0e
Binary files /dev/null and b/examples/tales/src/assets/stickers/shpooky_love.tgs differ
diff --git a/examples/tales/src/assets/stickers/shpooky_party.tgs b/examples/tales/src/assets/stickers/shpooky_party.tgs
new file mode 100644
index 0000000..c9bb85a
Binary files /dev/null and b/examples/tales/src/assets/stickers/shpooky_party.tgs differ
diff --git a/examples/tales/src/assets/stickers/shpooky_speed.tgs b/examples/tales/src/assets/stickers/shpooky_speed.tgs
new file mode 100644
index 0000000..2759813
Binary files /dev/null and b/examples/tales/src/assets/stickers/shpooky_speed.tgs differ
diff --git a/examples/tales/src/config.ts b/examples/tales/src/config.ts
new file mode 100644
index 0000000..3aaa513
--- /dev/null
+++ b/examples/tales/src/config.ts
@@ -0,0 +1,199 @@
+import { defineConfig } from '@tok/generation';
+
+import Story from './story/StoryExample.vue';
+
+export default defineConfig({
+ theme: 'dark',
+ locale: {
+ fallback: 'en',
+ ru: import('./locales/ru.json'),
+ en: import('./locales/en.json'),
+ },
+ definePressets: {
+ story: Story,
+ },
+ currencyConfig: {
+ currency: '_currency.value',
+ align: '_currency.align',
+ },
+ pages: [
+ {
+ slides: [
+ {
+ media: {
+ type: 'image',
+ src: import('./assets/img/main.png'),
+ webp: import('./assets/img/main.webp'),
+ style:
+ 'width: min(60vw, 420px); height: min(66.5vw, 465px); margin: 0 auto',
+ },
+ title: '_s1.title',
+ description: '_s1.description',
+ button: '_s1.button',
+ },
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/shpooky_love.tgs'),
+ },
+ title: '_s2.title',
+ list: [
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/smile.svg'),
+ },
+ text: '_s2.list1',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/learn.svg'),
+ },
+ text: '_s2.list2',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/play.svg'),
+ },
+ text: '_s2.list3',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/book.svg'),
+ },
+ text: '_s2.list4',
+ },
+ ],
+ button: '_s2.button',
+ },
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/shpooky_easy.tgs'),
+ },
+ title: '_s3.title',
+ description: '_s3.description',
+ list: [
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/count_1.svg'),
+ },
+ text: '_s3.list1',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/count_2.svg'),
+ },
+ text: '_s3.list2',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/count_3.svg'),
+ },
+ text: '_s3.list3',
+ },
+ ],
+ button: '_s3.button',
+ },
+ {
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/shpooky_speed.tgs'),
+ },
+ title: '_s4.title',
+ list: [
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/time.svg'),
+ },
+ text: '_s4.list1',
+ },
+ {
+ media: {
+ type: 'icon',
+ src: import('./assets/icons/play.svg'),
+ },
+ text: '_s4.list2',
+ },
+ ],
+ button: '_s4.button',
+ },
+ {
+ media: {
+ type: 'image',
+ src: import('./assets/img/colorful.png'),
+ webp: import('./assets/img/colorful.webp'),
+ style: 'aspect-ratio: 375/250',
+ },
+ title: '_s5.title',
+ description: '_s5.description',
+ button: '_s5.button',
+ },
+ {
+ media: {
+ type: 'image',
+ src: import('./assets/img/unicorn.png'),
+ webp: import('./assets/img/unicorn.webp'),
+ style: 'aspect-ratio: 375/209',
+ },
+ title: '_s6.title',
+ description: '_s6.description',
+ button: {
+ content: '_s6.button',
+ to: '/story',
+ },
+ },
+ {
+ extends: 'paywall',
+ media: {
+ type: 'sticker',
+ src: import('./assets/stickers/shpooky_party.tgs'),
+ },
+ title: '_paywall.title',
+ mainButtonText: '_paywall.main',
+ list: ['_paywall.feature1', '_paywall.feature2', '_paywall.feature3'],
+ products: [
+ {
+ id: 'monthly',
+ title: '_paywall.product1',
+ description: '_paywall.price1Text',
+ price: '_paywall.product1Price',
+ },
+ {
+ id: 'yearly',
+ title: '_paywall.product2',
+ description: '_paywall.price2Text',
+ price: '_paywall.product2Price',
+ discount: '_paywall.discount',
+ },
+ ],
+ popup: {
+ type: 'web',
+ title: '_paywall.popup.title',
+ },
+ links: [
+ {
+ text: '_paywall.policy',
+ href: '_paywall.policy_href',
+ },
+ {
+ text: '_paywall.terms',
+ href: '_paywall.terms_href',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ extends: 'story',
+ path: '/story',
+ },
+ ],
+});
diff --git a/examples/tales/src/locales/en.json b/examples/tales/src/locales/en.json
new file mode 100644
index 0000000..fa86241
--- /dev/null
+++ b/examples/tales/src/locales/en.json
@@ -0,0 +1,81 @@
+{
+ "_currency": {
+ "value": "USD",
+ "align": "left"
+ },
+ "_wtHref": "https://wondertales.io/?utm_source=telegram_onboarding",
+ "_wtHrefText": "wondertales.io",
+ "_s1": {
+ "title": "Wonder Tales",
+ "description": "Your one-stop destination for incredible custom tales for kids",
+ "button": "Let's start"
+ },
+ "_s2": {
+ "title": "Give your children stories:",
+ "list1": "Which entertain",
+ "list2": "They teach valuable life lessons",
+ "list3": "Also in audio format for toddlers",
+ "list4": "New stories every day",
+ "button": "Next"
+ },
+ "_s3": {
+ "title": "Easy",
+ "description": "It will be easy and individual Just three steps:",
+ "list1": "Choose the main character",
+ "list2": "Specify the second character of the fairy tal",
+ "list3": "Choose one of the morals prepared and worked out in advance",
+ "button": "What else?"
+ },
+ "_s4": {
+ "title": "Fast",
+ "list1": "The first page can be seen in 2-3 minutes",
+ "list2": "Our fairy tales can be listened to in audio format",
+ "button": "Wow!"
+ },
+ "_s5": {
+ "title": "Colorful",
+ "description": "Fairy tales are complemented by vivid illustrations created by artificial intelligence and inspired by the best illustrators of children's books",
+ "button": "Next"
+ },
+ "_s6": {
+ "title": "Available in several languages",
+ "description": "Read the same fairy tale to your children in English or Russian",
+ "button": "See the example"
+ },
+ "_paywall": {
+ "title": "Tales Unbounded",
+ "policy": "Privacy policy",
+ "policy_href": "https://google.com",
+ "terms": "Terms of use",
+ "terms_href": "https://google.com",
+ "main": "Subscribe",
+ "feature1": "An endless number of fairy tales",
+ "feature2": "Access to fairy tales anytime",
+ "feature3": "All fairy tales are saved",
+ "product1": "1 month",
+ "product1Price": 8.95,
+ "discount": "Discount 34%",
+ "price1Text": "8.95$ per month",
+ "price2Text": "5.95$ per month",
+ "product2": "1 year",
+ "product2Price": 71.4,
+ "popup": {
+ "title": "Choose the payment method",
+ "telegram_payments": "Bank Card",
+ "wallet_pay": "Wallet Pay"
+ }
+ },
+ "_story": {
+ "title": "Story",
+ "name": "Kayley and the Magical Adventures of Dr Kitty Cat",
+ "page1": "Once upon a time, in a small town called Cloverville, lived a sweet little girl named Kayley. Kayley had a heart full of love and kindness, and she loved nothing more than cuddling with her fluffy white kitten, Dr Kitty Cat. One beautiful evening, as the sun began to set, Kayley and Dr Kitty Cat decided to go on a magical adventure. They tiptoed out of their house, filled with excitement and wonder. Holding hands, they walked towards the forest, where they knew magical creatures lived. As they entered the enchanted forest, they spotted a shimmering stream filled with fish as colorful as rainbows. A tiny fairy flying above the water called out to them, \"Hello, Kayley and Dr Kitty Cat! Welcome to our magical realm!",
+ "page2": "Would you like to play with us?\" Kayley's eyes sparkled like stars as she replied, \"Oh yes, fairy friend! We would love to join you on your adventures!\" Dr Kitty Cat meowed in agreement. The fairy sprinkled magical dust on Kayley and Dr Kitty Cat, turning them into pixies. They fluttered their wings and joined a game of tag with the fairy and her friends. They flew through the trees, giggling and laughing, feeling the wind tickle their faces. Suddenly, they heard a loud roar coming from deep within the forest.",
+ "page3": "It was a gentle dragon named Sparkle, who had lost her way. Kayley and Dr Kitty Cat knew they had to help her find her path again. \"Don't worry, Sparkle! We will find your home together!\" Kayley reassured the dragon. They followed Sparkle's footsteps, through dark caves and over sparkling rivers until they reached a beautiful waterfall. Sparkle's eyes widened with joy as she realized she had found her home. \"Thank you, kind friends!",
+ "page4": "You have shown me the way back to where I belong!\" Kayley and Dr Kitty Cat waved goodbye to Sparkle, happy to have helped her. As they continued their adventure, they stumbled upon a meadow filled with singing flowers. Each flower had a unique tune, creating a harmonious melody. The flowers invited Kayley and Dr Kitty Cat to dance with them. They twirled and spun, their laughter echoing through the meadow as they became part of the song. The flowers whispered secrets about the magic of kindness, reminding Kayley and Dr Kitty Cat to treat others the way they would like to be treated. Leaving the meadow with joyful hearts, Kayley and Dr Kitty Cat found themselves in a enchanted village made entirely of sweets.",
+ "page5": "Houses made of gingerbread and candy cane trees surrounded them. The villagers were colorful gummy bears and friendly lollipops. The villagers offered Kayley and Dr Kitty Cat a special treat - a magical lollipop that could grant one wish. Kayley thought for a moment and turned to Dr Kitty Cat, \"What do you think, Dr Kitty Cat? What should we wish for?\" Dr Kitty Cat purred thoughtfully and then whispered in Kayley's ear, \"Let's wish for happiness and kindness to fill the world.\" Kayley smiled and made her wish, knowing that together, they could make a difference. With their wish granted, the magical lollipop filled the village with an enchanting glow.",
+ "page6": "The villagers clapped and cheered as Kayley and Dr Kitty Cat's wish spread, bringing kindness and love to everyone they met. As the night grew darker, Kayley and Dr Kitty Cat realized it was time to head back home. They thanked the villagers and started their journey back, feeling grateful for the incredible adventures and the magical friends they had made along the way. Hand in hand, Kayley and Dr Kitty Cat returned to their cozy home in Cloverville. They cuddled up in bed, knowing that they had experienced a truly enchanting adventure. As they drifted off to sleep, the magic of the day stayed with them, reminding them to always treat others with kindness and love. And so, in Cloverville, Kayley and Dr Kitty Cat's story was whispered throughout generations, reminding everyone of the importance of treating others the way they would like to be treated – with love, kindness, and a touch of enchantment.",
+ "end": "THE END",
+ "back": "Continue",
+ "mainButton": "Create your own fairy tale"
+ }
+}
\ No newline at end of file
diff --git a/examples/tales/src/locales/ru.json b/examples/tales/src/locales/ru.json
new file mode 100644
index 0000000..000917e
--- /dev/null
+++ b/examples/tales/src/locales/ru.json
@@ -0,0 +1,86 @@
+{
+ "_currency": {
+ "value": "RUB",
+ "align": "right"
+ },
+ "_wtHref": "https://wonder-tales.ru/?utm_source=telegram_onboarding",
+ "_wtHrefText": "wonder-tales.ru",
+ "_s1": {
+ "title": "Wonder Tales",
+ "description": "Ваш пункт назначения для невероятных индивидуальных сказок для детей",
+ "button": "Начнем!"
+ },
+ "_s2": {
+ "title": "Дарите вашим детям истории:",
+ "list1": "Которые развлекают",
+ "list2": "Учат ценным жизненным урокам",
+ "list3": "Еще и в аудиоформате для самых маленьких",
+ "list4": "Каждый день новые истории",
+ "button": "Дальше"
+ },
+ "_s3": {
+ "title": "Легко",
+ "description": "Это будет легко и индивидуально Всего три шага:",
+ "list1": "Выберите главного героя",
+ "list2": "Укажите второго персонажа сказки",
+ "list3": "Выберите одну из заготовленных и проработанных заранее моралей",
+ "button": "Что еще?"
+ },
+ "_s4": {
+ "title": "Быстро",
+ "list1": "Первую страницу можно увидеть уже через 2-3 минуты",
+ "list2": "Наши сказки можно прослушать в аудиоформате",
+ "button": "Вау!"
+ },
+ "_s5": {
+ "title": "Красочно",
+ "description": "Сказки дополняются яркими иллюстрациями, созданными исскусственным интеллектом и вдохновленными лучшими иллюстраторами детских книг",
+ "button": "Дальше"
+ },
+ "_s6": {
+ "title": "Доступно на нескольких языках",
+ "description": "Читайте одну и ту же сказку своим детям на Русском или Английском",
+ "button": "Посмотреть пример"
+ },
+ "_paywall": {
+ "title": "Сказки Без границ",
+ "policy": "Политика конфиденциальности",
+ "policy_href": "https://google.com",
+ "terms": "Соглашение о подписке",
+ "terms_href": "https://google.com",
+ "main": "Подписаться",
+ "feature1": "Бесконечное количество сказок",
+ "feature2": "Доступ к сказкам в любое время",
+ "feature3": "Все сказки сохраняются",
+ "product1": "1 месяц",
+ "product1Price": 180,
+ "price1Text": "180 ₽ в месяц",
+ "discount": "Скидка 34%",
+ "product2": "1 год",
+ "product2Price": 1440,
+ "price2Text": "120 ₽ в месяц",
+ "popup": {
+ "title": "Выберите метод оплаты",
+ "telegram_payments": "Оплата картой",
+ "wallet_pay": "Wallet Pay"
+ }
+ },
+ "_story": {
+ "title": "Сказка",
+ "name": "Кейли и волшебные приключения доктора Китти Кэт",
+ "page1": "Давным-давно в маленьком городке Кловервилль жила милая маленькая девочка по имени Кейли. Сердце Кейли было полно любви и доброты, и она не любила ничего больше, чем обниматься со своим пушистым белым котенком Доктором Китти Кэт. Одним прекрасным вечером, когда солнце начало садиться, Кейли и доктор Китти Кэт решили отправиться в волшебное приключение. Они на цыпочках вышли из дома, полные волнения и удивления. Держась за руки, они пошли к лесу, где, как они знали, жили волшебные существа. Войдя в заколдованный лес, они заметили мерцающий ручей, наполненный рыбой, яркой, как радуга. Крошечная фея, летающая над водой, окликнула их: «Привет, Кейли и доктор Китти Кэт! Добро пожаловать в наше волшебное царство!",
+ "page2": "Хочешь поиграть с нами?» Глаза Кейли сверкнули, как звезды, и она ответила: «О да, сказочная подруга! Мы хотели бы присоединиться к вам в ваших приключениях!\" Доктор Китти Кэт мяукнула в знак согласия. Фея посыпала волшебной пылью Кейли и доктора Китти Кэт, превратив их в пикси. Они затрепетали крыльями и присоединились к игре в метки с феей и ее Друзья. Они летали между деревьями, хихикая и смеясь, чувствуя, как ветер щекочет их лица. Внезапно они услышали громкий рев, доносившийся из глубины леса.",
+ "page3": "Это была нежная драконица по имени Искорка, которая сбилась с пути. Кейли и доктор Китти Кэт знали, что им нужно помочь ей снова найти свой путь. «Не волнуйся, Искорка! Мы вместе найдём твой дом!» Кейли успокоила дракона. Они следовали по следам Искорки через темные пещеры и сверкающие реки, пока не достигли прекрасного водопада. Глаза Искорки расширились от радости, когда она поняла, что нашла свой дом. «Спасибо вам, добрые друзья!",
+ "page4": "Вы показали мне путь обратно туда, где я принадлежу!» Кейли и доктор Китти Кэт помахали на прощание Искорке, счастливые, что помогли ей. Продолжая свое приключение, они наткнулись на луг, наполненный поющими цветами. Каждый цветок имел уникальный мелодия, создавая гармоничную мелодию. Цветы пригласили Кейли и доктора Китти Кэт потанцевать с ними. Они кружились и кружились, их смех эхом разносился по лугу, становясь частью песни. Цветы нашептывали тайны о волшебстве доброты, напоминая Кейли и доктор Китти Кэт должны относиться к другим так, как они хотели бы, чтобы относились к ним.Покинув луг с радостными сердцами, Кейли и доктор Китти Кэт оказались в волшебной деревне, полностью состоящей из сладостей.",
+ "page5": "Их окружали пряничные домики и леденцы. Жители деревни представляли собой разноцветных мармеладных мишек и дружелюбных леденцов. Жители деревни предложили Кейли и доктору Китти Кэт особое угощение — волшебный леденец, способный исполнить одно желание. Кейли на мгновение задумалась и повернулась к доктору Китти Кэт: «Что вы думаете, доктор Китти Кэт? Чего нам следует желать?» Доктор Китти Кэт задумчиво мурлыкала, а затем прошептала на ухо Кейли: «Давайте пожелаем, чтобы счастье и доброта наполнили мир». Кейли улыбнулась и загадала свое желание, зная, что вместе они смогут изменить ситуацию. Когда их желание было исполнено, волшебный леденец наполнил деревню очаровательным сиянием.",
+ "page6": "Жители деревни аплодировали и приветствовали желание Кейли и доктора Китти Кэт распространяться, принося доброту и любовь всем, кого они встречали. Когда ночь стала темнее, Кейли и доктор Китти Кэт поняли, что пора возвращаться домой. Они поблагодарили жителей деревни и отправились обратно, чувствуя благодарность за невероятные приключения и волшебных друзей, которых они нашли на этом пути. Рука об руку Кейли и доктор Китти Кэт вернулись в свой уютный дом в Кловервилле. Они прижались друг к другу в постели, зная, что пережили поистине феерическое приключение. Когда они засыпали, волшебство дня оставалось с ними, напоминая им всегда относиться к другим с добротой и любовью. Итак, в Кловервилле история Кейли и доктора Китти Кэт передавалась шепотом из поколения в поколение, напоминая каждому о важности относиться к другим так, как они хотели бы, чтобы относились к ним – с любовью, добротой и оттенком очарования.",
+ "end": "КОНЕЦ",
+ "back": "Продолжить",
+ "mainButton": "Создать свою сказку"
+ },
+ "_alerts": {
+ "payment": {
+ "canceled": "Вы отменили выбор оплаты"
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/tales/src/main.ts b/examples/tales/src/main.ts
new file mode 100644
index 0000000..5d02ad1
--- /dev/null
+++ b/examples/tales/src/main.ts
@@ -0,0 +1,6 @@
+import { bootstrap } from '@tok/generation';
+
+import App from './App.vue';
+import { default as config } from './config';
+
+bootstrap(App, config);
diff --git a/examples/tales/src/story/StoryExample.vue b/examples/tales/src/story/StoryExample.vue
new file mode 100644
index 0000000..e86db9a
--- /dev/null
+++ b/examples/tales/src/story/StoryExample.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/tales/src/story/assets/1.png b/examples/tales/src/story/assets/1.png
new file mode 100644
index 0000000..9baf6b5
Binary files /dev/null and b/examples/tales/src/story/assets/1.png differ
diff --git a/examples/tales/src/story/assets/1.webp b/examples/tales/src/story/assets/1.webp
new file mode 100644
index 0000000..fbad201
Binary files /dev/null and b/examples/tales/src/story/assets/1.webp differ
diff --git a/examples/tales/src/story/assets/2.png b/examples/tales/src/story/assets/2.png
new file mode 100644
index 0000000..320e917
Binary files /dev/null and b/examples/tales/src/story/assets/2.png differ
diff --git a/examples/tales/src/story/assets/2.webp b/examples/tales/src/story/assets/2.webp
new file mode 100644
index 0000000..878e708
Binary files /dev/null and b/examples/tales/src/story/assets/2.webp differ
diff --git a/examples/tales/src/story/assets/3.png b/examples/tales/src/story/assets/3.png
new file mode 100644
index 0000000..2e926f1
Binary files /dev/null and b/examples/tales/src/story/assets/3.png differ
diff --git a/examples/tales/src/story/assets/3.webp b/examples/tales/src/story/assets/3.webp
new file mode 100644
index 0000000..80c55ff
Binary files /dev/null and b/examples/tales/src/story/assets/3.webp differ
diff --git a/examples/tales/src/story/assets/4.png b/examples/tales/src/story/assets/4.png
new file mode 100644
index 0000000..1ba456c
Binary files /dev/null and b/examples/tales/src/story/assets/4.png differ
diff --git a/examples/tales/src/story/assets/4.webp b/examples/tales/src/story/assets/4.webp
new file mode 100644
index 0000000..d0b66b3
Binary files /dev/null and b/examples/tales/src/story/assets/4.webp differ
diff --git a/examples/tales/src/story/assets/5.png b/examples/tales/src/story/assets/5.png
new file mode 100644
index 0000000..b57bc5a
Binary files /dev/null and b/examples/tales/src/story/assets/5.png differ
diff --git a/examples/tales/src/story/assets/5.webp b/examples/tales/src/story/assets/5.webp
new file mode 100644
index 0000000..3dba22d
Binary files /dev/null and b/examples/tales/src/story/assets/5.webp differ
diff --git a/examples/tales/src/story/assets/6.png b/examples/tales/src/story/assets/6.png
new file mode 100644
index 0000000..c323cc2
Binary files /dev/null and b/examples/tales/src/story/assets/6.png differ
diff --git a/examples/tales/src/story/assets/6.webp b/examples/tales/src/story/assets/6.webp
new file mode 100644
index 0000000..aedb451
Binary files /dev/null and b/examples/tales/src/story/assets/6.webp differ
diff --git a/examples/tales/src/story/components/EndPage.vue b/examples/tales/src/story/components/EndPage.vue
new file mode 100644
index 0000000..7d9d3b3
--- /dev/null
+++ b/examples/tales/src/story/components/EndPage.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/components/ListItem/README.md b/packages/generation/components/ListItem/README.md
new file mode 100644
index 0000000..737ea9c
--- /dev/null
+++ b/packages/generation/components/ListItem/README.md
@@ -0,0 +1,16 @@
+# ListItem
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+This component can render a list item (li) with [Media](../Media/README.md) if media is provided
+
+## Props
+
+All available props see in [ListItem.props.ts](./ListItem.props.ts)
+
+## i18n
+
+The component natively supports [i18n](../../../i18n/README.md) for text values.
+
+You can provide a text as a locale token, and it will be dynamically translated
diff --git a/packages/generation/components/ListItem/index.ts b/packages/generation/components/ListItem/index.ts
new file mode 100644
index 0000000..7e92ea1
--- /dev/null
+++ b/packages/generation/components/ListItem/index.ts
@@ -0,0 +1,2 @@
+export * from './ListItem.props';
+export { default as ListItem } from './ListItem.vue';
diff --git a/packages/generation/components/Media/Emodji.vue b/packages/generation/components/Media/Emodji.vue
new file mode 100644
index 0000000..1c41da5
--- /dev/null
+++ b/packages/generation/components/Media/Emodji.vue
@@ -0,0 +1,62 @@
+
+
+ {{ src }}
+
+
+
+
+
+
diff --git a/packages/generation/components/Media/Icon.vue b/packages/generation/components/Media/Icon.vue
new file mode 100644
index 0000000..5216ad2
--- /dev/null
+++ b/packages/generation/components/Media/Icon.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/components/Media/Image.vue b/packages/generation/components/Media/Image.vue
new file mode 100644
index 0000000..4830d85
--- /dev/null
+++ b/packages/generation/components/Media/Image.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/components/Media/Media.presset.props.ts b/packages/generation/components/Media/Media.presset.props.ts
new file mode 100644
index 0000000..502e2aa
--- /dev/null
+++ b/packages/generation/components/Media/Media.presset.props.ts
@@ -0,0 +1,70 @@
+import { StickerProps } from '@tok/telegram-ui/components/Sticker';
+import { SvgIconProps } from '@tok/ui/components/SvgIcon';
+import { CSSProperties } from 'vue';
+
+export type _MediaLoader = Promise<{ default: T }>;
+
+export type IconPressetProps = {
+ type: 'icon';
+ src: string | _MediaLoader;
+
+ style?: string | CSSProperties;
+} & Omit;
+
+export type StickerPressetProps = {
+ type: 'sticker';
+ src: Promise | null;
+
+ size?: number | [number, number];
+
+ style?: string | CSSProperties;
+} & Pick;
+
+// todo: Find a way to translate images
+// src: '_i18n.imageSrc' -> /locales/en.ts: import('../assets/*.png');
+export type ImagePressetProps = {
+ type: 'image';
+ src: Promise<
+ | typeof import('*.png')
+ | typeof import('*.jpg')
+ | typeof import('*.webp')
+ | any
+ >;
+ webp?: Promise;
+
+ style?: string | CSSProperties;
+
+ static?: boolean;
+};
+
+export type EmodjiPressetProps = {
+ type: 'emodji';
+
+ src: string;
+
+ size?: number | [number, number];
+
+ style?: string | CSSProperties;
+};
+
+export type VideoPressetProps = {
+ type: 'video';
+
+ src: Promise;
+
+ poster?: Promise<
+ | typeof import('*.png')
+ | typeof import('*.jpg')
+ | typeof import('*.webp')
+ | any
+ >;
+
+ style?: string | CSSProperties;
+};
+
+export type MediaPressetProps =
+ | IconPressetProps
+ | StickerPressetProps
+ | ImagePressetProps
+ | VideoPressetProps
+ | EmodjiPressetProps;
diff --git a/packages/generation/components/Media/Media.presset.vue b/packages/generation/components/Media/Media.presset.vue
new file mode 100644
index 0000000..03d6424
--- /dev/null
+++ b/packages/generation/components/Media/Media.presset.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/components/Media/README.md b/packages/generation/components/Media/README.md
new file mode 100644
index 0000000..6a4f7ff
--- /dev/null
+++ b/packages/generation/components/Media/README.md
@@ -0,0 +1,18 @@
+# Media
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+This component supports 5 different types of media:
+
+1. [Video](./VideoPresset.vue)
+2. [Image](./Image.vue)
+3. [Icon](./Icon.vue)
+4. [Emodji](./Emodji.vue)
+5. [Sticker](./Sticker.vue)
+
+Sticker will be loaded asynchronously
+
+## Props
+
+All available props see in [Media.props.ts](./Media.props.ts)
diff --git a/packages/generation/components/Media/Sticker.vue b/packages/generation/components/Media/Sticker.vue
new file mode 100644
index 0000000..0a2c30b
--- /dev/null
+++ b/packages/generation/components/Media/Sticker.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/components/Media/index.ts b/packages/generation/components/Media/index.ts
new file mode 100644
index 0000000..0cc04e9
--- /dev/null
+++ b/packages/generation/components/Media/index.ts
@@ -0,0 +1,2 @@
+export * from './Media.presset.props';
+export { default as MediaPresset } from './Media.presset.vue';
diff --git a/packages/generation/components/Media/useLoadedImage.ts b/packages/generation/components/Media/useLoadedImage.ts
new file mode 100644
index 0000000..593483c
--- /dev/null
+++ b/packages/generation/components/Media/useLoadedImage.ts
@@ -0,0 +1,36 @@
+import { MaybeComputedRef, resolveRef } from '@tok/ui/types';
+import { computed, ref, watch } from 'vue';
+
+import { _MediaLoader } from './Media.presset.props';
+
+export function useLoadedImage(
+ src: MaybeComputedRef<_MediaLoader | string | undefined> = ''
+) {
+ const srcRef = resolveRef(src);
+
+ const onlySrcLoader = computed(() => {
+ const value = srcRef.value;
+
+ return value && typeof value === 'object' && 'then' in value ? value : null;
+ });
+
+ const loadedImage = ref();
+
+ const loadImage = (value: _MediaLoader) => {
+ value.then((m) => {
+ loadedImage.value = m.default;
+ });
+ };
+
+ watch(
+ onlySrcLoader,
+ (value) => {
+ if (value) {
+ loadImage(value);
+ }
+ },
+ { immediate: true }
+ );
+
+ return loadedImage;
+}
diff --git a/packages/generation/components/PaywallPopup/PaywallPopup.props.ts b/packages/generation/components/PaywallPopup/PaywallPopup.props.ts
new file mode 100644
index 0000000..1f12d8a
--- /dev/null
+++ b/packages/generation/components/PaywallPopup/PaywallPopup.props.ts
@@ -0,0 +1,38 @@
+import type { _GenerationPaywallPopup } from '@tok/generation/defineConfig';
+
+export type PaywallPopupProps = _GenerationPaywallPopup & {
+ opened: boolean;
+};
+
+export type PaywallPopupEmits = {
+ (e: 'update:opened', value: boolean): void;
+ (e: 'onSelect', value: string | undefined): void;
+};
+
+const defaultButtons = [
+ {
+ id: 'telegram_payments',
+ media: {
+ type: 'emodji' as const,
+ src: '💳',
+ },
+ type: 'default' as const,
+ text: 'Bank card',
+ },
+ {
+ id: 'wallet_pay',
+ media: {
+ type: 'emodji' as const,
+ src: '👛',
+ },
+ type: 'default' as const,
+ text: 'Wallet pay',
+ },
+];
+
+export const PaywallPopupDefaultProps = {
+ type: 'telegram',
+ title: 'Choose the payment method',
+ message: '',
+ buttons: () => defaultButtons,
+} as const;
diff --git a/packages/generation/components/PaywallPopup/PaywallPopup.vue b/packages/generation/components/PaywallPopup/PaywallPopup.vue
new file mode 100644
index 0000000..10bb22c
--- /dev/null
+++ b/packages/generation/components/PaywallPopup/PaywallPopup.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/components/PaywallPopup/README.md b/packages/generation/components/PaywallPopup/README.md
new file mode 100644
index 0000000..072012c
--- /dev/null
+++ b/packages/generation/components/PaywallPopup/README.md
@@ -0,0 +1,16 @@
+# PaywallPopup
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+A wrapper around [TelegramPopup](../../../telegram-ui/components/TelegramPopup/README.md) with the ability to display a button-icon inside the web `Popup` component as specified in the [Media.presset](../Media/README.md).
+
+## Props
+
+All available props see in [PaywallPopup.props.ts](./PaywallPopup.props.ts)
+
+## i18n
+
+The component natively supports [i18n](../../../i18n/README.md) for text values inside buttons.
+
+You can provide a text as a locale token, and it will be dynamically translated
diff --git a/packages/generation/components/PaywallPopup/index.ts b/packages/generation/components/PaywallPopup/index.ts
new file mode 100644
index 0000000..f1d9936
--- /dev/null
+++ b/packages/generation/components/PaywallPopup/index.ts
@@ -0,0 +1,2 @@
+export * from './PaywallPopup.props';
+export { default as PaywallPopup } from './PaywallPopup.vue';
diff --git a/packages/generation/components/PrimitivePaywall/PrimitivePaywall.props.ts b/packages/generation/components/PrimitivePaywall/PrimitivePaywall.props.ts
new file mode 100644
index 0000000..9595fa0
--- /dev/null
+++ b/packages/generation/components/PrimitivePaywall/PrimitivePaywall.props.ts
@@ -0,0 +1,15 @@
+import type { PrimitiveSlideProps } from '@tok/generation/components/PrimitiveSlide';
+import type {
+ _GenerationPrimitivePaywallConfig,
+ _GenerationPrimitivePaywallProduct,
+} from '@tok/generation/defineConfig';
+
+export type PrimitivePaywallProps = Pick &
+ _GenerationPrimitivePaywallConfig & {
+ selectedProduct: _GenerationPrimitivePaywallProduct | null;
+ };
+
+export const PrimitivePaywallDefaultProps = {
+ links: () => [],
+ mainButtonText: 'Continue',
+} as const;
diff --git a/packages/generation/components/PrimitivePaywall/PrimitivePaywall.vue b/packages/generation/components/PrimitivePaywall/PrimitivePaywall.vue
new file mode 100644
index 0000000..bed4eeb
--- /dev/null
+++ b/packages/generation/components/PrimitivePaywall/PrimitivePaywall.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/components/PrimitivePaywall/README.md b/packages/generation/components/PrimitivePaywall/README.md
new file mode 100644
index 0000000..729fe32
--- /dev/null
+++ b/packages/generation/components/PrimitivePaywall/README.md
@@ -0,0 +1,19 @@
+# PaywallPopup
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component extends the [Slide.presset](../../pressets/slide/README.md) component.
+2. It overrides the default behavior of the MainButton inside the [Slide.presset](../../pressets/slide/README.md) component.
+3. It manages the MainButton with its own logic, including:
+ - Translating the mainButtonText.
+ - Replacing {price} inside the mainButtonText.
+ - Displaying a popup with the payment method.
+ - Tracking the selected product.
+ - If a product isn't selected, the MainButton won't be displayed
+4. It renders [Link](../../../ui/components/Link/README.md).
+5. It sends data to the bot when the payment method is selected.
+
+## Props
+
+All available props see in [PrimitivePaywall.props.ts](./PrimitivePaywall.props.ts)
diff --git a/packages/generation/components/PrimitivePaywall/index.ts b/packages/generation/components/PrimitivePaywall/index.ts
new file mode 100644
index 0000000..45a4c80
--- /dev/null
+++ b/packages/generation/components/PrimitivePaywall/index.ts
@@ -0,0 +1,2 @@
+export * from './PrimitivePaywall.props';
+export { default as PrimitivePaywall } from './PrimitivePaywall.vue';
diff --git a/packages/generation/components/PrimitiveSlide/PrimitiveSlide.props.ts b/packages/generation/components/PrimitiveSlide/PrimitiveSlide.props.ts
new file mode 100644
index 0000000..3a0b249
--- /dev/null
+++ b/packages/generation/components/PrimitiveSlide/PrimitiveSlide.props.ts
@@ -0,0 +1,18 @@
+import type { _GenerationSlideConfig } from '@tok/generation/defineConfig';
+
+export type PrimitiveSlideProps = Pick<
+ _GenerationSlideConfig,
+ 'media' | 'textAlign' | 'shape' | 'background' | 'button' | 'extends'
+> & {
+ active?: boolean;
+};
+
+export type PrimitiveSlideEmits = {
+ (e: 'onClick'): void;
+};
+
+export const PrimitiveSlideDefaultProps = {
+ button: 'Next',
+ textAlign: 'left',
+ shape: 'square',
+} as const;
diff --git a/packages/generation/components/PrimitiveSlide/PrimitiveSlide.vue b/packages/generation/components/PrimitiveSlide/PrimitiveSlide.vue
new file mode 100644
index 0000000..f5afc31
--- /dev/null
+++ b/packages/generation/components/PrimitiveSlide/PrimitiveSlide.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/components/PrimitiveSlide/README.md b/packages/generation/components/PrimitiveSlide/README.md
new file mode 100644
index 0000000..c87bbc5
--- /dev/null
+++ b/packages/generation/components/PrimitiveSlide/README.md
@@ -0,0 +1,14 @@
+# PrimitiveSlide
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component can render [Media.presset](../Media/README.md).
+2. It manages the `textAlign` property inside the content section.
+3. Can change the positioning of media and content depending on the `shape` prop
+4. It handles [MainButton](../../../telegram-ui/components/MainButton/README.md) behavior, including text translation.
+5. It triggers focus on a fake `div` element to initiate blur on inputs.
+
+## Props
+
+All available props see in [PrimitiveSlide.props.ts](./PrimitiveSlide.props.ts)
diff --git a/packages/generation/components/PrimitiveSlide/index.ts b/packages/generation/components/PrimitiveSlide/index.ts
new file mode 100644
index 0000000..2a5be36
--- /dev/null
+++ b/packages/generation/components/PrimitiveSlide/index.ts
@@ -0,0 +1,2 @@
+export * from './PrimitiveSlide.props';
+export { default as PrimitiveSlide } from './PrimitiveSlide.vue';
diff --git a/packages/generation/components/README.md b/packages/generation/components/README.md
new file mode 100644
index 0000000..cc82b75
--- /dev/null
+++ b/packages/generation/components/README.md
@@ -0,0 +1,8 @@
+# Components
+
+1. [DrawPresset](./DrawPresset/README.md)
+2. [ListItem](./ListItem/README.md)
+3. [Media](./Media/README.md)
+4. [PaywallPopup](./PaywallPopup/README.md)
+5. [PrimitivePaywall](./PrimitiveSlide/README.md)
+6. [PrimitiveSlide](./PrimitiveSlide/README.md)
diff --git a/packages/generation/defineConfig.ts b/packages/generation/defineConfig.ts
new file mode 100644
index 0000000..dba7d38
--- /dev/null
+++ b/packages/generation/defineConfig.ts
@@ -0,0 +1,158 @@
+import type { MediaPressetProps } from '@tok/generation/components/Media';
+import type { ThemeConfigParam } from '@tok/generation/tokens';
+import type { FlatButtonProps } from '@tok/ui/components/FlatButton';
+import type { CurrencyOptions } from '@tok/ui/tokens';
+import type { PopupButton } from '@twa-dev/types';
+import type { CSSProperties } from 'vue';
+
+export type PaywalPopupPressetButtons = (PopupButton & {
+ media?: MediaPressetProps;
+})[];
+
+export type _GenerationSlideConfig = {
+ extends?: 'slide';
+
+ media?: MediaPressetProps;
+
+ title: string;
+
+ description?: string;
+
+ pagination?: 'count';
+
+ shape?: 'rounded' | 'square' | 'stacked';
+
+ textAlign?: 'left' | 'center' | 'right';
+
+ background?: string;
+
+ list?: _GenerationListOptionsConfig;
+
+ button: string | (Pick & { content: string }) | null;
+};
+
+export type _GenerationFormControlConfig = {
+ id: string;
+ placeholder: string;
+ type?: 'checkbox' | 'number' | 'text' | string;
+ style?: string | CSSProperties;
+};
+
+export type _GenerationFormConfig = Omit<_GenerationSlideConfig, 'extends'> & {
+ extends: 'form';
+
+ form: _GenerationFormControlConfig[];
+};
+
+export type _GenerationListOptionsConfig = (
+ | {
+ media?: MediaPressetProps;
+ text: string;
+ }
+ | string
+)[];
+
+export type _GenerationPaywallPopup = {
+ // telegram by default
+ type?: 'web' | 'telegram';
+ title?: string;
+ message?: string;
+ buttons?: PaywalPopupPressetButtons;
+};
+
+export type _GenerationPrimitivePaywallConfig = Omit<
+ _GenerationSlideConfig,
+ 'button' | 'extends'
+> & {
+ // can be shown with price "Something {price} subscribe"
+ mainButtonText?: string;
+
+ links: { text: string; href: string }[];
+
+ popup?: _GenerationPaywallPopup;
+};
+
+export type _GenerationPrimitivePaywallProduct = {
+ id: string;
+ price: number | string;
+ title: string;
+ description: string;
+};
+
+export type _GenerationPaywallProduct = _GenerationPrimitivePaywallProduct & {
+ discount?: string;
+};
+
+export type _GenerationPaywallConfig = _GenerationPrimitivePaywallConfig & {
+ extends: 'paywall';
+
+ products: _GenerationPaywallProduct[];
+};
+
+export type _GenerationPaywallSingleConfig =
+ _GenerationPrimitivePaywallConfig & {
+ extends: 'paywall_single';
+
+ product: _GenerationPrimitivePaywallProduct & {
+ media?: MediaPressetProps;
+ };
+ };
+
+export type _GenerationPaywallRowProductConfig =
+ _GenerationPrimitivePaywallProduct & {
+ bestText?: string;
+ };
+
+export type _GenerationPaywallRowConfig = _GenerationPrimitivePaywallConfig & {
+ extends: 'paywall_row';
+
+ products: _GenerationPaywallRowProductConfig[];
+};
+
+export type _GenerationCarouselConfig> = {
+ extends?: 'base';
+ slides: (
+ | _GenerationPaywallConfig
+ | _GenerationPaywallSingleConfig
+ | _GenerationPaywallRowConfig
+ | _GenerationSlideConfig
+ | _GenerationFormConfig
+ | { extends: keyof T; [key: string]: any }
+ )[];
+};
+
+type BootstrapPage> = {
+ path?: string;
+} & (
+ | _GenerationCarouselConfig
+ | _GenerationPaywallConfig
+ | _GenerationPaywallSingleConfig
+ | _GenerationPaywallRowConfig
+ | _GenerationSlideConfig
+ | _GenerationFormConfig
+ | { extends: keyof TCustom }
+);
+
+export type DefinedPressetsKeys> = T extends {
+ extends: infer E;
+}
+ ? E
+ : T extends { extends?: string }
+ ? 'base' | 'slide'
+ : never;
+
+export type BootstrapConfig> = {
+ theme?: ThemeConfigParam;
+ locale?: {
+ fallback: string;
+ } & Record;
+ currencyConfig?: CurrencyOptions;
+ definePressets?: TDefined;
+ pages: BootstrapPage[];
+};
+
+export function defineConfig>(
+ config: BootstrapConfig
+) {
+ return config;
+}
diff --git a/packages/generation/index.ts b/packages/generation/index.ts
new file mode 100644
index 0000000..0054eb8
--- /dev/null
+++ b/packages/generation/index.ts
@@ -0,0 +1,3 @@
+export { bootstrap } from './bootstrap';
+export { defineConfig } from './defineConfig';
+export { default as Root } from './Root.vue';
diff --git a/packages/generation/package.json b/packages/generation/package.json
new file mode 100644
index 0000000..5af89e9
--- /dev/null
+++ b/packages/generation/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@tok/generation",
+ "version": "0.0.0",
+ "main": "./index.ts",
+ "types": "./index.ts",
+ "dependencies": {
+ "@tok/i18n": "*",
+ "@tok/telegram-ui": "*",
+ "@tok/ui": "*",
+ "vue": "^3.3.4",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@tok/eslint-config": "*",
+ "@tok/tsconfig": "*",
+ "@types/node": "^20.8.0",
+ "@types/pako": "^1.0.1",
+ "@vitejs/plugin-vue": "^4.2.3",
+ "pako": "1.0.11",
+ "vite": "^4.4.9",
+ "vite-svg-loader": "^4.0.0"
+ }
+}
\ No newline at end of file
diff --git a/packages/generation/plugins/README.md b/packages/generation/plugins/README.md
new file mode 100644
index 0000000..f5db654
--- /dev/null
+++ b/packages/generation/plugins/README.md
@@ -0,0 +1,13 @@
+# Plugins
+
+1. [definePressets](./definePressets/DefinePressets.plugin.ts)
+
+The main plugin for defining presets within an application generated using the `bootstrap` function
+
+2. [formState](./formState/FormState.plugin.ts)
+
+Plugin for defining form state within an application generated using the `bootstrap` function
+
+3. [theme](./theme/Theme.plugin.ts)
+
+Plugin for defining theme value within an application generated using the `bootstrap` function
diff --git a/packages/generation/plugins/definePressets/DefinePressets.plugin.ts b/packages/generation/plugins/definePressets/DefinePressets.plugin.ts
new file mode 100644
index 0000000..634191e
--- /dev/null
+++ b/packages/generation/plugins/definePressets/DefinePressets.plugin.ts
@@ -0,0 +1,14 @@
+import {
+ DEFINED_PRESSETS_TOKEN,
+ predefinedPressets,
+} from '@tok/generation/tokens';
+import { Plugin } from 'vue';
+
+export const DefinePressetsPlugin: Plugin<{}> = {
+ install(app, options) {
+ app.provide(DEFINED_PRESSETS_TOKEN, {
+ ...predefinedPressets,
+ ...options,
+ });
+ },
+};
diff --git a/packages/generation/plugins/definePressets/index.ts b/packages/generation/plugins/definePressets/index.ts
new file mode 100644
index 0000000..2bf2187
--- /dev/null
+++ b/packages/generation/plugins/definePressets/index.ts
@@ -0,0 +1 @@
+export * from './DefinePressets.plugin';
diff --git a/packages/generation/plugins/formState/FormState.plugin.ts b/packages/generation/plugins/formState/FormState.plugin.ts
new file mode 100644
index 0000000..07a599a
--- /dev/null
+++ b/packages/generation/plugins/formState/FormState.plugin.ts
@@ -0,0 +1,17 @@
+import { FORM_STATE_TOKEN } from '@tok/generation/tokens';
+import { App, Plugin, ref } from 'vue';
+
+export const FormStatePlugin: Plugin = {
+ install(app: App) {
+ const state = ref>({});
+
+ const update = (value: Record) => {
+ state.value = {
+ ...state.value,
+ ...value,
+ };
+ };
+
+ app.provide(FORM_STATE_TOKEN, { state, update });
+ },
+};
diff --git a/packages/generation/plugins/formState/index.ts b/packages/generation/plugins/formState/index.ts
new file mode 100644
index 0000000..e987eff
--- /dev/null
+++ b/packages/generation/plugins/formState/index.ts
@@ -0,0 +1 @@
+export * from './FormState.plugin';
diff --git a/packages/generation/plugins/theme/Theme.plugin.ts b/packages/generation/plugins/theme/Theme.plugin.ts
new file mode 100644
index 0000000..af5e995
--- /dev/null
+++ b/packages/generation/plugins/theme/Theme.plugin.ts
@@ -0,0 +1,8 @@
+import { THEME_TOKEN, ThemeConfigParam } from '@tok/generation/tokens';
+import { App, Plugin } from 'vue';
+
+export const ThemePlugin: Plugin = {
+ install(app: App, themeValue: ThemeConfigParam) {
+ app.provide(THEME_TOKEN, themeValue);
+ },
+};
diff --git a/packages/generation/plugins/theme/index.ts b/packages/generation/plugins/theme/index.ts
new file mode 100644
index 0000000..2a716d8
--- /dev/null
+++ b/packages/generation/plugins/theme/index.ts
@@ -0,0 +1 @@
+export * from './Theme.plugin';
diff --git a/packages/generation/pressets/README.md b/packages/generation/pressets/README.md
new file mode 100644
index 0000000..70d51a9
--- /dev/null
+++ b/packages/generation/pressets/README.md
@@ -0,0 +1,158 @@
+# Pressets
+
+1. [base](./base/README.md)
+2. [form](./form/README.md)
+3. [paywall](./paywall/README.md)
+4. [paywall_row](./paywall_row/README.md)
+5. [paywall_single](./paywall_single/README.md)
+6. [slide](./slide/README.md)
+7. [Route](./Route.vue)
+
+## Base
+
+```ts
+defineConfig({
+ pages: [
+ {
+ // extends: '', <- empty extends
+ slides: [
+ // ...
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+or
+
+```ts
+defineConfig({
+ pages: [
+ {
+ extends: 'base',
+ slides: [
+ // ...
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+## Slide
+
+```ts
+defineConfig({
+ pages: [
+ {
+ // extends: '', <- empty extends
+ slides: [
+ {
+ // extends: '',
+ title: 'hello',
+ // ...
+ },
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+or
+
+```ts
+defineConfig({
+ pages: [
+ {
+ extends: 'base',
+ slides: [
+ {
+ extends: 'slide',
+ title: 'Hello',
+ // ...
+ },
+ {
+ // extends: '', <- empty extend
+ title: 'Hello',
+ // ...
+ },
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+## Form
+
+```ts
+defineConfig({
+ pages: [
+ {
+ extends: 'form',
+ form: [
+ {
+ id: 'id',
+ type: 'text',
+ placeholder: 'text',
+ },
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+## Paywall
+
+```ts
+defineConfig({
+ pages: [
+ {
+ extends: 'paywall',
+ products: [
+ // ...
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+## Paywall Row
+
+```ts
+defineConfig({
+ pages: [
+ {
+ extends: 'paywall_row',
+ products: [
+ // ...
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+## Paywall Signle
+
+```ts
+defineConfig({
+ pages: [
+ {
+ extends: 'paywall_single',
+ products: [
+ // ...
+ ],
+ // ...
+ },
+ ],
+});
+```
+
+## Route.vue
+
+The component will extract the configuration from the route's meta parameter to render the next preset and bind all configuration properties to this component
diff --git a/packages/generation/pressets/Route.vue b/packages/generation/pressets/Route.vue
new file mode 100644
index 0000000..239a3b7
--- /dev/null
+++ b/packages/generation/pressets/Route.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/packages/generation/pressets/base/README.md b/packages/generation/pressets/base/README.md
new file mode 100644
index 0000000..7f1e009
--- /dev/null
+++ b/packages/generation/pressets/base/README.md
@@ -0,0 +1,14 @@
+# Base.presset
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component is based on the [Carousel](../../../ui/components/Carousel/README.md) component.
+2. It tracks the active index of the carousel and sets it into the query.
+3. It safely restores the active index from the query.
+4. It defines a safe accessor for the [CAROUSEL_ACCESSOR_TOKEN](../../use/README.md).
+5. It renders presets using the [DrawPresset](../../components/DrawPresset/README.md) component.
+
+## Props
+
+All available props see in [base.presset.props.ts](./base.presset.props.ts)
diff --git a/packages/generation/pressets/base/base.presset.props.ts b/packages/generation/pressets/base/base.presset.props.ts
new file mode 100644
index 0000000..c286723
--- /dev/null
+++ b/packages/generation/pressets/base/base.presset.props.ts
@@ -0,0 +1,7 @@
+import type { _GenerationCarouselConfig } from '@tok/generation/defineConfig';
+
+export type BasePressetProps = _GenerationCarouselConfig;
+
+export const BasePressetDefaultProps = {
+ slides: () => [],
+} as const;
diff --git a/packages/generation/pressets/base/base.presset.vue b/packages/generation/pressets/base/base.presset.vue
new file mode 100644
index 0000000..49042a8
--- /dev/null
+++ b/packages/generation/pressets/base/base.presset.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/base/index.ts b/packages/generation/pressets/base/index.ts
new file mode 100644
index 0000000..68fdf80
--- /dev/null
+++ b/packages/generation/pressets/base/index.ts
@@ -0,0 +1,2 @@
+export * from './base.presset.props';
+export { default as BasePresset } from './base.presset.vue';
diff --git a/packages/generation/pressets/form/README.md b/packages/generation/pressets/form/README.md
new file mode 100644
index 0000000..33826b8
--- /dev/null
+++ b/packages/generation/pressets/form/README.md
@@ -0,0 +1,17 @@
+# Form.presset
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component is built upon the [Slide.presset](../slide/README.md) component.
+2. It can render controls based on its configuration:
+ - InputText (type="text")
+ - InputText (type="number")
+ - CheckboxBlock (type="checkbox")
+3. It supports various types of inputs that can be configured within [defineConfig](../../defineConfig.ts), such as type="date."
+4. It handles all controls and sets their values to the formState via [FORM_STATE_TOKEN](../../tokens/README.md).
+5. It supports restoring values from the formState even when a control has been destroyed.
+
+## Props
+
+All available props see in [form.presset.props.ts](./form.presset.props.ts)
diff --git a/packages/generation/pressets/form/form.presset.props.ts b/packages/generation/pressets/form/form.presset.props.ts
new file mode 100644
index 0000000..67a5566
--- /dev/null
+++ b/packages/generation/pressets/form/form.presset.props.ts
@@ -0,0 +1,15 @@
+import type { _GenerationFormConfig } from '@tok/generation/defineConfig';
+
+export type FormPressetProps = Omit<_GenerationFormConfig, 'extends'>;
+
+const defaultForm = [
+ {
+ id: 'id1',
+ placeholder: 'placeholder for type: text',
+ type: 'text' as const,
+ },
+];
+
+export const FormPressetDefaultProps = {
+ form: () => defaultForm,
+} as const;
diff --git a/packages/generation/pressets/form/form.presset.vue b/packages/generation/pressets/form/form.presset.vue
new file mode 100644
index 0000000..72f63c5
--- /dev/null
+++ b/packages/generation/pressets/form/form.presset.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/form/index.ts b/packages/generation/pressets/form/index.ts
new file mode 100644
index 0000000..67f9e62
--- /dev/null
+++ b/packages/generation/pressets/form/index.ts
@@ -0,0 +1,2 @@
+export * from './form.presset.props';
+export { default as FormPresset } from './form.presset.vue';
diff --git a/packages/generation/pressets/paywall/BaseProduct.vue b/packages/generation/pressets/paywall/BaseProduct.vue
new file mode 100644
index 0000000..509810b
--- /dev/null
+++ b/packages/generation/pressets/paywall/BaseProduct.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/paywall/README.md b/packages/generation/pressets/paywall/README.md
new file mode 100644
index 0000000..21d7b21
--- /dev/null
+++ b/packages/generation/pressets/paywall/README.md
@@ -0,0 +1,12 @@
+# Paywall.presset
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component is built upon the [PrimitivePaywall.presset](../../components/PrimitivePaywall/README.md) component.
+2. It can display products in a column, where only one can be selected.
+3. It translates the title, description, and discount of the products.
+
+## Props
+
+All available props see in [paywall.presset.props.ts](./paywall.presset.props.ts)
diff --git a/packages/generation/pressets/paywall/index.ts b/packages/generation/pressets/paywall/index.ts
new file mode 100644
index 0000000..2ec5a07
--- /dev/null
+++ b/packages/generation/pressets/paywall/index.ts
@@ -0,0 +1,2 @@
+export * from './paywall.presset.props';
+export { default as PaywallPresset } from './paywall.presset.vue';
diff --git a/packages/generation/pressets/paywall/paywall.presset.props.ts b/packages/generation/pressets/paywall/paywall.presset.props.ts
new file mode 100644
index 0000000..49a5b03
--- /dev/null
+++ b/packages/generation/pressets/paywall/paywall.presset.props.ts
@@ -0,0 +1,7 @@
+import type { _GenerationPaywallConfig } from '@tok/generation/defineConfig';
+
+export type PaywallPressetProps = Omit<_GenerationPaywallConfig, 'extends'>;
+
+export const PaywallPressetDefaultProps = {
+ products: () => [],
+} as const;
diff --git a/packages/generation/pressets/paywall/paywall.presset.vue b/packages/generation/pressets/paywall/paywall.presset.vue
new file mode 100644
index 0000000..4faf496
--- /dev/null
+++ b/packages/generation/pressets/paywall/paywall.presset.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/paywall_row/Product.vue b/packages/generation/pressets/paywall_row/Product.vue
new file mode 100644
index 0000000..fc538fa
--- /dev/null
+++ b/packages/generation/pressets/paywall_row/Product.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/paywall_row/README.md b/packages/generation/pressets/paywall_row/README.md
new file mode 100644
index 0000000..df86572
--- /dev/null
+++ b/packages/generation/pressets/paywall_row/README.md
@@ -0,0 +1,13 @@
+# PaywallRow.presset
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component is built upon the [PrimitivePaywall.presset](../../components/PrimitivePaywall/README.md) component.
+2. It can display products in a **row**, where only one can be selected.
+3. It translates the "title", "description", and "bestText" of the products.
+4. It can detect the length of products to ensure user-friendly product selection behavior.
+
+## Props
+
+All available props see in [paywall_row.presset.props.ts](./paywall_row.presset.props.ts)
diff --git a/packages/generation/pressets/paywall_row/index.ts b/packages/generation/pressets/paywall_row/index.ts
new file mode 100644
index 0000000..4ce7c10
--- /dev/null
+++ b/packages/generation/pressets/paywall_row/index.ts
@@ -0,0 +1,2 @@
+export * from './paywall_row.presset.props';
+export { default as PaywallRowPresset } from './paywall_row.presset.vue';
diff --git a/packages/generation/pressets/paywall_row/paywall_row.presset.props.ts b/packages/generation/pressets/paywall_row/paywall_row.presset.props.ts
new file mode 100644
index 0000000..033fd24
--- /dev/null
+++ b/packages/generation/pressets/paywall_row/paywall_row.presset.props.ts
@@ -0,0 +1,10 @@
+import type { _GenerationPaywallRowConfig } from '@tok/generation/defineConfig';
+
+export type PaywallRowPressetProps = Omit<
+ _GenerationPaywallRowConfig,
+ 'extends'
+>;
+
+export const PaywallRowPressetDefaultProps = {
+ products: () => [],
+} as const;
diff --git a/packages/generation/pressets/paywall_row/paywall_row.presset.vue b/packages/generation/pressets/paywall_row/paywall_row.presset.vue
new file mode 100644
index 0000000..40b067c
--- /dev/null
+++ b/packages/generation/pressets/paywall_row/paywall_row.presset.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/paywall_single/README.md b/packages/generation/pressets/paywall_single/README.md
new file mode 100644
index 0000000..728d480
--- /dev/null
+++ b/packages/generation/pressets/paywall_single/README.md
@@ -0,0 +1,12 @@
+# PaywallSingle.presset
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component is built upon the [PrimitivePaywall.presset](../../components/PrimitivePaywall/README.md) component.
+2. It can display only one product, which is always selected.
+3. It translates the title and description of the product.
+
+## Props
+
+All available props see in [paywall_single.presset.props.ts](./paywall_single.presset.props.ts)
diff --git a/packages/generation/pressets/paywall_single/index.ts b/packages/generation/pressets/paywall_single/index.ts
new file mode 100644
index 0000000..c8885d9
--- /dev/null
+++ b/packages/generation/pressets/paywall_single/index.ts
@@ -0,0 +1,2 @@
+export * from './paywall_single.presset.props';
+export { default as PaywallSinglePresset } from './paywall_single.presset.vue';
diff --git a/packages/generation/pressets/paywall_single/paywall_single.presset.props.ts b/packages/generation/pressets/paywall_single/paywall_single.presset.props.ts
new file mode 100644
index 0000000..8a698cb
--- /dev/null
+++ b/packages/generation/pressets/paywall_single/paywall_single.presset.props.ts
@@ -0,0 +1,17 @@
+import type { _GenerationPaywallSingleConfig } from '@tok/generation/defineConfig';
+
+export type PaywallSinglePressetProps = Omit<
+ _GenerationPaywallSingleConfig,
+ 'extends'
+>;
+
+const defaultProduct = {
+ id: 'id1',
+ title: 'Product Title',
+ price: 99.99,
+ description: 'Product description',
+};
+
+export const PaywallSinglePressetDefaultProps = {
+ product: () => defaultProduct,
+} as const;
diff --git a/packages/generation/pressets/paywall_single/paywall_single.presset.vue b/packages/generation/pressets/paywall_single/paywall_single.presset.vue
new file mode 100644
index 0000000..d580abc
--- /dev/null
+++ b/packages/generation/pressets/paywall_single/paywall_single.presset.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/pressets/slide/README.md b/packages/generation/pressets/slide/README.md
new file mode 100644
index 0000000..4a43a79
--- /dev/null
+++ b/packages/generation/pressets/slide/README.md
@@ -0,0 +1,15 @@
+# Slide.presset
+
+> [!NOTE]
+> This component is only intended for use with [bootstrap.ts](../../bootstrap.ts).
+
+1. This component is based on the [PrimitiveSlide.presset](../../components/PrimitiveSlide/README.md) component.
+2. It can display pagination if it's inside a carousel.
+3. It translates the title and description of the slide.
+4. It can render lists:
+ - If the `list` prop is of type string[], it will add default media to every list item.
+5. It handles MainButton.onClick to navigate to the next slide.
+
+## Props
+
+All available props see in [slide.presset.props.ts](./slide.presset.props.ts)
diff --git a/packages/generation/pressets/slide/index.ts b/packages/generation/pressets/slide/index.ts
new file mode 100644
index 0000000..839f06d
--- /dev/null
+++ b/packages/generation/pressets/slide/index.ts
@@ -0,0 +1,2 @@
+export * from './slide.presset.props';
+export { default as SlidePresset } from './slide.presset.vue';
diff --git a/packages/generation/pressets/slide/slide.presset.props.ts b/packages/generation/pressets/slide/slide.presset.props.ts
new file mode 100644
index 0000000..9f9438b
--- /dev/null
+++ b/packages/generation/pressets/slide/slide.presset.props.ts
@@ -0,0 +1,16 @@
+import type { PrimitiveSlideProps } from '@tok/generation/components/PrimitiveSlide';
+import type { _GenerationSlideConfig } from '@tok/generation/defineConfig';
+
+export type SlidePressetProps = PrimitiveSlideProps &
+ Pick<_GenerationSlideConfig, 'title' | 'description' | 'pagination' | 'list'>;
+
+export const defaultSlideListMedia = {
+ type: 'icon' as const,
+ src: 'checkmark-fill',
+} as const;
+
+export const SlidePressetDefaultProps = {
+ title: 'Title',
+ description: '',
+ list: () => [],
+} as const;
diff --git a/packages/generation/pressets/slide/slide.presset.vue b/packages/generation/pressets/slide/slide.presset.vue
new file mode 100644
index 0000000..1f60bf9
--- /dev/null
+++ b/packages/generation/pressets/slide/slide.presset.vue
@@ -0,0 +1,91 @@
+
+
+
+ {{ slideCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/generation/tokens/README.md b/packages/generation/tokens/README.md
new file mode 100644
index 0000000..93ae46b
--- /dev/null
+++ b/packages/generation/tokens/README.md
@@ -0,0 +1,17 @@
+# Tokens
+
+1. [definedPressets.token](./definedPressets.token.ts)
+
+Token that you can use to access all defined presets. Within definedPresets.token, you can find all the predefined presets that are used across all example projects
+
+2. [formState.token](./formState.token.ts)
+
+Token that you can use to access the state of the form. This state will be populated with values inside the [form.presset](../pressets/README.md).
+
+3. [Theme.token](./theme.token.ts)
+
+Token that provides access to the theme variable defined within the `defineConfig` function
+
+4. [wasInteraction.token](./wasInteraction.token.ts)
+
+Token that provides access to a boolean variable representing the first interaction on the page
diff --git a/packages/generation/tokens/definedPressets.token.ts b/packages/generation/tokens/definedPressets.token.ts
new file mode 100644
index 0000000..e9103a6
--- /dev/null
+++ b/packages/generation/tokens/definedPressets.token.ts
@@ -0,0 +1,34 @@
+import { DefinedPressetsKeys } from '@tok/generation/defineConfig';
+import { BasePresset } from '@tok/generation/pressets/base';
+import { SlidePresset } from '@tok/generation/pressets/slide';
+import { defineAsyncComponent, InjectionKey } from 'vue';
+
+export type DefinePressets = Record<
+ DefinedPressetsKeys,
+ ReturnType
+>;
+
+export const predefinedPressets: DefinePressets = {
+ base: BasePresset,
+ slide: SlidePresset,
+ paywall: defineAsyncComponent(() =>
+ import('@tok/generation/pressets/paywall').then((m) => m.PaywallPresset)
+ ),
+ form: defineAsyncComponent(() =>
+ import('@tok/generation/pressets/form').then((m) => m.FormPresset)
+ ),
+ paywall_single: defineAsyncComponent(() =>
+ import('@tok/generation/pressets/paywall_single').then(
+ (m) => m.PaywallSinglePresset
+ )
+ ),
+ paywall_row: defineAsyncComponent(() =>
+ import('@tok/generation/pressets/paywall_row').then(
+ (m) => m.PaywallRowPresset
+ )
+ ),
+};
+
+export const DEFINED_PRESSETS_TOKEN = Symbol() as InjectionKey<
+ Record
+>;
diff --git a/packages/generation/tokens/formState.token.ts b/packages/generation/tokens/formState.token.ts
new file mode 100644
index 0000000..e946ad6
--- /dev/null
+++ b/packages/generation/tokens/formState.token.ts
@@ -0,0 +1,8 @@
+import { InjectionKey, Ref } from 'vue';
+
+type State = {
+ state: Ref>;
+ update(value: Record): void;
+};
+
+export const FORM_STATE_TOKEN = Symbol() as InjectionKey;
diff --git a/packages/generation/tokens/index.ts b/packages/generation/tokens/index.ts
new file mode 100644
index 0000000..31beb8e
--- /dev/null
+++ b/packages/generation/tokens/index.ts
@@ -0,0 +1,4 @@
+export * from './definedPressets.token';
+export * from './formState.token';
+export * from './theme.token';
+export * from './wasInteraction.token';
diff --git a/packages/generation/tokens/theme.token.ts b/packages/generation/tokens/theme.token.ts
new file mode 100644
index 0000000..89086f3
--- /dev/null
+++ b/packages/generation/tokens/theme.token.ts
@@ -0,0 +1,5 @@
+import { InjectionKey } from 'vue';
+
+export type ThemeConfigParam = 'light' | 'dark' | 'auto';
+
+export const THEME_TOKEN = Symbol() as InjectionKey;
diff --git a/packages/generation/tokens/wasInteraction.token.ts b/packages/generation/tokens/wasInteraction.token.ts
new file mode 100644
index 0000000..d260ac7
--- /dev/null
+++ b/packages/generation/tokens/wasInteraction.token.ts
@@ -0,0 +1,3 @@
+import { InjectionKey, Ref } from 'vue';
+
+export const WAS_INTERACTION_TOKEN = Symbol() as InjectionKey>;
diff --git a/packages/generation/tsconfig.json b/packages/generation/tsconfig.json
new file mode 100644
index 0000000..954d712
--- /dev/null
+++ b/packages/generation/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@tok/tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "types": ["pako", "@types/node", "vite/client"]
+ },
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"]
+}
diff --git a/packages/generation/types/tgs.d.ts b/packages/generation/types/tgs.d.ts
new file mode 100644
index 0000000..cf5ae23
--- /dev/null
+++ b/packages/generation/types/tgs.d.ts
@@ -0,0 +1,9 @@
+declare type TelegramStickerJson = {
+ tgs: number;
+ v: string;
+ fr: number;
+};
+
+declare module '*.tgs' {
+ export default TelegramStickerJson;
+}
diff --git a/packages/generation/use/README.md b/packages/generation/use/README.md
new file mode 100644
index 0000000..a394336
--- /dev/null
+++ b/packages/generation/use/README.md
@@ -0,0 +1,25 @@
+# Use
+
+1. [useCarouse](./carousel/index.ts)
+
+Provides you with access to [Carousel](../../ui/components/Carousel/README.md) inside [Base.presset.ts](../pressets/README.md) for managing or reading `activeIndex`, `length` (for pagination), and safly setting `activeIndex` via `MainButton` or `BackButton`.
+
+## Usage
+
+> [!IMPORTANT]
+> Ensure that you call this function within one of the 'children' of the Base.presset component or the component that defines the accessor for the `CAROUSEL_ACCESSOR_TOKEN`. Otherwise, the function will return `null`
+
+```vue
+
+```
diff --git a/packages/generation/use/carousel/index.ts b/packages/generation/use/carousel/index.ts
new file mode 100644
index 0000000..1f14b43
--- /dev/null
+++ b/packages/generation/use/carousel/index.ts
@@ -0,0 +1,23 @@
+import { inject, InjectionKey, Ref } from 'vue';
+
+type Accessor = {
+ next: () => void;
+ back: () => void;
+ set: (index: number) => void;
+ index: Ref;
+ length: Ref;
+};
+
+export const CAROUSEL_ACCESSOR_TOKEN = Symbol() as InjectionKey;
+
+export function useCarousel(): Accessor | null {
+ const accessor = inject(CAROUSEL_ACCESSOR_TOKEN, null);
+
+ if (accessor === null) {
+ console.warn(
+ 'You are using carousel methods outside of the carousel component'
+ );
+ }
+
+ return accessor;
+}
diff --git a/packages/i18n/.eslintrc.js b/packages/i18n/.eslintrc.js
new file mode 100644
index 0000000..a5e5ec9
--- /dev/null
+++ b/packages/i18n/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+ root: true,
+ extends: ['@tok/eslint-config'],
+ parserOptions: { tsconfigRootDir: __dirname },
+};
diff --git a/packages/i18n/README.md b/packages/i18n/README.md
new file mode 100644
index 0000000..6019a22
--- /dev/null
+++ b/packages/i18n/README.md
@@ -0,0 +1,127 @@
+# @tok/i18n
+
+A minimalistic package for handling localization in your applications
+
+## Setup
+
+```ts
+// main.ts
+import { TokI18nPlugin } from '@tok/i18n';
+import { createApp } from 'vue';
+import App from './App.vue';
+
+const locales = {
+ default: 'en',
+ asyncLocales: {
+ ch: import('./locale/ch.json')
+ },
+ messages: {
+ en: {
+ 'hello': 'Hello!'
+ }
+ }
+;}
+
+createApp(App).use(TokI18nPlugin, locales).mount('#app');
+```
+
+## Usage
+
+### useTranslated
+
+This will track the value in the hello ref and always return the translated value if available.
+
+```vue
+
+ {{ translatedHello }}
+
+
+
+```
diff --git a/packages/i18n/index.ts b/packages/i18n/index.ts
new file mode 100644
index 0000000..93e9ab1
--- /dev/null
+++ b/packages/i18n/index.ts
@@ -0,0 +1,2 @@
+export * from './plugins';
+export * from './use';
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
new file mode 100644
index 0000000..85aa85b
--- /dev/null
+++ b/packages/i18n/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@tok/i18n",
+ "version": "0.0.0",
+ "main": "./index.ts",
+ "types": "./index.ts",
+ "dependencies": {
+ "vue": "^3.3.4"
+ },
+ "devDependencies": {
+ "@tok/eslint-config": "*",
+ "@tok/tsconfig": "*"
+ }
+}
\ No newline at end of file
diff --git a/packages/i18n/plugins/index.ts b/packages/i18n/plugins/index.ts
new file mode 100644
index 0000000..73cb2f8
--- /dev/null
+++ b/packages/i18n/plugins/index.ts
@@ -0,0 +1,30 @@
+import { InjectionKey, Plugin, Ref, ref } from 'vue';
+
+type Loader> = Promise<{ default: T }>;
+
+type Options = {
+ default: string;
+ asyncLocales: Record;
+ messages?: Record;
+};
+
+export const TOK_I18N_TOKEN = Symbol() as InjectionKey<{
+ fallbackLocale: string;
+ locale: Ref;
+ loaders: Record;
+ messages: Ref>;
+}>;
+
+export const TokI18nPlugin: Plugin = {
+ install(app, options) {
+ const locale = ref(options.default);
+ const _messages = ref(options.messages || {});
+
+ app.provide(TOK_I18N_TOKEN, {
+ fallbackLocale: options.default,
+ locale,
+ loaders: options.asyncLocales,
+ messages: _messages,
+ });
+ },
+};
diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json
new file mode 100644
index 0000000..954d712
--- /dev/null
+++ b/packages/i18n/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@tok/tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "types": ["pako", "@types/node", "vite/client"]
+ },
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"]
+}
diff --git a/packages/i18n/use/index.ts b/packages/i18n/use/index.ts
new file mode 100644
index 0000000..19ab1d9
--- /dev/null
+++ b/packages/i18n/use/index.ts
@@ -0,0 +1 @@
+export * from './useI18n';
diff --git a/packages/i18n/use/useI18n.ts b/packages/i18n/use/useI18n.ts
new file mode 100644
index 0000000..7bbfbcf
--- /dev/null
+++ b/packages/i18n/use/useI18n.ts
@@ -0,0 +1,129 @@
+import { TOK_I18N_TOKEN } from '@tok/i18n/plugins';
+import { computed, ComputedRef, inject, MaybeRefOrGetter, ref } from 'vue';
+
+const resolve = (r: T) => {
+ return typeof r === 'function' ? computed(r as any) : ref(r);
+};
+
+const tokTranslate = (
+ messages: Record,
+ key: string
+): T | undefined => {
+ const [_key, ..._rest] = key.split('.');
+
+ if (_key in messages && _rest.length === 0) {
+ return messages[_key] as T;
+ }
+
+ if (_key in messages) {
+ return tokTranslate((messages as any)[_key], _rest.join('.'));
+ }
+
+ return undefined;
+};
+
+export function useI18n() {
+ const instance = inject(TOK_I18N_TOKEN)!;
+
+ const availableFromAsync = Object.keys(instance.loaders || {});
+ const availableStatic = Object.keys(instance.messages.value || {});
+
+ const available = new Set([...availableFromAsync, ...availableStatic]);
+
+ const load = (locale: string) => {
+ if (!instance) {
+ return Promise.reject('Not found i18n instance');
+ }
+
+ const loader = instance.loaders[locale];
+
+ if (!loader) {
+ return Promise.reject('Not found locale');
+ }
+
+ return loader.then((m) => m.default);
+ };
+
+ const setMessages = (locale: string, messages: Record) => {
+ if (!instance) {
+ return;
+ }
+
+ const _messages = instance.messages.value;
+
+ instance.messages.value = {
+ ..._messages,
+ [locale]: messages,
+ };
+ };
+
+ const localed = computed(() => {
+ const _locale = instance?.locale?.value;
+
+ if (!_locale) {
+ return {};
+ }
+
+ const messages = instance?.messages.value?.[_locale];
+
+ return messages || {};
+ });
+
+ const translate = (key: T, fallback?: T): T => {
+ if (typeof key !== 'string') {
+ return key as T;
+ }
+
+ if (!key) {
+ return fallback ?? key;
+ }
+
+ const _locale = instance?.locale?.value;
+ const _localed =
+ (instance?.messages.value?.[_locale] as Record) || {};
+
+ const firstTry = tokTranslate(_localed, `${key}`);
+
+ if (firstTry) {
+ return firstTry as T;
+ }
+
+ const fallbackLocaled =
+ instance!.messages.value?.[instance!.fallbackLocale] || {};
+
+ const secondTry = tokTranslate(fallbackLocaled as {}, `${key}`) as T;
+
+ return secondTry ?? fallback ?? key;
+ };
+
+ const useTranslated = (
+ key: MaybeRefOrGetter,
+ fallback?: T
+ ): ComputedRef => {
+ const keyRef = resolve(key);
+
+ return computed(() => {
+ localed.value;
+ instance.messages.value;
+
+ const _value = keyRef.value;
+
+ if (typeof _value !== 'string') {
+ return _value as T;
+ }
+
+ return translate(_value as T, fallback);
+ });
+ };
+
+ return {
+ fallbackLocale: instance.fallbackLocale || 'en',
+ available: Array.from(available),
+ load,
+ setMessages,
+ locale: instance.locale,
+ messages: instance.messages,
+ useTranslated,
+ translate,
+ };
+}
diff --git a/packages/telegram-ui/.eslintrc.js b/packages/telegram-ui/.eslintrc.js
new file mode 100644
index 0000000..a5e5ec9
--- /dev/null
+++ b/packages/telegram-ui/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+ root: true,
+ extends: ['@tok/eslint-config'],
+ parserOptions: { tsconfigRootDir: __dirname },
+};
diff --git a/packages/telegram-ui/README.md b/packages/telegram-ui/README.md
new file mode 100644
index 0000000..46f4c56
--- /dev/null
+++ b/packages/telegram-ui/README.md
@@ -0,0 +1,52 @@
+# @tok/telegram-ui
+
+This package offers a convenient wrapper around the [@twa-dev/sdk](https://github.com/twa-dev/SDK), providing Vue-like components and use-case solutions for Popups, MainButton, BackButton, and Theme integration.
+
+## Components
+
+1. [BackButton](./components/BackButton/README.md)
+2. [MainButton](./components/MainButton/README.md)
+3. [Sticker](./components/Sticker/README.md)
+4. [TelegramPopup](./components/TelegramPopup/README.md)
+
+# Use
+
+## [useTelegramSdk](./use/sdk/index.ts)
+
+Returns whole instance of [@twa-dev/sdk](https://github.com/twa-dev/SDK)
+
+### Usage
+
+```vue
+
+```
+
+## [useTheme](./use/theme/index.ts)
+
+useTheme will track colorScheme from Telegram sdk in reactive way
+
+### Usage
+
+```vue
+
+```
diff --git a/packages/telegram-ui/components/BackButton/BackButton.props.ts b/packages/telegram-ui/components/BackButton/BackButton.props.ts
new file mode 100644
index 0000000..b758bd4
--- /dev/null
+++ b/packages/telegram-ui/components/BackButton/BackButton.props.ts
@@ -0,0 +1,18 @@
+import type { FlatButtonProps } from '@tok/ui/components/FlatButton';
+
+export type BackButtonProps = {
+ type?: 'telegram' | 'web';
+
+ show?: boolean;
+
+ appearance?: FlatButtonProps['appearance'];
+};
+
+export type BackButtonEmits = {
+ (e: 'onClick'): void;
+};
+
+export const BackButtonDefaultProps = {
+ type: 'telegram',
+ appearance: 'flat',
+} as const;
diff --git a/packages/telegram-ui/components/BackButton/BackButton.vue b/packages/telegram-ui/components/BackButton/BackButton.vue
new file mode 100644
index 0000000..3d617b5
--- /dev/null
+++ b/packages/telegram-ui/components/BackButton/BackButton.vue
@@ -0,0 +1,86 @@
+
+
+ Back
+
+
+
+
+
+
diff --git a/packages/telegram-ui/components/BackButton/README.md b/packages/telegram-ui/components/BackButton/README.md
new file mode 100644
index 0000000..8f66b76
--- /dev/null
+++ b/packages/telegram-ui/components/BackButton/README.md
@@ -0,0 +1,54 @@
+# BackButton
+
+Wrapper around Telegram.BackButton with fallback to [FlatButton](../../../ui/components/FlatButton/README.md)
+
+We can determine whether the `TelegramSdk.BackButton` is displayed using
+the `TelegramSdk.BackButton.isVisible` property after calling the `TelegramSdk.BackButton.show()` method.
+If the `BackButton` is not available, we will display the `FlatButton` as a fallback.
+This can be useful during local development in a browser.
+
+You can easily switch between the telegram and web view modes if you prefer not to use `Telegram.BackButton`.
+
+By default, the type is set to 'telegram'
+
+## Props
+
+All available props see in [BackButton.props.ts](./BackButton.props.ts)
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+switch to FlatButton:
+
+```vue
+
+
+
+
+
+```
diff --git a/packages/telegram-ui/components/BackButton/index.ts b/packages/telegram-ui/components/BackButton/index.ts
new file mode 100644
index 0000000..06f6bcc
--- /dev/null
+++ b/packages/telegram-ui/components/BackButton/index.ts
@@ -0,0 +1,2 @@
+export * from './BackButton.props';
+export { default as BackButton } from './BackButton.vue';
diff --git a/packages/telegram-ui/components/MainButton/MainButton.props.ts b/packages/telegram-ui/components/MainButton/MainButton.props.ts
new file mode 100644
index 0000000..e86e641
--- /dev/null
+++ b/packages/telegram-ui/components/MainButton/MainButton.props.ts
@@ -0,0 +1,23 @@
+import type { HapticFeedback } from '@twa-dev/types';
+
+type HapticStyle = Parameters[0];
+
+export type MainButtonProps = {
+ text: string;
+
+ disabled?: boolean;
+
+ progress?: boolean;
+
+ color?: string;
+
+ textColor?: string;
+
+ keepAlive?: boolean;
+
+ haptic?: HapticStyle;
+};
+
+export type MainButtonEmits = {
+ (e: 'onClick'): void;
+};
diff --git a/packages/telegram-ui/components/MainButton/MainButton.vue b/packages/telegram-ui/components/MainButton/MainButton.vue
new file mode 100644
index 0000000..f05ba89
--- /dev/null
+++ b/packages/telegram-ui/components/MainButton/MainButton.vue
@@ -0,0 +1,77 @@
+
+
+
diff --git a/packages/telegram-ui/components/MainButton/README.md b/packages/telegram-ui/components/MainButton/README.md
new file mode 100644
index 0000000..218a696
--- /dev/null
+++ b/packages/telegram-ui/components/MainButton/README.md
@@ -0,0 +1,70 @@
+# @tok/telegram-ui MainButton
+
+Wrapper around Telegram.MainButton
+
+Unfortunately, we couldn't find a way to dynamically determine whether the `Telegram.MainButton` is displayed, so there is no fallback to [FlatButton](../../../ui/components/FlatButton/README.md) in this component, which would be useful during local development
+
+## Props
+
+All available props see in [MainButton.props.ts](./MainButton.props.ts)
+
+The props mirror the props from `Telegram.MainButton` with some ours
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+### Haptic Feedback
+
+```vue
+
+
+
+
+
+```
+
+### KeepAlive
+
+By default, `MainButton` attempts to hide itself when `onBeforeUnmount` occurs. You can disable this behavior with the `keepAlive` prop.
+
+```vue
+
+
+
+
+
+```
diff --git a/packages/telegram-ui/components/MainButton/index.ts b/packages/telegram-ui/components/MainButton/index.ts
new file mode 100644
index 0000000..809059d
--- /dev/null
+++ b/packages/telegram-ui/components/MainButton/index.ts
@@ -0,0 +1,2 @@
+export * from './MainButton.props';
+export { default as MainButton } from './MainButton.vue';
diff --git a/packages/telegram-ui/components/Sticker/README.md b/packages/telegram-ui/components/Sticker/README.md
new file mode 100644
index 0000000..c4e838e
--- /dev/null
+++ b/packages/telegram-ui/components/Sticker/README.md
@@ -0,0 +1,63 @@
+# Sticker
+
+Using [Lottie-web](https://github.com/airbnb/lottie-web) inside
+
+This component can render a `Lottie animation` using `Lottie JSON`. For rendering stickers, we unzip `.tgs` files, which are essentially `Lottie JSON`, on the fly and place this JSON inside the component.
+
+> [!IMPORTANT]
+>
+> [Lottie-web](https://github.com/airbnb/lottie-web) is a heavy library that can significantly increase your application bundle size. See examples on how to load this component asynchronously
+
+We have created a small [Vite plugin](../../../../app/_internal/tgs.loader.ts) that can unpack your `.tgs` stickers into `JSON format` with helps of [pako package](https://www.npmjs.com/package/pako/v/1.0.11) (version `1.0.11` is important in this case). It unpacks them during the build process, so it doesn't affect the runtime of your application
+
+> [!NOTE]
+>
+> This is not the best solution because `.json` files are much larger than `.tgs` files. We will provide a better solution for rendering your stickers in the future.
+> We are open to your suggestions on how to implement this component, so feel free to open pull requests
+
+## Props
+
+All available props see in [Sticker.props.ts](./Sticker.props.ts)
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+```vue
+
+
+
+
+
+```
diff --git a/packages/telegram-ui/components/Sticker/Sticker.props.ts b/packages/telegram-ui/components/Sticker/Sticker.props.ts
new file mode 100644
index 0000000..8b7a91e
--- /dev/null
+++ b/packages/telegram-ui/components/Sticker/Sticker.props.ts
@@ -0,0 +1,18 @@
+export type StickerProps = {
+ json: TelegramStickerJson;
+
+ autoplay?: boolean;
+
+ loop?: boolean;
+
+ speed?: number;
+
+ playOnClick?: boolean;
+};
+
+export const StickerDefaultProps = {
+ autoplay: true,
+ loop: true,
+ speed: 1,
+ playOnClick: true,
+} as const;
diff --git a/packages/telegram-ui/components/Sticker/Sticker.vue b/packages/telegram-ui/components/Sticker/Sticker.vue
new file mode 100644
index 0000000..a9d8836
--- /dev/null
+++ b/packages/telegram-ui/components/Sticker/Sticker.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/packages/telegram-ui/components/Sticker/index.ts b/packages/telegram-ui/components/Sticker/index.ts
new file mode 100644
index 0000000..d2c0008
--- /dev/null
+++ b/packages/telegram-ui/components/Sticker/index.ts
@@ -0,0 +1,2 @@
+export * from './Sticker.props';
+export { default as Sticker } from './Sticker.vue';
diff --git a/packages/telegram-ui/components/TelegramPopup/README.md b/packages/telegram-ui/components/TelegramPopup/README.md
new file mode 100644
index 0000000..ffa1cc3
--- /dev/null
+++ b/packages/telegram-ui/components/TelegramPopup/README.md
@@ -0,0 +1,100 @@
+# TelegramPopup
+
+Wrapper around Telegram.showPopup with fallback to [Popup](../../../ui/components/Popup/README.md)
+
+We can determine whether the `TelegramPopup` is displayed using this following code:
+
+```ts
+try {
+ Telegram.showPopup();
+} catch (e) {
+ // do other stuff
+}
+```
+
+If the `showPopup` is not available, we will display the `Popup` as a fallback.
+
+If you want to use only [Popup](../../../ui/components/Popup/README.md) consider using it directly instead of this.
+
+This can be useful during local development in a browser.
+
+You can easily switch between the telegram and web view modes if you prefer not to use `Telegram.showPopup` method
+
+By default, component will always try to open `TelegramPopup`
+
+## Props
+
+All available props see in [TelegramPopup.props.ts](./TelegramPopup.props.ts)
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+Same api, only difference is `type="web"`
+
+```vue
+
+
+
+
+
+```
diff --git a/packages/telegram-ui/components/TelegramPopup/TelegramPopup.props.ts b/packages/telegram-ui/components/TelegramPopup/TelegramPopup.props.ts
new file mode 100644
index 0000000..9d9910a
--- /dev/null
+++ b/packages/telegram-ui/components/TelegramPopup/TelegramPopup.props.ts
@@ -0,0 +1,27 @@
+import { PopupButton } from '@twa-dev/types';
+
+export type TelegramPopupProps = {
+ type?: 'web' | 'telegram';
+ modelValue: boolean;
+ title: string;
+ message?: string;
+ buttons: T[];
+};
+
+export type TelegramPopupEmits = {
+ (e: 'update:modelValue', value: boolean): void;
+ (e: 'onSelect', value: string | undefined): void;
+};
+
+export type TelegramPopupSlots = {
+ 'button-icon'?: (props: { item: T }) => void;
+};
+
+const buttons: PopupButton[] = [];
+
+export const TelegramPopupDefaultProps = {
+ title: '',
+ message: '',
+ type: 'telegram',
+ buttons: () => buttons,
+} as const;
diff --git a/packages/telegram-ui/components/TelegramPopup/TelegramPopup.vue b/packages/telegram-ui/components/TelegramPopup/TelegramPopup.vue
new file mode 100644
index 0000000..72f5d2b
--- /dev/null
+++ b/packages/telegram-ui/components/TelegramPopup/TelegramPopup.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/telegram-ui/components/TelegramPopup/TelegramPopupButton.vue b/packages/telegram-ui/components/TelegramPopup/TelegramPopupButton.vue
new file mode 100644
index 0000000..2f0b67e
--- /dev/null
+++ b/packages/telegram-ui/components/TelegramPopup/TelegramPopupButton.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
diff --git a/packages/telegram-ui/components/TelegramPopup/index.ts b/packages/telegram-ui/components/TelegramPopup/index.ts
new file mode 100644
index 0000000..7eda339
--- /dev/null
+++ b/packages/telegram-ui/components/TelegramPopup/index.ts
@@ -0,0 +1,2 @@
+export * from './TelegramPopup.props';
+export { default as TelegramPopup } from './TelegramPopup.vue';
diff --git a/packages/telegram-ui/package.json b/packages/telegram-ui/package.json
new file mode 100644
index 0000000..f51529c
--- /dev/null
+++ b/packages/telegram-ui/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@tok/telegram-ui",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tok/ui": "*",
+ "@twa-dev/sdk": "^6.9.0",
+ "lottie-web": "^5.12.2",
+ "vue": "^3.3.4",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@tok/eslint-config": "*",
+ "@tok/tsconfig": "*"
+ }
+}
\ No newline at end of file
diff --git a/packages/telegram-ui/tsconfig.json b/packages/telegram-ui/tsconfig.json
new file mode 100644
index 0000000..1618238
--- /dev/null
+++ b/packages/telegram-ui/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@tok/tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"]
+}
diff --git a/packages/telegram-ui/types/tgs.d.ts b/packages/telegram-ui/types/tgs.d.ts
new file mode 100644
index 0000000..4ba85aa
--- /dev/null
+++ b/packages/telegram-ui/types/tgs.d.ts
@@ -0,0 +1,11 @@
+declare type TelegramStickerJson = {
+ tgs: number;
+ v: string;
+ fr: number;
+};
+
+// TODO: Find a way to set this type once
+// instead of copying and pasting it from project to project.
+declare module '*.tgs' {
+ export default TelegramStickerJson;
+}
diff --git a/packages/telegram-ui/use/sdk/index.ts b/packages/telegram-ui/use/sdk/index.ts
new file mode 100644
index 0000000..f8743cb
--- /dev/null
+++ b/packages/telegram-ui/use/sdk/index.ts
@@ -0,0 +1,5 @@
+import Telegram from '@twa-dev/sdk';
+
+export function useTelegramSdk() {
+ return Telegram;
+}
diff --git a/packages/telegram-ui/use/theme/index.ts b/packages/telegram-ui/use/theme/index.ts
new file mode 100644
index 0000000..7baad7d
--- /dev/null
+++ b/packages/telegram-ui/use/theme/index.ts
@@ -0,0 +1 @@
+export * from './useTheme';
diff --git a/packages/telegram-ui/use/theme/useTheme.ts b/packages/telegram-ui/use/theme/useTheme.ts
new file mode 100644
index 0000000..39a7bf1
--- /dev/null
+++ b/packages/telegram-ui/use/theme/useTheme.ts
@@ -0,0 +1,26 @@
+import { useTelegramSdk } from '@tok/telegram-ui/use/sdk';
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+
+export function useTheme(value: 'light' | 'dark' | 'auto' = 'auto') {
+ const sdk = useTelegramSdk();
+
+ const init = value === 'auto' ? sdk.colorScheme : value;
+
+ const theme = ref(init);
+
+ const onThemeChange = () => {
+ theme.value = sdk.colorScheme;
+ };
+
+ onMounted(() => {
+ if (value === 'auto') {
+ sdk.onEvent('themeChanged', onThemeChange);
+ }
+ });
+
+ onBeforeUnmount(() => {
+ sdk.offEvent('themeChanged', onThemeChange);
+ });
+
+ return theme;
+}
diff --git a/packages/tsconfig/README.md b/packages/tsconfig/README.md
new file mode 100644
index 0000000..e594837
--- /dev/null
+++ b/packages/tsconfig/README.md
@@ -0,0 +1,23 @@
+# @tok/tsconfig
+
+Basic tsconfig.json for your applications
+
+## Usage
+
+1. Run the following command:
+
+ ```bash
+ npm i @tok/tsconfig --save-dev --workspace=
+ ```
+
+2. Create a `tsconfig.json` file within your app.
+
+3. Add the following default configuration to the `tsconfig.json` file:
+
+ ```json
+ {
+ "extends": "@tok/tsconfig/tsconfig.base.json",
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"]
+ }
+ ```
diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json
new file mode 100644
index 0000000..8afc856
--- /dev/null
+++ b/packages/tsconfig/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@tok/tsconfig",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ }
+}
\ No newline at end of file
diff --git a/packages/tsconfig/tsconfig.base.json b/packages/tsconfig/tsconfig.base.json
new file mode 100644
index 0000000..82da60a
--- /dev/null
+++ b/packages/tsconfig/tsconfig.base.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "composite": false,
+ "declaration": true,
+ "declarationMap": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "inlineSources": false,
+ "isolatedModules": true,
+ "moduleResolution": "node",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "target": "esnext",
+ "module": "esnext",
+ "jsx": "preserve",
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "sourceMap": false,
+ "removeComments": true,
+ "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
+ "types": ["vite/client"]
+ },
+ "exclude": ["node_modules"]
+}
diff --git a/packages/ui/.eslintrc.js b/packages/ui/.eslintrc.js
new file mode 100644
index 0000000..a5e5ec9
--- /dev/null
+++ b/packages/ui/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+ root: true,
+ extends: ['@tok/eslint-config'],
+ parserOptions: { tsconfigRootDir: __dirname },
+};
diff --git a/packages/ui/README.md b/packages/ui/README.md
new file mode 100644
index 0000000..1adf8e2
--- /dev/null
+++ b/packages/ui/README.md
@@ -0,0 +1,146 @@
+# @tok/ui
+
+- [Components](./components/README.md)
+- [Directives](./directives/README.md)
+- [Dom](./dom/README.md)
+- [Plugins](./plugins/README.md)
+- [Styles](./styles/README.md)
+- [Tokens](./tokens/README.md)
+- [Types](./types/README.md)
+- [Use](./use/README.md)
+- [Utility](./utility/README.md)
+- [Manual Installation Guide](#manual-installation-guide)
+
+## Manual Installation Guide
+
+Use this guide to manually install the UI package in your project.
+
+1. Run the following command to add a link from your app to this package:
+
+ ```bash
+ npm run @tok/ui --workspace=
+ ```
+
+2. Import our global styles in your `main.ts` file:
+
+ ```ts
+ // main.ts
+ import '@tok/ui/styles/global.scss';
+
+ import { createApp } from 'vue';
+ import App from './App.vue';
+
+ createApp(App).mount('#app');
+ ```
+
+3. If you want to use the built-in [Popups](./components/Popup/README.md) and [Alerts](./use/alerts/README.md), add the [Root](./components/Root/README.md) component to your `App.vue` file as shown below:
+
+ ```vue
+
+
+
+
+
+
+
+
+
+ ```
+
+4. Also for [Alerts](./use/alerts/README.md), you should add AlertsPlugin in your main.ts file:
+
+ ```ts
+ // main.ts
+ import '@tok/ui/styles/global.scss';
+
+ import { createApp } from 'vue';
+ import App from './App.vue';
+ import { AlertsPlugin } from '@tok/ui/plugins/alerts';
+
+ createApp(App).use(AlertsPlugin).mount('#app');
+ ```
+
+ And use them as:
+
+ ```vue
+
+ ```
+
+5. If you want to use Currency, you can specify config for currency once:
+
+ ```ts
+ // main.ts
+ import '@tok/ui/styles/global.scss';
+
+ import { createApp } from 'vue';
+ import App from './App.vue';
+ import { CurrencyPlugin } from '@tok/ui/plugins/currency';
+
+ const currencyOptions = {
+ // currency symbol alignment
+ // default: 'left'
+ align?: 'left' | 'right';
+
+ // currency symbol
+ // default: 'USD'
+ currency?: CurrencyVariants;
+
+ // separator for decimal 1.00 or 1,00 as you wish
+ // default '.'
+ decimalSeparator?: string;
+
+ // separator for thousand 1_000_000 or 1x000x000
+ // default ' '
+ thousandSeparator?: string;
+ };
+
+ createApp(App).use(CurrencyPlugin, currencyOptions).mount('#app');
+ ```
+
+ Or you can specify currency config in place:
+
+ ```vue
+
+
+
+
+
+ ```
+
+6. If you want to use our mixins, import them in your component:
+
+ ```vue
+
+
+
+
+
+
+
+ ```
+
+7. For localization use TokI18nPlugin from [i18n packages](../i18n/README.md)
diff --git a/packages/ui/components/Alert/Alert.props.ts b/packages/ui/components/Alert/Alert.props.ts
new file mode 100644
index 0000000..21da172
--- /dev/null
+++ b/packages/ui/components/Alert/Alert.props.ts
@@ -0,0 +1,50 @@
+import { Component, VNode } from 'vue';
+
+export type AlertProps = {
+ type?: 'success' | 'error' | 'telegram' | string;
+
+ // The "content" property can be a string or another component, and it will receive the "context" prop.
+ content?: string | Component;
+
+ // The "closable" property determines whether the alert can be closed or not.
+ closable?: boolean;
+
+ // You can pass data to the "content" as a component through the "data" property.
+ data?: T;
+};
+
+// helper type to get correct context inside your component in alert
+/*
+ Usage:
+ CustomAlert.vue:
+
+
+ {{ context.data.sayHello ? 'Hello' : 'Bye' }}
+
+
+
+
+ type Data = {
+ sayHello?: boolean;
+ };
+
+ defineProps>();
+*/
+export type AlertContextProps = {
+ context: {
+ close: () => void;
+ data: T;
+ };
+};
+
+export type AlertSlots = {
+ default?: (props: {}) => ReadonlyArray;
+};
+
+export type AlertEmits = {
+ (e: 'close'): void;
+};
+
+export const AlertDefaultProps = {
+ type: 'success',
+} as const;
diff --git a/packages/ui/components/Alert/Alert.vue b/packages/ui/components/Alert/Alert.vue
new file mode 100644
index 0000000..44e774e
--- /dev/null
+++ b/packages/ui/components/Alert/Alert.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/components/Alert/README.md b/packages/ui/components/Alert/README.md
new file mode 100644
index 0000000..1d8460c
--- /dev/null
+++ b/packages/ui/components/Alert/README.md
@@ -0,0 +1,81 @@
+# Alert Component
+
+The component is used in the [useAlerts()](../../use/alerts/README.md) function
+
+> [!IMPORTANT]
+> To make useAlerts() work, it is essential to wrap your entire application in the [Root](../Root/README.md) component
+
+## Props
+
+All available props see in [Alert.props.ts](./Alert.props.ts)
+
+## Usage
+
+```vue
+
+
+
+
+
+
+```
+
+## Customization
+
+```vue
+
+
+
+
+
+```
+
+```scss
+/* global.styles.scss */
+.tok-alert {
+ &[data-type='custom'] {
+ background: red;
+ color: white;
+
+ .tok-alert {
+ // color for icon
+ &-icon {
+ color: white;
+ }
+
+ // color for close icon
+ &-close {
+ color: black;
+ }
+ }
+ }
+}
+```
+
+or inside other component with style in scoped mode
+
+```vue
+
+
+
+
+
+
+
+
+
+```
diff --git a/packages/ui/components/Alert/index.ts b/packages/ui/components/Alert/index.ts
new file mode 100644
index 0000000..6d542de
--- /dev/null
+++ b/packages/ui/components/Alert/index.ts
@@ -0,0 +1,2 @@
+export * from './Alert.props';
+export { default as Alert } from './Alert.vue';
diff --git a/packages/ui/components/Carousel/Carousel.props.ts b/packages/ui/components/Carousel/Carousel.props.ts
new file mode 100644
index 0000000..06b8ea0
--- /dev/null
+++ b/packages/ui/components/Carousel/Carousel.props.ts
@@ -0,0 +1,49 @@
+import { VNode } from 'vue';
+
+export type CarouselProps = {
+ // Current index
+ modelValue: number;
+
+ // Number of slides shown at the same time
+ itemsCount: number;
+
+ // Elements inside carousel
+ items: ReadonlyArray;
+
+ /*
+ Whether or not slider can be dragged by clicking and holding
+
+ This parameter only works on desktop devices and is ignored on mobile devices.
+ The determination of whether the device is mobile is made through the `isMobile` function
+ */
+ draggable?: boolean;
+
+ /*
+ Number of pixels that must be traversed before the carousel recognizes a dragging action
+ It's helpful when there's a scrollable element inside the carousel
+ */
+ threshold?: number;
+
+ // Custom padding between elements
+ paddingPx?: number;
+};
+
+export type CarouselEmits = {
+ // update current index
+ (e: 'update:modelValue', value: number): void;
+};
+
+export type CarouselSlots = {
+ default: (props: { item: T; index: number }) => ReadonlyArray;
+};
+
+export type CarouselExpose = {
+ next: () => void;
+ back: () => void;
+};
+
+export const CarouselDefaultProps = {
+ draggable: false,
+ threshold: 0,
+ paddingPx: 8,
+} as const;
diff --git a/packages/ui/components/Carousel/Carousel.vue b/packages/ui/components/Carousel/Carousel.vue
new file mode 100644
index 0000000..20f7cd0
--- /dev/null
+++ b/packages/ui/components/Carousel/Carousel.vue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/components/Carousel/README.md b/packages/ui/components/Carousel/README.md
new file mode 100644
index 0000000..ba2139f
--- /dev/null
+++ b/packages/ui/components/Carousel/README.md
@@ -0,0 +1,60 @@
+# Carousel
+
+Allows you to rotate through arbitrary items.
+
+Multiple items can be shown simultaneously
+
+## Props
+
+All available props see in [Carousel.props.ts](./Carousel.props.ts)
+
+## Usage
+
+```vue
+
+
+ {{ item }}
+
+
+
+
+
+
+
+```
diff --git a/packages/ui/components/Carousel/carousel-scroll.directive.ts b/packages/ui/components/Carousel/carousel-scroll.directive.ts
new file mode 100644
index 0000000..2310e5d
--- /dev/null
+++ b/packages/ui/components/Carousel/carousel-scroll.directive.ts
@@ -0,0 +1,73 @@
+import { isMobile } from '@tok/ui/dom/platform';
+import { DirectiveBinding, ObjectDirective } from 'vue';
+
+type Binding = {
+ onEvent: (value: 1 | -1) => void;
+};
+
+type FunctionArgs = (
+ ...args: Args
+) => Return;
+
+const wheelListeners = new Map void>();
+
+const throttle = (_invoke: T, ms: number): T => {
+ let prev = 0;
+
+ const filter = (...args: any[]) => {
+ const now = Date.now();
+
+ if (now - prev > ms) {
+ prev = now;
+
+ return _invoke(...args);
+ }
+ };
+
+ return filter as T;
+};
+
+const beforeMount = (
+ element: HTMLElement,
+ { value }: DirectiveBinding
+) => {
+ const callback = throttle((num: 1 | -1) => {
+ value.onEvent(num);
+ }, 500);
+
+ const wheel = ({ deltaX }: WheelEvent) => {
+ if (Math.abs(deltaX) <= 20) {
+ return;
+ }
+
+ callback(Math.sign(deltaX) as -1 | 1);
+
+ element.scrollLeft = 10;
+ };
+
+ if (!isMobile()) {
+ element.addEventListener('wheel', wheel, { passive: true });
+
+ wheelListeners.set(element, wheel);
+ }
+};
+
+const beforeUnmount = (element: HTMLElement) => {
+ const listener = wheelListeners.get(element);
+
+ if (listener) {
+ element.removeEventListener('wheel', listener);
+
+ wheelListeners.delete(element);
+ }
+};
+
+/**
+ * Directive for emitting scroll events with values -1 (to the left) or 1 (to the right).
+ * This directive is designed for desktop use only.
+ * Identifies a mobile device using the isMobile function
+ */
+export const CarouselScrollDirective: ObjectDirective = {
+ beforeMount,
+ beforeUnmount,
+};
diff --git a/packages/ui/components/Carousel/index.ts b/packages/ui/components/Carousel/index.ts
new file mode 100644
index 0000000..e0de6d2
--- /dev/null
+++ b/packages/ui/components/Carousel/index.ts
@@ -0,0 +1,2 @@
+export * from './Carousel.props';
+export { default as Carousel } from './Carousel.vue';
diff --git a/packages/ui/components/CheckboxBlock/CheckboxBlock.props.ts b/packages/ui/components/CheckboxBlock/CheckboxBlock.props.ts
new file mode 100644
index 0000000..f1127f5
--- /dev/null
+++ b/packages/ui/components/CheckboxBlock/CheckboxBlock.props.ts
@@ -0,0 +1,22 @@
+export type CheckboxBlockProps = {
+ modelValue: boolean | null;
+
+ placeholder?: string;
+
+ size?: 's' | 'm' | 'l' | string;
+
+ shape?: 'rounded' | string;
+
+ disabled?: boolean;
+
+ invalid?: boolean;
+};
+
+export type CheckboxBlockEmits = {
+ (e: 'update:modelValue', v: boolean): void;
+};
+
+export const CheckboxBlockDefaultProps = {
+ placeholder: '',
+ size: 'm',
+} as const;
diff --git a/packages/ui/components/CheckboxBlock/CheckboxBlock.vue b/packages/ui/components/CheckboxBlock/CheckboxBlock.vue
new file mode 100644
index 0000000..c8f09e5
--- /dev/null
+++ b/packages/ui/components/CheckboxBlock/CheckboxBlock.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
diff --git a/packages/ui/components/CheckboxBlock/README.md b/packages/ui/components/CheckboxBlock/README.md
new file mode 100644
index 0000000..af517f3
--- /dev/null
+++ b/packages/ui/components/CheckboxBlock/README.md
@@ -0,0 +1,131 @@
+# CheckboxBlock
+
+## Props
+
+All available props see in [CheckboxBlock.props.ts](./CheckboxBlock.props.ts)
+
+## Figma
+
+[Component in figma project](https://www.figma.com/file/ssQqPZ2vqZhD4QF2xyCTd2/Telegram-Onboarding--ToolKit?type=design&node-id=139-1045&mode=design&t=6yuiDJRdwfFJ7dVT-0)
+
+## i18n
+
+The component natively supports [i18n](../../../i18n/README.md) for placeholder values.
+
+You can provide a placeholder as a locale token, and it will be dynamically translated
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+## Customization
+
+### Sizes
+
+```vue
+
+