π¨ Recipe Toggle UI β Design Tokens & Spec
For: Socrates π§ (callback handling), Wadsworth π (routing)
Author: Daedalus π¨
Project: Icarus Recipe Integration β Phase 1
Date: 2026-04-26
1. Message Layout (ASCII)
βββββββββββββββββββββββββββββββββββββββ
β π Creamy Tomato and Spinach Pasta β
β From: budgetbytes.com β
β β
β Tap to deselect items you don't need:β
β β
β β
2 cups penne pasta β
β β
1 Tbsp olive oil β
β β ~~1 small onion~~ β
β β
2 cloves garlic β
β β
1 (14.5 oz) can crushed tomatoes β
β β
β [β
Commit to List (4 items)] [β] β
βββββββββββββββββββββββββββββββββββββββ
2. Inline Keyboard Structure
Row Structure (Telegram InlineKeyboardMarkup)
Row 0: [text: "π Creamy Tomato and Spinach Pasta"] β non-interactive caption
Row 1: [text: "From: budgetbytes.com"] β non-interactive
Row 2: [text: " "] β spacer (inline with no callback)
Row 3: [text: "Tap to deselect items you don't need:"] β instruction
Row 4: [callback_data: "toggle:{id}:{0}"] β ingredient 0
Row 5: [callback_data: "toggle:{id}:{1}"] β ingredient 1
Row 6: [callback_data: "toggle:{id}:{2}"] β ingredient 2
Row 7: [callback_data: "toggle:{id}:{3}"] β ingredient 3
...
Row N: [callback_data: "commit:{id}"] [callback_data: "cancel:{id}"] β actions
Important: Telegram buttons are callback-only, not static text. The caption lines (rows 0-3 above) must be rendered as plain message text, not keyboard rows. The inline keyboard only contains:
- Toggle buttons (one per ingredient)
- Commit + Cancel buttons
Button Text (per state)
| State | Format | Example |
|---|---|---|
| Selected | β
{ingredient text} |
β
2 cups penne pasta |
| Deselected | β {strikethrough text} |
β ~2 cups penne pasta~ |
Note: Telegram inline buttons do not support native strikethrough. Use MarkdownV2 in the message text for strikethrough on deselected items (e.g., ~2 cups penne pasta~), and use the β emoji on the button label itself to indicate state.
Actually β better approach: put ingredient text in the message body (not in button labels), and use the button labels purely as state indicators.
Revised: Message Body + Button Labels
Message caption text (full message body):
π Creamy Tomato and Spinach Pasta
From: budgetbytes.com
Tap to deselect items you don't need:
β
2 cups penne pasta
β
1 Tbsp olive oil
β ~1 small onion~
β
2 cloves garlic
Inline keyboard buttons (minimal):
Row 0: [β
#1] [β
#2] [β] [β
#4] β index-based, compact
Row 1: [β
Commit (4)] [β Cancel]
This keeps the keyboard compact on mobile. Each ingredient button's label is just β
or β with the index number. Full ingredient text lives in the message body.
OR β the even simpler approach for mobile thumbs:
Row 0: [1/β
] [2/β
] [3/β] [4/β
] β compact grid, 4 per row
Row 1: [5/β
] [6/β
] β more if needed
Row 2: [β
Commit (4)] [β Cancel]
3. Color & Emoji Tokens
| Element | Emoji/Token | Hex (Telegram theme) |
|---|---|---|
| Selected state | β | Telegram blue #40A7E3 (system) |
| Deselected state | β | Gray #8E8E93 (system) |
| Commit (active) | β | Green #34C759 (system) |
| Commit (empty) | β | Gray, disabled text |
| Cancel | β | Red #FF3B30 (system) |
| Recipe header | π | Variable per cuisine type |
| Action complete | β or β | Green #34C759 |
Telegram uses system accent colors β we don't set colors on buttons. The emoji indicators carry the state.
4. Interaction Flow
Initial Send
- User shares recipe URL to Icarus bot
- Bot enriches β sends ingredient list as new message
- Inline keyboard rendered with all β and full count
Toggle (callback: toggle:{recipe_id}:{index})
- Bot receives callback
- Flips
selectedboolean for that ingredient - Edits message β updates both:
- Message caption text (β ββ + strikethrough in body)
- Inline keyboard (button labels update) - Bot answers callback (
answerCallbackQueryβ no toast needed)
Commit (callback: commit:{recipe_id})
- Bot receives callback
- Collects all selected ingredients
- Writes to SQLite (temp table β grocery list on confirm)
- Edits message: removes keyboard, shows confirmation:
β Added 4 ingredients to grocery list - Optionally: deletes the recipe message after 30s
Cancel (callback: cancel:{recipe_id})
- Bot receives callback
- Edits message: removes keyboard, shows:
β Canceled β nothing added to list - Clears temp state
Timeout (5 min TTL)
- On next interaction after 5 min: edit message to:
β° Timed out β send the recipe again to start fresh - Remove keyboard entirely
5. Data Flow
User taps [β
#1]
β callback_data: "toggle:creamy-tomato-abc123:0"
β Bot handler: flip_selected("creamy-tomato-abc123", 0)
β SQLite: UPDATE toggle_state SET selected = NOT selected
β Re-read all ingredients for this recipe
β Build new caption text + new keyboard markup
β editMessageText(chat_id, message_id, new_caption, new_keyboard)
β answerCallbackQuery(callback_id)
SQLite schema (temp state, 5-min TTL):
CREATE TABLE recipe_toggles (
recipe_id TEXT NOT NULL,
ingredient_index INTEGER NOT NULL,
selected INTEGER NOT NULL DEFAULT 1,
chat_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (recipe_id, ingredient_index)
);
Grocery list schema (on commit):
CREATE TABLE grocery_list_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipe_id TEXT NOT NULL,
ingredient_text TEXT NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
purchased INTEGER NOT NULL DEFAULT 0,
chat_id INTEGER NOT NULL
);
6. Edge Cases
| Case | Behavior |
|---|---|
| All deselected | Commit button shows "Select at least 1 item" β disabled callback |
| Single ingredient | Works same β one button row |
| 15+ ingredients | Paginate: show first 12 + "β¦and 3 more" as non-interactive text. Keyboard only has 12 toggle buttons |
| Stale callback (recipe deleted in DB) | answerCallbackQuery("Recipe expired β send again"), edit message to "Recipe no longer available" |
| Duplicate recipe share | Start fresh β overwrite previous temp state for that chat |
| User in group chat vs DM | Same flow; store chat_id to scope state |
| Commit with 0 items | Blocked at button level β callback returns "Select at least 1 item" |
7. Callback Data Quick Reference
toggle:{recipe_id}:{index} β Flips ingredient selection
commit:{recipe_id} β Writes selected to grocery list
cancel:{recipe_id} β Abandons temp selection
Where {recipe_id} is a URL-safe slug (e.g. creamy-tomato-spinach-pasta-abc123) and {index} is zero-based integer.
8. Success Checklist
- [x] Message layout documented (ASCII)
- [x] Inline keyboard structure specified
- [x] Button state transitions (text + emoji)
- [x] Callback data format documented
- [x] SQLite schemas for temp state + grocery list
- [x] Edge cases covered
- [x] Mobile-width handling (compact button labels)
- [ ] Handoff doc:
shared/project-docs/recipe-ui-handoff.md
Daedalus π¨