Introduce the StreamDesign token system in the Compose Video SDK - #1803
Introduce the StreamDesign token system in the Compose Video SDK#1803andremion wants to merge 2 commits into
Conversation
|
@CodeRabbit review |
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
|
SDK Size Comparison 📏
|
4a74b62 to
8a9043d
Compare
|
@CodeRabbit review |
|
8a9043d to
e464c7a
Compare
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.
e464c7a to
6c9281d
Compare
|
gpunto
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Same over accentError — buttonDestructiveTextOnAccent (StreamDesign.kt:248).
| StyleSize.M -> VideoTheme.typography.captionEmphasis.withColor( | ||
| VideoTheme.colors.textPrimary, | ||
| ) | ||
| else -> VideoTheme.typography.bodyEmphasis.withColor(VideoTheme.colors.textPrimary) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Same over accentError, as are CancelCallAction.kt:52 and LeaveCallAction.kt:48 — buttonDestructiveTextOnAccent.
| backgroundColor = VideoTheme.colors.baseSheetPrimary, | ||
| contentColor = VideoTheme.colors.baseSheetPrimary, | ||
| backgroundColor = VideoTheme.colors.backgroundCoreApp, | ||
| contentColor = VideoTheme.colors.backgroundCoreApp, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.



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
coreandvideonamespaces of design-system-tokens PR Docs/tutorial cleanup #66 (commit8ad91b47); the chat namespace is absent by construction. The JSON snapshot stays local and unversioned, as in chat.theme/design/StreamDesign.kt:Colorswith 60 public semantic tokens derived from thebrandandchromescales (default(brand, chrome)/defaultDark(brand, chrome),ColorScale.from(brandColor)) plus internal component tokens (button, badge, control, input, label, tab), andTypographywith the design system text styles.StreamPrimitiveColorsandStreamTokens(spacing, radius, sizes, strokes, font primitives, component sizes) are internal.theme/VideoDesignTokens.kt, so thedesignpackage can move to a shared module unchanged (AND-1482).VideoTheme:isInDarkModeselects the palette; the ripple derives from the colors;LocalContentColoristextPrimarybecause the new typography carries no color; the text style providers set role colors explicitly.VideoUiConfigreplaces theallowUIAutomationTestandreactionMapperparameters.StreamTokens; the apps use literal values becauseStreamTokensis internal. Labels drawn over video usetextOnAccent(white in both modes). The moderation warning takes its colors from the theme.GenericContainer.roundnessis aCornerSize. The unusedtextSizeResourcehelper is removed.bgspelling of the token paths and of chat Android (iOS expands it toBackground), and component tokens stay internal as in chat Android (iOS exposes them).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, theVideoThemeparameters,StreamTheme.reactionMapperandrippleConfiguration,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/imagesis the review artifact. Light and dark now render differently for the first time; compare any*_in_dark_mode.pngwith its light counterpart, for exampleParticipantsPortraitTest_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(includesverifyPaparazziDebugwith 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.