Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 57 additions & 28 deletions .claude/skills/update-design-tokens/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,44 @@ covered below.
| --- | --- |
| source of truth | `tokens/{core,chat,video}/semantics/{light,dark}.json` upstream |
| upstream Flutter build | `build/flutter/tokens/lib/src/{android,ios,web}/{light,dark}/stream_tokens.dart` |
| vendored here | `packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/{light,dark}/stream_tokens.dart` |
| colors vendored here | `lib/src/theme/primitives/internal/tokens/{light,dark}/stream_tokens.dart` |
| dimensions vendored here | `lib/src/theme/primitives/internal/tokens/stream_tokens_dimensions.dart` (one copy — mode-independent) |
| root semantics | `lib/src/theme/semantics/stream_color_scheme.dart` |
| derived values | each component's `build` / defaults, reading `colorScheme.*` |

The vendored files are **maintained by hand** — there is no sync command, and the
upstream build output is not copied in verbatim. Only `stream_colors.dart` and
`stream_color_scheme.dart` import them; no component theme or widget ever
references `StreamTokens`. (`stream_color_swatch_helper.dart` generates shades in
HCT from a seed and never reads a token — it is measured *against* the vendored
values, not driven by them.)

**Colors are byte-identical across the three upstream flavors; typography is not
— and for type the flavor is `web`.** Only web carries the `Geist` family this
package ships; android resolves to Roboto, iOS to SF Pro, and iOS also runs a
size up at every step (`typographyFontSizeMd` is 17 there against 16 on
android/web). So read colors from any flavor and type from `web`.

Dimensions — spacing, radius, sizes, line heights, weights — are identical across
all three, so no flavor choice arises. They do come from upstream, but
`StreamSpacing`, `StreamRadius` and `StreamTokensTypography` hard-code the values
instead of reading the vendored constants, so a dimension change upstream has to
be applied to those classes by hand.
upstream build output is not copied in verbatim. Their readers are only the
primitive and semantic classes: `stream_colors.dart` and
`stream_color_scheme.dart` for the colors, and `stream_spacing.dart`,
`stream_radius.dart` and `stream_tokens_typography.dart` for the dimensions. No
component theme or widget ever references `StreamTokens` directly — a component
reads a `colorScheme` field or one of those classes.
(`stream_color_swatch_helper.dart` is *not* a reader: it generates shades in HCT
from a seed, and is measured against the vendored values rather than driven by
them.)

**Colors are byte-identical across the three upstream flavors, and so is every
dimension except font size.** iOS runs a size up at almost every step
(`typographyFontSizeMd` is 17 there against 16 on android and web), which is why
that one group is vendored per flavor while the rest is not. Read colors from any
flavor; read a font size from the flavor whose scale you are editing.

The font *family* does not arise: this package never sets one for text, and ships
no text font — only the generated `Stream Icons` face. Upstream's
`typographyFontFamilySans` is therefore not vendored.

Spacing, radius, line heights and font weights are identical across all three
flavors, so no flavor choice arises for them. They live in
`internal/tokens/stream_tokens_dimensions.dart`, one copy rather than one per
mode, read by `StreamSpacing`, `StreamRadius`, `StreamLineHeight` and
`StreamFontWeight`.

**Font sizes are the exception**: iOS runs a size up at almost every step, so
they mirror upstream's flavor split in
`internal/tokens/{android,ios}/stream_tokens_font_size.dart` and feed
`StreamFontSize.android` and `StreamFontSize.ios`. A size change has to be taken
from the matching flavor — `check:tokens` enforces that both declare the same
names, but it cannot tell you a value came from the wrong one.

Only core and chat semantics are vendored into `internal/tokens/`; the video
namespace is not. That is about *vendoring*, not about impact — a video token can
Expand Down Expand Up @@ -347,21 +363,34 @@ component from a token, which is the one thing a component must not do — a
constant bypasses the seedable color scheme, so a custom brand or chrome stops
applying.

Dimensions and type are a separate matter. Their upstream values *are* mirrored
here, but in `StreamSpacing`, `StreamRadius` and `StreamTokensTypography`, which
hard-code them rather than reading a token constant. So a spacing or radius change
is applied to those classes by hand — and taking the type values from the wrong
flavor is a live mistake, since only `web` carries the `Geist` family.

That hand-copying is the same hazard as baking a hex where a swatch belongs, one
layer up: nothing ties the class to the constant it mirrors, so the two drift
silently. The fix is to have those classes read the constants, not to add more
unread ones.
Spacing, radius and line heights live in `stream_tokens_dimensions.dart` instead
— one mode-independent file, read by `StreamSpacing`, `StreamRadius` and
`StreamLineHeight`. Edit the value there and the classes follow; do not
re-introduce a literal in a class, which is the same hazard as baking a hex where
a swatch belongs, one layer up.

One upstream dimension is deliberately absent: **`radiusNone`**, since the
analyzer's `use_named_constants` prefers `Radius.zero` over `circular(0)`. The
font family is not carried either — this package never sets one for text, only
for the emoji and icon fonts. Weights are carried as `FontWeight` rather than the
raw 400/500/600/700, for the same reason the rest are `double`: it is the type
Flutter consumes, and `FontWeight` has no public constructor from a number.

**`melos run check:tokens` enforces all of this.** It fails when a constant in
`internal/tokens/` is never referenced, and when `light/` and `dark/` disagree
about which constants exist — the second because a field resolving from a constant
in one mode but not the other falls back silently rather than failing. So there is
no judgment call about what belongs: add a constant when a field or class will
read it, and CI tells you if you got it wrong.

Run it after any token edit, alongside `analyze`. Its allowlist is empty and worth
keeping that way; an entry there is a token the SDK carries without using.

## After editing

```bash
melos run analyze
melos run check:tokens
melos run test:flutter
```

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/stream_core_flutter_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ jobs:
run: |
melos run analyze

- name: Check Barrels
- name: Check barrels and tokens
run: |
melos run check:barrels
melos run check:tokens

- name: Check formatting
run: |
Expand Down
17 changes: 12 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,23 @@ Generated files have `.g.theme.dart` extension. After modifying `.theme.dart` fi
Colors originate in [design-system-tokens](https://github.com/GetStream/design-system-tokens),
the same repo the icons come from. `theme/primitives/internal/tokens/{light,dark}/stream_tokens.dart`
holds the vendored values; it is maintained by hand, is not part of the public API,
and only `stream_colors.dart` and `stream_color_scheme.dart` read it.
and only `stream_colors.dart` and `stream_color_scheme.dart` read it. Dimensions
live beside it in `internal/tokens/stream_tokens_dimensions.dart` — one
mode-independent copy, read by `StreamSpacing`, `StreamRadius`,
`StreamLineHeight` and `StreamFontWeight`. Font sizes are the one group that
differs per platform, so they mirror upstream's flavor split in
`internal/tokens/{android,ios}/stream_tokens_font_size.dart` and feed the two
`StreamFontSize` scales.

Only the **root semantics** are mapped to a `StreamColorScheme` field. Upstream's
derived tokens (`badge/*`, `button/*`, `avatar/*`) get no field — components
re-derive them from `colorScheme.*` in their own defaults. Typography, spacing and
radius do come from upstream, but `StreamTokensTypography`, `StreamSpacing` and
`StreamRadius` hard-code the values rather than reading a token constant, so a
dimension change is applied to those classes by hand. `StreamColorScheme` is
re-derive them from `colorScheme.*` in their own defaults. `StreamColorScheme` is
exported from `core.dart`, so every field on it is public API.

`melos run check:tokens` fails on a vendored constant nothing reads, and on
`light/` and `dark/` disagreeing about which constants exist — so add a constant
only when a field or class will read it, and let CI catch the rest.

A field's dartdoc comes from the token's own `$description` in the upstream JSON —
quote it rather than inventing prose, but resolve the aliases first, since a
description tracks the token's own light/dark progression and not a comparison
Expand Down
8 changes: 8 additions & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ scripts:
run: melos exec -c 1 --file-exists="check_barrels.yaml" -- "dart run \$MELOS_ROOT_PATH/scripts/check_barrels.dart"
description: Validate the public-barrel contract for packages with a check_barrels.yaml config.

check:tokens:
# Deliberately not `melos exec`: any package filter that stops matching
# selects zero packages and reports SUCCESS, which is the one outcome this
# check must never produce silently. Run once from the root instead, and
# let the script fail if the package or its token directory is missing.
run: dart run scripts/check_tokens.dart
description: Fail if a vendored design token is unreferenced, or if light/ and dark/ disagree.

generate:dart:
run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs"
description: Build all generated files for Dart packages in this project.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// Font-size tokens from the design system, android flavor.
///
/// The one dimension group that differs per platform: iOS runs a size up at
/// almost every step, so the token repo publishes a separate set. Read through
/// `StreamFontSize.android`, which is the public surface.
///
/// Declared as `double` because that is what `TextStyle` takes; the token repo
/// emits them without a decimal point.
class StreamTokensFontSize {
StreamTokensFontSize._();

static const double typographyFontSizeMicro = 8;
static const double typographyFontSizeXxs = 10;
static const double typographyFontSizeXs = 12;
static const double typographyFontSizeSm = 14;
static const double typographyFontSizeMd = 16;
static const double typographyFontSizeLg = 18;
static const double typographyFontSizeXl = 20;
static const double typographyFontSize2xl = 24;
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,120 +126,6 @@ class StreamTokens {
static const lime700 = Color(0xFF355315);
static const lime800 = Color(0xFF203A00);
static const lime900 = Color(0xFF112100);
static const size2 = 2;
static const size4 = 4;
static const size6 = 6;
static const size8 = 8;
static const size12 = 12;
static const size16 = 16;
static const size20 = 20;
static const size24 = 24;
static const size32 = 32;
static const size40 = 40;
static const size48 = 48;
static const size64 = 64;
static const size28 = 28;
static const size80 = 80;
static const size128 = 128;
static const size240 = 240;
static const size320 = 320;
static const size480 = 480;
static const size560 = 560;
static const size640 = 640;
static const size760 = 760;
static const size144 = 144;
static const size208 = 208;
static const size56 = 56;
static const radius0 = 0;
static const radius2 = 2;
static const radius4 = 4;
static const radius6 = 6;
static const radius8 = 8;
static const radius12 = 12;
static const radius16 = 16;
static const radius20 = 20;
static const radius24 = 24;
static const radius32 = 32;
static const radiusFull = 9999;
static const space0 = 0;
static const space2 = 2;
static const space4 = 4;
static const space8 = 8;
static const space12 = 12;
static const space16 = 16;
static const space20 = 20;
static const space24 = 24;
static const space32 = 32;
static const space40 = 40;
static const space48 = 48;
static const space64 = 64;
static const space80 = 80;
static const w100 = 1;
static const w150 = 1.5;
static const w200 = 2;
static const w300 = 3;
static const w400 = 4;
static const w120 = 1.2;
static const fontFamilyGeist = 'Geist';
static const fontFamilyGeistMono = 'Geist Mono';
static const fontFamilySfPro = 'SF Pro';
static const fontFamilySfMono = 'SF Mono';
static const fontFamilyRoboto = 'Roboto';
static const fontFamilyRobotoMono = 'Roboto Mono';
static const fontWeightW400 = 400;
static const fontWeightW500 = 500;
static const fontWeightW600 = 600;
static const fontWeightW700 = 700;
static const fontSizeSize8 = 8;
static const fontSizeSize10 = 10;
static const fontSizeSize11 = 11;
static const fontSizeSize12 = 12;
static const fontSizeSize13 = 13;
static const fontSizeSize14 = 14;
static const fontSizeSize16 = 16;
static const fontSizeSize15 = 15;
static const fontSizeSize17 = 17;
static const fontSizeSize18 = 18;
static const fontSizeSize20 = 20;
static const fontSizeSize22 = 22;
static const fontSizeSize24 = 24;
static const fontSizeSize28 = 28;
static const fontSizeSize32 = 32;
static const fontSizeSize40 = 40;
static const fontSizeSize48 = 48;
static const fontSizeSize64 = 64;
static const lineHeightLineHeight8 = 8;
static const lineHeightLineHeight10 = 10;
static const lineHeightLineHeight12 = 12;
static const lineHeightLineHeight13 = 13;
static const lineHeightLineHeight14 = 14;
static const lineHeightLineHeight15 = 15;
static const lineHeightLineHeight16 = 16;
static const lineHeightLineHeight17 = 17;
static const lineHeightLineHeight18 = 18;
static const lineHeightLineHeight20 = 20;
static const lineHeightLineHeight24 = 24;
static const lineHeightLineHeight28 = 28;
static const lineHeightLineHeight32 = 32;
static const lineHeightLineHeight40 = 40;
static const lineHeightLineHeight48 = 48;
static const typographyFontFamilySans = 'Geist';
static const typographyFontFamilyMono = 'Geist Mono';
static const typographyFontWeightRegular = 400;
static const typographyFontWeightMedium = 500;
static const typographyFontWeightSemiBold = 600;
static const typographyFontWeightBold = 700;
static const typographyFontSizeXxs = 10;
static const typographyFontSizeXs = 12;
static const typographyFontSizeSm = 14;
static const typographyFontSizeMd = 16;
static const typographyFontSizeLg = 18;
static const typographyFontSizeXl = 20;
static const typographyFontSize2xl = 24;
static const typographyFontSizeMicro = 8;
static const typographyLineHeightTight = 16;
static const typographyLineHeightNormal = 20;
static const typographyLineHeightRelaxed = 24;
static final lightElevation1 = [
const BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.05),
Expand Down Expand Up @@ -360,72 +246,6 @@ class StreamTokens {
offset: Offset(0, 20),
),
];
static const radiusNone = 0;
static const radiusXxs = 2;
static const radiusXs = 4;
static const radiusSm = 6;
static const radiusMd = 8;
static const radiusLg = 12;
static const radiusXl = 16;
static const radius2xl = 20;
static const radiusMax = 9999;
static const radius3xl = 24;
static const radius4xl = 32;
static const spacingNone = 0;
static const spacingXxs = 4;
static const spacingXs = 8;
static const spacingSm = 12;
static const spacingMd = 16;
static const spacingXl = 24;
static const spacing2xl = 32;
static const spacing3xl = 40;
static const spacingLg = 20;
static const spacingXxxs = 2;
static const deviceRadius = 8;
static const deviceSafeAreaBottom = 0;
static const deviceSafeAreaTop = 0;
static const messageBubbleRadiusGroupTop = 20;
static const messageBubbleRadiusGroupMiddle = 20;
static const messageBubbleRadiusGroupBottom = 20;
static const messageBubbleRadiusTail = 0;
static const messageBubbleRadiusAttachment = 12;
static const messageBubbleRadiusAttachmentInline = 8;
static const composerRadiusFixed = 24;
static const composerRadiusFloating = 24;
static const buttonRadiusLg = 9999;
static const buttonRadiusMd = 9999;
static const buttonRadiusSm = 9999;
static const buttonRadiusFull = 9999;
static const buttonVisualHeightSm = 32;
static const buttonVisualHeightMd = 40;
static const buttonVisualHeightLg = 48;
static const buttonVisualHeightXs = 24;
static const buttonHitTargetMinHeight = 48;
static const buttonHitTargetMinWidth = 48;
static const buttonPaddingYLg = 14;
static const buttonPaddingYMd = 10;
static const buttonPaddingYSm = 6;
static const buttonPaddingYXs = 4;
static const buttonPaddingXIconOnlyLg = 14;
static const buttonPaddingXIconOnlyMd = 10;
static const buttonPaddingXIconOnlySm = 6;
static const buttonPaddingXIconOnlyXs = 4;
static const buttonPaddingXWithLabelLg = 16;
static const buttonPaddingXWithLabelMd = 16;
static const buttonPaddingXWithLabelSm = 16;
static const buttonPaddingXWithLabelXs = 12;
static const iconSizeXs = 12;
static const iconSizeSm = 16;
static const iconSizeMd = 20;
static const iconSizeLg = 32;
static const iconStrokeSubtle = 1.2;
static const iconStrokeDefault = 1.5;
static const iconStrokeEmphasis = 2;
static const emojiSm = 16;
static const emojiMd = 24;
static const emojiLg = 32;
static const emojiXl = 48;
static const emoji2xl = 64;
static const backgroundCoreHighlight = Color(0xFF5F1A05);
static const backgroundCoreOverlayLight = Color(0xBF000000);
static const backgroundCoreOverlayDark = Color(0x80000000);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// Font-size tokens from the design system, ios flavor.
///
/// The one dimension group that differs per platform: iOS runs a size up at
/// almost every step, so the token repo publishes a separate set. Read through
/// `StreamFontSize.ios`, which is the public surface.
///
/// Declared as `double` because that is what `TextStyle` takes; the token repo
/// emits them without a decimal point.
class StreamTokensFontSize {
StreamTokensFontSize._();

static const double typographyFontSizeMicro = 8;
static const double typographyFontSizeXxs = 12;
static const double typographyFontSizeXs = 13;
static const double typographyFontSizeSm = 15;
static const double typographyFontSizeMd = 17;
static const double typographyFontSizeLg = 20;
static const double typographyFontSizeXl = 22;
static const double typographyFontSize2xl = 24;
}
Loading
Loading