From 08d9c895bf5af2bc6c58c9738c399df1ba060276 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 08:43:33 +0200 Subject: [PATCH 01/15] docs(ui): extract the icon workflow into an update-icons skill Icon work has enough load-bearing constraints that a prose section in CLAUDE.md could not carry them: upstream groups icons by component ownership rather than subject, so a name has to be searched across core, chat and video; glyph code points are append-only, so deleting an SVG silently repoints every icon after it; and the multicolour file-type assets follow a separate path with none of those rules. The skill covers finding an icon upstream, the size and naming conventions, RTL mirroring, retiring an icon through deprecated.txt and fix_data.yaml, and the file-type assets. CLAUDE.md and STYLE_GUIDE.md keep a short description of what to expect and point at it. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-icons/SKILL.md | 203 +++++++++++++++++++++++++++ CLAUDE.md | 86 ++---------- STYLE_GUIDE.md | 35 ++--- 3 files changed, 230 insertions(+), 94 deletions(-) create mode 100644 .claude/skills/update-icons/SKILL.md diff --git a/.claude/skills/update-icons/SKILL.md b/.claude/skills/update-icons/SKILL.md new file mode 100644 index 00000000..db0b339e --- /dev/null +++ b/.claude/skills/update-icons/SKILL.md @@ -0,0 +1,203 @@ +--- +name: update-icons +description: > + Add, update, retire or debug icons in stream_core_flutter — pulling SVGs from GetStream/design-system-tokens + into `assets_source/icons/`, regenerating the icon font and `StreamIcons` via `melos run generate:icons`, + handling RTL mirroring, deprecations and code points, and updating the multicolour file-type SVGs. Use whenever + an icon needs adding or replacing, whenever `melos run generate:icons` fails or warns, whenever an icon renders + as the wrong glyph or a box, whenever an icon should mirror in RTL, and whenever someone asks whether an icon + exists upstream. Also use before deleting any SVG from `assets_source/icons/` — a bare delete silently + repoints every icon after it. +allowed-tools: + - Bash + - Read + - Edit + - Write +--- + +# Updating icons + +Icons come from +[GetStream/design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons), +the same repo the colour tokens come from. `melos run generate:icons` turns +`assets_source/icons/` into a font (`lib/fonts/stream_icons_font.otf`) plus the +`StreamIcons` / `StreamIconData` classes. + +The single fact that governs everything here: **glyph code points are +append-only**. The font ships as a binary asset, so a shifted code point silently +repoints every icon after it in any app that has not rebuilt. That is why +deleting an SVG needs a deprecation entry, why names must be unique, and why +`assets_source/icon_log.g.txt` is never hand-edited. + +## Finding the icon upstream + +Upstream groups by *product*, then style, then size: + +``` +assets/icons/{core,chat,video}/flat/{12,16,20,32}/ +assets/icons/{core,chat,video}/line/ +assets/icons/chat/filetype/ +``` + +Products follow **component ownership, not subject matter**, so the product tells +you nothing about what an icon depicts: `camera` is a **chat** icon (the +composer's camera button) while `camera-flip-fill` is **video**. Search all three +before concluding an icon does not exist: + +```bash +# from a design-system-tokens checkout (normally a sibling of this repo) +find assets/icons -name "*camera*" -path "*/flat/*" +``` + +Only `flat/` (solid filled paths) goes into the font. `line/` is a stroke-based +outline set covering nearly the same names and is **deliberately unused** — the +two styles do not read as one set, so do not mix them in. `chat/filetype/` is +multicolour and takes a completely separate path (see the end of this file). + +## Copying it in + +This repo carries a single size-keyed tree, no product split: +`assets_source/icons/{16,20,32}/`. The generator emits one font and one +`StreamIcons` class, so there is nothing for a product segment to key off — which +also means a `core` name and a `video` name collide here exactly as two sizes +would. + +Naming on the way in: + +- **`20/` is the default** and holds essentially everything. Strip the upstream + size suffix: `core/flat/20/account-20.svg` → `20/account.svg`. +- **Off-default sizes need a name that cannot collide with their `20/` sibling.** + `32/` established `-large`: `chat/flat/32/camera-32.svg` → `32/camera-large.svg`. + `16/` has no convention yet — its single icon (`xmark-small`) was copied bare + back when nothing in `20/` shared the name. + +Copy in **only the sizes a design actually calls for**. Upstream ships everything +in all four sizes; do not bulk-copy a folder to "have it available" — every name +burns a permanent code point. + +**Names must be unique across sizes and across products.** The generator fails on +a duplicate rather than letting directory order pick a winner. This is a live +trap: upstream ships `xmark-small` in both `core/flat/16` and `core/flat/20` +**with different artwork**. We ship the 16px one. Adopting the 20px variant too +means giving it a distinct name and a new code point — never silently swapping +the artwork behind the existing name, which would repaint the glyph everywhere it +is already used. + +## RTL mirroring + +If the icon should mirror in RTL layouts, add its base name to `_rtlIcons` in +`scripts/generate_icons.dart` so the generator emits +`matchTextDirection: true`. + +This covers the obvious directional glyphs (arrows, chevrons, `reply`, `send`, +`sidebar`) but also icons whose *metaphor* is directional and reads wrong +unmirrored (`audio`, `megaphone`, `search`, `video`). Skip anything symmetric or +brand-owned (a bell, a heart, a logo). When unsure, look at what comparable icons +in the list already do. + +## Regenerating + +```bash +melos run generate:icons +``` + +Two expected messages, both harmless: + +- a warning about **mixed viewBox sizes** — that is the `16/` and `32/` folders + doing their job. +- nothing about re-centering: `stream_icons.yaml` sets `normalize: false` on + purpose. The default re-centers each glyph by its bounding box, which undoes + deliberate optical offsets (the `play-fill` triangle is nudged right inside its + viewBox so it reads as centered to the eye). + +Commit the SVG sources, the regenerated font, the Dart output **and** the updated +`icon_log.g.txt` together — they have to stay in sync. Never hand-edit the +generated `stream_icons.dart` / `stream_icons.g.dart`, the font, or the log. + +The log records the date each icon was first seen and the generator orders glyphs +by it, which is what preserves code points across runs. Reordering it reshuffles +the font. + +## Broken or odd-looking source SVGs + +Fix the artwork **upstream**, with a PR to design-system-tokens — never add a +repair step to the generator. The generator's job is to validate and fail loudly, +not to paper over source defects; a workaround here means every other platform +keeps consuming the broken file. + +## Retiring an icon + +A bare delete is what shifts code points, so deleting an SVG requires a line in +`assets_source/deprecated.txt`: + +``` +deprecated;replacement;included +more;more-horizontal;true +``` + +- **`replacement`** — the icon whose SVG draws the glyph. A deprecated icon always + keeps its glyph and therefore its code point; pointing at a replacement is what + lets you delete the retired SVG and still render something sensible. A + self-reference (`more;more;true`) keeps the original artwork while retiring just + the name. +- **`included`** — whether the name survives in the generated Dart. `true` emits + `StreamIcons.more` and `StreamIconData.more` annotated with + `@Deprecated('Use moreHorizontal instead.')`; `false` drops both while the glyph + stays in the font. + +Entries are effectively permanent — removing one releases its glyph and shifts +every later code point. The generator fails if a replacement has no SVG file, or +if a deprecated name has neither an SVG nor a logged code point (which means a +typo). + +Deprecating also means a `dart fix` migration in +`packages/stream_core_flutter/lib/fix_data.yaml`, plus the usual +`### 🛑 Breaking / Removals` CHANGELOG entry. Two things specific to icons: + +- `element.uris` must list the declaring library **and** every barrel it is + exported from (`core.dart`, `chat.dart`, `stream_core_flutter.dart`). A + transform whose uris miss the barrel the consumer actually imported never + fires. +- `StreamIcons.copyWith(more: ...)` needs its own transform with + `inMixin: "_$StreamIcons"`. `copyWith` is generated onto that private mixin, + which does not inherit the field's `@Deprecated`, so the call raises no warning + while the field exists — the transform only fires once the field is deleted and + the call becomes an `undefined_named_parameter` error. + +**Verify a transform by running it, not by reading the YAML.** Write a throwaway +file exercising each call shape (bare constant, instance field, constructor +argument, `copyWith`), run `dart fix --dry-run`, then re-run with the transform +removed to confirm the fix disappears — the analyzer offers generic "did you +mean" fixes that are easy to mistake for your own. + +## File-type icons + +`chat/filetype/` follows a completely separate path and **none of the rules above +apply** — no generator, no font, no code points. The SVGs are copied into +`packages/stream_core_flutter/assets/file_type/`, declared as assets in +`pubspec.yaml`, and resolved by path at runtime by `StreamFileTypeIcon`. Updating +them is copy-and-rename. + +Upstream names them by t-shirt size; this repo renames each to its **pixel +height**, because the widget interpolates that height straight into the asset path +(`assets/file_type/filetype-pdf-${size.height.toInt()}.svg`): + +| upstream | here | dimensions | +| --- | --- | --- | +| `-sm` | `-24` | 19×24 | +| `-md` | `-32` | 26×32 | +| `-lg` | `-40` | 32×40 | +| `-xl` | `-48` | 40×48 | + +Sizes are uniform across kinds, so `filetype-pdf-lg.svg` → +`filetype-pdf-40.svg`, `filetype-audio-lg.svg` → `filetype-audio-40.svg`. The +glyphs are portrait — the width is *not* what the name encodes. + +All nine kinds (`audio`, `code`, `compression`, `other`, `pdf`, `presentation`, +`spreadsheet`, `text`, `video`) must ship in all four sizes: a missing file is a +runtime asset failure, not a compile error, so keep the set complete. + +When diffing these against upstream, **expect every file to differ even when +nothing changed** — each Figma re-export bumps the `clip0_…` element ids and +jitters path coordinates in the 4th decimal. Compare the rendered artwork, not +the bytes, and do not re-copy all 36 files just to absorb that noise. diff --git a/CLAUDE.md b/CLAUDE.md index 798a3150..0f486f24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,78 +44,20 @@ melos run gen-l10n # regenerate localizations ### Icons -Source SVGs in `packages/stream_core_flutter/assets_source/icons/` come from the [design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons) repository. When adding or updating icons, pull the latest SVGs from that repo first, then run `melos run generate:icons` to regenerate the icon font and Dart classes. - -Upstream groups its icons by *product* first, then style, then size: - -``` -assets/icons/{core,chat,video}/flat/{12,16,20,32}/ -assets/icons/{core,chat,video}/line/ -assets/icons/chat/filetype/ -``` - -- **Products** — `core/` (~100 icons), `chat/` (~19) and `video/` (~30) mirror the token structure so each SDK ships only the icons it needs. Classification follows *component ownership*, not the Figma category, so a product does not tell you what an icon depicts and vice versa: `camera` is a **chat** icon (the composer's camera button) while `camera-flip-fill` is **video**. Search all three products before concluding an icon does not exist upstream. -- `flat/{12,16,20,32}/` — solid filled paths. **This is the only style that goes into the icon font.** -- `line/` — stroke-based outline variants of (almost) the same names, one unsized folder per product. Deliberately unused; do not mix them in, the two styles do not read as one set. -- `chat/filetype/` — multicolor file-type icons at four sizes. Shipped by the SDK, but **not through the icon font** — they are multicolor, so they could never be font glyphs. See [File-type icons](#file-type-icons) below. - -**This repo does not carry the product split.** `assets_source/icons/` is a single size-keyed tree fed from all three products at once (today 101 core, 23 chat, 28 video), because the generator emits one font and one `StreamIcons` class — there is nothing for a product segment to key off. So a `core` name and a `video` name collide here exactly as two sizes would (see below). Upstream has no cross-product duplicate today; if one ever appears, one of the two has to be renamed on the way in. - -Upstream ships every icon in all four `flat/` sizes. This repo does **not** mirror that: `20/` is the default and holds essentially everything, and a smaller or larger variant is copied in only when a design actually calls for one — today that is `16/xmark-small.svg` and seven `-large` icons in `32/`. Do not bulk-copy a size folder to "have it available"; every name you add burns a permanent code point (see below). - -Upstream names carry a size suffix that this repo strips or rewrites: everything in `20/` keeps its bare name (`core/flat/20/account-20.svg` → `20/account.svg`), while an off-default size must be given a name that cannot collide with its `20/` sibling. `32/` established `-large` for this (`chat/flat/32/camera-32.svg` → `32/camera-large.svg`). `16/` has no such convention yet — its single icon was copied over bare, back when no `20/` icon shared the name. - -**Names must be unique across the size folders — and across the three products.** Glyphs are keyed by bare filename, so copying a whole upstream size folder is how you accidentally end up with e.g. `16/xmark-small.svg` and `20/xmark-small.svg` competing for one glyph. The generator fails on a duplicate rather than letting directory-listing order pick a winner. - -This is a live trap, not a hypothetical: upstream now ships `xmark-small` in both `core/flat/16` and `core/flat/20`, with **different artwork**. We ship the 16px one as `16/xmark-small.svg`. Adopting the 20px variant too would mean giving it a distinct name and a new code point — never silently swapping the artwork behind the existing name, which would repaint the glyph everywhere it is already used. - -**Code points are append-only.** `assets_source/icon_log.g.txt` records the date each icon was first seen, and the generator orders glyphs by that date so every icon keeps its code point across runs. The font ships as `lib/fonts/stream_icons_font.otf`, so a shifted code point silently repoints every icon after it in any app that has not rebuilt. Never reorder or hand-edit the log. - -**Deleting an icon therefore requires a deprecation entry** in `assets_source/deprecated.txt` — one `deprecated;replacement;included` line per icon: - -``` -more;more-horizontal;true -``` - -- `replacement` — the icon whose SVG draws the glyph. A deprecated icon always keeps its glyph, and with it its code point; pointing at a replacement is what lets you delete the retired SVG and still render something sensible. Naming itself (`more;more;true`) keeps the original artwork while retiring the name. -- `included` — whether the name survives in the generated Dart. `true` emits `StreamIcons.more` and `StreamIconData.more` annotated with `@Deprecated('Use moreHorizontal instead.')`; `false` drops both while the glyph stays in the font. - -Entries are effectively permanent — removing one releases its glyph and shifts every later code point. The generator fails if a replacement has no SVG file, or if a deprecated name has neither an SVG file nor a logged code point (which means a typo). - -Deprecating an icon also means adding transforms to `lib/fix_data.yaml`; see [Deprecations](#deprecations). - -#### File-type icons - -`chat/filetype/` follows a completely separate path from everything above. The SVGs are -copied into `packages/stream_core_flutter/assets/file_type/` and shipped as runtime -assets (declared in `pubspec.yaml`), then resolved by path at runtime from -`StreamFileTypeIcon`. No generator, no font, no code points — so none of the -append-only rules above apply, and updating them is just copy-and-rename. - -Upstream names them by t-shirt size; this repo renames each to its **pixel height**, -because `StreamFileTypeIcon` interpolates that height straight into the asset path -(`assets/file_type/filetype-pdf-${size.height.toInt()}.svg`): - -| upstream | here | dimensions | -| --- | --- | --- | -| `-sm` | `-24` | 19×24 | -| `-md` | `-32` | 26×32 | -| `-lg` | `-40` | 32×40 | -| `-xl` | `-48` | 40×48 | - -The sizes are uniform across all kinds, so `filetype-pdf-lg.svg` → `filetype-pdf-40.svg`, -`filetype-audio-lg.svg` → `filetype-audio-40.svg`, and so on. Note the glyphs are -portrait, not square — the width is *not* what the name encodes. - -All nine kinds (`audio`, `code`, `compression`, `other`, `pdf`, `presentation`, -`spreadsheet`, `text`, `video`) ship in all four sizes; a missing file is a runtime -asset failure, not a compile error, so keep the set complete. - -When diffing these against upstream, expect **every file to differ even when nothing -changed**: each Figma re-export bumps the `clip0_…` element ids and jitters path -coordinates in the 4th decimal. Compare the rendered artwork, not the bytes, and do -not re-copy all 36 files just to absorb that noise. - +`StreamIcons` is generated: an icon font (`lib/fonts/stream_icons_font.otf`) plus +Dart constants, built by `melos run generate:icons` from the source SVGs in +`packages/stream_core_flutter/assets_source/icons/`. Those SVGs are copied from the +[design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons) +repo. Multicolour file-type icons are not part of the font — they ship as runtime +assets in `assets/file_type/` and are resolved by path by `StreamFileTypeIcon`. + +Glyph code points are append-only and recorded in `assets_source/icon_log.g.txt`, +so adding, renaming or retiring an icon has consequences beyond the file you touch. +Never hand-edit the generated Dart, the font, or the log. + +**Use the `update-icons` skill** for any icon work — it covers finding an icon +upstream, the naming and size conventions, RTL mirroring, deprecations, and the +file-type assets. ## Design diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 23d2f976..f617a37f 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1356,28 +1356,19 @@ shape. ### Icons -Source SVGs live in `packages/stream_core_flutter/assets_source/icons/`. They come -from the [design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons) -repository. - -When adding or updating icons: - -1. Pull the latest SVGs from `design-system-tokens/assets/icons/` into - `assets_source/icons/`. -2. If the icon should mirror in RTL layouts, add its base name to the - `_rtlIcons` list in `scripts/generate_icons.dart` so the generator emits - `matchTextDirection: true` for it. This covers obvious directional glyphs - (arrows, chevrons, `reply`, `send`, `sidebar`) but also icons with - directional metaphors that read wrong when unmirrored (`audio`, `megaphone`, - `search`, `video`). Skip icons that are symmetric or shouldn't mirror - (a bell, a heart, brand logos). If in doubt, look at what comparable icons - already do in `_rtlIcons`. -3. Run `melos run generate:icons` to regenerate the icon font and the - `StreamIcons` class. -4. Commit both the SVG sources and the regenerated font + Dart output together — - they must stay in sync. - -Do not edit the generated `StreamIcons.dart` or the icon font by hand. +Icons are generated, not hand-written: `melos run generate:icons` builds the icon +font and the `StreamIcons` / `StreamIconData` classes from the source SVGs in +`packages/stream_core_flutter/assets_source/icons/`, which are copied from the +[design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons) +repo. + +Do not edit the generated `stream_icons.dart`, the font, or +`assets_source/icon_log.g.txt` by hand. Commit SVG sources and regenerated output +together — they must stay in sync. + +Adding, renaming or retiring an icon affects more than the file you touch, because +glyph code points are append-only. **Use the `update-icons` skill** for any icon +work. ## Commits, PRs, and changelogs From 7f64acb3522bc8a7464d7ee2823b5c77c98ae1f8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 08:43:47 +0200 Subject: [PATCH 02/15] docs(ui): extract the token workflow into an update-design-tokens skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Syncing a colour token from design-system-tokens is not a copy: this package vendors only the root semantics, and the rules for what to vendor, what to name it and how a default should resolve were nowhere written down. The skill records those rules. A StreamColorScheme field drops the core/utility group segment. Its default resolves through the generated chrome/brand swatch when the token aliases one — a baked hex there silently ignores a custom brand seed — through another field when the token aliases another semantic, and through a vendored constant only for a raw hex. Upstream's derived tokens are deliberately absent, re-derived instead in each component's defaults. Two scripts carry the parts worth automating. flatten_tokens.py diffs the flattened semantics, because the generated output is key-sorted and a re-sort buries real value changes in hundreds of moved lines — which is how a live accent/warning change once reached review unnoticed. map_token_usage.py maps a field to the files that read it in any repo at any ref. It also covers tracing impact into the SDKs, since a namespace does not tell you which repo to open: a tokens/video change can be implemented only by a core component here, and chat components live in this repo outright. Derived tokens cannot be found that way at all, so references/derived-token-map.md holds those traces by hand. Finally it documents coordinating a change across repos through a git dependency override on a shared branch. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 320 ++++++++++++++++++ .../references/derived-token-map.md | 58 ++++ .../scripts/flatten_tokens.py | 101 ++++++ .../scripts/map_token_usage.py | 80 +++++ CLAUDE.md | 20 +- STYLE_GUIDE.md | 14 +- 6 files changed, 588 insertions(+), 5 deletions(-) create mode 100644 .claude/skills/update-design-tokens/SKILL.md create mode 100644 .claude/skills/update-design-tokens/references/derived-token-map.md create mode 100755 .claude/skills/update-design-tokens/scripts/flatten_tokens.py create mode 100755 .claude/skills/update-design-tokens/scripts/map_token_usage.py diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md new file mode 100644 index 00000000..efade408 --- /dev/null +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -0,0 +1,320 @@ +--- +name: update-design-tokens +description: > + Sync colour design tokens from the GetStream/design-system-tokens repo into stream_core_flutter, and assess what + an upstream token change means for this package. Use whenever a design-system-tokens PR or commit needs + reviewing ("what does this token PR do to us?", "assess the impact of this change"), whenever a colour value or + a new semantic token has to land in `theme/primitives/internal/tokens/`, whenever a new `StreamColorScheme` + field is being added or wired up, and whenever someone asks where a colour comes from or why a token resolves + the way it does. Covers tracing the blast radius into the consuming SDKs (stream-chat-flutter for chat tokens, + stream-video-flutter for video), where the derived values actually live. Also use before hard-coding any `Color(0x...)` in this package — the answer is almost always a + token or a colorScheme field instead. +allowed-tools: + - Bash + - Read + - Edit + - Write +--- + +# Updating design tokens + +Colours in `stream_core_flutter` originate in +[GetStream/design-system-tokens](https://github.com/GetStream/design-system-tokens) +— the same repo the icons come from. This skill covers reading a change upstream +and landing it here. + +Two things make this less mechanical than it sounds: **this package deliberately +vendors only a fraction of what upstream publishes**, and **the generated output +is sorted**, so a diff of it hides value changes in re-sort noise. Both are +covered below. + +## What lives where + +| | | +| --- | --- | +| 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` | +| 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_color_scheme.dart`, `stream_colors.dart` and +`stream_color_swatch_helper.dart` import them; no component theme or widget ever +references `StreamTokens`. + +Colours are byte-identical across the three upstream platform flavours (only +typography differs), so when you do read the upstream build, the flavour is +irrelevant — pick any. Typography, spacing and radius are **not** synced at all: +`stream_tokens_typography.dart` composes `TextStyle`s on a single `Geist` family, +and `stream_spacing.dart` / `stream_radius.dart` declare their own scales. + +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 +still be implemented by a core component here, and chat components live in this +repo outright. See [Downstream component defaults](#downstream-component-defaults). + +## Reading an upstream change + +Diff the **flattened source**, never the generated Dart/Kotlin/Swift. Upstream's +generator sorts keys, so adding one token re-sorts the file and a real value +change hides among hundreds of moved lines. This is not hypothetical — it is how +a live `accent/warning` change once reached review unnoticed. + +```bash +# from a checkout of design-system-tokens +git fetch origin +python3 /scripts/flatten_tokens.py --diff main FETCH_HEAD tokens/core/semantics/light.json +``` + +Run it for each namespace and mode the change touches (`core`/`chat` here, +`light` and `dark` both — they alias different primitives and can drift apart). +Output is `ADDED` / `REMOVED` / `CHANGED` per token path. Without `--diff` the +script just prints one revision as sorted `path = value` lines. + +Values stay as authored (`{yellow.200}`, not the resolved hex) so that an alias +change reads differently from a raw-value change. + +Then classify what you found: + +- **`CHANGED` on a token this package vendors or maps** — the real work. Trace it + to its `StreamColorScheme` field and to any component reading that field. +- **`ADDED`** — additive; adopt it only when a component actually needs it (see + below). +- **`REMOVED` / renamed** — check whether the old name appears here at all before + treating it as breaking, then check the SDK that owns the namespace, since a + renamed derived token is often implemented there rather than here. + +When assessing impact, distinguish a token **definition** from a **paint site**. +A grep for the field name will mostly hit the colour scheme and generated +`.g.theme.dart` plumbing; what matters is whether a widget renders with it: + +```bash +grep -rn "accentWarning" packages/stream_core_flutter/lib/src --include="*.dart" \ + | grep -v "theme/semantics/stream_color_scheme" +``` + +A change to a field nothing paints with is real API surface but no visual change +— say so plainly rather than reporting it as a regression. + +## Downstream component defaults + +A clean bill of health *here* does not mean no impact. Because this package +vendors only root semantics, upstream's derived tokens are implemented as +**component defaults in a consuming SDK** — so a derived-token change has no +counterpart in this repo at all and can only be assessed downstream. + +The namespace does **not** tell you which repo to open. It tells you which SDK to +check *in addition to* this one: + +| changed under | also check | why | +| --- | --- | --- | +| `tokens/core/**` | both SDKs | core is shared by everything | +| `tokens/chat/**` | stream-chat-flutter | but chat components live *here*, under `chat.dart`, so the work is usually in this repo | +| `tokens/video/**` | stream-video-flutter | video tokens can still land here — see below | + +A `tokens/video/**` change reaching this repo is not hypothetical: +`control/call-control-error-badge/*` is a video token whose only implementation is +`StreamErrorBadge`, a core component that video merely wraps. Never conclude "video +namespace, not our problem" from the path alone. + +Which ref to inspect: + +| repo | ref | +| --- | --- | +| stream-chat-flutter | `origin/master` | +| stream-video-flutter | `origin/v2` — the design-system work lives there, not on `main` | + +Both are normally checked out as siblings of this repo; locate them rather than +assuming a path, and ask if neither is present. Prefer a local checkout over +GitHub code search, which indexes only default branches and would miss video +entirely. + +### Finding the readers + +For a **root semantic** (anything with a `StreamColorScheme` field), the mapping is +derivable — don't keep notes on it, generate it: + +```bash +python3 /scripts/map_token_usage.py origin/v2 accentWarning +# accentWarning connection_quality_indicator_defaults, connection_quality_indicator_theme +``` + +Omit the field name for the whole scheme (~28 lines for video). The script matches +both `colorScheme.x` and the `_colorScheme.x` used inside `_Defaults` classes, skips +tests, and inspects a ref directly so nothing needs checking out. + +Two limits worth knowing, both by design: + +- It reports where a field is **read**, one indirection from the widget that renders + it — a component theme's defaults class shows up rather than the widget consuming + that theme. Follow the theme field on to the widget when the answer needs to name + a component. +- It cannot see **derived** tokens at all, because they have no field. Those are in + `references/derived-token-map.md`, hand-traced, with the caveat that it points at + repos which move independently — verify a row before acting on it. + +When you do trace a derived token by hand, add the row. That is the only mapping +worth recording: the greppable half goes stale the moment someone edits a widget, +while the non-greppable half is what nobody can reconstruct without repeating your +work. + +## Coordinating a change across repos + +A token change that needs work in both this package and an SDK cannot be validated +in one branch: chat and video depend on `stream_core_flutter` **from pub**, so an +edit here is invisible to them until it is released. Wire them together with a git +dependency override, in this order: + +1. Branch and push here first — the override resolves against the remote, so a + local commit is not enough. +2. Branch in the consumer under the **same name** (see below) and point its + `stream_core_flutter` at the core branch. +3. `melos bootstrap` in the consumer. Both halves are now buildable and reviewable + together. + +### Branch naming + +The override's `ref:` is the core branch, so its name is shared vocabulary across +repos rather than a private detail — matching names are what let someone find the +other half of a change. + +- **If core is already on a branch for this work, use that branch everywhere.** + Reuse it rather than cutting a second one, and give the consumer's branch the same + name. +- **Otherwise name the core branch `feat/update-tokens-{feature}`**, where + `{feature}` is the main thing that changed in the tokens — the subject of the + change, not a token path. The PR that added the on-elevation pair and restructured + the indicators would be `feat/update-tokens-on-elevation`. + +The override needs a `path:`, because this package is not at the repo root: + +```yaml +dependency_overrides: + stream_core_flutter: + git: + url: https://github.com/GetStream/stream-core-flutter.git + ref: feat/update-tokens-on-elevation + path: packages/stream_core_flutter +``` + +**Where that block goes differs per consumer**, and getting it wrong looks like the +override being silently ignored: + +| consumer | resolution | put `dependency_overrides` in | +| --- | --- | --- | +| stream-video-flutter | pub workspace (`resolution: workspace`) | the **root** `pubspec.yaml` — it already has an overrides block | +| stream-chat-flutter | melos, no pub workspace | the **consuming package's** pubspec, `packages/stream_chat_flutter/pubspec.yaml` | + +**The override is expected to merge — do not treat it as something to strip before +the consumer's PR lands.** This package is not released on every change, so gating +each consumer PR on a core release would stall them. The override lives on the +consumer's default branch and comes off only when that SDK is released: at that +point this package is released too, and the dependency goes back to a published +version constraint. + +Because the ref is a branch, the branch has to keep resolving for as long as the +override is merged — so the core branch outlives its own PR. Do not delete it on +merge, and if you rebase it, remember every consumer that points at it resolves the +new tip. + +Distinct from the sibling-path override this repo's CI guidance warns about — a git +ref is reproducible off-machine, where a `path:` to a sibling checkout is not. Do +not reach for a path override to make a cross-repo change build. + +## Naming + +Upstream's generator flattens `group/subgroup/name` into camelCase. The +`StreamColorScheme` field then **drops the `core` / `utility` group segment**, +because Flutter has no such layer: + +``` +upstream token vendored constant colorScheme field +border/utility/warning borderUtilityWarning borderWarning +background/core/on-accent backgroundCoreOnAccent backgroundOnAccent +``` + +Some fields also shorten further where the upstream suffix carried no meaning +here (`background/core/surface-default` → `backgroundSurface`). Match the +existing neighbours in `stream_color_scheme.dart` rather than deriving the name +mechanically. + +## Wiring a field default + +Look at what the token aliases upstream. The answer decides whether a vendored +constant is needed at all — and getting it wrong is how a custom brand colour +silently stops applying: + +- **`{chrome.*}` or `{brand.*}`** → resolve through the generated swatch: + `chrome.shade100`, `brand.shade500`, `chrome[0] ?? StreamColors.white`. + Never the baked hex. These scales are regenerated from a seed colour, so a + hard-coded value ignores `StreamColorScheme.light(brand: ...)`. +- **another semantic** → alias the field: `borderWarning ??= accentWarning`, + `textLink ??= accentPrimary`. +- **a raw hex, or a primitive outside those two scales** (a `yellow`, a + transparent black) → add a constant to both vendored files and read it: + `light_tokens.StreamTokens.backgroundCoreHighlight`. + +Only the third case earns a vendored constant. Add the same name to **both** +`light/` and `dark/`, keeping the file's existing ordering. + +## Root semantics only + +Upstream also publishes derived semantics — `badge/*`, `button/*`, `avatar/*`. +**Do not vendor or map those.** They are re-derived in Dart from the root +semantics, at the component's defaults: + +```dart +// lib/src/components/badge/stream_badge_notification.dart +Color get errorBackgroundColor => _colorScheme.accentError; +``` + +So upstream `badge/bg-error` has no counterpart here, by design — a component +theme reads `colorScheme.accentError` instead. This keeps the token surface small +and keeps every component overridable through one seedable colour scheme. + +The vendored files still carry a historical full dump of ~500 constants, of which +roughly a third are read. Treat the unread ones as dead weight: don't add more, +and don't take their presence as precedent. + +## After editing + +```bash +melos run analyze +melos run test:flutter +``` + +Regenerate only if you touched a `.theme.dart` annotation (adding a colour-scheme +field does): `melos run generate:flutter`. A new field also needs wiring into the +gallery's Theme Studio (`apps/design_system_gallery/lib/config/theme_configuration.dart` +and `widgets/theme_studio/theme_customization_panel.dart`) — follow the +surrounding fields. + +Goldens only move if a component actually paints with the changed colour. The +palette golden (`test/theme/goldens/ci/stream_theme_color_generation.png`) covers +seed-generated brand/chrome ladders, not semantic accents, so a semantic value +change usually leaves it alone. When goldens do drift, remember local `macos/` +images are review-only — committed `ci/` goldens come from the update-goldens +workflow. + +## Changelog + +`StreamColorScheme` is exported from `core.dart`, so **every one of its fields is +public API**. A token whose *value* changes is a visual change for anyone reading +the field instead of overriding it, and needs a `### 🔄 Changed` CHANGELOG entry +under `## Upcoming` even though no signature moved. Name the old and new resolved +values — that is what a consumer diffing screenshots needs. + +A field that is removed or renamed follows the deprecation policy in +`STYLE_GUIDE.md` (annotate, `### 🛑 Breaking / Removals`, and a `fix_data.yaml` +transform). + +## Contrast is not automatic + +Token aliases carry no contrast guarantee, and upstream can move a value across +the light/dark divide — a warning colour going from orange to a pale yellow flips +which text colour is legible on it. When adopting a changed fill, check what text +or icon token is painted on top of it, and say so if the pairing no longer works. +`accent/*` values have no `on-*` counterpart in the core namespace, so this has to +be reasoned about rather than looked up. diff --git a/.claude/skills/update-design-tokens/references/derived-token-map.md b/.claude/skills/update-design-tokens/references/derived-token-map.md new file mode 100644 index 00000000..816a7369 --- /dev/null +++ b/.claude/skills/update-design-tokens/references/derived-token-map.md @@ -0,0 +1,58 @@ +# Derived-token map + +Where upstream's **derived** chat and video semantics are actually implemented. + +This file exists because that mapping is not greppable. Derived tokens have no +`StreamColorScheme` field — the SDK inlines the value as a swatch or root-semantic +read inside a component-theme default, so nothing in the code carries the token's +name. `indicator/sound-indicator/speaking` is implemented as a `speakingColor` +defaulting to `colorScheme.brand.shade300`; no search for "speaking" or +"soundIndicator" reaches it. + +Root semantics need no entry here — they have a field, so +`scripts/map_token_usage.py` finds their readers in seconds. Only add a row when +the connection cost you a manual trace. + +**Verify a row before acting on it.** These point at other repos, which move +independently and will not update this file. Opening the named file to confirm is +cheap; trusting a stale row is not. If a row is wrong, fix it in the same change +that discovered the problem — and add rows as you trace new ones, so the next +person pays the cost once. + +## Video + +Reference ref: `origin/v2` in stream-video-flutter (the design-system branch). + +| upstream token | resolves to | implemented in | component | +| --- | --- | --- | --- | +| `indicator/connection-quality/poor` | `{accent.error}` | video · `indicators/connection_quality_indicator_defaults.dart` → `poorColor` | `StreamConnectionQualityIndicator` | +| `indicator/connection-quality/fair` | `{accent.warning}` | video · same file → `fairColor` | `StreamConnectionQualityIndicator` | +| `indicator/connection-quality/great` | `{accent.success}` | video · same file → `greatColor` | `StreamConnectionQualityIndicator` | +| `indicator/sound-indicator/speaking` | `{brand.400}` | video · `theme/components/participant_label_theme.dart` → `speakingColor` | `StreamAudioIndicator` | +| `control/call-control-error-badge/bg` | `{accent.warning}` | **core** · `components/badge/stream_error_badge.dart` → `colorScheme.accentError` | `StreamErrorBadge`, wrapped by video's `CallButtonBadge` | +| `control/call-control-error-badge/text` | `{base.black}` | **core** · same file → `colorScheme.textOnAccent` | `StreamErrorBadge` | +| `indicator/microphone-level/bar-active` | `{brand.400}` | not implemented — the lobby level meter is new | — | +| `indicator/microphone-level/bar-inactive` | `{chrome.200}` | not implemented | — | + +Two things this table is worth reading for: + +- **A video token can land in this repo.** The call-control error badge is a video + token whose only implementation is `StreamErrorBadge`, a core component. Video's + `CallButtonBadge` just wraps it. So "video namespace" never means "not our + problem" — it means check the video SDK *as well*. +- **`speakingColor` resolves `brand.shade300` while the token says `{brand.400}`.** + Whether that is a deliberate deviation or drift is unresolved; treat it as a + question to ask, not a bug to fix silently. + +## Chat + +Chat components live in this repo, under the `chat.dart` barrel, so a +`tokens/chat/**` change usually means work **here** rather than in +stream-chat-flutter — the reverse of the intuition the namespace suggests. +Chat semantics are also vendored into `internal/tokens/` with a `chat` prefix +(`chatReplyIndicatorIncoming`, `chatTextTypingIndicator`). + +Reference ref: `origin/master` in stream-chat-flutter. + +No manually-traced rows yet. Add them as they come up, in the same shape as the +video table. diff --git a/.claude/skills/update-design-tokens/scripts/flatten_tokens.py b/.claude/skills/update-design-tokens/scripts/flatten_tokens.py new file mode 100755 index 00000000..ec1311d2 --- /dev/null +++ b/.claude/skills/update-design-tokens/scripts/flatten_tokens.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Flatten a design-token semantics JSON into sorted `path = value` lines. + +The token repo's generated output is sorted by key, so any change that adds or +renames a token re-sorts the whole file and buries real value changes in +hundreds of lines of move noise. Diffing the flattened source instead makes an +added / removed / changed token obvious. + +Usage: + + # one revision + flatten_tokens.py tokens/core/semantics/light.json + + # compare two revisions of the same file (run from the token repo) + flatten_tokens.py --diff main tokens/on-elevation-and-indicators \\ + tokens/core/semantics/light.json + +The --diff form shells out to `git show :` for each ref, so it needs +a git checkout of the token repo but no network. Values are left as authored — +`{yellow.200}` stays an alias rather than being resolved — because an alias +change and a hex change want to be read differently. +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys + + +def flatten(node: dict, prefix: str = "") -> dict[str, str]: + """Collapse a nested token tree into {"group/name": "$value"}.""" + out: dict[str, str] = {} + for key, value in node.items(): + if not isinstance(value, dict): + continue + if "$value" in value: + out[prefix + key] = value["$value"] + else: + out.update(flatten(value, f"{prefix}{key}/")) + return out + + +def load(path: str, ref: str | None = None) -> dict[str, str]: + if ref is None: + with open(path) as handle: + return flatten(json.load(handle)) + result = subprocess.run( + ["git", "show", f"{ref}:{path}"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + sys.exit( + f"error: cannot read {path} at ref '{ref}'.\n" + f" git said: {result.stderr.strip()}\n" + f" A PR branch is often not in your local checkout yet — fetch it first:\n" + f" git fetch origin {ref}\n" + f" then pass the ref as 'FETCH_HEAD' or 'origin/{ref}'." + ) + return flatten(json.loads(result.stdout)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("path", help="path to a semantics JSON file") + parser.add_argument( + "--diff", + nargs=2, + metavar=("BASE_REF", "HEAD_REF"), + help="compare the file at two git refs instead of printing one", + ) + args = parser.parse_args() + + if not args.diff: + for key, value in sorted(load(args.path).items()): + print(f"{key} = {value}") + return 0 + + base_ref, head_ref = args.diff + base, head = load(args.path, base_ref), load(args.path, head_ref) + + removed = sorted(k for k in base if k not in head) + added = sorted(k for k in head if k not in base) + changed = sorted(k for k in base if k in head and base[k] != head[k]) + + print(f"{args.path}: {len(base)} -> {len(head)} tokens") + for key in removed: + print(f" REMOVED {key} = {base[key]}") + for key in added: + print(f" ADDED {key} = {head[key]}") + for key in changed: + print(f" CHANGED {key}: {base[key]} -> {head[key]}") + if not (removed or added or changed): + print(" (no semantic changes)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/update-design-tokens/scripts/map_token_usage.py b/.claude/skills/update-design-tokens/scripts/map_token_usage.py new file mode 100755 index 00000000..4d64f212 --- /dev/null +++ b/.claude/skills/update-design-tokens/scripts/map_token_usage.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Map `StreamColorScheme` fields to the files that read them, in any repo. + +Answers "who paints with this colour?" for a consuming SDK without reading it +into context file by file. Run it against a ref rather than a working tree, so a +design-system branch can be inspected without checking it out. + + map_token_usage.py # whole scheme + map_token_usage.py accentWarning # one field + +Both receiver spellings are matched (`colorScheme.x` and the `_colorScheme.x` +used inside `_Defaults` classes), and test files are skipped by default. + +Scope note: this finds where a field is *read*, which is one indirection away +from the widget that renders it — a component theme's defaults class will show +up rather than the widget consuming that theme. Follow the theme field to the +widget when the answer needs to name a component. + +It also only sees root semantics, the ones with a `colorScheme` field. Derived +chat/video tokens have no field at all — the SDK inlines them as swatch reads +(`colorScheme.brand.shade300`) inside component-theme defaults, which no token +name will match. Those live in `references/derived-token-map.md` instead. +""" + +from __future__ import annotations + +import argparse +import collections +import re +import subprocess +import sys + +PATTERN = r"_\?colorScheme\.[A-Za-z][A-Za-z0-9]*" +LINE_RE = re.compile(r"^(?P.*?):(?P.*?):(?P\d+):.*?_?colorScheme\.(?P[A-Za-z0-9]+)") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("repo", help="path to the consuming SDK checkout") + parser.add_argument("ref", help="git ref to inspect, e.g. origin/v2") + parser.add_argument("field", nargs="?", help="limit to one colorScheme field") + parser.add_argument("--include-tests", action="store_true") + args = parser.parse_args() + + pattern = rf"_\?colorScheme\.{args.field}\b" if args.field else PATTERN + result = subprocess.run( + ["git", "grep", "-Ion", pattern, args.ref, "--", "*.dart"], + cwd=args.repo, + capture_output=True, + text=True, + ) + # git grep exits 1 on "no matches", which is an answer, not an error. + if result.returncode not in (0, 1): + sys.exit(f"error: git grep failed in {args.repo}\n{result.stderr.strip()}") + + usage: dict[str, set[str]] = collections.defaultdict(set) + for line in result.stdout.splitlines(): + match = LINE_RE.match(line) + if not match: + continue + path = match.group("path") + if not args.include_tests and ("/test/" in path or path.endswith("_test.dart")): + continue + usage[match.group("field")].add(path.split("/")[-1].removesuffix(".dart")) + + if not usage: + target = f"field '{args.field}'" if args.field else "any colorScheme field" + print(f"no reads of {target} in {args.repo} at {args.ref}") + print("If this is a repo whose design-system work lives on a branch, check that") + print("branch — the default branch may not depend on stream_core_flutter at all.") + return 0 + + width = max(len(f) for f in usage) + 2 + for field in sorted(usage): + print(f"{field:<{width}} {', '.join(sorted(usage[field]))}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/CLAUDE.md b/CLAUDE.md index 0f486f24..de6f0e7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,10 +105,28 @@ Uses `theme_extensions_builder` to generate Material 3 theme extensions. The hie 1. **Primitives** — raw design tokens: colors, typography, spacing, radius, icons 2. **Semantics** — semantic mappings (e.g., `primaryColor`, `bodyText`) 3. **Component themes** — per-widget theme classes (50+ components), defined in `theme/components/` -4. **Tokens** — light/dark concrete values in `theme/primitives/internal/tokens/` (figma-generated, not part of the public API) +4. **Tokens** — light/dark concrete values in `theme/primitives/internal/tokens/`, copied by hand from the design-token repo and not part of the public API (see [Design tokens](#design-tokens)) Generated files have `.g.theme.dart` extension. After modifying `.theme.dart` files, run `melos run generate:flutter`. +### Design tokens + +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_color_scheme.dart`, `stream_colors.dart` and +`stream_color_swatch_helper.dart` read it. + +Only the **root semantics** are vendored. Upstream's derived tokens (`badge/*`, +`button/*`, `avatar/*`) have no counterpart here by design — components re-derive +them from `colorScheme.*` in their own defaults. Typography, spacing and radius do +not come from upstream at all. `StreamColorScheme` is exported from `core.dart`, so +every field on it is public API. + +**Use the `update-design-tokens` skill** when syncing a token change or assessing +an upstream PR — it covers the naming rules, how a field default should resolve, +and how to read a change without drowning in the generator's re-sort noise. + ### Component Structure (`stream_core_flutter/lib/src/components/`) Components are organized by category: `avatar/`, `buttons/`, `badge/`, `list/`, `message_composer/`, `emoji/`, `context_menu/`, `controls/`, `common/`, `accessories/`. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index f617a37f..cedf68fa 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1251,8 +1251,12 @@ Themes are generated via `theme_extensions_builder`. **Never hand-roll `copyWith The hierarchy is layered: **primitives** (`theme/primitives/`, raw tokens) → **semantics** (`theme/semantics/`, semantic mappings) → **component themes** -(`theme/components/`, per-widget classes, 50+) → **tokens** (figma-generated, -internal). +(`theme/components/`, per-widget classes, 50+) → **tokens** (vendored from the +design-token repo, internal). + +Only root semantic tokens are vendored; a component's derived values are resolved +from `colorScheme.*` in its own defaults, never from a token constant. Use the +`update-design-tokens` skill when syncing a token change. Adding a new component theme: @@ -1513,8 +1517,10 @@ only the ones this PR bumped. So: - **Melos commands**: `melos.yaml` — every task the repo runs. - **Design source**: the Chat SDK Design System Figma project — accessed via the Figma MCP when implementing UI. -- **Design tokens**: the [design-system-tokens](https://github.com/GetStream/design-system-tokens) - sibling repo (mirrored internally in the theme primitives). +- **Design tokens and icons**: the [design-system-tokens](https://github.com/GetStream/design-system-tokens) + sibling repo (colours mirrored into the theme primitives, SVGs into + `assets_source/icons/`). The `update-design-tokens` and `update-icons` skills + cover syncing from it. When something isn't covered here and isn't obvious from surrounding code, prefer to ask in the PR rather than guessing. If a convention isn't documented, propose From 2762ea870b3f38bdfb33280230d0709540bde35a Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 12:47:09 +0200 Subject: [PATCH 03/15] feat(ui): sync colour tokens from design-system-tokens 4ef9b54 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds StreamColorScheme.backgroundOnElevation and borderOnElevation for the new background/core/on-elevation and border/core/on-elevation tokens. Both alias the chrome scale, so the defaults resolve through the generated swatch rather than a vendored constant — a baked hex there would ignore a custom chrome seed. They step up in dark mode, where an elevated surface has already lightened and the plain surface and border values lose their edge against it. Also moves accent/warning from orange to yellow, which upstream changed in the same commit without mentioning it in the PR description: #F26D10 to #F6BF57 in light, #FA922B to #FCD579 in dark. borderWarning defaults to accentWarning so it moves too. The visible effect is StreamErrorBadge's warning style, whose icon is already pinned to black — the new value only carries dark foregrounds, at roughly 1.7:1 against white. The ci/ goldens for that badge need regenerating through the update-goldens workflow; the 56 failing macos goldens here are pre-existing local drift, unchanged by this commit. Co-Authored-By: Claude Opus 5 --- .../lib/app/gallery_app.directories.g.dart | 1 + .../lib/config/theme_configuration.dart | 14 ++++++ .../theme_customization_panel.dart | 14 ++++++ packages/stream_core_flutter/CHANGELOG.md | 16 +++++++ .../stream_checkbox_theme.g.theme.dart | 2 +- .../stream_context_menu_theme.g.theme.dart | 16 ++++--- .../stream_emoji_button_theme.g.theme.dart | 2 +- .../stream_emoji_chip_theme.g.theme.dart | 2 +- ...m_jump_to_unread_button_theme.g.theme.dart | 16 ++++--- ...age_composer_attachment_theme.g.theme.dart | 16 ++++--- ...edit_message_attachment_theme.g.theme.dart | 16 ++++--- ...link_preview_attachment_theme.g.theme.dart | 16 ++++--- ...mposer_reply_attachment_theme.g.theme.dart | 16 ++++--- .../stream_reaction_picker_theme.g.theme.dart | 16 ++++--- .../stream_snackbar_theme.g.theme.dart | 16 ++++--- .../stream_text_input_theme.g.theme.dart | 48 +++++++++++-------- .../internal/tokens/dark/stream_tokens.dart | 4 +- .../internal/tokens/light/stream_tokens.dart | 4 +- .../theme/semantics/stream_color_scheme.dart | 24 ++++++++++ .../stream_color_scheme.g.theme.dart | 21 ++++++++ .../lib/src/theme/stream_theme.g.theme.dart | 5 +- 21 files changed, 198 insertions(+), 87 deletions(-) diff --git a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart index bc8f4899..f3f9335c 100644 --- a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart +++ b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart @@ -10,6 +10,7 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes + import 'package:design_system_gallery/components/accessories/stream_audio_waveform.dart' as _design_system_gallery_components_accessories_stream_audio_waveform; import 'package:design_system_gallery/components/accessories/stream_emoji.dart' diff --git a/apps/design_system_gallery/lib/config/theme_configuration.dart b/apps/design_system_gallery/lib/config/theme_configuration.dart index 3e162514..cb4bd26d 100644 --- a/apps/design_system_gallery/lib/config/theme_configuration.dart +++ b/apps/design_system_gallery/lib/config/theme_configuration.dart @@ -52,6 +52,7 @@ class ThemeConfiguration extends ChangeNotifier { Color? _backgroundSurfaceStrong; Color? _backgroundSurfaceCard; Color? _backgroundOnAccent; + Color? _backgroundOnElevation; Color? _backgroundHighlight; Color? _backgroundScrim; Color? _backgroundOverlayLight; @@ -74,6 +75,7 @@ class ThemeConfiguration extends ChangeNotifier { Color? _borderStrong; Color? _borderOnAccent; Color? _borderOnSurface; + Color? _borderOnElevation; Color? _borderOpacitySubtle; Color? _borderOpacityStrong; @@ -139,6 +141,7 @@ class ThemeConfiguration extends ChangeNotifier { Color get backgroundSurfaceStrong => _backgroundSurfaceStrong ?? _themeData.colorScheme.backgroundSurfaceStrong; Color get backgroundSurfaceCard => _backgroundSurfaceCard ?? _themeData.colorScheme.backgroundSurfaceCard; Color get backgroundOnAccent => _backgroundOnAccent ?? _themeData.colorScheme.backgroundOnAccent; + Color get backgroundOnElevation => _backgroundOnElevation ?? _themeData.colorScheme.backgroundOnElevation; Color get backgroundHighlight => _backgroundHighlight ?? _themeData.colorScheme.backgroundHighlight; Color get backgroundScrim => _backgroundScrim ?? _themeData.colorScheme.backgroundScrim; Color get backgroundOverlayLight => _backgroundOverlayLight ?? _themeData.colorScheme.backgroundOverlayLight; @@ -161,6 +164,7 @@ class ThemeConfiguration extends ChangeNotifier { Color get borderStrong => _borderStrong ?? _themeData.colorScheme.borderStrong; Color get borderOnAccent => _borderOnAccent ?? _themeData.colorScheme.borderOnAccent; Color get borderOnSurface => _borderOnSurface ?? _themeData.colorScheme.borderOnSurface; + Color get borderOnElevation => _borderOnElevation ?? _themeData.colorScheme.borderOnElevation; Color get borderOpacitySubtle => _borderOpacitySubtle ?? _themeData.colorScheme.borderOpacitySubtle; Color get borderOpacityStrong => _borderOpacityStrong ?? _themeData.colorScheme.borderOpacityStrong; @@ -231,6 +235,7 @@ class ThemeConfiguration extends ChangeNotifier { void setBackgroundSurfaceStrong(Color color) => _update(() => _backgroundSurfaceStrong = color); void setBackgroundSurfaceCard(Color color) => _update(() => _backgroundSurfaceCard = color); void setBackgroundOnAccent(Color color) => _update(() => _backgroundOnAccent = color); + void setBackgroundOnElevation(Color color) => _update(() => _backgroundOnElevation = color); void setBackgroundHighlight(Color color) => _update(() => _backgroundHighlight = color); void setBackgroundScrim(Color color) => _update(() => _backgroundScrim = color); void setBackgroundOverlayLight(Color color) => _update(() => _backgroundOverlayLight = color); @@ -251,6 +256,7 @@ class ThemeConfiguration extends ChangeNotifier { void setBorderStrong(Color color) => _update(() => _borderStrong = color); void setBorderOnAccent(Color color) => _update(() => _borderOnAccent = color); void setBorderOnSurface(Color color) => _update(() => _borderOnSurface = color); + void setBorderOnElevation(Color color) => _update(() => _borderOnElevation = color); void setBorderOpacitySubtle(Color color) => _update(() => _borderOpacitySubtle = color); void setBorderOpacityStrong(Color color) => _update(() => _borderOpacityStrong = color); @@ -331,6 +337,7 @@ class ThemeConfiguration extends ChangeNotifier { bool get backgroundSurfaceStrongIsCustom => _backgroundSurfaceStrong != null; bool get backgroundSurfaceCardIsCustom => _backgroundSurfaceCard != null; bool get backgroundOnAccentIsCustom => _backgroundOnAccent != null; + bool get backgroundOnElevationIsCustom => _backgroundOnElevation != null; bool get backgroundHighlightIsCustom => _backgroundHighlight != null; bool get backgroundScrimIsCustom => _backgroundScrim != null; bool get backgroundOverlayLightIsCustom => _backgroundOverlayLight != null; @@ -351,6 +358,7 @@ class ThemeConfiguration extends ChangeNotifier { bool get borderStrongIsCustom => _borderStrong != null; bool get borderOnAccentIsCustom => _borderOnAccent != null; bool get borderOnSurfaceIsCustom => _borderOnSurface != null; + bool get borderOnElevationIsCustom => _borderOnElevation != null; bool get borderOpacitySubtleIsCustom => _borderOpacitySubtle != null; bool get borderOpacityStrongIsCustom => _borderOpacityStrong != null; @@ -402,6 +410,7 @@ class ThemeConfiguration extends ChangeNotifier { void resetBackgroundSurfaceStrong() => _update(() => _backgroundSurfaceStrong = null); void resetBackgroundSurfaceCard() => _update(() => _backgroundSurfaceCard = null); void resetBackgroundOnAccent() => _update(() => _backgroundOnAccent = null); + void resetBackgroundOnElevation() => _update(() => _backgroundOnElevation = null); void resetBackgroundHighlight() => _update(() => _backgroundHighlight = null); void resetBackgroundScrim() => _update(() => _backgroundScrim = null); void resetBackgroundOverlayLight() => _update(() => _backgroundOverlayLight = null); @@ -422,6 +431,7 @@ class ThemeConfiguration extends ChangeNotifier { void resetBorderStrong() => _update(() => _borderStrong = null); void resetBorderOnAccent() => _update(() => _borderOnAccent = null); void resetBorderOnSurface() => _update(() => _borderOnSurface = null); + void resetBorderOnElevation() => _update(() => _borderOnElevation = null); void resetBorderOpacitySubtle() => _update(() => _borderOpacitySubtle = null); void resetBorderOpacityStrong() => _update(() => _borderOpacityStrong = null); @@ -475,6 +485,7 @@ class ThemeConfiguration extends ChangeNotifier { _backgroundSurfaceStrong = null; _backgroundSurfaceCard = null; _backgroundOnAccent = null; + _backgroundOnElevation = null; _backgroundHighlight = null; _backgroundScrim = null; _backgroundOverlayLight = null; @@ -494,6 +505,7 @@ class ThemeConfiguration extends ChangeNotifier { _borderStrong = null; _borderOnAccent = null; _borderOnSurface = null; + _borderOnElevation = null; _borderOpacitySubtle = null; _borderOpacityStrong = null; // Border Utility @@ -563,6 +575,7 @@ class ThemeConfiguration extends ChangeNotifier { backgroundSurfaceStrong: _backgroundSurfaceStrong, backgroundSurfaceCard: _backgroundSurfaceCard, backgroundOnAccent: _backgroundOnAccent, + backgroundOnElevation: _backgroundOnElevation, backgroundHighlight: _backgroundHighlight, backgroundScrim: _backgroundScrim, backgroundOverlayLight: _backgroundOverlayLight, @@ -582,6 +595,7 @@ class ThemeConfiguration extends ChangeNotifier { borderStrong: _borderStrong, borderOnAccent: _borderOnAccent, borderOnSurface: _borderOnSurface, + borderOnElevation: _borderOnElevation, borderOpacitySubtle: _borderOpacitySubtle, borderOpacityStrong: _borderOpacityStrong, // Border Utility diff --git a/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart b/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart index 3ce90aa8..b1a5d947 100644 --- a/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart +++ b/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart @@ -376,6 +376,13 @@ class _ThemeCustomizationPanelState extends State { onColorChanged: config.setBackgroundOnAccent, onReset: config.resetBackgroundOnAccent, ), + ColorPickerTile( + label: 'backgroundOnElevation', + color: config.backgroundOnElevation, + isDefault: !config.backgroundOnElevationIsCustom, + onColorChanged: config.setBackgroundOnElevation, + onReset: config.resetBackgroundOnElevation, + ), ColorPickerTile( label: 'backgroundHighlight', color: config.backgroundHighlight, @@ -552,6 +559,13 @@ class _ThemeCustomizationPanelState extends State { onColorChanged: config.setBorderOnSurface, onReset: config.resetBorderOnSurface, ), + ColorPickerTile( + label: 'borderOnElevation', + color: config.borderOnElevation, + isDefault: !config.borderOnElevationIsCustom, + onColorChanged: config.setBorderOnElevation, + onReset: config.resetBorderOnElevation, + ), ColorPickerTile( label: 'borderOpacitySubtle', color: config.borderOpacitySubtle, diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 082c92e7..f0b9a4f0 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -7,6 +7,22 @@ (the default) or `.warning` — and `StreamErrorBadge.showBorder`. - Added `StreamErrorBadgeTheme` and `StreamErrorBadgeThemeData`, carrying a background and foreground color per style. +- Added `StreamColorScheme.backgroundOnElevation` and `borderOnElevation`, the + design system's `background/core/on-elevation` and `border/core/on-elevation` + tokens. For controls sitting inside a floating surface — a menu, dialog or + popover — where the plain surface and border values lose their edge in dark + mode, because the elevated surface has already lightened. + +### 🔄 Changed + +- `StreamColorScheme.accentWarning` moved from orange to yellow, following the + design system's `accent/warning` token now resolving to `yellow.200` in light + and `yellow.150` in dark: `#F26D10` → `#F6BF57` (light) and `#FA922B` → + `#FCD579` (dark). `borderWarning` defaults to `accentWarning`, so it moves with + it. The visible effect is `StreamErrorBadge` with + `StreamErrorBadgeStyle.warning`, whose icon is already pinned to black; anything + overriding either field is unaffected. The new value is legible under dark text + only — white on `#F6BF57` is roughly 1.7:1 — so pair it with a dark foreground. ## 0.5.1 diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_checkbox_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_checkbox_theme.g.theme.dart index fe589ecb..8566c116 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_checkbox_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_checkbox_theme.g.theme.dart @@ -120,7 +120,7 @@ mixin _$StreamCheckboxStyle { Color.lerp, ), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: WidgetStateBorderSide.lerp(a.side, b.side, t), + side: t < 0.5 ? a.side : b.side, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_context_menu_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_context_menu_theme.g.theme.dart index a0f552c6..6785adb8 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_context_menu_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_context_menu_theme.g.theme.dart @@ -102,10 +102,10 @@ mixin _$StreamContextMenuStyle { backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), elevation: lerpDouble$(a.elevation, b.elevation, t), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: a.side == null - ? b.side - : b.side == null - ? a.side + side: a.side == null || b.side == null + ? t < 0.5 + ? a.side + : b.side : BorderSide.lerp(a.side!, b.side!, t), padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t), ); @@ -144,9 +144,11 @@ mixin _$StreamContextMenuStyle { backgroundColor: other.backgroundColor, elevation: other.elevation, shape: other.shape, - side: _this.side != null && other.side != null - ? BorderSide.merge(_this.side!, other.side!) - : other.side, + side: _this.side == null + ? other.side + : other.side == null + ? _this.side + : BorderSide.merge(_this.side!, other.side!), padding: other.padding, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_button_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_button_theme.g.theme.dart index aaa19780..e25d48f3 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_button_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_button_theme.g.theme.dart @@ -118,7 +118,7 @@ mixin _$StreamEmojiButtonThemeStyle { t, Color.lerp, ), - side: WidgetStateBorderSide.lerp(a.side, b.side, t), + side: t < 0.5 ? a.side : b.side, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_chip_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_chip_theme.g.theme.dart index 8bf65a2e..0285a374 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_chip_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_emoji_chip_theme.g.theme.dart @@ -140,7 +140,7 @@ mixin _$StreamEmojiChipThemeStyle { maximumSize: Size.lerp(a.maximumSize, b.maximumSize, t), padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: WidgetStateBorderSide.lerp(a.side, b.side, t), + side: t < 0.5 ? a.side : b.side, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_jump_to_unread_button_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_jump_to_unread_button_theme.g.theme.dart index 9f11f7bd..7ce106c7 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_jump_to_unread_button_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_jump_to_unread_button_theme.g.theme.dart @@ -32,10 +32,10 @@ mixin _$StreamJumpToUnreadButtonThemeData { return StreamJumpToUnreadButtonThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: a.side == null - ? b.side - : b.side == null - ? a.side + side: a.side == null || b.side == null + ? t < 0.5 + ? a.side + : b.side : BorderSide.lerp(a.side!, b.side!, t), elevation: lerpDouble$(a.elevation, b.elevation, t), shadowColor: Color.lerp(a.shadowColor, b.shadowColor, t), @@ -93,9 +93,11 @@ mixin _$StreamJumpToUnreadButtonThemeData { return copyWith( backgroundColor: other.backgroundColor, shape: other.shape, - side: _this.side != null && other.side != null - ? BorderSide.merge(_this.side!, other.side!) - : other.side, + side: _this.side == null + ? other.side + : other.side == null + ? _this.side + : BorderSide.merge(_this.side!, other.side!), elevation: other.elevation, shadowColor: other.shadowColor, padding: other.padding, diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_attachment_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_attachment_theme.g.theme.dart index 8156bfe1..bc4ab343 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_attachment_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_attachment_theme.g.theme.dart @@ -32,10 +32,10 @@ mixin _$StreamMessageComposerAttachmentThemeData { return StreamMessageComposerAttachmentThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: a.side == null - ? b.side - : b.side == null - ? a.side + side: a.side == null || b.side == null + ? t < 0.5 + ? a.side + : b.side : BorderSide.lerp(a.side!, b.side!, t), padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t), ); @@ -73,9 +73,11 @@ mixin _$StreamMessageComposerAttachmentThemeData { return copyWith( backgroundColor: other.backgroundColor, shape: other.shape, - side: _this.side != null && other.side != null - ? BorderSide.merge(_this.side!, other.side!) - : other.side, + side: _this.side == null + ? other.side + : other.side == null + ? _this.side + : BorderSide.merge(_this.side!, other.side!), padding: other.padding, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_edit_message_attachment_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_edit_message_attachment_theme.g.theme.dart index e687a716..39811090 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_edit_message_attachment_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_edit_message_attachment_theme.g.theme.dart @@ -44,10 +44,10 @@ mixin _$StreamMessageComposerEditMessageAttachmentThemeData { b.thumbnailShape, t, ), - thumbnailSide: a.thumbnailSide == null - ? b.thumbnailSide - : b.thumbnailSide == null - ? a.thumbnailSide + thumbnailSide: a.thumbnailSide == null || b.thumbnailSide == null + ? t < 0.5 + ? a.thumbnailSide + : b.thumbnailSide : BorderSide.lerp(a.thumbnailSide!, b.thumbnailSide!, t), thumbnailSize: Size.lerp(a.thumbnailSize, b.thumbnailSize, t), ); @@ -101,9 +101,11 @@ mixin _$StreamMessageComposerEditMessageAttachmentThemeData { other.subtitleTextStyle, padding: other.padding, thumbnailShape: other.thumbnailShape, - thumbnailSide: _this.thumbnailSide != null && other.thumbnailSide != null - ? BorderSide.merge(_this.thumbnailSide!, other.thumbnailSide!) - : other.thumbnailSide, + thumbnailSide: _this.thumbnailSide == null + ? other.thumbnailSide + : other.thumbnailSide == null + ? _this.thumbnailSide + : BorderSide.merge(_this.thumbnailSide!, other.thumbnailSide!), thumbnailSize: other.thumbnailSize, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_link_preview_attachment_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_link_preview_attachment_theme.g.theme.dart index 03f3bf25..d699e331 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_link_preview_attachment_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_link_preview_attachment_theme.g.theme.dart @@ -43,10 +43,10 @@ mixin _$StreamMessageComposerLinkPreviewAttachmentThemeData { b.thumbnailShape, t, ), - thumbnailSide: a.thumbnailSide == null - ? b.thumbnailSide - : b.thumbnailSide == null - ? a.thumbnailSide + thumbnailSide: a.thumbnailSide == null || b.thumbnailSide == null + ? t < 0.5 + ? a.thumbnailSide + : b.thumbnailSide : BorderSide.lerp(a.thumbnailSide!, b.thumbnailSide!, t), thumbnailSize: Size.lerp(a.thumbnailSize, b.thumbnailSize, t), ); @@ -97,9 +97,11 @@ mixin _$StreamMessageComposerLinkPreviewAttachmentThemeData { other.subtitleTextStyle, padding: other.padding, thumbnailShape: other.thumbnailShape, - thumbnailSide: _this.thumbnailSide != null && other.thumbnailSide != null - ? BorderSide.merge(_this.thumbnailSide!, other.thumbnailSide!) - : other.thumbnailSide, + thumbnailSide: _this.thumbnailSide == null + ? other.thumbnailSide + : other.thumbnailSide == null + ? _this.thumbnailSide + : BorderSide.merge(_this.thumbnailSide!, other.thumbnailSide!), thumbnailSize: other.thumbnailSize, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_reply_attachment_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_reply_attachment_theme.g.theme.dart index 0b048cd5..e376f2d9 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_reply_attachment_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_message_composer_reply_attachment_theme.g.theme.dart @@ -44,10 +44,10 @@ mixin _$StreamMessageComposerReplyAttachmentThemeData { b.thumbnailShape, t, ), - thumbnailSide: a.thumbnailSide == null - ? b.thumbnailSide - : b.thumbnailSide == null - ? a.thumbnailSide + thumbnailSide: a.thumbnailSide == null || b.thumbnailSide == null + ? t < 0.5 + ? a.thumbnailSide + : b.thumbnailSide : BorderSide.lerp(a.thumbnailSide!, b.thumbnailSide!, t), thumbnailSize: Size.lerp(a.thumbnailSize, b.thumbnailSize, t), ); @@ -101,9 +101,11 @@ mixin _$StreamMessageComposerReplyAttachmentThemeData { other.subtitleTextStyle, padding: other.padding, thumbnailShape: other.thumbnailShape, - thumbnailSide: _this.thumbnailSide != null && other.thumbnailSide != null - ? BorderSide.merge(_this.thumbnailSide!, other.thumbnailSide!) - : other.thumbnailSide, + thumbnailSide: _this.thumbnailSide == null + ? other.thumbnailSide + : other.thumbnailSide == null + ? _this.thumbnailSide + : BorderSide.merge(_this.thumbnailSide!, other.thumbnailSide!), thumbnailSize: other.thumbnailSize, ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_reaction_picker_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_reaction_picker_theme.g.theme.dart index 1deeceb0..7f9307e8 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_reaction_picker_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_reaction_picker_theme.g.theme.dart @@ -35,10 +35,10 @@ mixin _$StreamReactionPickerThemeData { elevation: lerpDouble$(a.elevation, b.elevation, t), spacing: lerpDouble$(a.spacing, b.spacing, t), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: a.side == null - ? b.side - : b.side == null - ? a.side + side: a.side == null || b.side == null + ? t < 0.5 + ? a.side + : b.side : BorderSide.lerp(a.side!, b.side!, t), ); } @@ -80,9 +80,11 @@ mixin _$StreamReactionPickerThemeData { elevation: other.elevation, spacing: other.spacing, shape: other.shape, - side: _this.side != null && other.side != null - ? BorderSide.merge(_this.side!, other.side!) - : other.side, + side: _this.side == null + ? other.side + : other.side == null + ? _this.side + : BorderSide.merge(_this.side!, other.side!), ); } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_snackbar_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_snackbar_theme.g.theme.dart index 9e359f71..00299dc0 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_snackbar_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_snackbar_theme.g.theme.dart @@ -102,10 +102,10 @@ mixin _$StreamSnackbarStyle { backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), foregroundColor: Color.lerp(a.foregroundColor, b.foregroundColor, t), shape: OutlinedBorder.lerp(a.shape, b.shape, t), - side: a.side == null - ? b.side - : b.side == null - ? a.side + side: a.side == null || b.side == null + ? t < 0.5 + ? a.side + : b.side : BorderSide.lerp(a.side!, b.side!, t), elevation: lerpDouble$(a.elevation, b.elevation, t), padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t), @@ -162,9 +162,11 @@ mixin _$StreamSnackbarStyle { backgroundColor: other.backgroundColor, foregroundColor: other.foregroundColor, shape: other.shape, - side: _this.side != null && other.side != null - ? BorderSide.merge(_this.side!, other.side!) - : other.side, + side: _this.side == null + ? other.side + : other.side == null + ? _this.side + : BorderSide.merge(_this.side!, other.side!), elevation: other.elevation, padding: other.padding, margin: other.margin, diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart index da2f06a7..ca8e81bf 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart @@ -126,20 +126,20 @@ mixin _$StreamTextInputStyle { b.borderRadius, t, ), - border: a.border == null - ? b.border - : b.border == null - ? a.border + border: a.border == null || b.border == null + ? t < 0.5 + ? a.border + : b.border : BorderSide.lerp(a.border!, b.border!, t), - focusBorder: a.focusBorder == null - ? b.focusBorder - : b.focusBorder == null - ? a.focusBorder + focusBorder: a.focusBorder == null || b.focusBorder == null + ? t < 0.5 + ? a.focusBorder + : b.focusBorder : BorderSide.lerp(a.focusBorder!, b.focusBorder!, t), - errorBorder: a.errorBorder == null - ? b.errorBorder - : b.errorBorder == null - ? a.errorBorder + errorBorder: a.errorBorder == null || b.errorBorder == null + ? t < 0.5 + ? a.errorBorder + : b.errorBorder : BorderSide.lerp(a.errorBorder!, b.errorBorder!, t), fillColor: Color.lerp(a.fillColor, b.fillColor, t), contentPadding: EdgeInsetsGeometry.lerp( @@ -240,15 +240,21 @@ mixin _$StreamTextInputStyle { other.helperSuccessStyle, helperAffinity: other.helperAffinity, borderRadius: other.borderRadius, - border: _this.border != null && other.border != null - ? BorderSide.merge(_this.border!, other.border!) - : other.border, - focusBorder: _this.focusBorder != null && other.focusBorder != null - ? BorderSide.merge(_this.focusBorder!, other.focusBorder!) - : other.focusBorder, - errorBorder: _this.errorBorder != null && other.errorBorder != null - ? BorderSide.merge(_this.errorBorder!, other.errorBorder!) - : other.errorBorder, + border: _this.border == null + ? other.border + : other.border == null + ? _this.border + : BorderSide.merge(_this.border!, other.border!), + focusBorder: _this.focusBorder == null + ? other.focusBorder + : other.focusBorder == null + ? _this.focusBorder + : BorderSide.merge(_this.focusBorder!, other.focusBorder!), + errorBorder: _this.errorBorder == null + ? other.errorBorder + : other.errorBorder == null + ? _this.errorBorder + : BorderSide.merge(_this.errorBorder!, other.errorBorder!), fillColor: other.fillColor, contentPadding: other.contentPadding, constraints: other.constraints, diff --git a/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/dark/stream_tokens.dart b/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/dark/stream_tokens.dart index f259355e..4a783860 100644 --- a/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/dark/stream_tokens.dart +++ b/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/dark/stream_tokens.dart @@ -466,7 +466,7 @@ class StreamTokens { static const backgroundElevationElevation3 = Color(0xFF565656); static const borderUtilityFocus = Color(0x4078A8FF); static const borderUtilityError = Color(0xFFFC526A); - static const borderUtilityWarning = Color(0xFFFA922B); + static const borderUtilityWarning = Color(0xFFFCD579); static const borderUtilitySuccess = Color(0xFF00C384); static const borderUtilitySelected = Color(0x26FFFFFF); static const borderUtilityDisabled = Color(0xFF323232); @@ -596,7 +596,7 @@ class StreamTokens { static const avatarPresenceBorder = Color(0xFF000000); static const accentPrimary = Color(0xFF4586FF); static const accentSuccess = Color(0xFF00C384); - static const accentWarning = Color(0xFFFA922B); + static const accentWarning = Color(0xFFFCD579); static const accentError = Color(0xFFFC526A); static const accentNeutral = Color(0xFFABABAB); static const brand50 = Color(0xFF091A3B); diff --git a/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/light/stream_tokens.dart b/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/light/stream_tokens.dart index dfe8c11e..8595b4e3 100644 --- a/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/light/stream_tokens.dart +++ b/packages/stream_core_flutter/lib/src/theme/primitives/internal/tokens/light/stream_tokens.dart @@ -470,7 +470,7 @@ class StreamTokens { static const borderUtilityFocused = Color(0xFFC3D9FF); static const borderUtilityActive = Color(0xFF005FFF); static const borderUtilitySuccess = Color(0xFF00A46E); - static const borderUtilityWarning = Color(0xFFF26D10); + static const borderUtilityWarning = Color(0xFFF6BF57); static const borderUtilityError = Color(0xFFD90D10); static const borderUtilityDisabled = Color(0xFFEBEEF1); static const borderUtilityDisabledOnSurface = Color(0xFFD5DBE1); @@ -595,7 +595,7 @@ class StreamTokens { static const avatarPresenceBorder = Color(0xFFFFFFFF); static const accentPrimary = Color(0xFF005FFF); static const accentSuccess = Color(0xFF00A46E); - static const accentWarning = Color(0xFFF26D10); + static const accentWarning = Color(0xFFF6BF57); static const accentError = Color(0xFFD90D10); static const accentNeutral = Color(0xFF687385); static const brand50 = Color(0xFFF3F7FF); diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart index 93c13e80..72d45782 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart @@ -70,6 +70,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundSurfaceCard, Color? backgroundOnAccent, + Color? backgroundOnElevation, Color? backgroundHighlight, Color? backgroundScrim, Color? backgroundOverlayLight, @@ -90,6 +91,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? borderOnAccent, Color? borderOnInverse, Color? borderOnSurface, + Color? borderOnElevation, Color? borderOpacitySubtle, Color? borderOpacityStrong, // Border - Utility @@ -139,6 +141,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong ??= chrome.shade150; backgroundSurfaceCard ??= chrome.shade50; backgroundOnAccent ??= chrome[0] ?? StreamColors.white; + backgroundOnElevation ??= chrome.shade100; backgroundHighlight ??= light_tokens.StreamTokens.backgroundCoreHighlight; backgroundScrim ??= light_tokens.StreamTokens.backgroundCoreScrim; backgroundOverlayLight ??= light_tokens.StreamTokens.backgroundCoreOverlayLight; @@ -161,6 +164,7 @@ class StreamColorScheme with _$StreamColorScheme { borderOnAccent ??= chrome[0] ?? StreamColors.white; borderOnInverse ??= chrome[0] ?? StreamColors.white; borderOnSurface ??= chrome.shade300; + borderOnElevation ??= chrome.shade150; borderOpacitySubtle ??= light_tokens.StreamTokens.borderCoreOpacitySubtle; borderOpacityStrong ??= light_tokens.StreamTokens.borderCoreOpacityStrong; @@ -231,6 +235,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong: backgroundSurfaceStrong, backgroundSurfaceCard: backgroundSurfaceCard, backgroundOnAccent: backgroundOnAccent, + backgroundOnElevation: backgroundOnElevation, backgroundHighlight: backgroundHighlight, backgroundScrim: backgroundScrim, backgroundOverlayLight: backgroundOverlayLight, @@ -246,6 +251,7 @@ class StreamColorScheme with _$StreamColorScheme { borderOnAccent: borderOnAccent, borderOnInverse: borderOnInverse, borderOnSurface: borderOnSurface, + borderOnElevation: borderOnElevation, borderSubtle: borderSubtle, borderStrong: borderStrong, borderOpacitySubtle: borderOpacitySubtle, @@ -295,6 +301,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundSurfaceCard, Color? backgroundOnAccent, + Color? backgroundOnElevation, Color? backgroundHighlight, Color? backgroundScrim, Color? backgroundOverlayLight, @@ -316,6 +323,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? borderOnAccent, Color? borderOnInverse, Color? borderOnSurface, + Color? borderOnElevation, // Border - Utility Color? borderFocus, Color? borderDisabled, @@ -363,6 +371,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong ??= chrome.shade150; backgroundSurfaceCard ??= chrome.shade100; backgroundOnAccent ??= chrome[1000] ?? StreamColors.white; + backgroundOnElevation ??= chrome.shade150; backgroundHighlight ??= dark_tokens.StreamTokens.backgroundCoreHighlight; backgroundScrim ??= dark_tokens.StreamTokens.backgroundCoreScrim; backgroundOverlayLight ??= dark_tokens.StreamTokens.backgroundCoreOverlayLight; @@ -387,6 +396,7 @@ class StreamColorScheme with _$StreamColorScheme { borderOnAccent ??= chrome[1000] ?? StreamColors.white; borderOnInverse ??= chrome[0] ?? StreamColors.black; borderOnSurface ??= chrome.shade300; + borderOnElevation ??= chrome.shade300; // Border - Utility borderFocus ??= brand.shade150; @@ -455,6 +465,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong: backgroundSurfaceStrong, backgroundSurfaceCard: backgroundSurfaceCard, backgroundOnAccent: backgroundOnAccent, + backgroundOnElevation: backgroundOnElevation, backgroundHighlight: backgroundHighlight, backgroundScrim: backgroundScrim, backgroundOverlayLight: backgroundOverlayLight, @@ -473,6 +484,7 @@ class StreamColorScheme with _$StreamColorScheme { borderOnAccent: borderOnAccent, borderOnInverse: borderOnInverse, borderOnSurface: borderOnSurface, + borderOnElevation: borderOnElevation, borderSubtle: borderSubtle, borderFocus: borderFocus, borderDisabled: borderDisabled, @@ -544,6 +556,7 @@ class StreamColorScheme with _$StreamColorScheme { required this.backgroundSurfaceStrong, required this.backgroundSurfaceCard, required this.backgroundOnAccent, + required this.backgroundOnElevation, required this.backgroundHighlight, required this.backgroundScrim, required this.backgroundOverlayLight, @@ -563,6 +576,7 @@ class StreamColorScheme with _$StreamColorScheme { required this.borderOnAccent, required this.borderOnInverse, required this.borderOnSurface, + required this.borderOnElevation, required this.borderOpacitySubtle, required this.borderOpacityStrong, // Border - Utility @@ -666,6 +680,11 @@ class StreamColorScheme with _$StreamColorScheme { /// Surface that must remain white across themes (e.g., media controls over video). final Color backgroundOnAccent; + /// Background for controls sitting inside a floating surface — a menu, dialog + /// or popover. Steps up in dark mode, where the elevated surface has already + /// lightened and a plain surface background would disappear into it. + final Color backgroundOnElevation; + /// Highlight background (e.g., quoted message, search hit). final Color backgroundHighlight; @@ -734,6 +753,11 @@ class StreamColorScheme with _$StreamColorScheme { /// The border color on surface backgrounds. final Color borderOnSurface; + /// Border for controls sitting inside a floating surface — a menu, dialog or + /// popover. Steps up in dark mode to keep the edge visible once the elevated + /// surface has lightened. Use [borderOnSurface] on a plain surface instead. + final Color borderOnElevation; + /// Image frame border treatment (subtle opacity). final Color borderOpacitySubtle; diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart index 08cadda1..efaa4210 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart @@ -71,6 +71,11 @@ mixin _$StreamColorScheme { b.backgroundOnAccent, t, )!, + backgroundOnElevation: Color.lerp( + a.backgroundOnElevation, + b.backgroundOnElevation, + t, + )!, backgroundHighlight: Color.lerp( a.backgroundHighlight, b.backgroundHighlight, @@ -139,6 +144,11 @@ mixin _$StreamColorScheme { borderOnAccent: Color.lerp(a.borderOnAccent, b.borderOnAccent, t)!, borderOnInverse: Color.lerp(a.borderOnInverse, b.borderOnInverse, t)!, borderOnSurface: Color.lerp(a.borderOnSurface, b.borderOnSurface, t)!, + borderOnElevation: Color.lerp( + a.borderOnElevation, + b.borderOnElevation, + t, + )!, borderOpacitySubtle: Color.lerp( a.borderOpacitySubtle, b.borderOpacitySubtle, @@ -191,6 +201,7 @@ mixin _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundSurfaceCard, Color? backgroundOnAccent, + Color? backgroundOnElevation, Color? backgroundHighlight, Color? backgroundScrim, Color? backgroundOverlayLight, @@ -211,6 +222,7 @@ mixin _$StreamColorScheme { Color? borderOnAccent, Color? borderOnInverse, Color? borderOnSurface, + Color? borderOnElevation, Color? borderOpacitySubtle, Color? borderOpacityStrong, Color? borderFocus, @@ -254,6 +266,8 @@ mixin _$StreamColorScheme { backgroundSurfaceCard: backgroundSurfaceCard ?? _this.backgroundSurfaceCard, backgroundOnAccent: backgroundOnAccent ?? _this.backgroundOnAccent, + backgroundOnElevation: + backgroundOnElevation ?? _this.backgroundOnElevation, backgroundHighlight: backgroundHighlight ?? _this.backgroundHighlight, backgroundScrim: backgroundScrim ?? _this.backgroundScrim, backgroundOverlayLight: @@ -277,6 +291,7 @@ mixin _$StreamColorScheme { borderOnAccent: borderOnAccent ?? _this.borderOnAccent, borderOnInverse: borderOnInverse ?? _this.borderOnInverse, borderOnSurface: borderOnSurface ?? _this.borderOnSurface, + borderOnElevation: borderOnElevation ?? _this.borderOnElevation, borderOpacitySubtle: borderOpacitySubtle ?? _this.borderOpacitySubtle, borderOpacityStrong: borderOpacityStrong ?? _this.borderOpacityStrong, borderFocus: borderFocus ?? _this.borderFocus, @@ -329,6 +344,7 @@ mixin _$StreamColorScheme { backgroundSurfaceStrong: other.backgroundSurfaceStrong, backgroundSurfaceCard: other.backgroundSurfaceCard, backgroundOnAccent: other.backgroundOnAccent, + backgroundOnElevation: other.backgroundOnElevation, backgroundHighlight: other.backgroundHighlight, backgroundScrim: other.backgroundScrim, backgroundOverlayLight: other.backgroundOverlayLight, @@ -349,6 +365,7 @@ mixin _$StreamColorScheme { borderOnAccent: other.borderOnAccent, borderOnInverse: other.borderOnInverse, borderOnSurface: other.borderOnSurface, + borderOnElevation: other.borderOnElevation, borderOpacitySubtle: other.borderOpacitySubtle, borderOpacityStrong: other.borderOpacityStrong, borderFocus: other.borderFocus, @@ -401,6 +418,7 @@ mixin _$StreamColorScheme { _other.backgroundSurfaceStrong == _this.backgroundSurfaceStrong && _other.backgroundSurfaceCard == _this.backgroundSurfaceCard && _other.backgroundOnAccent == _this.backgroundOnAccent && + _other.backgroundOnElevation == _this.backgroundOnElevation && _other.backgroundHighlight == _this.backgroundHighlight && _other.backgroundScrim == _this.backgroundScrim && _other.backgroundOverlayLight == _this.backgroundOverlayLight && @@ -422,6 +440,7 @@ mixin _$StreamColorScheme { _other.borderOnAccent == _this.borderOnAccent && _other.borderOnInverse == _this.borderOnInverse && _other.borderOnSurface == _this.borderOnSurface && + _other.borderOnElevation == _this.borderOnElevation && _other.borderOpacitySubtle == _this.borderOpacitySubtle && _other.borderOpacityStrong == _this.borderOpacityStrong && _other.borderFocus == _this.borderFocus && @@ -466,6 +485,7 @@ mixin _$StreamColorScheme { _this.backgroundSurfaceStrong, _this.backgroundSurfaceCard, _this.backgroundOnAccent, + _this.backgroundOnElevation, _this.backgroundHighlight, _this.backgroundScrim, _this.backgroundOverlayLight, @@ -486,6 +506,7 @@ mixin _$StreamColorScheme { _this.borderOnAccent, _this.borderOnInverse, _this.borderOnSurface, + _this.borderOnElevation, _this.borderOpacitySubtle, _this.borderOpacityStrong, _this.borderFocus, diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart index fccde8d6..f765ee78 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart @@ -160,9 +160,8 @@ mixin _$StreamTheme on ThemeExtension { radius: StreamRadius.lerp(_this.radius, other.radius, t)!, spacing: StreamSpacing.lerp(_this.spacing, other.spacing, t)!, typography: StreamTypography.lerp(_this.typography, other.typography, t)!, - colorScheme: - (_this.colorScheme.lerp(other.colorScheme, t) as StreamColorScheme), - textTheme: (_this.textTheme.lerp(other.textTheme, t) as StreamTextTheme), + colorScheme: _this.colorScheme.lerp(other.colorScheme, t), + textTheme: _this.textTheme.lerp(other.textTheme, t), boxShadow: StreamBoxShadow.lerp(_this.boxShadow, other.boxShadow, t)!, appBarTheme: StreamAppBarThemeData.lerp( _this.appBarTheme, From 5e79d7db67e1676759171cd4508157d40830baff Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 13:03:11 +0200 Subject: [PATCH 04/15] docs(ui): pin SHAs in the override, and ask before widening API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two rules learned wiring the on-elevation and call-control-badge tokens through to stream-video-flutter. Always pin a commit SHA in the git override, never a branch name: a branch ref resolves to whatever the tip is at pub get time and writes that SHA into pubspec.lock, so the locked version drifts as the branch moves and two checkouts of the same consumer commit can resolve differently. This replaces the previous advice to use the branch name as the ref, and with it the claim that the branch must outlive its PR. Also: repoint only the package the change touches. stream_core and stream_core_flutter come from this repo but are separate git dependencies with separate pins, and a consumer is often pinned to an older core than main. And a token for an internal component is a signal that the component belongs to the design-system surface, so it usually wants to be public with a Stream prefix — but that widens the public API, so implement the theme and ask rather than deciding it inside a token sync. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 39 +++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index efade408..5381e729 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -119,6 +119,18 @@ A `tokens/video/**` change reaching this repo is not hypothetical: `StreamErrorBadge`, a core component that video merely wraps. Never conclude "video namespace, not our problem" from the path alone. +### A token for an internal component + +Upstream giving a component its own token is a signal that the component is part +of the design system's surface, so a themeable, public component is usually the +right shape — even where today's implementation is internal. Public components are +named with a `Stream` prefix. + +**Ask before making one public.** Widening the public API is a maintenance +commitment the SDK carries until the next major version, and that is the owner's +call, not a detail to slip into a token sync. Implement the theme, note that the +widget it themes is internal, and put the question to them. + Which ref to inspect: | repo | ref | @@ -169,16 +181,16 @@ dependency override, in this order: 1. Branch and push here first — the override resolves against the remote, so a local commit is not enough. -2. Branch in the consumer under the **same name** (see below) and point its - `stream_core_flutter` at the core branch. +2. Take the pushed SHA (`git rev-parse HEAD`), branch in the consumer under the + **same name** (see below), and point its `stream_core_flutter` at that SHA. 3. `melos bootstrap` in the consumer. Both halves are now buildable and reviewable together. ### Branch naming -The override's `ref:` is the core branch, so its name is shared vocabulary across -repos rather than a private detail — matching names are what let someone find the -other half of a change. +The `ref:` is a SHA, so the branch name is not load-bearing for resolution — but +it is shared vocabulary across repos, and matching names are what let someone find +the other half of a change. - **If core is already on a branch for this work, use that branch everywhere.** Reuse it rather than cutting a second one, and give the consumer's branch the same @@ -195,7 +207,7 @@ dependency_overrides: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter.git - ref: feat/update-tokens-on-elevation + ref: 2762ea870b3f38bdfb33280230d0709540bde35a # a commit, never a branch path: packages/stream_core_flutter ``` @@ -214,10 +226,17 @@ consumer's default branch and comes off only when that SDK is released: at that point this package is released too, and the dependency goes back to a published version constraint. -Because the ref is a branch, the branch has to keep resolving for as long as the -override is merged — so the core branch outlives its own PR. Do not delete it on -merge, and if you rebase it, remember every consumer that points at it resolves the -new tip. +**Always pin a commit SHA, never a branch name.** A branch ref resolves to +whatever the tip happens to be at `pub get` time and writes that SHA into +`pubspec.lock`, so the locked version drifts unpredictably as the branch moves and +two checkouts of the same consumer commit can resolve differently. A SHA is stable +and survives the branch being rebased or deleted. + +**Repoint only the package your change touches.** `stream_core` and +`stream_core_flutter` come from this repo but are separate git dependencies with +separate pins, and a consumer is often pinned to an older core than `main`. +Dragging `stream_core` forward for a change that only touches +`stream_core_flutter` pulls in unrelated churn — the error layer, for one. Distinct from the sibling-path override this repo's CI guidance warns about — a git ref is reproducible off-machine, where a `path:` to a sibling checkout is not. Do From 32ee7d4ce143398cd4753f02da582e9293c86477 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 13:10:06 +0200 Subject: [PATCH 05/15] docs(ui): regenerate goldens on CI, and use American English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the update_goldens workflow as the way to verify a token change. It runs on ubuntu, which is what makes the images match CI, and commits every changed PNG back to the dispatched branch — so the bot's diff doubles as a before/after of every component the color touched, which is worth attaching to a PR when the change is a value move. Also states plainly that every macOS-variant golden fails locally whatever you change, because only the ci/ images are committed and there are no macos/ ones at all. That noise reads as breakage; the way to tell them apart is to revert the edit and watch the same tests fail. Two caveats worth knowing: the regeneration step is continue-on-error, so a green run does not mean the goldens rebuilt cleanly, and the workflow commits to whatever ref is dispatched. Normalizes both skills to American English (colour -> color). Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 59 ++++++++++++++----- .../scripts/map_token_usage.py | 2 +- .claude/skills/update-icons/SKILL.md | 6 +- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index 5381e729..aac2ae58 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -1,11 +1,11 @@ --- name: update-design-tokens description: > - Sync colour design tokens from the GetStream/design-system-tokens repo into stream_core_flutter, and assess what + Sync color design tokens from the GetStream/design-system-tokens repo into stream_core_flutter, and assess what an upstream token change means for this package. Use whenever a design-system-tokens PR or commit needs - reviewing ("what does this token PR do to us?", "assess the impact of this change"), whenever a colour value or + reviewing ("what does this token PR do to us?", "assess the impact of this change"), whenever a color value or a new semantic token has to land in `theme/primitives/internal/tokens/`, whenever a new `StreamColorScheme` - field is being added or wired up, and whenever someone asks where a colour comes from or why a token resolves + field is being added or wired up, and whenever someone asks where a color comes from or why a token resolves the way it does. Covers tracing the blast radius into the consuming SDKs (stream-chat-flutter for chat tokens, stream-video-flutter for video), where the derived values actually live. Also use before hard-coding any `Color(0x...)` in this package — the answer is almost always a token or a colorScheme field instead. @@ -18,7 +18,7 @@ allowed-tools: # Updating design tokens -Colours in `stream_core_flutter` originate in +Colors in `stream_core_flutter` originate in [GetStream/design-system-tokens](https://github.com/GetStream/design-system-tokens) — the same repo the icons come from. This skill covers reading a change upstream and landing it here. @@ -44,7 +44,7 @@ upstream build output is not copied in verbatim. Only `stream_color_swatch_helper.dart` import them; no component theme or widget ever references `StreamTokens`. -Colours are byte-identical across the three upstream platform flavours (only +Colors are byte-identical across the three upstream platform flavours (only typography differs), so when you do read the upstream build, the flavour is irrelevant — pick any. Typography, spacing and radius are **not** synced at all: `stream_tokens_typography.dart` composes `TextStyle`s on a single `Geist` family, @@ -87,7 +87,7 @@ Then classify what you found: renamed derived token is often implemented there rather than here. When assessing impact, distinguish a token **definition** from a **paint site**. -A grep for the field name will mostly hit the colour scheme and generated +A grep for the field name will mostly hit the color scheme and generated `.g.theme.dart` plumbing; what matters is whether a widget renders with it: ```bash @@ -262,12 +262,12 @@ mechanically. ## Wiring a field default Look at what the token aliases upstream. The answer decides whether a vendored -constant is needed at all — and getting it wrong is how a custom brand colour +constant is needed at all — and getting it wrong is how a custom brand color silently stops applying: - **`{chrome.*}` or `{brand.*}`** → resolve through the generated swatch: `chrome.shade100`, `brand.shade500`, `chrome[0] ?? StreamColors.white`. - Never the baked hex. These scales are regenerated from a seed colour, so a + Never the baked hex. These scales are regenerated from a seed color, so a hard-coded value ignores `StreamColorScheme.light(brand: ...)`. - **another semantic** → alias the field: `borderWarning ??= accentWarning`, `textLink ??= accentPrimary`. @@ -291,7 +291,7 @@ Color get errorBackgroundColor => _colorScheme.accentError; So upstream `badge/bg-error` has no counterpart here, by design — a component theme reads `colorScheme.accentError` instead. This keeps the token surface small -and keeps every component overridable through one seedable colour scheme. +and keeps every component overridable through one seedable color scheme. The vendored files still carry a historical full dump of ~500 constants, of which roughly a third are read. Treat the unread ones as dead weight: don't add more, @@ -304,18 +304,45 @@ melos run analyze melos run test:flutter ``` -Regenerate only if you touched a `.theme.dart` annotation (adding a colour-scheme +Regenerate only if you touched a `.theme.dart` annotation (adding a color-scheme field does): `melos run generate:flutter`. A new field also needs wiring into the gallery's Theme Studio (`apps/design_system_gallery/lib/config/theme_configuration.dart` and `widgets/theme_studio/theme_customization_panel.dart`) — follow the surrounding fields. -Goldens only move if a component actually paints with the changed colour. The +### Goldens + +Goldens only move if a component actually paints with the changed color. The palette golden (`test/theme/goldens/ci/stream_theme_color_generation.png`) covers seed-generated brand/chrome ladders, not semantic accents, so a semantic value -change usually leaves it alone. When goldens do drift, remember local `macos/` -images are review-only — committed `ci/` goldens come from the update-goldens -workflow. +change usually leaves it alone. + +**Expect every macOS-variant golden to fail locally, change or no change.** Only +the `ci/` images are committed — there are no `macos/` ones in the repo at all — +so on a Mac those tests have nothing to compare against. That is the noise you +will see, not evidence your change broke something, and the local failure count +tends to match the number of committed goldens exactly. When in doubt, prove it: +revert your edit, re-run the same test, and watch it fail identically. + +**Regenerate the `ci/` images on CI, not on your machine.** The workflow runs on +ubuntu, which is what makes them match CI in the first place: + +```bash +gh workflow run update_goldens.yml --ref +gh run list --workflow=update_goldens.yml --limit 1 # then: gh run watch +git pull # picks up "chore: Update Goldens" +``` + +It bootstraps the workspace, runs `melos run update:goldens`, and commits every +changed PNG back to the branch you dispatched, as the Stream SDK Bot. Dispatching +it is also the cheapest way to *see* what a color change did — the bot's diff is a +before/after of every affected component, which is worth attaching to the PR when +the change is a value move rather than a new token. + +Two caveats. The regeneration step is `continue-on-error`, so a green run does not +mean the goldens rebuilt cleanly — read the bot's commit and check the images moved +the way you expected, and that nothing you did not touch moved with them. And it +commits to whatever ref you dispatch, so pass your own branch. ## Changelog @@ -332,8 +359,8 @@ transform). ## Contrast is not automatic Token aliases carry no contrast guarantee, and upstream can move a value across -the light/dark divide — a warning colour going from orange to a pale yellow flips -which text colour is legible on it. When adopting a changed fill, check what text +the light/dark divide — a warning color going from orange to a pale yellow flips +which text color is legible on it. When adopting a changed fill, check what text or icon token is painted on top of it, and say so if the pairing no longer works. `accent/*` values have no `on-*` counterpart in the core namespace, so this has to be reasoned about rather than looked up. diff --git a/.claude/skills/update-design-tokens/scripts/map_token_usage.py b/.claude/skills/update-design-tokens/scripts/map_token_usage.py index 4d64f212..eb685383 100755 --- a/.claude/skills/update-design-tokens/scripts/map_token_usage.py +++ b/.claude/skills/update-design-tokens/scripts/map_token_usage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Map `StreamColorScheme` fields to the files that read them, in any repo. -Answers "who paints with this colour?" for a consuming SDK without reading it +Answers "who paints with this color?" for a consuming SDK without reading it into context file by file. Run it against a ref rather than a working tree, so a design-system branch can be inspected without checking it out. diff --git a/.claude/skills/update-icons/SKILL.md b/.claude/skills/update-icons/SKILL.md index db0b339e..f9162732 100644 --- a/.claude/skills/update-icons/SKILL.md +++ b/.claude/skills/update-icons/SKILL.md @@ -3,7 +3,7 @@ name: update-icons description: > Add, update, retire or debug icons in stream_core_flutter — pulling SVGs from GetStream/design-system-tokens into `assets_source/icons/`, regenerating the icon font and `StreamIcons` via `melos run generate:icons`, - handling RTL mirroring, deprecations and code points, and updating the multicolour file-type SVGs. Use whenever + handling RTL mirroring, deprecations and code points, and updating the multicolor file-type SVGs. Use whenever an icon needs adding or replacing, whenever `melos run generate:icons` fails or warns, whenever an icon renders as the wrong glyph or a box, whenever an icon should mirror in RTL, and whenever someone asks whether an icon exists upstream. Also use before deleting any SVG from `assets_source/icons/` — a bare delete silently @@ -19,7 +19,7 @@ allowed-tools: Icons come from [GetStream/design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons), -the same repo the colour tokens come from. `melos run generate:icons` turns +the same repo the color tokens come from. `melos run generate:icons` turns `assets_source/icons/` into a font (`lib/fonts/stream_icons_font.otf`) plus the `StreamIcons` / `StreamIconData` classes. @@ -52,7 +52,7 @@ find assets/icons -name "*camera*" -path "*/flat/*" Only `flat/` (solid filled paths) goes into the font. `line/` is a stroke-based outline set covering nearly the same names and is **deliberately unused** — the two styles do not read as one set, so do not mix them in. `chat/filetype/` is -multicolour and takes a completely separate path (see the end of this file). +multicolor and takes a completely separate path (see the end of this file). ## Copying it in From ebe561944a770cdc7607314203b5f621b8a3b86e Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:12:45 +0000 Subject: [PATCH 06/15] chore: Update Goldens --- .../ci/stream_error_badge_border_toggle.png | Bin 8690 -> 8709 bytes .../ci/stream_error_badge_dark_matrix.png | Bin 3904 -> 3958 bytes .../ci/stream_error_badge_light_matrix.png | Bin 3883 -> 3838 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_border_toggle.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_border_toggle.png index 208799175df3a0ba61bf42debbf4d779a955cc05..7c3ec7d4760c0702e9eec41b9ae5d97b761e16ca 100644 GIT binary patch literal 8709 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 literal 8690 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 diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_dark_matrix.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_dark_matrix.png index 687cd4cd2570e62028f3a2491f34a73cc51d92b7..399a3901ba455d25a0bb6aca80298d39a6138421 100644 GIT binary patch literal 3958 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 literal 3904 zcmd6qc`%z@`^Rq-wO5OxC`D1VwzaF)zI!a8h^3ExYc(OB2+>yQVh^P#swxQLu{4(2 zN=w!DkZP2WYHbOjq76~xmp*^|=9zcqH}lSK=AC)ZALl;zb)PxcT;Ka#=X0)yzd2e7 z@JjLm03cvvZSDd9>WwnH(fe%>kY)}^~Qvf?AMFs#t zijDbIw?Cdz=Md44f0PVvjYA=h>WjAT1g!6}@n@gsv{0XXuKJAq(ed&+v%%}=<~H|` zISWHpoAXMeI>s@lKT%!0=N#{UnhUGF{aoY@noU&nSTy ztQfkJhQ0s@{`saoV?nt-G6#ae*bf4cga5AU&Ox1qOC@Z&)xIx>V(wi(b8akZN4d_c z{5eJvua%mT8Q}eaxF?U-`o^6QGJ9Y2H~{!dr5phOaVt4C02pw#X2pF=4dzZr*Z@pYxu)ir_W-`SRV;Cr$pHbBn zvLC!z2QkYPTZt(8b|1 zGHy|HHI6Sl@9TEr27!YuJg zE0qyy>rEJiGQK;b#YkBCi})^#t{>8(mbn+k*}ve868@zVi7s8l{A*I<0Q~4h32L6S zm%rh|P_CVocXJ_{HN{UHgc~KPFKa_8h+lrcRBBRNgxG{CoYEA@531SfKhS!dbETJI!XbiwAemcq!~1Nny6T`)9^nKtNvp!8 z6COwCXutRpcGqmw$YY7ZWp#>@4dWFU6jC$u&uZFkF?#(D#<;~ zT!Jf@5X6pW1(7Q$aNPJol}WM2_%U@R0ApA(M7)=ce0{@)rFtEO#0r!W))R?V+GpBv z*=`{#hR4U`!@TIB+-Er&hpo55!SQId=5v-os#CeY51;Q`mCxF*t8;K8qJC}d^Rbaw?sj-e;ELjk z;|K|Cv^gCq>#oExxA!Sdmp^P^Z!ghcZmm`A#OHf)>Lnfv^s=>w96eJ5c&nHWLIb+= zMwC&axsy~ii1o$Q^7FhXYM@j#bM&&rDlAgxSXPx^{8b^N4zOtRm!ya_a&pag>)d)~HRxU)Of>JhwUCbU(W`Yj@aZs`zG-k4(CeE@F-Kj#VT7Ja z+aNSV#0}d(wB0DU6-CoG}2eVK#nd6_A z&x0Cf0u7D;vX*uK0NhPDnTo#fnM=ZW(B4X%>yT;j$G*?Jr1P+_d75xV-33GBwW!E{f6O;A!R+!k%Jj^{sawm#U~>?jR95Qk=v$v(&7l zB=KG<4Z`=9#kI3e`7a*)O^x;tW`~fVcl0%)O53MuuRQokspj-;zMRaPrz~VMdnu8ZHtFu~#h| zxH-Qp+BLd)=3xwnwPTnq=hf;)+^Ac=Q#zb6)$LA72Hc{&&|rB+U?sdIAs)9()UmZg z2dRkUgcq~#?XF*nGMTC9z49i;Ts+?`v>^TRmk{3pk{i%AH-c zn)j3Wu}iedYh^!wsJvywFPA28_B$KX!<%83j-&_OgWI@T(=02MMIKT{_hh;}N#fhL zJjpVZ*YfuhuUl=iJ)~Wmd~??=ab~9BZciGOSnBo!)9jO+7u8mzSffCwDiL4{!rp8n zuMAuL`~XpWN1-2m%-ZsK%Z$ZMKGnZYGf}?JOrF;sT+bNa)<+45V^C5JjzBX&-YJ=LxWo;if|JAj;j-Y*r<%7T3%A=Gpn##Vp4yfv#7}W2C*2&YrT_}SFWr(Y z8I*TWvuRG!Ja(VItnyYnorrhK*>uGen4L&$9Ry>nBZ}URPTFc0zFk zBc2<+)=8T%vfzsQM0;qm5I`~@21JM{aI+?g`VGM%>4eSCsVe*$N&9^&y)8mx;0uuR zsDcPS8zdTJ5jUQ|4tIp^$i=TCa+daS|;;^@niMKAVW zMBRf&sE+XRn@7HKlzC*`wa&p_z>`|xh1&uX2aWe>a28I>f8o?eeCDS_cMRQPC77cT zV|-&gmgbWIzIoq}YBF2J!)PRFfOKZ6%+wMygYP2R=`(M!1Gl?%;Cb@RJ5T-fk1f}e zAVc~3(3nLhLU6HQSAstX(O}Jh)|SWbD0t<5VSg)&Hg&k(F+f6+nTsl@Uv; zuhcOgcYWa%%i!CtjxhdEFjlwxAkr4nIkUSFWW~Dq@$s8~uT4fCQ2f7SaRmGUr5YDs@(^cMDH@ia(1Oei4tcC*W!$)bi z8_ubWh)I>ILA)aVx oKE@+Tn^hbhlXw5uN@*wQ*8WaWSaE|Vt6&6dEF8_Trv7*T2Q_&zZ2$lO diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_light_matrix.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_light_matrix.png index 52dda52143c56bf8646d30c886333a386c215081..4c4a5aa25cd00723f528f2345814ad6232e231ec 100644 GIT binary patch literal 3838 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 WwBl)}r$X$pfT@us?74wk;(r2REhL`+ literal 3883 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 From d7a45892741179f7700c0bd8f320a325ca3f1046 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 13:17:17 +0200 Subject: [PATCH 07/15] docs(ui): record the last synced upstream commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in the repo said which design-system-tokens state the vendored tokens correspond to, and unlike a token's readers that fact cannot be recovered by grepping — you would have to bisect upstream comparing values. So the reference file now opens with the last synced commit, and the skill points at it: the next sync is a diff from there to origin/main rather than a hunt for the right base. Stated as "every semantic change up to here has been triaged", not "the vendored files mirror this commit", because they do not: 459 names are shared with 4ef9b54, 50 exist only here from before upstream's namespace split, and 98 only upstream, mostly derived tokens this package deliberately skips. Renames the file's title to match what it now holds — sync state and the derived-token traces. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 8 ++++++ .../references/derived-token-map.md | 28 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index aac2ae58..633c4971 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -68,6 +68,14 @@ git fetch origin python3 /scripts/flatten_tokens.py --diff main FETCH_HEAD tokens/core/semantics/light.json ``` +To catch up rather than review one PR, diff from the last synced commit — recorded +under **Last sync** in `references/derived-token-map.md`, and worth updating there +whenever you land a sync, since nothing in the repo itself records it: + +```bash +python3 /scripts/flatten_tokens.py --diff origin/main tokens/core/semantics/light.json +``` + Run it for each namespace and mode the change touches (`core`/`chat` here, `light` and `dark` both — they alias different primitives and can drift apart). Output is `ADDED` / `REMOVED` / `CHANGED` per token path. Without `--diff` the diff --git a/.claude/skills/update-design-tokens/references/derived-token-map.md b/.claude/skills/update-design-tokens/references/derived-token-map.md index 816a7369..601fc52e 100644 --- a/.claude/skills/update-design-tokens/references/derived-token-map.md +++ b/.claude/skills/update-design-tokens/references/derived-token-map.md @@ -1,4 +1,30 @@ -# Derived-token map +# Token sync state + +## Last sync + +| | | +| --- | --- | +| upstream commit | `4ef9b54bf93f2e42f346340690296dfca480ebc9` | +| upstream PR | [design-system-tokens#73](https://github.com/GetStream/design-system-tokens/pull/73) | +| date | 2026-09-10 | + +**Update this on every sync.** Nothing in the repo records which upstream state the +vendored tokens correspond to, and unlike a token's readers it cannot be recovered +by grepping — you would have to bisect upstream comparing values. One line here +turns the next sync into a mechanical diff: + +```bash +python3 /scripts/flatten_tokens.py --diff 4ef9b54 origin/main tokens/core/semantics/light.json +``` + +Read it as *"every semantic change up to here has been triaged"*, not *"the vendored +files mirror this commit"*. They do not, and knowingly so: 459 names are shared with +`4ef9b54`, 50 exist only here (pre-dating upstream's core/chat/video namespace +split — `backgroundElevationElevation0`, `avatarPaletteBg1`), and 98 exist only +upstream, most of them derived tokens this package deliberately does not vendor. +Adopting the namespace split is its own migration, not part of a routine sync. + +## Derived-token map Where upstream's **derived** chat and video semantics are actually implemented. From 9004c8f85d481fd416e6dae532fb145e88cb0103 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 13:29:14 +0200 Subject: [PATCH 08/15] docs(ui): regenerate the siblings' goldens, and keep dimensions out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stream-chat-flutter and stream-video-flutter each ship the same update_goldens.yml, and a value change moves their goldens as surely as this package's — a component they own paints with the field. The call control badge going red to yellow moved call_control_button and call_feature_button, neither of which lives here, and the signal was their CI failing on the variant: CI goldens. Dispatch it on the consumer's branch once its override points at the core commit, so the images rendered are the ones the change actually produces. Also states what belongs in the vendored files: a constant only when a field will read it, in both modes, because an unread constant reads as an invitation to paint a component from a token — which bypasses the seedable color scheme and breaks a custom brand. Dimensions and type never belong there; they come from StreamSpacing, StreamRadius and StreamTokensTypography. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 30 ++++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index 633c4971..a3a472bd 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -301,9 +301,18 @@ So upstream `badge/bg-error` has no counterpart here, by design — a component theme reads `colorScheme.accentError` instead. This keeps the token surface small and keeps every component overridable through one seedable color scheme. -The vendored files still carry a historical full dump of ~500 constants, of which -roughly a third are read. Treat the unread ones as dead weight: don't add more, -and don't take their presence as precedent. +**Add a constant only when a field will read it**, and add it to both `light/` and +`dark/`. An unread constant is not harmless: it reads as an invitation to paint a +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 never belong there at all. Spacing, radius, sizes, font sizes +and weights come from `StreamSpacing`, `StreamRadius` and +`StreamTokensTypography`, which declare their own scales — upstream's type is +per-platform where this package composes a single `Geist` family. If a design +calls for a new spacing or radius step, it goes in those classes, not in a token +file. ## After editing @@ -352,6 +361,21 @@ mean the goldens rebuilt cleanly — read the bot's commit and check the images the way you expected, and that nothing you did not touch moved with them. And it commits to whatever ref you dispatch, so pass your own branch. +**Run it on the consuming SDKs too.** stream-chat-flutter and +stream-video-flutter each have the same `update_goldens.yml`, and a value change +moves their goldens as surely as it moves this package's — a component they own +paints with the field. Their CI fails on the `variant: CI` goldens, which is the +signal, and dispatching the workflow on the consumer's branch is the fix: + +```bash +gh workflow run update_goldens.yml --repo GetStream/stream-video-flutter --ref +``` + +Do it once the consumer's override points at your core commit, so the images it +renders are the ones the change actually produces. Expect this on any value +change: the call control badge going red to yellow moved +`call_control_button` and `call_feature_button`, neither of which is in this repo. + ## Changelog `StreamColorScheme` is exported from `core.dart`, so **every one of its fields is From e2d4c6642027ad79d41f17b12b650aae9f389008 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 13:53:56 +0200 Subject: [PATCH 09/15] =?UTF-8?q?docs(ui):=20correct=20the=20flavor=20clai?= =?UTF-8?q?m=20=E2=80=94=20type=20comes=20from=20web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Said dimensions and typography were not synced from upstream at all. Wrong: the vendored files mirror upstream's values exactly, ordering included, and StreamSpacing / StreamRadius / StreamTokensTypography hold the same numbers. Typography specifically comes from the web flavor, which is the one place the Geist family this package ships appears — android resolves to Roboto, iOS to SF Pro, and iOS also runs a size up at every step. So "pick any flavor" holds for colors and is wrong for type. The dimension values are identical across all three flavors, so no choice arises there, but the classes hard-code them rather than reading the vendored constants — the same hazard as baking a hex where a swatch belongs, one layer up, since nothing ties the class to the constant it mirrors. Also finishes the American English pass: the earlier sweep only covered colour and left flavour behind. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 34 +++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index a3a472bd..781565fc 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -44,11 +44,17 @@ upstream build output is not copied in verbatim. Only `stream_color_swatch_helper.dart` import them; no component theme or widget ever references `StreamTokens`. -Colors are byte-identical across the three upstream platform flavours (only -typography differs), so when you do read the upstream build, the flavour is -irrelevant — pick any. Typography, spacing and radius are **not** synced at all: -`stream_tokens_typography.dart` composes `TextStyle`s on a single `Geist` family, -and `stream_spacing.dart` / `stream_radius.dart` declare their own scales. +**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. 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 @@ -264,7 +270,7 @@ background/core/on-accent backgroundCoreOnAccent backgroundOnAccent Some fields also shorten further where the upstream suffix carried no meaning here (`background/core/surface-default` → `backgroundSurface`). Match the -existing neighbours in `stream_color_scheme.dart` rather than deriving the name +existing neighbors in `stream_color_scheme.dart` rather than deriving the name mechanically. ## Wiring a field default @@ -307,12 +313,16 @@ 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 never belong there at all. Spacing, radius, sizes, font sizes -and weights come from `StreamSpacing`, `StreamRadius` and -`StreamTokensTypography`, which declare their own scales — upstream's type is -per-platform where this package composes a single `Geist` family. If a design -calls for a new spacing or radius step, it goes in those classes, not in a token -file. +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. ## After editing From 7c2347590ba5f9f7389c8b1d205eb81801304a04 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 14:39:46 +0200 Subject: [PATCH 10/15] docs(ui): shorten the changelog entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both new entries ran to a paragraph where their neighbours run to a line. Reviewer called it too verbose, and rightly — the changelog is scanned by consumers deciding whether an upgrade affects them, so the values belong there and the reasoning belongs in the PR. Adds the rule where it will be seen next time: the changelog policy in STYLE_GUIDE.md, and the skill's changelog section. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 8 ++++++-- STYLE_GUIDE.md | 6 ++++-- packages/stream_core_flutter/CHANGELOG.md | 18 +++++------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index 781565fc..3accce30 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -391,8 +391,12 @@ change: the call control badge going red to yellow moved `StreamColorScheme` is exported from `core.dart`, so **every one of its fields is public API**. A token whose *value* changes is a visual change for anyone reading the field instead of overriding it, and needs a `### 🔄 Changed` CHANGELOG entry -under `## Upcoming` even though no signature moved. Name the old and new resolved -values — that is what a consumer diffing screenshots needs. +under `## Upcoming` even though no signature moved. + +**A line or two.** Name the old and new resolved values, since that is what a +consumer diffing screenshots needs, and say if the new value constrains what can +sit on it. Everything else — why upstream changed it, contrast ratios, which +component made it visible — belongs in the PR. A field that is removed or renamed follows the deprecation policy in `STYLE_GUIDE.md` (annotate, `### 🛑 Breaking / Removals`, and a `fix_data.yaml` diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index cedf68fa..d82d8370 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1391,8 +1391,10 @@ PR titles follow [Conventional Commits](https://www.conventionalcommits.org/): ### Changelog policy Every PR that changes package behavior updates the affected package's -`CHANGELOG.md` under the `Upcoming` heading. Entries live under one of these -sub-headings: +`CHANGELOG.md` under the `Upcoming` heading. **Keep entries to a line or two** — +what changed and, for a value change, the old and new values. Rationale, contrast +figures and migration detail belong in the PR, not here. Entries live under one of +these sub-headings: ```markdown ## Upcoming diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index f0b9a4f0..0731e295 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -7,22 +7,14 @@ (the default) or `.warning` — and `StreamErrorBadge.showBorder`. - Added `StreamErrorBadgeTheme` and `StreamErrorBadgeThemeData`, carrying a background and foreground color per style. -- Added `StreamColorScheme.backgroundOnElevation` and `borderOnElevation`, the - design system's `background/core/on-elevation` and `border/core/on-elevation` - tokens. For controls sitting inside a floating surface — a menu, dialog or - popover — where the plain surface and border values lose their edge in dark - mode, because the elevated surface has already lightened. +- Added `StreamColorScheme.backgroundOnElevation` and `borderOnElevation`, for + controls inside a floating surface such as a menu or popover. ### 🔄 Changed -- `StreamColorScheme.accentWarning` moved from orange to yellow, following the - design system's `accent/warning` token now resolving to `yellow.200` in light - and `yellow.150` in dark: `#F26D10` → `#F6BF57` (light) and `#FA922B` → - `#FCD579` (dark). `borderWarning` defaults to `accentWarning`, so it moves with - it. The visible effect is `StreamErrorBadge` with - `StreamErrorBadgeStyle.warning`, whose icon is already pinned to black; anything - overriding either field is unaffected. The new value is legible under dark text - only — white on `#F6BF57` is roughly 1.7:1 — so pair it with a dark foreground. +- `StreamColorScheme.accentWarning` — and `borderWarning`, which defaults to it — + moved from orange to yellow: `#F26D10` → `#F6BF57` light, `#FA922B` → `#FCD579` + dark. Pair it with a dark foreground. ## 0.5.1 From 5a5ea359a47498f7f320bbe7718b5bc4f1811532 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 14:42:31 +0200 Subject: [PATCH 11/15] fix(ui): raise the theme_extensions_builder floor to 7.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regenerated theme files in this branch come from 7.5.0, which replaced `WidgetStateBorderSide.lerp(a, b, t)` with a midpoint step. That is a fix, not a downgrade: `lerp` returns a `_LerpSides`, which only implements `WidgetStateProperty`, while the style constructors cast to `WidgetStateBorderSide?` — so the old output threw `type '_LerpSides' is not a subtype of type 'WidgetStateBorderSide?'` on any theme animation where both endpoints set a side. At `^7.2.0` an older builder could regenerate and put the crash back, so the floor moves to the version that emits the fix. The constraint lives in melos.yaml, which bootstrap propagates into the package pubspec — editing the pubspec alone is reverted on the next bootstrap. Regenerating at the new floor produces no diff, so the committed output already matches it. Co-Authored-By: Claude Opus 5 --- melos.yaml | 2 +- packages/stream_core_flutter/CHANGELOG.md | 2 ++ packages/stream_core_flutter/pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/melos.yaml b/melos.yaml index d2dbc4d2..d4fea389 100644 --- a/melos.yaml +++ b/melos.yaml @@ -59,7 +59,7 @@ command: mocktail: ^1.0.4 stream_channel: ^2.1.4 test: ^1.26.2 - theme_extensions_builder: ^7.2.0 + theme_extensions_builder: ^7.5.0 scripts: postclean: diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 0731e295..6df94219 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -15,6 +15,8 @@ - `StreamColorScheme.accentWarning` — and `borderWarning`, which defaults to it — moved from orange to yellow: `#F26D10` → `#F6BF57` light, `#FA922B` → `#FCD579` dark. Pair it with a dark foreground. +- Fixed `lerp` on theme styles carrying a `WidgetStateBorderSide` throwing a cast + error. The border side now steps at the midpoint rather than interpolating. ## 0.5.1 diff --git a/packages/stream_core_flutter/pubspec.yaml b/packages/stream_core_flutter/pubspec.yaml index e4c04564..0e959230 100644 --- a/packages/stream_core_flutter/pubspec.yaml +++ b/packages/stream_core_flutter/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: build_runner: ^2.10.5 flutter_test: sdk: flutter - theme_extensions_builder: ^7.2.0 + theme_extensions_builder: ^7.5.0 flutter: uses-material-design: true From fe6ba53709fbc677642d177a288471bbed210501 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 16:28:24 +0200 Subject: [PATCH 12/15] fix(ui): correct the borderOnElevation doc, and guard the lerp fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dartdoc was upstream's `$description` copied verbatim, and upstream is wrong: `border/core/on-elevation` resolves to `{chrome.300}` in dark, identical to `border/core/on-surface`, so nothing "steps up". It is the light mode that differs, where the field is softer than `borderOnSurface`. Reported upstream. The `WidgetStateBorderSide` lerp crash had no test — the existing `StreamTheme.lerp` coverage leaves every nested style null, short-circuiting before the branch is reached. The new test fails on main with the original `_LerpSides is not a subtype of WidgetStateBorderSide?` cast error. Also moves the fix bullet out of `### 🔄 Changed`, which the style guide reserves for what is neither new API nor a fix, and records the nullable `BorderSide` endpoint change the regeneration also made. Co-Authored-By: Claude Opus 5 --- packages/stream_core_flutter/CHANGELOG.md | 10 ++- .../theme/semantics/stream_color_scheme.dart | 5 +- .../test/theme/component_style_lerp_test.dart | 75 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 packages/stream_core_flutter/test/theme/component_style_lerp_test.dart diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 6df94219..ae1391fe 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -10,13 +10,19 @@ - Added `StreamColorScheme.backgroundOnElevation` and `borderOnElevation`, for controls inside a floating surface such as a menu or popover. +### 🐛 Bug Fixes + +- Fixed `lerp` on theme styles carrying a `WidgetStateBorderSide` throwing a cast + error. The border side now steps at the midpoint rather than interpolating. +- Fixed `lerp` on a theme style whose border side is set on one end only. It now + steps at the midpoint instead of applying the non-null side across the whole + transition. + ### 🔄 Changed - `StreamColorScheme.accentWarning` — and `borderWarning`, which defaults to it — moved from orange to yellow: `#F26D10` → `#F6BF57` light, `#FA922B` → `#FCD579` dark. Pair it with a dark foreground. -- Fixed `lerp` on theme styles carrying a `WidgetStateBorderSide` throwing a cast - error. The border side now steps at the midpoint rather than interpolating. ## 0.5.1 diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart index 72d45782..97f5a03c 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart @@ -754,8 +754,9 @@ class StreamColorScheme with _$StreamColorScheme { final Color borderOnSurface; /// Border for controls sitting inside a floating surface — a menu, dialog or - /// popover. Steps up in dark mode to keep the edge visible once the elevated - /// surface has lightened. Use [borderOnSurface] on a plain surface instead. + /// popover. Softer than [borderOnSurface] in light mode, where elevated + /// surfaces are white and need less of an edge; the two converge in dark. + /// Use [borderOnSurface] on a plain surface instead. final Color borderOnElevation; /// Image frame border treatment (subtle opacity). diff --git a/packages/stream_core_flutter/test/theme/component_style_lerp_test.dart b/packages/stream_core_flutter/test/theme/component_style_lerp_test.dart new file mode 100644 index 00000000..25de2f08 --- /dev/null +++ b/packages/stream_core_flutter/test/theme/component_style_lerp_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/core.dart'; + +/// Guards the border-side branches of the generated `lerp` methods. +/// +/// These are emitted by `theme_extensions_builder`, so a generator bump can +/// change them without anything in this repo being edited. The +/// [WidgetStateBorderSide] case in particular shipped broken in 0.5.1: the +/// existing `StreamTheme.lerp` coverage leaves every nested style null, which +/// short-circuits before the branch is ever reached. +void main() { + group('lerp on a style with a WidgetStateBorderSide', () { + // `WidgetStateBorderSide.lerp` returns Flutter's private `_LerpSides`, + // which implements `WidgetStateProperty` and *not* + // `WidgetStateBorderSide` — so the `as WidgetStateBorderSide?` cast in + // `StreamCheckboxStyle`'s constructor threw on every non-null lerp. + final a = StreamCheckboxStyle.from(side: const BorderSide(color: Color(0xFF112233))); + final b = StreamCheckboxStyle.from(side: const BorderSide(color: Color(0xFFFFFFFF), width: 3)); + + test('does not throw part-way through a transition', () { + for (final t in const [0.0, 0.25, 0.5, 0.75, 1.0]) { + expect(() => StreamCheckboxStyle.lerp(a, b, t), returnsNormally, reason: 't = $t'); + } + }); + + test('steps at the midpoint rather than interpolating', () { + const enabled = {}; + + expect(StreamCheckboxStyle.lerp(a, b, 0.25)?.side?.resolve(enabled), equals(a.side?.resolve(enabled))); + expect(StreamCheckboxStyle.lerp(a, b, 0.75)?.side?.resolve(enabled), equals(b.side?.resolve(enabled))); + }); + + test('holds at both endpoints', () { + const enabled = {}; + + expect(StreamCheckboxStyle.lerp(a, b, 0)?.side?.resolve(enabled), equals(a.side?.resolve(enabled))); + expect(StreamCheckboxStyle.lerp(a, b, 1)?.side?.resolve(enabled), equals(b.side?.resolve(enabled))); + }); + }); + + group('lerp on a style whose plain BorderSide is set on one end only', () { + const withSide = StreamContextMenuStyle(side: BorderSide(color: Color(0xFF00FF00))); + const withoutSide = StreamContextMenuStyle(); + + test('holds at both endpoints', () { + // The pre-7.5.0 generator returned the non-null side across the whole + // range, so a border popped in at t = 0 and lerp(a, b, 0) != a. + expect(StreamContextMenuStyle.lerp(withoutSide, withSide, 0)?.side, isNull); + expect(StreamContextMenuStyle.lerp(withoutSide, withSide, 1)?.side, equals(withSide.side)); + }); + + test('steps at the midpoint', () { + expect(StreamContextMenuStyle.lerp(withoutSide, withSide, 0.25)?.side, isNull); + expect(StreamContextMenuStyle.lerp(withoutSide, withSide, 0.75)?.side, equals(withSide.side)); + }); + }); + + group('merge keeps a border side the argument leaves null', () { + const base = StreamContextMenuStyle(side: BorderSide(color: Color(0xFF00FF00))); + + test('receiver survives an argument with no side', () { + expect(base.merge(const StreamContextMenuStyle()).side, equals(base.side)); + }); + + test('both sides go through BorderSide.merge when both are set', () { + // `BorderSide.merge` asserts `canMerge`, so the two have to share a + // color, and it sums their widths rather than replacing one with the + // other — the argument does not simply win here. + const other = StreamContextMenuStyle(side: BorderSide(color: Color(0xFF00FF00), width: 2)); + + expect(base.merge(other).side?.width, equals(3)); + }); + }); +} From 67c294ad0fccc880c66e0da09ad98f6a19adcc24 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 16:28:34 +0200 Subject: [PATCH 13/15] docs(ui): correct the token docs, and record where dartdocs come from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things were wrong. `stream_color_swatch_helper.dart` was listed as a reader of the vendored tokens; it generates shades in HCT from a seed and never touches `StreamTokens`. "Typography, spacing and radius do not come from upstream at all" is false — they are published upstream and mirrored by hand into `StreamTokensTypography`, `StreamSpacing` and `StreamRadius`, which is what the skill already said, so the two contradicted each other. And the derived-token-map rows for the call-control error badge pointed at the error members for tokens that resolve to warning values. Adds the provenance that was missing: a field's dartdoc is the token's own `$description`, which is why a stale upstream description reaches this repo unexamined. Says to verify it against the resolved values first. Also: the example `ref:` was a commit on this branch and would dangle after squash; `map_token_usage.py` matches Material's `colorScheme` identically, which matters because it is meant to run against the Material-based SDKs; both scripts mangled their own `--help`; and the sync-state counts named no command and could not be reproduced. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 46 ++++++++++++++++--- .../references/derived-token-map.md | 18 +++++--- .../scripts/flatten_tokens.py | 3 +- .../scripts/map_token_usage.py | 10 +++- CLAUDE.md | 21 +++++---- STYLE_GUIDE.md | 2 +- 6 files changed, 76 insertions(+), 24 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index 3accce30..09a7f262 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -39,10 +39,11 @@ covered below. | 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_color_scheme.dart`, `stream_colors.dart` and -`stream_color_swatch_helper.dart` import them; no component theme or widget ever -references `StreamTokens`. +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 @@ -181,6 +182,14 @@ Two limits worth knowing, both by design: `references/derived-token-map.md`, hand-traced, with the caveat that it points at repos which move independently — verify a row before acting on it. +And one that is not by design: the pattern matches the bare string `colorScheme.`, +so **Material's** `Theme.of(context).colorScheme.surface` is reported exactly like a +`StreamColorScheme` read. There are none in this repo, which uses +`StreamTheme.of(context).colorScheme`, but the consuming SDKs are Material apps +where the collision is real. Check the `Theme.of` receiver before reading a hit as +a Stream token — and read a *clean* result with the same suspicion, since app and +example directories are not filtered either. + When you do trace a derived token by hand, add the row. That is the only mapping worth recording: the greppable half goes stale the moment someone edits a widget, while the non-greppable half is what nobody can reconstruct without repeating your @@ -221,7 +230,7 @@ dependency_overrides: stream_core_flutter: git: url: https://github.com/GetStream/stream-core-flutter.git - ref: 2762ea870b3f38bdfb33280230d0709540bde35a # a commit, never a branch + ref: dbf84703ceb4dc19a7c847707428e8727d867203 # a commit, never a branch path: packages/stream_core_flutter ``` @@ -283,8 +292,9 @@ silently stops applying: `chrome.shade100`, `brand.shade500`, `chrome[0] ?? StreamColors.white`. Never the baked hex. These scales are regenerated from a seed color, so a hard-coded value ignores `StreamColorScheme.light(brand: ...)`. -- **another semantic** → alias the field: `borderWarning ??= accentWarning`, - `textLink ??= accentPrimary`. +- **another semantic** → alias the field: `borderWarning ??= accentWarning`. + Check both modes before generalising — `textLink` aliases `accentPrimary` in + light but resolves `brand.shade600` in dark, so the two factories can differ. - **a raw hex, or a primitive outside those two scales** (a `yellow`, a transparent black) → add a constant to both vendored files and read it: `light_tokens.StreamTokens.backgroundCoreHighlight`. @@ -292,6 +302,28 @@ silently stops applying: Only the third case earns a vendored constant. Add the same name to **both** `light/` and `dark/`, keeping the file's existing ordering. +### Writing the dartdoc + +The field's dartdoc comes from the token's own `$description`, which upstream +carries alongside `$value` in the semantics JSON: + +```bash +python3 -c "import json;d=json.load(open('tokens/core/semantics/light.json'));\ +print(d['border']['core']['on-elevation'])" +``` + +Quote it rather than inventing prose — it is the designer's statement of intent, +and matching wording is what lets the next person recognise the field as that +token. Swap upstream's token paths for `[fieldName]` references. + +**Verify the claim against the resolved light and dark values first.** These +descriptions are hand-written and can be stale: upstream's +`border/core/on-elevation` says it "steps up in dark mode to keep the edge +visible" while resolving to `{chrome.300}` in dark — identical to +`border/core/on-surface`, the field it tells you to contrast it with. When a +description and the values disagree, document what the values encode and open an +issue upstream, so the source gets fixed rather than the copy. + ## Root semantics only Upstream also publishes derived semantics — `badge/*`, `button/*`, `avatar/*`. diff --git a/.claude/skills/update-design-tokens/references/derived-token-map.md b/.claude/skills/update-design-tokens/references/derived-token-map.md index 601fc52e..b2faa360 100644 --- a/.claude/skills/update-design-tokens/references/derived-token-map.md +++ b/.claude/skills/update-design-tokens/references/derived-token-map.md @@ -18,10 +18,16 @@ python3 /scripts/flatten_tokens.py --diff 4ef9b54 origin/main tokens/core ``` Read it as *"every semantic change up to here has been triaged"*, not *"the vendored -files mirror this commit"*. They do not, and knowingly so: 459 names are shared with -`4ef9b54`, 50 exist only here (pre-dating upstream's core/chat/video namespace -split — `backgroundElevationElevation0`, `avatarPaletteBg1`), and 98 exist only -upstream, most of them derived tokens this package deliberately does not vendor. +files mirror this commit"*. They do not, and knowingly so: the vendored set is +neither a subset nor a superset of upstream. Some names pre-date upstream's +core/chat/video namespace split and survive only here (`backgroundElevationElevation0`, +`avatarPaletteBg1`); many upstream names — mostly derived tokens — are deliberately +not vendored; and `light/` and `dark/` do not even hold the same set. Compare them +yourself rather than trusting a count that rots: + +```bash +python3 /scripts/flatten_tokens.py tokens/core/semantics/light.json +``` Adopting the namespace split is its own migration, not part of a routine sync. ## Derived-token map @@ -55,8 +61,8 @@ Reference ref: `origin/v2` in stream-video-flutter (the design-system branch). | `indicator/connection-quality/fair` | `{accent.warning}` | video · same file → `fairColor` | `StreamConnectionQualityIndicator` | | `indicator/connection-quality/great` | `{accent.success}` | video · same file → `greatColor` | `StreamConnectionQualityIndicator` | | `indicator/sound-indicator/speaking` | `{brand.400}` | video · `theme/components/participant_label_theme.dart` → `speakingColor` | `StreamAudioIndicator` | -| `control/call-control-error-badge/bg` | `{accent.warning}` | **core** · `components/badge/stream_error_badge.dart` → `colorScheme.accentError` | `StreamErrorBadge`, wrapped by video's `CallButtonBadge` | -| `control/call-control-error-badge/text` | `{base.black}` | **core** · same file → `colorScheme.textOnAccent` | `StreamErrorBadge` | +| `control/call-control-error-badge/bg` | `{accent.warning}` | **core** · `components/badge/stream_error_badge.dart` → `warningBackgroundColor`, i.e. `colorScheme.accentWarning` | `StreamErrorBadge`, wrapped by video's `CallButtonBadge` | +| `control/call-control-error-badge/text` | `{base.black}` | **core** · same file → `warningForegroundColor`, a literal `StreamColors.black` — `textOnAccent` resolves to white in *both* modes and cannot satisfy `{base.black}` | `StreamErrorBadge` | | `indicator/microphone-level/bar-active` | `{brand.400}` | not implemented — the lobby level meter is new | — | | `indicator/microphone-level/bar-inactive` | `{chrome.200}` | not implemented | — | diff --git a/.claude/skills/update-design-tokens/scripts/flatten_tokens.py b/.claude/skills/update-design-tokens/scripts/flatten_tokens.py index ec1311d2..ab3c9a83 100755 --- a/.claude/skills/update-design-tokens/scripts/flatten_tokens.py +++ b/.claude/skills/update-design-tokens/scripts/flatten_tokens.py @@ -63,7 +63,8 @@ def load(path: str, ref: str | None = None) -> dict[str, str]: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("path", help="path to a semantics JSON file") parser.add_argument( "--diff", diff --git a/.claude/skills/update-design-tokens/scripts/map_token_usage.py b/.claude/skills/update-design-tokens/scripts/map_token_usage.py index eb685383..b3fa6d02 100755 --- a/.claude/skills/update-design-tokens/scripts/map_token_usage.py +++ b/.claude/skills/update-design-tokens/scripts/map_token_usage.py @@ -16,6 +16,13 @@ up rather than the widget consuming that theme. Follow the theme field to the widget when the answer needs to name a component. +The match is on the bare string `colorScheme.`, so Material's +`Theme.of(context).colorScheme.surface` is reported identically to a +`StreamColorScheme` read. This repo uses `StreamTheme.of(context).colorScheme` and +has no collisions, but the consuming SDKs are Material apps where it is real — +check the receiver before trusting a hit, and note that app/example directories +are not filtered either, only tests. + It also only sees root semantics, the ones with a `colorScheme` field. Derived chat/video tokens have no field at all — the SDK inlines them as swatch reads (`colorScheme.brand.shade300`) inside component-theme defaults, which no token @@ -35,7 +42,8 @@ def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("repo", help="path to the consuming SDK checkout") parser.add_argument("ref", help="git ref to inspect, e.g. origin/v2") parser.add_argument("field", nargs="?", help="limit to one colorScheme field") diff --git a/CLAUDE.md b/CLAUDE.md index de6f0e7e..7a80a516 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,14 +114,19 @@ 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_color_scheme.dart`, `stream_colors.dart` and -`stream_color_swatch_helper.dart` read it. - -Only the **root semantics** are vendored. Upstream's derived tokens (`badge/*`, -`button/*`, `avatar/*`) have no counterpart here by design — components re-derive -them from `colorScheme.*` in their own defaults. Typography, spacing and radius do -not come from upstream at all. `StreamColorScheme` is exported from `core.dart`, so -every field on it is public API. +and only `stream_colors.dart` and `stream_color_scheme.dart` read it. + +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 +exported from `core.dart`, so every field on it is public API. + +A field's dartdoc comes from the token's own `$description` in the upstream JSON — +quote it rather than inventing prose, but check it against the resolved light and +dark values first, because those descriptions can be stale. **Use the `update-design-tokens` skill** when syncing a token change or assessing an upstream PR — it covers the naming rules, how a field default should resolve, diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index d82d8370..e7222878 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1366,7 +1366,7 @@ font and the `StreamIcons` / `StreamIconData` classes from the source SVGs in [design-system-tokens](https://github.com/GetStream/design-system-tokens/tree/main/assets/icons) repo. -Do not edit the generated `stream_icons.dart`, the font, or +Do not edit the generated `stream_icons.dart`, `stream_icons.g.dart`, the font, or `assets_source/icon_log.g.txt` by hand. Commit SVG sources and regenerated output together — they must stay in sync. From 16869c57889cfc778342d614c8e53d3bf2b40a01 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 16:30:32 +0200 Subject: [PATCH 14/15] docs(ui): restore the upstream wording, and add the nuance it omits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My earlier commit called upstream's `border/core/on-elevation` description wrong. It is not. "Steps up in dark mode to keep the edge visible once the elevated surface has lightened" describes the token's own progression — `{chrome.150}` in light to `{chrome.300}` in dark, as `background/core/elevation-2` goes `{chrome.0}` to `{chrome.100}`. I read it as a comparison with `borderOnSurface` and rewrote accurate wording. What is true, and what the description leaves out, is that the same `{chrome.300}` is also `borderOnSurface` in dark — so the "use borderOnSurface on a plain surface" guidance only changes anything in light. That is now appended rather than substituted. The skill's dartdoc section carried the same mistake as its worked example; it now warns that a description tracks the token's own light/dark step rather than a comparison with the sibling it names, and to add the missing nuance instead of replacing wording that is accurate. Co-Authored-By: Claude Opus 5 --- .claude/skills/update-design-tokens/SKILL.md | 16 +++++++++------- CLAUDE.md | 5 +++-- .../src/theme/semantics/stream_color_scheme.dart | 7 ++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.claude/skills/update-design-tokens/SKILL.md b/.claude/skills/update-design-tokens/SKILL.md index 09a7f262..8bb65881 100644 --- a/.claude/skills/update-design-tokens/SKILL.md +++ b/.claude/skills/update-design-tokens/SKILL.md @@ -316,13 +316,15 @@ Quote it rather than inventing prose — it is the designer's statement of inten and matching wording is what lets the next person recognise the field as that token. Swap upstream's token paths for `[fieldName]` references. -**Verify the claim against the resolved light and dark values first.** These -descriptions are hand-written and can be stale: upstream's -`border/core/on-elevation` says it "steps up in dark mode to keep the edge -visible" while resolving to `{chrome.300}` in dark — identical to -`border/core/on-surface`, the field it tells you to contrast it with. When a -description and the values disagree, document what the values encode and open an -issue upstream, so the source gets fixed rather than the copy. +**Read the claim against the resolved light and dark values first**, and note +what it is *not* saying. A description usually tracks the token's own light→dark +progression, not a comparison with the sibling it names: `border/core/on-elevation` +"steps up in dark mode" because it goes `{chrome.150}` → `{chrome.300}` as the +elevated surface lightens — while in that same mode it lands on `{chrome.300}`, +exactly `border/core/on-surface`, the token it tells you to use instead. Both +halves are true and they are easy to read as contradictory. Resolve the aliases +before deciding a description is wrong, and add the nuance the description omits +rather than replacing wording that is accurate. ## Root semantics only diff --git a/CLAUDE.md b/CLAUDE.md index 7a80a516..5df6310d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,8 +125,9 @@ dimension change is applied to those classes by hand. `StreamColorScheme` is exported from `core.dart`, so every field on it is public API. A field's dartdoc comes from the token's own `$description` in the upstream JSON — -quote it rather than inventing prose, but check it against the resolved light and -dark values first, because those descriptions can be stale. +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 +with the sibling it names. **Use the `update-design-tokens` skill** when syncing a token change or assessing an upstream PR — it covers the naming rules, how a field default should resolve, diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart index 97f5a03c..b308039d 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart @@ -754,9 +754,10 @@ class StreamColorScheme with _$StreamColorScheme { final Color borderOnSurface; /// Border for controls sitting inside a floating surface — a menu, dialog or - /// popover. Softer than [borderOnSurface] in light mode, where elevated - /// surfaces are white and need less of an edge; the two converge in dark. - /// Use [borderOnSurface] on a plain surface instead. + /// popover. Steps up in dark mode to keep the edge visible once the elevated + /// surface has lightened. Use [borderOnSurface] on a plain surface instead — + /// though the two resolve to the same value in dark, so the choice only shows + /// in light. final Color borderOnElevation; /// Image frame border treatment (subtle opacity). From b71e4ff133a0755313c19ff710c8ab95d02e8dd9 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 16:48:42 +0200 Subject: [PATCH 15/15] fix(ui): relax the theme_extensions_builder floor back to ^7.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7.5.0 requires Dart >=3.13.0, while this package declares `sdk: ^3.12.0` and `flutter: >=3.44.0`. That combination cannot resolve, which is what `legacy_version_analyze` exists to catch — it checks the N-1 stable still works "if the package claims to support that version". The claim is accurate: `theme_extensions_builder` is a dev_dependency, so a consumer on Dart 3.12 is unaffected and the published package really does still support it. `^7.2.0` lets pub pick 7.5.0 on a modern SDK (verified in the lockfile) and an older 7.x on 3.12, so the committed generated output is what a current contributor reproduces. The floor was raised to stop someone regenerating with an old builder and silently reinstating the `WidgetStateBorderSide` cast crash. That is now guarded by a test instead, which is the more durable fix — a constraint only protects whoever has the right SDK, a test fails for everyone. Co-Authored-By: Claude Opus 5 --- melos.yaml | 2 +- packages/stream_core_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/melos.yaml b/melos.yaml index d4fea389..d2dbc4d2 100644 --- a/melos.yaml +++ b/melos.yaml @@ -59,7 +59,7 @@ command: mocktail: ^1.0.4 stream_channel: ^2.1.4 test: ^1.26.2 - theme_extensions_builder: ^7.5.0 + theme_extensions_builder: ^7.2.0 scripts: postclean: diff --git a/packages/stream_core_flutter/pubspec.yaml b/packages/stream_core_flutter/pubspec.yaml index 0e959230..e4c04564 100644 --- a/packages/stream_core_flutter/pubspec.yaml +++ b/packages/stream_core_flutter/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: build_runner: ^2.10.5 flutter_test: sdk: flutter - theme_extensions_builder: ^7.5.0 + theme_extensions_builder: ^7.2.0 flutter: uses-material-design: true