# 🎨 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 1. User shares recipe URL to Icarus bot 2. Bot enriches β†’ sends ingredient list as new message 3. Inline keyboard rendered with all βœ… and full count ### Toggle (callback: `toggle:{recipe_id}:{index}`) 1. Bot receives callback 2. Flips `selected` boolean for that ingredient 3. **Edits message** β€” updates both: - Message caption text (βœ…β†”βŒ + strikethrough in body) - Inline keyboard (button labels update) 4. Bot answers callback (`answerCallbackQuery` β€” no toast needed) ### Commit (callback: `commit:{recipe_id}`) 1. Bot receives callback 2. Collects all selected ingredients 3. Writes to SQLite (temp table β†’ grocery list on confirm) 4. Edits message: removes keyboard, shows confirmation: ``` βœ… Added 4 ingredients to grocery list ``` 5. Optionally: deletes the recipe message after 30s ### Cancel (callback: `cancel:{recipe_id}`) 1. Bot receives callback 2. Edits message: removes keyboard, shows: ``` ❌ Canceled β€” nothing added to list ``` 3. Clears temp state ### Timeout (5 min TTL) 1. On next interaction after 5 min: edit message to: ``` ⏰ Timed out β€” send the recipe again to start fresh ``` 2. 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): ```sql 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): ```sql 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 🎨