import { Bot } from "gramio"; import { Accordion, AsyncSelect, Back, Breadcrumbs, Button, Calendar, Case, Checkbox, Column, Confirm, Const, Counter, CurrentPage, Dialog, FirstPage, Form, Format, Grid, LastPage, List, Multi, Multiselect, NextPage, PinPad, PrevPage, Progress, Radio, Rating, Row, ScrollingGroup, Select, Slider, Start, Stepper, SwitchTo, Tabs, TagInput, TextInput, Toggle, Url, addTag, dialogs, getInput, getPin, getRating, getSelected, getSlider, getTab, getTags, getToggle, isChecked, } from "../src/index.ts"; /** * One cohesive widget gallery โ€” a runnable cheat-sheet of (almost) the whole * widget API: text, stateful (+ getSelected/getToggle/isChecked), form widgets + * a validated Form wizard (+ getPin), data/selection, Calendar, paginated list, * and TextInput validation (+ getInput). */ interface Item { id: string; title: string; } const TAGS: Item[] = [ { id: "news", title: "News" }, { id: "deals", title: "Deals" }, { id: "tips", title: "Tips" }, ]; const CRUMBS = { menu: "๐Ÿ  Gallery", data: "๐Ÿ—‚ Data" }; const COLORS = [ { id: "red", emoji: "๐ŸŸฅ" }, { id: "green", emoji: "๐ŸŸฉ" }, { id: "blue", emoji: "๐ŸŸฆ" }, { id: "black", emoji: "โฌ›" }, ]; const PRODUCTS = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `๐Ÿ“ฆ Product #${i + 1}`, })); const TAB_CONTENT: Record = { overview: "๐Ÿ“‹ A premium widget that does it all.", specs: "โš™๏ธ 12 cores ยท 32 GB ยท 1 TB ยท 120 Hz", reviews: "โญโญโญโญยฝ โ€” best purchase this year!", }; const demo = new Dialog("widgets") .window("menu", { text: "๐Ÿงฉ Widget gallery โ€” pick a category:", keyboard: Column([ SwitchTo("๐Ÿ“ Text", "text"), SwitchTo("๐ŸŽ› Stateful", "stateful"), SwitchTo("โœ๏ธ Forms", "forms"), SwitchTo("๐Ÿ—‚ Data & selection", "data"), SwitchTo("๐Ÿ“… Calendar", "calendar"), SwitchTo("๐Ÿ“œ Long list", "list"), SwitchTo("โŒจ๏ธ Input", "input"), Url("๐ŸŒ GramIO docs", "https://gramio.dev"), ]), }) // โ”€โ”€โ”€ text widgets: Const / Format / Case / Progress / List โ”€โ”€โ”€ .window("text", { getter: () => ({ status: "active", progress: 65, logs: ["build", "test", "deploy"], }), text: Multi([ Const("๐Ÿ“ Text widgets:"), Format("Status: {status}"), // {key} interpolation Case("status", { active: Const("๐ŸŸข running"), default: Const("โšช idle"), }), Progress({ value: (d) => d.progress as number }), List({ items: (d) => d.logs as string[], item: (line, i) => `${i + 1}. ${line}`, }), ]), keyboard: Column([Back("โ—€ Back")]), }) // โ”€โ”€โ”€ stateful widgets keep their own state; read it with the getX helpers โ”€โ”€โ”€ .window("stateful", { getter: (ctx) => ({ tags: TAGS, subs: getSelected(ctx.dialog, "subs"), notify: isChecked(ctx.dialog, "notify"), theme: getToggle(ctx.dialog, "theme"), }), text: (d) => `๐ŸŽ› notify=${d.notify ? "on" : "off"} ยท theme#${d.theme} ยท subs=[${d.subs.join(",")}]`, keyboard: Column([ Counter({ id: "qty", default: 1, min: 0, max: 9, text: (d) => `Qty: ${d.value}`, }), Checkbox({ id: "notify", checkedText: "๐Ÿ”” Notify: on", uncheckedText: "๐Ÿ”• Notify: off", }), Toggle({ id: "theme", items: TAGS, itemId: (t) => t.id, text: (s) => `Theme: ${s.item.title}`, }), Radio({ id: "plan", items: (d) => d.tags as Item[], itemId: (t) => t.id, checkedText: (s) => `๐Ÿ”˜ ${s.item.title}`, uncheckedText: (s) => `โšช ${s.item.title}`, }), Multiselect({ id: "subs", items: (d) => d.tags as Item[], itemId: (t) => t.id, checkedText: (s) => `โœ… ${s.item.title}`, uncheckedText: (s) => `โฌœ ${s.item.title}`, }), Back("โ—€ Back"), ]), }) // โ”€โ”€โ”€ form widgets submenu: Rating / Slider / PinPad / TagInput / Confirm + Form โ”€โ”€โ”€ .window("forms", { text: "โœ๏ธ Forms โ€” pick a widget:", keyboard: Column([ SwitchTo("โญ Rating", "rating"), SwitchTo("๐ŸŽš Slider", "slider"), SwitchTo("๐Ÿ”ข PIN pad", "pin"), SwitchTo("๐Ÿท Tags", "tags"), SwitchTo("๐Ÿ—‘ Confirm", "confirm"), Start("๐Ÿง™ Sign-up wizard", "signup"), Back("โ—€ Back"), ]), }) .window("rating", { getter: (ctx) => ({ stars: getRating(ctx.dialog, "stars") }), text: (d) => d.stars ? `Thanks for the ${d.stars}/5! โญ` : "How would you rate us?", keyboard: Column([Rating({ id: "stars" }), Back("โ—€ Forms")]), }) .window("slider", { getter: (ctx) => ({ vol: getSlider(ctx.dialog, "vol") }), text: (d) => `๐Ÿ”Š Volume: ${d.vol}%`, keyboard: Column([ Slider({ id: "vol", min: 0, max: 100, step: 10, default: 50 }), Back("โ—€ Forms"), ]), }) .window("pin", { getter: (ctx) => ({ pin: getPin(ctx.dialog, "pin") }), text: (d) => `๐Ÿ” Enter a 4-digit PIN (so far: ${d.pin || "ยทยทยทยท"})`, keyboard: Column([ PinPad({ id: "pin", length: 4, onComplete: (ctx, code) => ctx.answer({ text: `PIN set to ${code} โœ…`, show_alert: true }), }), Back("โ—€ Forms"), ]), }) .window("tags", { getter: (ctx) => ({ tags: getTags(ctx.dialog, "tags") }), text: (d) => d.tags.length ? `๐Ÿท ${d.tags.join(" ")}\n\nSend a word to add ยท tap a chip to remove.` : "๐Ÿท Send a word to add your first tag.", keyboard: Column([TagInput({ id: "tags", max: 8 }), Back("โ—€ Forms")]), onMessage: (ctx) => { if (!ctx.text) return; addTag(ctx.dialog, "tags", ctx.text, 8); return ctx.dialog.show(); }, }) .window("confirm", { text: "Delete your account? This cannot be undone.", keyboard: Column([ Confirm({ id: "del", yesText: "๐Ÿ—‘ Delete", noText: "โ†ฉ๏ธ Keep it", onConfirm: (ctx) => ctx.answer("Account deleted (just kidding) ๐Ÿ˜…"), onCancel: (ctx) => ctx.switchTo("forms"), }), Back("โ—€ Forms"), ]), }) // โ”€โ”€โ”€ data & selection submenu: Tabs / Accordion / Grid / AsyncSelect โ”€โ”€โ”€ .window("data", { text: Multi([Breadcrumbs({ labels: CRUMBS }), Const("Pick a demo:")]), keyboard: Column([ SwitchTo("๐Ÿ—‚ Tabs", "tabs"), SwitchTo("โ“ Accordion", "accordion"), SwitchTo("๐ŸŽจ Grid", "grid"), SwitchTo("๐Ÿ“š Async list", "async"), Back("โ—€ Back"), ]), }) .window("tabs", { getter: (ctx) => ({ tab: getTab(ctx.dialog, "tab", "overview") }), text: (d) => `๐Ÿ“ฆ Product\n\n${TAB_CONTENT[d.tab] ?? ""}`, keyboard: Column([ Tabs({ id: "tab", default: "overview", items: [ { id: "overview", label: "Overview" }, { id: "specs", label: "Specs" }, { id: "reviews", label: "Reviews" }, ], }), Back("โ—€ Data"), ]), }) .window("accordion", { text: "โ“ Tap a question:", keyboard: Column([ Accordion({ id: "faq", sections: [ { id: "ship", header: "๐Ÿšš How long is shipping?", body: Column([Url("๐Ÿ“ฆ Track an order", "https://gramio.dev")]), }, { id: "ret", header: "โ†ฉ๏ธ Return policy?", body: Column([ Button("Start a return", { id: "return", onClick: (ctx) => ctx.answer("Return started โœ…"), }), ]), }, ], }), Back("โ—€ Data"), ]), }) .window("grid", { text: "๐ŸŽจ Pick a color:", keyboard: Column([ Grid<{ id: string; emoji: string }>({ id: "color", items: () => COLORS, itemId: (c) => c.id, text: (c) => c.emoji, width: 4, onClick: (ctx, id) => ctx.answer(`You picked ${id} ๐ŸŽจ`), }), Back("โ—€ Data"), ]), }) .window("async", { text: "๐Ÿ“š Browse the catalog:", keyboard: Column([ AsyncSelect<{ id: number; name: string }>({ id: "prod", pageSize: 6, load: ({ offset, limit }) => PRODUCTS.slice(offset, offset + limit), itemId: (p) => p.id, text: (p) => p.name, onClick: (ctx, id) => ctx.answer(`Opened product #${id}`), }), Back("โ—€ Data"), ]), }) // โ”€โ”€โ”€ calendar with min date + marked days โ”€โ”€โ”€ .window("calendar", { text: "Pick a date:", keyboard: Column([ Calendar({ id: "cal", minDate: new Date(Date.UTC(2020, 0, 1)), marks: (date) => (date.getUTCDate() === 1 ? "โ€ข1" : undefined), onSelect: (ctx, date) => ctx.answer(date.toISOString().slice(0, 10)), }), Back("โ—€ Back"), ]), }) // โ”€โ”€โ”€ paginated list with standalone pagers + a page indicator โ”€โ”€โ”€ .window("list", { text: CurrentPage("scroll", (page, total) => `๐Ÿ“œ Page ${page}/${total}`), getter: () => ({ rows: Array.from({ length: 50 }, (_, i) => ({ id: i, title: `Item #${i + 1}`, })), }), keyboard: Column([ ScrollingGroup( [ Select<{ id: number; title: string }>({ id: "row", items: (d) => d.rows as { id: number; title: string }[], itemId: (r) => r.id, text: (s) => s.item.title, onClick: (ctx, id) => ctx.answer(`Picked ${id}`), }), ], { id: "scroll", height: 6, pager: false }, // pager off โ†’ use standalone ), Row([ FirstPage({ scrollId: "scroll" }), PrevPage({ scrollId: "scroll" }), NextPage({ scrollId: "scroll" }), LastPage({ scrollId: "scroll" }), ]), Back("โ—€ Back"), ]), }) // โ”€โ”€โ”€ typed text input with validation; read it back with getInput โ”€โ”€โ”€ .window("input", { getter: (ctx) => ({ last: getInput(ctx.dialog, "num") }), text: (d) => d.last !== undefined ? `Last number: ${d.last}. Send another:` : "Send me a number:", input: TextInput({ id: "num", parse: (t) => { const n = Number(t); if (!Number.isFinite(n)) throw new Error("not a number"); return n; }, onSuccess: (ctx, n) => ctx.send(`Got ${n} (ร—2 = ${n * 2})`), onError: (ctx) => ctx.send("That's not a number โ€” try again."), }), keyboard: Column([Back("โ—€ Back")]), }); // A validated wizard โ€” `schema` accepts any Standard Schema (zod / valibot / // arktype) or a plain `(raw) => value` parser that throws to reject. const signup = Form({ id: "signup", header: Stepper({ steps: ["Name", "Age", "Email"], current: (d) => d.step as number, }), fields: [ { id: "name", prompt: "๐Ÿ‘ค What should I call you?", schema: (raw) => { const name = raw.trim(); if (name.length < 2) throw new Error("That name is too short."); return name; }, }, { id: "age", prompt: "๐ŸŽ‚ How old are you?", schema: (raw) => { const n = Number(raw); if (!Number.isInteger(n) || n < 1 || n > 150) throw new Error("Please enter a valid age."); return n; }, }, { id: "email", prompt: "๐Ÿ“ง Your email?", schema: (raw) => { if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(raw.trim())) throw new Error("That doesn't look like an email."); return raw.trim(); }, }, ], onSubmit: async (ctx, v) => { await ctx.send(`๐ŸŽ‰ All set, ${v.name}!\nAge: ${v.age}\nEmail: ${v.email}`); return ctx.dialog.done(); // pop the wizard โ†’ back to the "forms" window }, }); new Bot(process.env.BOT_TOKEN as string) .extend(dialogs([demo, signup])) .command("start", (ctx) => ctx.dialog.start("widgets")) .start();