πŸ“„ recipe-toggle-ui.md 8,033 bytes Apr 26, 2026 πŸ“‹ Raw

🎨 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):

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 🎨