Add the shared design token system to StreamCoreUI - #81
Conversation
Brings the generated design system tokens into StreamCoreUI so Video and Chat build against one source instead of each keeping a copy. ColorPalette holds the semantic colour tokens and DesignSystemTokens the layout ones (spacing, radii, icon sizing, stroke widths, elevations). Both are classes of lazy properties, so semantic tokens derive from the ramps and foundation scale: overriding brand500 or radiusXl before the first read cascades to everything built on it. The full token set is kept, including the messaging surfaces, so neither SDK has to re-derive what the other needs on every re-sync.
Generated by 🚫 Danger |
The generator emits one flat token file covering every Stream product, but each SDK only draws part of it. Keep the shared set here and hand the messaging and calling groups to the SDKs that own them, so neither product exposes the other's vocabulary. TokenScope.md records the split, since it is applied by hand and has to survive the next re-sync. Add fonts and generic icons alongside the tokens, and a SharedAppearance that holds all four. Every SDK defaults to the same instance, so an app running more than one of them reskins them together. Fonts and images are vended for both UIKit and SwiftUI rather than bridged: a symbol that reaches SwiftUI through Image(uiImage:) stops scaling with the surrounding font and needs its rendering mode forced before it tints.
ColorPalette and DesignSystemTokens now live on Appearance. Product SDKs attach their own configuration through a bag store, so CoreUI never imports Chat or Video. @dynamicMemberLookup on ColorPalette lets each SDK flatten product tokens onto the palette without a public .chat / .call property.
Icons stay on each product SDK; this module is not the place for a shared image set.
The product prefix is Video, not Call.
Appearance is UI configuration and is only meant to be read and written on the main thread, so the bag stores no longer need an unchecked Sendable escape.
Shared typography is out of scope for this pass, so CoreUI only vends the shared colour palette and layout tokens.
Appearance is a shared configuration object, and marking it MainActor forced assumeIsolated at every injection boundary. Keep it a plain class and leave thread use to the caller.
The type is not actor-isolated, so the compiler requires an explicit escape for the singleton.
…tokens. Each product owns its own appearance; Core now exposes a class with lazy colour and layout groups that Chat and Video can pass around together.
The type is the shared design-system token bag, so the name should match.
| public lazy var avatarPaletteBackground1: UIColor = UIColor(light: .blue150, dark: .blue600) | ||
| public lazy var avatarPaletteBackground2: UIColor = UIColor(light: .cyan150, dark: .cyan600) | ||
| public lazy var avatarPaletteBackground3: UIColor = UIColor(light: .green150, dark: .green600) | ||
| public lazy var avatarPaletteBackground4: UIColor = UIColor(light: .purple150, dark: .purple600) | ||
| public lazy var avatarPaletteBackground5: UIColor = UIColor(light: .yellow150, dark: .yellow600) | ||
| public lazy var avatarPaletteText1: UIColor = UIColor(light: .blue900, dark: .blue100) | ||
| public lazy var avatarPaletteText2: UIColor = UIColor(light: .cyan900, dark: .cyan100) | ||
| public lazy var avatarPaletteText3: UIColor = UIColor(light: .green900, dark: .green100) | ||
| public lazy var avatarPaletteText4: UIColor = UIColor(light: .purple900, dark: .purple100) | ||
| public lazy var avatarPaletteText5: UIColor = UIColor(light: .yellow900, dark: .yellow100) |
There was a problem hiding this comment.
We should probably ask @jurgenploeger to clean up some of these. We dropped variable colors and just use the default.
| public lazy var inputRadiusOptionCard: CGFloat = radiusXl | ||
| public lazy var inputRadiusPollOptionInput: CGFloat = radiusXl |
There was a problem hiding this comment.
Look chat specific, buy probably fine
There was a problem hiding this comment.
yup, yes they kook chat specific, that is the thing, AI will make mistakes on interperting these, so we need @jurgenploeger to set some metadata or something to identify Chat-only or - Video-only stuff
| /// | ||
| /// Tokens derive lazily, so override the ramps before the first read. | ||
| extension DesignSystemTokens { | ||
| public final class Colors { |
There was a problem hiding this comment.
MainActor? These are mutable and meant for UI.
There was a problem hiding this comment.
My goal is to make it MainActor, but doing so produced some changes in views, but that was before I using this already in some views. So I can probably but back main actor
| public final class DesignSystemTokens { | ||
| /// Shared colour tokens. | ||
| public var colors: Colors | ||
| /// Shared layout tokens. | ||
| public var layout: Layout |
There was a problem hiding this comment.
Should we go with MainActor for this?
There was a problem hiding this comment.
Yes that is the goal
| private lazy var subject: DesignSystemTokens! = .init() | ||
|
|
||
| override func tearDown() { | ||
| subject = nil | ||
| super.tearDown() | ||
| } |
There was a problem hiding this comment.
This is slightly confusing compared to the typical setup and teardown we have for other tests. I would personally prefer the old style of explicit setUp although this works because how xctest runs tests.
There was a problem hiding this comment.
Yeah I didnt noticed this, maybe it is the pattern here on video?
Adds
DesignSystemTokenstoStreamCoreUI: the shared colour and layout tokens every Stream SDK draws from. Fonts, icons and images stay on each product SDK.Architecture
DesignSystemTokensis a class grouped by kind (colors,layout). Each SDK owns its own appearance (VideoAppearance, laterChatAppearance) and is constructed with aDesignSystemTokensinstance. Pass the same instance into both so they reskin together.flowchart TB subgraph core ["StreamCoreUI"] DT["DesignSystemTokens"] DT --> C["Colors\n175 tokens, lazy"] DT --> L["Layout\n66 tokens, lazy"] end subgraph video ["StreamVideoSwiftUI"] VA["VideoAppearance"] VA --> DT VA --> VC["VideoAppearance.Colors\n14 tokens"] VC --> C end subgraph chat ["StreamChat — follow-up"] CA["ChatAppearance"] CA --> DT CA --> CHC["ChatAppearance.Colors\n37 chat* tokens"] CHC --> C endColour and layout tokens are
lazy var. Override thebrandandchromeramps (or a scale token) before the first read so semantic tokens pick up the override.CoreUI does not import Chat or Video. Product colours live on each SDK's appearance (
videoAppearance.colors.indicatorSpeaking). Video keeps the existingAppearancefor views until they migrate.Token scope
The
design-system-tokensgenerator emits one flat file covering every Stream product. Product groups live on the SDK that owns them:The split is applied by hand.
DesignSystem/TokenScope.mdrecords it so a re-sync can re-apply it. Moving that upstream into the generator is IOS-2000.UIColor.init(light:dark:)stays internal. Chat already vends a public equivalent; a second one would be ambiguous at any call site that imports both modules.Testing
StreamCoreUI tests cover instance identity and the lazy-derivation boundary (override a ramp before vs after the first read).
Follow-ups
chatBackgroundMentionresolves to a raw ramp colour that is internal here, and whichUIColor.init(light:dark:)survives once Chat links CoreUI.