Widget designs (M1.9)

All four modes in compact and expanded sizes on iOS and Android, every state, rendering modes, multilingual cases and large text. Spec: docs/design/widgets.md. All design values come from tokens.css; the frame around each widget does not.

Sample content is illustrative and not reviewed learning content.

Word mode (free)

Term and meaning in compact; term, meaning, example and translation in expanded. Tapping anywhere except the speaker opens Word details. No Next: the card changes with the prepared queue.

Word
la ventana
window
iOS small 170×170
Word
la ventana
window
Abre la ventana, por favor.
Open the window, please.
iOS medium 364×170
Word
la ventana
window
Android compact 2×2
Word
la ventana
window
Abre la ventana, por favor.
Open the window, please.
Android expanded 4×2
Word
la ventana
window
iOS small 170×170
Word
la ventana
window
Abre la ventana, por favor.
Open the window, please.
iOS medium 364×170
Word
la ventana
window
Android compact 2×2
Word
la ventana
window
Abre la ventana, por favor.
Open the window, please.
Android expanded 4×2
ElementTokensSize / ruleAction
Containerwidget.color.bgpadding widget.padding.compact (12) / widget.padding.expanded (16); gap widget.gap.compact (8) / widget.gap.expanded (12)Whole widget: widgetURL vocelune://word/{routeId}/{targetSenseId} · Android: activity PendingIntent, same URI
Termwidget.color.fg, widget.text.term.* 22 / 28 semibold, lh widget.line-height.termWraps; never truncated. Before wrapping a single word, scale down to 80% at most (iOS minimumScaleFactor 0.8, Android autosize min 80%)—
Meaning, translationwidget.color.fg-secondary, widget.text.body.* 15 / 17Explanation-language direction per field—
Examplewidget.color.fg, bodyExpanded only; up to 2 lines, else omitted (not truncated)—
Headerwidget.color.fg-secondary, widget.color.icon, widget.text.meta 12 medium, widget.icon.meta 16One line; mode icon + mode name. Omitted in compact quiz front—
Speakerwidget.color.surface-subtle, widget.color.fg, widget.action.icon 2044×44 iOS / 48×48 Android; expanded onlyLink vocelune://play/{routeId}/{targetSenseId}?part=word · Android: activity PendingIntent. Opens the playback screen; audio starts after the app is active

Dialogue mode (Pro)

Two short turns in compact; turns with the highlighted target, translations and Next in expanded. Bubbles are generic: speaker A on the start side, speaker B on the end side, following the interface layout direction.

Dialogue
¿Tienes hambre?
Sí, vamos a comer.
iOS small 170×170
Dialogue
¿Tienes hambre?
Are you hungry?
Sí, vamos a comer.
Yes, let's go and eat.
Next
iOS medium 364×170
Dialogue
¿Tienes hambre?
Sí, vamos a comer.
Android compact 2×2
Dialogue
¿Tienes hambre?
Are you hungry?
Sí, vamos a comer.
Yes, let's go and eat.
Next
Android expanded 4×2
Dialogue
¿Tienes hambre?
Sí, vamos a comer.
iOS small 170×170
Dialogue
¿Tienes hambre?
Are you hungry?
Sí, vamos a comer.
Yes, let's go and eat.
Next
iOS medium 364×170
Dialogue
¿Tienes hambre?
Sí, vamos a comer.
Android compact 2×2
Dialogue
¿Tienes hambre?
Are you hungry?
Sí, vamos a comer.
Yes, let's go and eat.
Next
Android expanded 4×2
ElementTokensSize / ruleAction
Containerwidget.color.bgpadding widget.padding.compact (12) / widget.padding.expanded (16); gap widget.gap.compact (8) / widget.gap.expanded (12)Whole widget: widgetURL vocelune://word/{routeId}/{targetSenseId} · Android: activity PendingIntent, same URI
Bubble A / Bwidget.color.bubble-a / widget.color.bubble-b, widget.bubble.radius 12, padding 4×8, gap widget.bubble.gap 4max-width 88%; A aligned start, B aligned end—
Target highlightwidget.color.highlight + semibold + underline 2Grapheme range from content; never color alone—
Turn translationwidget.color.fg-secondary, bodyExpanded only; hidden (whole turn pair falls back to compact) if it does not fit—
Headerwidget.color.fg-secondary, widget.color.icon, widget.text.meta 12 medium, widget.icon.meta 16One line; mode icon + mode name. Omitted in compact quiz front—
Speakerwidget.color.surface-subtle, widget.color.fg, widget.action.icon 2044×44 iOS / 48×48 Android; expanded onlyLink vocelune://play/{routeId}/{targetSenseId}?part=word · Android: activity PendingIntent. Opens the playback screen; audio starts after the app is active
Nextwidget.color.accent, widget.color.on-accent, widget.text.label 15 semibold, widget.action.radiusmin-height 44 / 48; hugs label; full width in compactiOS Button(intent: WidgetNextIntent(instanceID, revision)) · Android broadcast …widget.action.NEXT (appWidgetId, revision). Runs without the app

Quiz mode (Pro)

The expanded interactive quiz is mandatory on both platforms. The first answer is final and its feedback persists until Next. Feedback is always symbol + word + color.

Front

Necesito una para el café.
cucharaventana
iOS small
Quiz
Necesito una para el café.
cucharaventanasilla
iOS medium
Necesito una para el café.
cucharaventana
Android compact
Quiz
Necesito una para el café.
cucharaventanasilla
Android expanded 4×3 (tall)
Necesito una para el café.
cucharaventana
iOS small
Quiz
Necesito una para el café.
cucharaventanasilla
iOS medium
Necesito una para el café.
cucharaventana
Android compact
Quiz
Necesito una para el café.
cucharaventanasilla
Android expanded 4×3 (tall)
ElementTokensSize / ruleAction
Containerwidget.color.bgpadding widget.padding.compact (12) / widget.padding.expanded (16); gap widget.gap.compact (8) / widget.gap.expanded (12)Non-option space opens details
Sentencewidget.color.fg, compact body 15 / expanded widget.line-height.sentence 17Max 2 lines in compact, 3 in expanded; if longer, use the compact variant, then Open quiz—
Gapunderline widget.gap-mark.width 2, min-width widget.gap-mark.min-width 40Announced as “blank”—
Optionswidget.color.option-border 1.5, widget.option.radius 8, widget.text.label 15 medium (17 expanded), gap widget.option.gap 8min-height 44 / 48. Compact: 2 options stacked, only if the exercise has 2 options that fit on one line each. Expanded: all options in one row; if any label wraps, stack (Android tall) or fall backiOS WidgetAnswerIntent(instanceID, occurrenceID, optionID, revision) · Android …widget.action.ANSWER. First answer is final; stale revision is ignored

Answered correct

Correct
Necesito una cuchara para el café.
Next
iOS small
CorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
iOS medium
Correct
Necesito una cuchara para el café.
Next
Android compact
CorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
cucharaventanasilla
Android expanded 4×3 (tall)
Correct
Necesito una cuchara para el café.
Next
iOS small
CorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
iOS medium
Correct
Necesito una cuchara para el café.
Next
Android compact
CorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
cucharaventanasilla
Android expanded 4×3 (tall)

Answered incorrect

Incorrect
Necesito una cuchara para el café.
Next
iOS small
IncorrectNext
Necesito una cuchara para el café.
Your answer: ventana
iOS medium
Incorrect
Necesito una cuchara para el café.
Next
Android compact
IncorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
cucharaventanasilla
Android expanded 4×3 (tall)
Incorrect
Necesito una cuchara para el café.
Next
iOS small
IncorrectNext
Necesito una cuchara para el café.
Your answer: ventana
iOS medium
Incorrect
Necesito una cuchara para el café.
Next
Android compact
IncorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
cucharaventanasilla
Android expanded 4×3 (tall)
ElementTokensSize / ruleAction
Feedback pillwidget.color.feedback.correct-* or incorrect-*; icon check-circle-fill / x-circle-fill 20Symbol + word + color; persistent until Next—
Filled sentenceanswer in widget.color.highlight, semibold, underlineCorrect answer always shown, also after a wrong answer—
Second linewidget.color.fg-secondaryCorrect: translation. Incorrect: “Your answer: …” (translation is in details)—
Options (tall only)correct: feedback.correct-bg/border 2 + check; chosen wrong: feedback.incorrect-bg/border 2 + cross; others dimmed widget.color.dividerNot tappable after answering—
Nextwidget.color.accent, widget.color.on-accent, widget.text.label 15 semibold, widget.action.radiusmin-height 44 / 48; hugs label; full width in compactiOS Button(intent: WidgetNextIntent(instanceID, revision)) · Android broadcast …widget.action.NEXT (appWidgetId, revision). Runs without the app

Fallback: Open quiz

Used when the options cannot be shown without truncation (three options in compact, long option labels, or large text).

Necesito una para el café.
Open quiz
iOS small · 3 options
Quiz
Necesito una para el café.
Open quiz
iOS medium · does not fit
Necesito una para el café.
Open quiz
Android compact
ElementTokensSize / ruleAction
Open quizprimary action tokens; icon arrow-square-out (mirrors in RTL)Replaces options; the sentence stays so the widget is still usefulLink vocelune://quiz/{occurrenceId} opens the same occurrence in the app

Flashcard mode (Pro)

Show swaps the front for the back; no animation is required (optional crossfade uses motion.reveal, none with reduced motion). Next advances without grading: widget flashcards are never graded in v1.

Front

Flashcard
madrugar
Show
iOS small 170×170
Flashcard
madrugar
Show
iOS medium 364×170
Flashcard
madrugar
Show
Android compact 2×2
Flashcard
madrugar
Show
Android expanded 4×2
Flashcard
madrugar
Show
iOS small 170×170
Flashcard
madrugar
Show
iOS medium 364×170
Flashcard
madrugar
Show
Android compact 2×2
Flashcard
madrugar
Show
Android expanded 4×2

Revealed

Flashcard
madrugar
to get up early
Next
iOS small 170×170
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
iOS medium 364×170
Flashcard
madrugar
to get up early
Next
Android compact 2×2
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
Android expanded 4×2
Flashcard
madrugar
to get up early
Next
iOS small 170×170
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
iOS medium 364×170
Flashcard
madrugar
to get up early
Next
Android compact 2×2
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
Android expanded 4×2
ElementTokensSize / ruleAction
Containerwidget.color.bgpadding widget.padding.compact (12) / widget.padding.expanded (16); gap widget.gap.compact (8) / widget.gap.expanded (12)Whole widget: widgetURL vocelune://word/{routeId}/{targetSenseId} · Android: activity PendingIntent, same URI
Front termwidget.text.term.* 22 / 28 semibold; optional phonetics widget.color.fg-secondaryVertically centered—
Showprimary action; icon eyemin-height 44 / 48iOS WidgetRevealIntent(instanceID, revision) · Android …widget.action.REVEAL
Backterm body semibold; meaning 17 (expanded) widget.color.fg; example + translationCompact back: term + meaning only—
Speakerwidget.color.surface-subtle, widget.color.fg, widget.action.icon 2044×44 iOS / 48×48 Android; expanded onlyLink vocelune://play/{routeId}/{targetSenseId}?part=word · Android: activity PendingIntent. Opens the playback screen; audio starts after the app is active
Nextwidget.color.accent, widget.color.on-accent, widget.text.label 15 semibold, widget.action.radiusmin-height 44 / 48; hugs label; full width in compactiOS Button(intent: WidgetNextIntent(instanceID, revision)) · Android broadcast …widget.action.NEXT (appWidgetId, revision). Runs without the app

Pro-locked, empty queue and storage unavailable

Pro-locked applies to Dialogue, Quiz and Flashcard for free users and after Pro expires: the widget downgrades to the free word display and keeps a Pro entry. Every locked action checks the cached access deadline first, even on a stale snapshot, and records nothing.

Pro-locked (quiz widget, free user)

Word
la ventana
window
Pro
iOS small 170×170
Word
la ventana
window
Quiz widgets are part of Pro
iOS medium 364×170
Word
la ventana
window
Pro
Android compact 2×2
Word
la ventana
window
Quiz widgets are part of Pro
Android expanded 4×2
Word
la ventana
window
Pro
iOS small 170×170
Word
la ventana
window
Quiz widgets are part of Pro
iOS medium 364×170
Word
la ventana
window
Pro
Android compact 2×2
Word
la ventana
window
Quiz widgets are part of Pro
Android expanded 4×2
ElementTokensSize / ruleAction
ContentWord mode contentSame layout as WordWhole widget: widgetURL vocelune://word/{routeId}/{targetSenseId} · Android: activity PendingIntent, same URI
Pro entrywidget.color.pro-bg, widget.color.pro-fg, lock-simple; compact: “Pro” chipExpanded: full-width row, min-height 44 / 48Link vocelune://pro?source=widget.{mode} opens the paywall

Empty queue

No cards ready
Open app to refresh
iOS small 170×170
No cards ready
Open the app to prepare new cards.
Open app
iOS medium 364×170
No cards ready
Open app to refresh
Android compact 2×2
No cards ready
Open the app to prepare new cards.
Open app
Android expanded 4×2
No cards ready
Open app to refresh
iOS small 170×170
No cards ready
Open the app to prepare new cards.
Open app
iOS medium 364×170
No cards ready
Open app to refresh
Android compact 2×2
No cards ready
Open the app to prepare new cards.
Open app
Android expanded 4×2
ElementTokensSize / ruleAction
Symbolcolor.brand.symbol, widget.icon.symbol 32Decorative (hidden from accessibility)—
Messagebody semibold + secondaryShown when the prepared queue is used up or refresh is delayed with nothing cachedwidgetURL vocelune://today
Open appghost action (subtle fill, accent label)Expanded only; compact uses the whole widgetwidgetURL vocelune://today

Storage unavailable

Before the first unlock after a restart, storage cannot be read. The widget shows a neutral brand face, no content and no actions besides opening the app; it never creates attempts.

iOS small 170×170
iOS medium 364×170
Android compact 2×2
Android expanded 4×2
iOS small 170×170
iOS medium 364×170
Android compact 2×2
Android expanded 4×2
ElementTokensSize / ruleAction
Containerwidget.color.bg-brandSymbol 32 centered, color.text.on-brand; no textwidgetURL vocelune://today

Rendering modes

Full color is shown in every section above (light and dark). Below: iOS accented rendering (used by the tinted and clear home-screen appearances) and Android dynamic color. These are simulations built from tokens; the real colors come from the system.

iOS accented · tinted (sample tint)

Quiz
Necesito una para el café.
cucharaventanasilla
Quiz front
CorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
Quiz correct
Flashcard
madrugar
Show
Flashcard front
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
Flashcard revealed

iOS accented · clear

Quiz
Necesito una para el café.
cucharaventanasilla
Quiz front
CorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
Quiz correct
Flashcard
madrugar
Show
Flashcard front
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
Flashcard revealed
Accented: the system removes the container background and renders content in two groups. Accent group (.widgetAccentable()): primary actions, the filled gap and highlight, feedback pills and their symbols, the Pro entry. Default group: everything else. Fills turn into outlines (check @Environment(\.widgetRenderingMode) == .accented), so no text ever sits on an opaque tinted fill. Correct and incorrect stay distinguishable by symbol and word.

Android 12+ dynamic color · light and dark (sample wallpaper scheme)

Quiz
Necesito una para el café.
cucharaventanasilla
Quiz front
IncorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
cucharaventanasilla
Quiz incorrect
Flashcard
madrugar
Show
Flashcard front
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
Flashcard revealed
Quiz
Necesito una para el café.
cucharaventanasilla
Quiz front
IncorrectNext
Necesito una cuchara para el café.
I need a spoon for the coffee.
cucharaventanasilla
Quiz incorrect
Flashcard
madrugar
Show
Flashcard front
Flashcard
madrugar
to get up early
Mañana tengo que madrugar.
Tomorrow I have to get up early.
Next
Flashcard revealed
Dynamic color: GlanceTheme with the system dynamic scheme on API 31+, and a ColorProviders scheme built from the brand tokens on API 29–30. Each widget.color.* token names its Material role in $extensions. Feedback and Pro colors are fixed brand tokens inside their own containers, so their contrast never depends on the wallpaper.

Multilingual rendering

Sample content, for layout only; linguistic review happens in the content pipeline. Direction, line height and line breaking come from each field's language (registry script), never from the interface language.

Word
كتاب
kitāb
book
en → ar · RTL target in an LTR interface: each field sets its own base direction, alignment follows the layout. Tall script at this size: example dropped (fallback)
Quiz
أشرب كل صباح.
القهوةالباب
en → ar · RTL quiz sentence; options keep the interface order
Correct
أشرب القهوة كل صباح.
Next
en → ar · answered, compact
כרטיסייה
la ventana
חלון
Abre la ventana, por favor.
פתח את החלון, בבקשה.
הבא
he → es · Hebrew interface: layout mirrors, the Spanish term stays LTR, Next arrow mirrors
דיאלוג
¿Tienes hambre?
אתה רעב?
Sí, vamos a comer.
כן, בוא נלך לאכול.
הבא
he → es · A on the start (right) side, B on the end side
Word
Geschwindigkeitsbegrenzung
speed limit
en → de · long compound: scaled to 80%, then hyphenated by language; never truncated
Flashcard
図書館
としょかん
Show
es → ja · CJK line height
Word
図書館
としょかん
biblioteca
図書館で本を借りました。
es → ja · unsegmented sentence wraps by the platform line breaker; example translation dropped (fallback)
Word
ห้องสมุด
library
ฉันไปห้องสมุดทุกวันเสาร์
en → th · tall script, dictionary line breaking; example translation dropped (fallback)
Word
图书馆
túshūguǎn
library
en → zh-Hans · CJK line height
Rules: set direction per text view from the field's language; wrap mixed-direction runs in bidi isolation (SwiftUI: separate Text views with explicit layout direction; Android: BidiFormatter.unicodeWrap). Apply typography.script.line-height-multiplier by script class. Line breaking is the platform's (ICU dictionaries for Thai, Lao, Khmer, Burmese; kinsoku for Japanese). Never slice strings to fit.

Large text and smallest sizes

Widgets follow the OS text size. Each layout declares a fallback chain and the first variant that fits without truncation is used: iOS ViewThatFits; Android chooses the layout from the widget size and Configuration.fontScale when the provider builds RemoteViews.

Flashcard
madrugar
to get up early
Next
iOS medium · 135% text · flashcard drops the example (fallback)
Quiz
Necesito una para el café.
Open quiz
iOS medium · 135% text · quiz falls back to Open quiz
Word
la ventana
window
Android compact · 135% text
Quiz
Necesito una para el café.
cucharaventanasilla
Android tall · 135% text · options still fit

Smallest supported sizes (100% text)

Necesito una para el café.
Open quiz
iOS small, smallest phone 148×148 · two stacked options no longer fit: Open quiz
Quiz
Necesito una para el café.
cucharaventanasilla
iOS medium, smallest phone 321×148
Flashcard
madrugar
to get up early
Next
Flashcard revealed 321×148 · example dropped (fallback)
LayoutTokensFallback chain (first that fits)
Quiz expanded—row options + header → row options without header → stacked options (Android tall) → compact variant → Open quiz—
Quiz compact—2 stacked options → 2 options in a row (short labels) → Open quiz—
Flashcard / Word expanded—full → drop example translation → drop example → compact layout—
Dialogue expanded—turns + translations → turns only → compact—
Touch targetswidget.action.min-height*, widget.option.min-height*44×44 pt iOS, 48×48 dp Android, gap ≥ 8 between targets—