Skip to content

Introduce the StreamDesign token system in the Compose Video SDK - #1803

Open
andremion wants to merge 2 commits into
develop-v2from
andrerego/and-1419-introduce-streamdesign-in-the-compose-video-sdk
Open

Introduce the StreamDesign token system in the Compose Video SDK#1803
andremion wants to merge 2 commits into
develop-v2from
andrerego/and-1419-introduce-streamdesign-in-the-compose-video-sdk

Conversation

@andremion

@andremion andremion commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Goal

Closes AND-1419: replace the video token layer (StreamColors, StreamDimens, StreamTypography, StreamShapes) with the shared StreamDesign token system, vendored like the chat SDK does, and give the Compose SDK a real light theme. Also carries the VideoUiConfig extraction (AND-1418 was merged into this ticket).

Implementation

  • The Kotlin token classes are generated from the core and video namespaces of design-system-tokens PR Docs/tutorial cleanup #66 (commit 8ad91b47); the chat namespace is absent by construction. The JSON snapshot stays local and unversioned, as in chat.
  • theme/design/StreamDesign.kt: Colors with 60 public semantic tokens derived from the brand and chrome scales (default(brand, chrome) / defaultDark(brand, chrome), ColorScale.from(brandColor)) plus internal component tokens (button, badge, control, input, label, tab), and Typography with the design system text styles. StreamPrimitiveColors and StreamTokens (spacing, radius, sizes, strokes, font primitives, component sizes) are internal.
  • Video-only tokens (indicator and accept-call colors) are internal extensions in theme/VideoDesignTokens.kt, so the design package can move to a shared module unchanged (AND-1482).
  • VideoTheme: isInDarkMode selects the palette; the ripple derives from the colors; LocalContentColor is textPrimary because the new typography carries no color; the text style providers set role colors explicitly. VideoUiConfig replaces the allowUIAutomationTest and reactionMapper parameters.
  • Usages migrated across the SDK, demo app and tutorials. The SDK uses StreamTokens; the apps use literal values because StreamTokens is internal. Labels drawn over video use textOnAccent (white in both modes). The moderation warning takes its colors from the theme. GenericContainer.roundness is a CornerSize. The unused textSizeResource helper is removed.
  • Two deliberate differences from the iOS token module (stream-core-swift#81): token names keep the bg spelling of the token paths and of chat Android (iOS expands it to Background), and component tokens stay internal as in chat Android (iOS exposes them).
  • Not changed: the XML resources in stream-video-android-ui-core, which the XML UI module still reads. Breaking API changes are recorded for the migration guide (AND-1436): the four token classes, the VideoTheme parameters, StreamTheme.reactionMapper and rippleConfiguration, StreamRippleConfiguration.

🎨 UI Changes

The new palette applies immediately, so every golden changes and the snapshot diff under stream-video-android-ui-compose/src/test/snapshots/images is the review artifact. Light and dark now render differently for the first time; compare any *_in_dark_mode.png with its light counterpart, for example ParticipantsPortraitTest_call_participants_list. Dark-mode avatar backgrounds change by design (PR #66 removed the avatar palette). Component sizes follow the token values (44dp controls are 48dp, 84dp is 80dp).

Testing

Manual: run the demo app, toggle the system dark mode, open a call, the lobby, the participants list and the settings dialogs; text stays readable on both palettes and labels over video stay white.

Ran: spotlessCheck, apiCheck, testDebugUnitTest (includes verifyPaparazziDebug with the default threshold, green twice), and the debug compilation of the demo app and the four tutorial modules. Detekt is not configured in this repo.

@andremion andremion added the pr:new-feature Adds new functionality label Sep 2, 2026
@andremion

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review skipped: 142 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.30 MB 12.45 MB 0.14 MB 🟢
stream-video-android-ui-xml 5.70 MB 5.75 MB 0.05 MB 🟢
stream-video-android-ui-compose 6.23 MB 5.81 MB -0.43 MB 🚀

@andremion
andremion force-pushed the andrerego/and-1419-introduce-streamdesign-in-the-compose-video-sdk branch 2 times, most recently from 4a74b62 to 8a9043d Compare September 2, 2026 15:50
@andremion

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review skipped: 127 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@andremion
andremion force-pushed the andrerego/and-1419-introduce-streamdesign-in-the-compose-video-sdk branch from 8a9043d to e464c7a Compare September 2, 2026 16:24
@andremion
andremion marked this pull request as ready for review September 2, 2026 16:41
@andremion
andremion requested a review from a team as a code owner September 2, 2026 16:41
Base automatically changed from andrerego/and-1472-bump-compose-bom-and-paparazzi-on-develop-v2-and-re-record to develop-v2 September 3, 2026 10:52
Move behavior settings out of VideoTheme into a VideoUiConfig data class,
mirroring ChatUiConfig in the chat SDK. VideoTheme now takes a single
config parameter (first, as in ChatTheme) exposed through
VideoTheme.config and LocalVideoUiConfig.

Removed VideoTheme parameters (breaking, targets develop-v2):
- allowUIAutomationTest and reactionMapper: moved into VideoUiConfig.
- isInDarkMode: was unused in the function body; there is no light
  palette yet. The StreamDesign work later on this branch reintroduces
  it wired to the light and dark palettes.
- rippleConfiguration: the type is an object, so the parameter could
  never carry a custom value. StreamRippleConfiguration is internal now;
  the colors-derived ripple comes with StreamDesign later on this
  branch.

Also removed the StreamTheme.reactionMapper and
StreamTheme.rippleConfiguration accessors. Reaction mapper call sites
now read VideoTheme.config.reactionMapper.

VideoRendererStyle flags were evaluated and stay where they are: they
are per-renderer-instance state (focus, screen share variants), not
global behavior. Revisit during AND-1425.

The Paparazzi helpers keep their isInDarkMode parameter as a documented
no-op so the test call sites do not churn twice; the StreamDesign work
wires it to the light and dark palettes. Snapshot goldens are
unchanged.
Replace the video token layer (StreamColors, StreamDimens,
StreamTypography, StreamShapes) with the shared StreamDesign token
system, vendored like the chat SDK does.

Token source: the core and video namespaces of design-system-tokens
after the namespace restructure (PR #66, commit 8ad91b47). The classes
in theme/design are generated from that source; the JSON snapshot is
kept locally in .figma-tokens and not versioned, as in chat:
- StreamDesign.Colors: 60 public semantic tokens (accent, text,
  background, border, avatar, system) derived from the brand and chrome
  scales at construction, plus internal component tokens (button, badge,
  control, input, label, tab). default() and defaultDark() take the
  scales as parameters; ColorScale.from(brandColor) re-brands from one
  color.
- StreamDesign.Typography: the design system text styles.
- StreamPrimitiveColors and StreamTokens (internal): raw palette,
  spacing, radius, sizes, strokes, font primitives and component sizes.
Video-only tokens (indicator and accept-call colors) are internal
extensions in the theme package, so the design package can move to a
shared module unchanged.

VideoTheme: isInDarkMode selects the light or dark palette; the ripple
derives from the colors; LocalContentColor is textPrimary because the
new typography carries no color. The text style providers set the role
colors explicitly.

Usages across the SDK, the demo app and the tutorials are migrated. The
SDK uses StreamTokens; the apps use literal values because StreamTokens
is internal. Labels drawn over video overlays use textOnAccent (white in
both modes) and the moderation warning takes its colors from the theme.
GenericContainer.roundness is a CornerSize now. The unused
textSizeResource helper is removed.

Goldens are re-recorded once: the new palette applies immediately and
light and dark now render differently. apiDump updated.
@andremion
andremion force-pushed the andrerego/and-1419-introduce-streamdesign-in-the-compose-video-sdk branch from e464c7a to 6c9281d Compare September 3, 2026 12:35
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

@gpunto gpunto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the full diff against develop-v2, and the token layer against chat since that is the reference.

The port is careful. Versus stream-chat-android: 43 shared primitives with zero hex drift, zero value drift on every shared semantic token in both palettes, same structural moves (StreamDimens/StreamShapes dropped, only colors + typography exposed), and streamRippleConfiguration an exact port. Every replaced dimension resolves back to the same value in dimens.xml. Taking the breaking API removals as expected for the next major.

Almost everything below is one mechanical mapping: basePrimary to textPrimary, applied regardless of the surface underneath. basePrimary was #E3E4E5, light in both modes; textPrimary flips to near-black in light. On a saturated accent or one of the always-dark overlays, the content inverts. Not a regression — this is the first release where the palettes actually differ — but it ships in the goldens.

The tokens for it already exist here: buttonPrimaryTextOnAccent, buttonDestructiveTextOnAccent, and the unused controlAcceptCallButtonText. Two cautions: textOnAccent is the white-in-both-modes one, textOnInverse is black in dark; and neutral surfaces should keep textPrimary, so it isn't a blanket swap. Affected goldens will need re-recording.

One process note: the Testing section says labels over video stay white, which holds for the label text but not the icons on those same surfaces — suggesting the manual pass ran in dark mode. Worth paging through the light-mode goldens, since CodeRabbit skipped twice on file count and Sonar wouldn't catch any of this.

Happy to be wrong where the new look is deliberate.

contentColor = VideoTheme.colors.basePrimary,
disabledBackgroundColor = VideoTheme.colors.buttonBrandDisabled,
backgroundColor = VideoTheme.colors.buttonPrimaryBg,
contentColor = VideoTheme.colors.textPrimary,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buttonPrimaryBg is accentPrimary, so this label goes near-black in light mode — DialogsTest_stream_dialog has "Continue" at roughly 3.3:1. buttonPrimaryTextOnAccent (StreamDesign.kt:206) is the pair.

contentColor = VideoTheme.colors.basePrimary,
disabledBackgroundColor = VideoTheme.colors.buttonAlertDisabled,
backgroundColor = VideoTheme.colors.buttonDestructiveBg,
contentColor = VideoTheme.colors.textPrimary,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same over accentErrorbuttonDestructiveTextOnAccent (StreamDesign.kt:248).

StyleSize.M -> VideoTheme.typography.captionEmphasis.withColor(
VideoTheme.colors.textPrimary,
)
else -> VideoTheme.typography.bodyEmphasis.withColor(VideoTheme.colors.textPrimary)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultButtonLabel bakes textPrimary into the TextStyle, and an explicit style colour beats LocalContentColor — so changing contentColor in StreamButtonStyle won't render until this moves too.

color = bgColor ?: VideoTheme.colors.alertSuccess,
iconTint = iconTint ?: VideoTheme.colors.basePrimary,
color = bgColor ?: VideoTheme.colors.accentSuccess,
iconTint = iconTint ?: VideoTheme.colors.textPrimary,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Icon over accentSuccess; see ControlActionsTest_accept_call_action. controlAcceptCallButtonText in VideoDesignTokens.kt was written for this line and is currently unused.

color = bgColor ?: VideoTheme.colors.alertWarning,
iconTint = iconTint ?: VideoTheme.colors.basePrimary,
color = bgColor ?: VideoTheme.colors.accentError,
iconTint = iconTint ?: VideoTheme.colors.textPrimary,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same over accentError, as are CancelCallAction.kt:52 and LeaveCallAction.kt:48buttonDestructiveTextOnAccent.

backgroundColor = VideoTheme.colors.baseSheetPrimary,
contentColor = VideoTheme.colors.baseSheetPrimary,
backgroundColor = VideoTheme.colors.backgroundCoreApp,
contentColor = VideoTheme.colors.backgroundCoreApp,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overrides the LocalContentColor = textPrimary that VideoTheme sets, for everything inside CallContent — so anything without its own colour, including integrator videoOverlayContent/appBarContent, draws in the background colour. Harmless before only because the old typography carried colours. AudioRoomContent.kt:158 too.

StyleSize.XS -> VideoTheme.typography.headingExtraSmall.withColor(
VideoTheme.colors.textPrimary,
)
else -> VideoTheme.typography.headingLarge.withColor(VideoTheme.colors.textPrimary)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: four sizes collapse into two, leaving headingSmall/headingMedium unreachable — DialogStyle.kt:42 asks for StyleSize.S and renders as L.

StyleSize.XS, StyleSize.S, StyleSize.M -> VideoTheme.typography.bodyM.wrapper()
else -> VideoTheme.typography.bodyL.wrapper()
},
default: TextStyleWrapper = VideoTheme.typography.bodyDefault.withColor(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: size is no longer read, so every size returns the same style.

@Composable
fun LoginScreen(onUserSelected: (TutorialUser) -> Unit) {
Surface(modifier = Modifier.fillMaxSize(), color = VideoTheme.colors.baseTertiary) {
Surface(modifier = Modifier.fillMaxSize(), color = VideoTheme.colors.textTertiary) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: a text token as a full-screen background — baseTertiary was a surface (#4C535B), so backgroundCoreSurfaceDefault. Line 187 same, and line 252 wants borderCoreDefault.

LocalRippleConfiguration provides rippleConfiguration.default(),
LocalReactionMapper provides reactionMapper,
LocalContentColor provides colors.textPrimary,
LocalRippleConfiguration provides streamRippleConfiguration(colors, lightTheme = !isInDarkMode),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, and the API removal is fine for a major: just noting the old default() deferred to LocalRippleConfiguration when set, so a wrapping MaterialTheme's ripple survived. This overrides unconditionally. Worth a migration-guide line if deliberate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature Adds new functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants