Skip to content

feat: M-Cards — dynamic card-rendering engine (SDUI) - #11

Open
ramonfsk wants to merge 1 commit into
devfrom
feat/m-cards-core
Open

feat: M-Cards — dynamic card-rendering engine (SDUI)#11
ramonfsk wants to merge 1 commit into
devfrom
feat/m-cards-core

Conversation

@ramonfsk

Copy link
Copy Markdown
Owner

Summary

  • New standalone :cards-core module (no dependency on :core) — a generic, data-driven card-rendering engine inspired by Adaptive Cards' vocabulary, not a fixed set of dedicated card types
  • Renders all 16 illustrative card layouts from the commissioned design: confirm, summary, carousel, rating, choices, form, location, checklist, datetime, progress, payment, contact, poll, weather, ordertracking, video
  • :core change: card-update now routes to a card by id across all messages, not just the in-progress one — needed for server-driven cards (progress/ordertracking) that update after the turn completes
  • New :cards-android (Compose) and cards-ios (SwiftUI) renderers, wired into both sample apps via a CardEngineStore adapter owned above the message list (so scrolling a half-filled form off-screen and back doesn't lose it)
  • Real video playback (AVKit VideoPlayer / Media3 ExoPlayer), not a placeholder
  • 16 new tools/fake-sse-server scenarios, one per card, validated against the real parser
  • docs/design-doc.md §8/§9/§15.3 updated to match what was actually built; new docs/cards-wire-schema.md field reference

What's verified

  • cards-core's own test suite (CardParserTest, CardEngineTest) — green
  • All 16 fake-sse-server card payloads round-tripped through the real CardParser (no Unsupported fallbacks, valid single-line SSE framing)
  • Not yet verified: actually seeing the 16 cards rendered on-screen in the Android emulator / iOS simulator — this PR is unit/parser-level validated only, visual QA is the next step

Test plan

  • Build and run sample-app-android/sample-app-ios, trigger each of the 16 card keywords via tools/fake-sse-server, and visually compare against the commissioned design
  • Exercise interactive cards specifically: form/checklist submit-gating, poll voting, carousel selection, rating stars, payment/contact host-action routing
  • Scroll a card with local input state (e.g. a half-filled form) off-screen and back — confirm the values survive
  • Confirm progress/ordertracking cards can still be updated via card-update after their message completes

🤖 Generated with Claude Code

Standalone :cards-core (no dependency on :core, by design — a candidate
for its own repo later) renders all 16 illustrative card layouts from
the commissioned design, generically and data-driven rather than as
fixed dedicated types:

- schema/CardElement.kt: element vocabulary (TextBlock, Image, Avatar,
  Media, Container, ColumnSet/Column, FactSet, ActionSet, Input.*,
  plus custom Carousel/Stepper), inspired by Adaptive Cards' vocabulary
  without adopting its schema
- CardParser: hand-parses the wire JsonObject recursively, never
  throws — an unrecognized type degrades only that element to
  Unsupported
- CardEngine/CardIntent: per-card runtime state (values/selections/
  answered actions), emits CardIntent (Notify/HostAction) rather than
  touching ChatSession directly — cards-android's/cards-ios's
  CardEngineStore is the thin adapter that turns a Notify into
  ChatSession.sendMessage
- CardActionHandler: cards-scoped sibling of design doc §9's
  ChatActionHandler, for genuinely sensitive actions (payment, saving
  a contact) that never round-trip as a synthesized chat message

Four resolutions found by actually composing the 16 cards rather than
assumed up front: poll percentages are server-computed
(Choice.percentage), not tallied client-side; choices/carousel's
"exclusive selection" is two different behaviors
(ActionSet.afterSelection); form/checklist gate submission via
CardAction.enabledWhenInputsFilled; checklist's aggregate notify
sentence comes from modeling it as one multi-select Input.ChoiceSet,
not N toggles.

:core change: ChatStateReducer only routed card-update to the
in-progress message, so a server push to an already-completed
message's card (progress/ordertracking, the case those cards exist
for) could never reach it. Now routes by card id across all messages.

Renderers: :cards-android (Compose, depends on new leaf
:design-tokens-android extracted from :ui-android so card chrome
matches bubble chrome) and cards-ios (SwiftUI Swift package). Video
cards get real playback (AVKit VideoPlayer / Media3 ExoPlayer), not a
placeholder. CardEngineStore is owned above the message list in both
ChatScreen/ChatView, not inside a recycled row, so a half-typed form
survives scrolling off-screen and back.

tools/fake-sse-server: one scripted scenario per card, real
wire-accurate payloads validated against CardParser.

Docs: design-doc.md §8 (real vocabulary replacing the old generic
sketch), §9 (CardActionHandler cross-reference), §15.3 (implemented
scope, corrected from the earlier draft), new
docs/cards-wire-schema.md (full field reference).

Also: cards-ios's SPM .build/ output wasn't covered by .gitignore
(only Gradle's build/ was) — added **/.build/.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant