From 244f77697caf2de1ef06c06f2428350d49e89b56 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 28 Jul 2026 10:00:08 +0200 Subject: [PATCH 1/4] chore(tutorial): align example app with the rewritten React chat tutorial The published tutorial was restructured (GetStream/getstream.io#345) into numbered steps 0-7 plus two optional recipes. This aligns the example app so a folder here maps 1:1 to a heading there. - Renumber the step folders to match the tutorial (2-client-setup through 7-emoji-picker), and rename the two non-linear steps to optional-*. - Add the missing 5-theming step. The tutorial teaches theming before the component overrides, but the app had no counterpart for it. - Scope every themed step's overrides under a `step-` class, applied by the browser in src/App.tsx. The step browser renders all steps in one document, so all eight stylesheets are live at once; steps 3 and 4 import the SDK stylesheet unlayered (as the tutorial has them), and unlayered CSS outranks every @layer regardless of specificity. Without the scope, a layered override does nothing and an unscoped one restyles the earlier steps. README documents this as the one deliberate deviation from the tutorial. - Fix the preview panel clipping the chat UI. The chrome panes are sized in viewport units and padded, so under the default content-box the padding was added on top of 100vh and pushed the composer below the fold. Sets border-box on the six named chrome classes only, so the SDK's own box-sizing is untouched. - Drop the 16 per-step main.tsx and index.html files. They predate the step browser, were never part of any build (vite build only ever emitted dist/index.html), and had become a maintenance coupling for the scope class. Each step folder now holds exactly the files the tutorial tells you to create. --- examples/tutorial/.env.example | 6 +- examples/tutorial/README.md | 60 +++++++++++++++++- .../tutorial/src/1-client-setup/index.html | 13 ---- examples/tutorial/src/1-client-setup/main.tsx | 9 --- .../App.tsx | 0 .../credentials.ts | 6 +- .../src/2-core-component-setup/index.html | 13 ---- .../src/2-core-component-setup/main.tsx | 9 --- .../tutorial/src/3-channel-list/index.html | 13 ---- .../tutorial/src/3-channel-list/layout.css | 53 ---------------- examples/tutorial/src/3-channel-list/main.tsx | 9 --- .../App.tsx | 2 +- .../layout.css | 0 .../stream-chat.d.ts | 0 examples/tutorial/src/4-channel-list/App.tsx | 57 +++++++++++++++++ .../tutorial/src/4-channel-list/layout.css | 21 +++++++ .../src/4-custom-ui-components/index.html | 13 ---- .../src/4-custom-ui-components/layout.css | 53 ---------------- .../src/4-custom-ui-components/main.tsx | 9 --- .../src/5-custom-attachment-type/index.html | 13 ---- .../src/5-custom-attachment-type/layout.css | 53 ---------------- .../src/5-custom-attachment-type/main.tsx | 9 --- .../src/{3-channel-list => 5-theming}/App.tsx | 2 +- examples/tutorial/src/5-theming/layout.css | 58 +++++++++++++++++ .../App.tsx | 2 +- .../src/6-custom-ui-components/layout.css | 58 +++++++++++++++++ .../tutorial/src/6-emoji-picker/index.html | 13 ---- .../tutorial/src/6-emoji-picker/layout.css | 53 ---------------- examples/tutorial/src/6-emoji-picker/main.tsx | 9 --- .../App.tsx | 2 +- .../tutorial/src/7-emoji-picker/layout.css | 58 +++++++++++++++++ examples/tutorial/src/7-livestream/index.html | 13 ---- examples/tutorial/src/7-livestream/layout.css | 53 ---------------- examples/tutorial/src/7-livestream/main.tsx | 9 --- examples/tutorial/src/App.tsx | 62 ++++++++++++------- .../App.tsx | 6 +- .../layout.css | 58 +++++++++++++++++ .../stream-chat.d.ts | 0 .../App.tsx | 2 +- .../src/optional-livestream/layout.css | 58 +++++++++++++++++ examples/tutorial/src/tutorial-main.css | 35 +++++++++-- 41 files changed, 514 insertions(+), 458 deletions(-) delete mode 100644 examples/tutorial/src/1-client-setup/index.html delete mode 100644 examples/tutorial/src/1-client-setup/main.tsx rename examples/tutorial/src/{1-client-setup => 2-client-setup}/App.tsx (100%) rename examples/tutorial/src/{1-client-setup => 2-client-setup}/credentials.ts (85%) delete mode 100644 examples/tutorial/src/2-core-component-setup/index.html delete mode 100644 examples/tutorial/src/2-core-component-setup/main.tsx delete mode 100644 examples/tutorial/src/3-channel-list/index.html delete mode 100644 examples/tutorial/src/3-channel-list/layout.css delete mode 100644 examples/tutorial/src/3-channel-list/main.tsx rename examples/tutorial/src/{2-core-component-setup => 3-core-component-setup}/App.tsx (95%) rename examples/tutorial/src/{2-core-component-setup => 3-core-component-setup}/layout.css (100%) rename examples/tutorial/src/{2-core-component-setup => 3-core-component-setup}/stream-chat.d.ts (100%) create mode 100644 examples/tutorial/src/4-channel-list/App.tsx create mode 100644 examples/tutorial/src/4-channel-list/layout.css delete mode 100644 examples/tutorial/src/4-custom-ui-components/index.html delete mode 100644 examples/tutorial/src/4-custom-ui-components/layout.css delete mode 100644 examples/tutorial/src/4-custom-ui-components/main.tsx delete mode 100644 examples/tutorial/src/5-custom-attachment-type/index.html delete mode 100644 examples/tutorial/src/5-custom-attachment-type/layout.css delete mode 100644 examples/tutorial/src/5-custom-attachment-type/main.tsx rename examples/tutorial/src/{3-channel-list => 5-theming}/App.tsx (94%) create mode 100644 examples/tutorial/src/5-theming/layout.css rename examples/tutorial/src/{4-custom-ui-components => 6-custom-ui-components}/App.tsx (98%) create mode 100644 examples/tutorial/src/6-custom-ui-components/layout.css delete mode 100644 examples/tutorial/src/6-emoji-picker/index.html delete mode 100644 examples/tutorial/src/6-emoji-picker/layout.css delete mode 100644 examples/tutorial/src/6-emoji-picker/main.tsx rename examples/tutorial/src/{6-emoji-picker => 7-emoji-picker}/App.tsx (96%) create mode 100644 examples/tutorial/src/7-emoji-picker/layout.css delete mode 100644 examples/tutorial/src/7-livestream/index.html delete mode 100644 examples/tutorial/src/7-livestream/layout.css delete mode 100644 examples/tutorial/src/7-livestream/main.tsx rename examples/tutorial/src/{5-custom-attachment-type => optional-custom-attachment-type}/App.tsx (94%) create mode 100644 examples/tutorial/src/optional-custom-attachment-type/layout.css rename examples/tutorial/src/{5-custom-attachment-type => optional-custom-attachment-type}/stream-chat.d.ts (100%) rename examples/tutorial/src/{7-livestream => optional-livestream}/App.tsx (95%) create mode 100644 examples/tutorial/src/optional-livestream/layout.css diff --git a/examples/tutorial/.env.example b/examples/tutorial/.env.example index 46895730ee..470fb5bd6c 100644 --- a/examples/tutorial/.env.example +++ b/examples/tutorial/.env.example @@ -1,5 +1,7 @@ -# Required: your Stream app's public key. -VITE_API_KEY=REPLACE_WITH_API_KEY +# Required: your Stream app's public key. This is the variable name that +# `getstream env --target vite` writes, so you can generate it instead of +# pasting it by hand. (VITE_API_KEY is still read as a fallback.) +VITE_STREAM_API_KEY=REPLACE_WITH_API_KEY # Optional. If unset, the app defaults to user_id "react-tutorial" and # derives user_name from it. You can also override either value per-run diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index 90854e0949..e20a99fda4 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -1,4 +1,62 @@ -This folder contains the source code for [Chat React tutorial](https://github.com/GetStream/getstream.io-tutorials/blob/main/chat/tutorials/react-tutorial.mdx). It contains multiple versions of apps representing the tutorial steps. +This folder contains the source code for the [Chat React tutorial](https://getstream.io/chat/sdk/react/tutorial/). It contains multiple versions of apps representing the tutorial steps. + +The tutorial source lives in the website repo at [`content/pages/chat_sdk_react_tutorial.mdx`](https://github.com/GetStream/getstream.io/blob/main/content/pages/chat_sdk_react_tutorial.mdx). (It used to live in `GetStream/getstream.io-tutorials`, which is now archived.) + +## Step folders + +Folder names match the tutorial's step numbers, so `4-channel-list` is the tutorial's "Step 4 - Add a channel list". The tutorial's Step 0 (environment) and Step 1 (project + credentials) have no runnable counterpart, so the folders start at 2. The two `optional-*` folders are the tutorial's optional recipes, which sit after the numbered path. + +| Folder | Tutorial section | +| --------------------------------- | ------------------------------------------------- | +| `2-client-setup` | Step 2 - Connect the client | +| `3-core-component-setup` | Step 3 - Get a working chat UI | +| `4-channel-list` | Step 4 - Add a channel list | +| `5-theming` | Step 5 - Theme it | +| `6-custom-ui-components` | Step 6 - Replace an SDK component | +| `7-emoji-picker` | Step 7 - Enable the emoji picker and autocomplete | +| `optional-custom-attachment-type` | Optional - add a custom attachment type | +| `optional-livestream` | Optional - a livestream-style chat app | + +If you change a step's code here, update the matching code block in the tutorial too, and vice versa. + +### One deliberate deviation: theme scoping + +The tutorial puts the custom theme tokens in a CSS layer: + +```css +@layer stream, stream-overrides; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +@layer stream-overrides { + .custom-theme { + /* tokens */ + } +} +``` + +The themed steps here instead use an unlayered, step-scoped selector: + +```css +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +.step-theming .custom-theme { + /* tokens */ +} +``` + +Why: the step browser renders every step in a single document, so all eight +stylesheets are live at once. Steps 3 and 4 import the SDK stylesheet +_unlayered_ (as the tutorial has them, since Step 5 is where you're taught to +move it into a layer), and unlayered CSS outranks every `@layer` regardless of +specificity. A layered override would silently do nothing, and an unscoped one +would restyle the earlier steps. + +The `step-` class is applied by the browser in `src/App.tsx`, on the wrapper +around the active step. + +**This deviation exists only to make the step browser work. In your own app, +follow the tutorial and keep the tokens in the layer.** The tutorial app is a Yarn workspace (`@stream-io/stream-chat-react-tutorial`) under the repo's monorepo, so it consumes the local `stream-chat-react` SDK through `workspace:^` and shares its dependencies with the root install. diff --git a/examples/tutorial/src/1-client-setup/index.html b/examples/tutorial/src/1-client-setup/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/1-client-setup/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/1-client-setup/main.tsx b/examples/tutorial/src/1-client-setup/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/1-client-setup/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/1-client-setup/App.tsx b/examples/tutorial/src/2-client-setup/App.tsx similarity index 100% rename from examples/tutorial/src/1-client-setup/App.tsx rename to examples/tutorial/src/2-client-setup/App.tsx diff --git a/examples/tutorial/src/1-client-setup/credentials.ts b/examples/tutorial/src/2-client-setup/credentials.ts similarity index 85% rename from examples/tutorial/src/1-client-setup/credentials.ts rename to examples/tutorial/src/2-client-setup/credentials.ts index 0236916296..77277926e1 100644 --- a/examples/tutorial/src/1-client-setup/credentials.ts +++ b/examples/tutorial/src/2-client-setup/credentials.ts @@ -8,7 +8,9 @@ // ?user_id=alice&user_name=Alice // + display name override // // Notes: -// - apiKey is the one thing you still need to set (via VITE_API_KEY). +// - apiKey is the one thing you still need to set. `getstream env --target vite` +// writes VITE_STREAM_API_KEY, which is what the tutorial tells you to run; +// VITE_API_KEY is still accepted for older local setups. // - The token endpoint and environment default to the values shared with // the other example apps in this repo; override with VITE_TOKEN_ENDPOINT // and VITE_TOKEN_ENVIRONMENT if you're pointing at a different Stream @@ -16,7 +18,7 @@ const searchParams = new URLSearchParams(window.location.search); -export const apiKey = import.meta.env.VITE_API_KEY; +export const apiKey = import.meta.env.VITE_STREAM_API_KEY || import.meta.env.VITE_API_KEY; export const userId = searchParams.get('user_id') || import.meta.env.VITE_USER_ID || 'react-tutorial'; diff --git a/examples/tutorial/src/2-core-component-setup/index.html b/examples/tutorial/src/2-core-component-setup/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/2-core-component-setup/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/2-core-component-setup/main.tsx b/examples/tutorial/src/2-core-component-setup/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/2-core-component-setup/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/3-channel-list/index.html b/examples/tutorial/src/3-channel-list/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/3-channel-list/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/3-channel-list/layout.css b/examples/tutorial/src/3-channel-list/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/3-channel-list/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/3-channel-list/main.tsx b/examples/tutorial/src/3-channel-list/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/3-channel-list/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/2-core-component-setup/App.tsx b/examples/tutorial/src/3-core-component-setup/App.tsx similarity index 95% rename from examples/tutorial/src/2-core-component-setup/App.tsx rename to examples/tutorial/src/3-core-component-setup/App.tsx index d6067dfb13..d7681f9890 100644 --- a/examples/tutorial/src/2-core-component-setup/App.tsx +++ b/examples/tutorial/src/3-core-component-setup/App.tsx @@ -14,7 +14,7 @@ import { import 'stream-chat-react/dist/css/index.css'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/2-core-component-setup/layout.css b/examples/tutorial/src/3-core-component-setup/layout.css similarity index 100% rename from examples/tutorial/src/2-core-component-setup/layout.css rename to examples/tutorial/src/3-core-component-setup/layout.css diff --git a/examples/tutorial/src/2-core-component-setup/stream-chat.d.ts b/examples/tutorial/src/3-core-component-setup/stream-chat.d.ts similarity index 100% rename from examples/tutorial/src/2-core-component-setup/stream-chat.d.ts rename to examples/tutorial/src/3-core-component-setup/stream-chat.d.ts diff --git a/examples/tutorial/src/4-channel-list/App.tsx b/examples/tutorial/src/4-channel-list/App.tsx new file mode 100644 index 0000000000..f86d16843f --- /dev/null +++ b/examples/tutorial/src/4-channel-list/App.tsx @@ -0,0 +1,57 @@ +import type { ChannelFilters, ChannelOptions, ChannelSort, User } from 'stream-chat'; +import { + Channel, + ChannelHeader, + ChannelList, + Chat, + MessageComposer, + MessageList, + Thread, + useCreateChatClient, + Window, +} from 'stream-chat-react'; + +import 'stream-chat-react/dist/css/index.css'; +import './layout.css'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; + +const user: User = { + id: userId, + name: userName, + image: `https://getstream.io/random_png/?name=${userName}`, +}; + +const sort: ChannelSort = { last_message_at: -1 }; +const filters: ChannelFilters = { + type: 'messaging', + members: { $in: [userId] }, +}; +const options: ChannelOptions = { + limit: 10, +}; + +const App = () => { + const client = useCreateChatClient({ + apiKey, + tokenOrProvider: tokenProvider, + userData: user, + }); + + if (!client) return
Setting up client & connection...
; + + return ( + + + + + + + + + + + + ); +}; + +export default App; diff --git a/examples/tutorial/src/4-channel-list/layout.css b/examples/tutorial/src/4-channel-list/layout.css new file mode 100644 index 0000000000..c3cf99687a --- /dev/null +++ b/examples/tutorial/src/4-channel-list/layout.css @@ -0,0 +1,21 @@ +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} \ No newline at end of file diff --git a/examples/tutorial/src/4-custom-ui-components/index.html b/examples/tutorial/src/4-custom-ui-components/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/4-custom-ui-components/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/4-custom-ui-components/layout.css b/examples/tutorial/src/4-custom-ui-components/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/4-custom-ui-components/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/4-custom-ui-components/main.tsx b/examples/tutorial/src/4-custom-ui-components/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/4-custom-ui-components/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/5-custom-attachment-type/index.html b/examples/tutorial/src/5-custom-attachment-type/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/5-custom-attachment-type/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/5-custom-attachment-type/layout.css b/examples/tutorial/src/5-custom-attachment-type/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/5-custom-attachment-type/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/5-custom-attachment-type/main.tsx b/examples/tutorial/src/5-custom-attachment-type/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/5-custom-attachment-type/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/3-channel-list/App.tsx b/examples/tutorial/src/5-theming/App.tsx similarity index 94% rename from examples/tutorial/src/3-channel-list/App.tsx rename to examples/tutorial/src/5-theming/App.tsx index 5d37369fdf..68fc6190b1 100644 --- a/examples/tutorial/src/3-channel-list/App.tsx +++ b/examples/tutorial/src/5-theming/App.tsx @@ -12,7 +12,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/5-theming/layout.css b/examples/tutorial/src/5-theming/layout.css new file mode 100644 index 0000000000..30a5ae9f24 --- /dev/null +++ b/examples/tutorial/src/5-theming/layout.css @@ -0,0 +1,58 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* Scoped, and deliberately NOT in a layer - this differs from the published + tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. + The step browser (src/App.tsx) renders every step in one document, and the + steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS + outranks every @layer, so a layered override would silently do nothing here, + and an unscoped one would leak into the other steps. + In your own app, follow the tutorial and keep these in the layer. */ +.step-theming .custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/4-custom-ui-components/App.tsx b/examples/tutorial/src/6-custom-ui-components/App.tsx similarity index 98% rename from examples/tutorial/src/4-custom-ui-components/App.tsx rename to examples/tutorial/src/6-custom-ui-components/App.tsx index 60566f2c27..2c212f76fb 100644 --- a/examples/tutorial/src/4-custom-ui-components/App.tsx +++ b/examples/tutorial/src/6-custom-ui-components/App.tsx @@ -17,7 +17,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/6-custom-ui-components/layout.css b/examples/tutorial/src/6-custom-ui-components/layout.css new file mode 100644 index 0000000000..fce10f4427 --- /dev/null +++ b/examples/tutorial/src/6-custom-ui-components/layout.css @@ -0,0 +1,58 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* Scoped, and deliberately NOT in a layer - this differs from the published + tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. + The step browser (src/App.tsx) renders every step in one document, and the + steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS + outranks every @layer, so a layered override would silently do nothing here, + and an unscoped one would leak into the other steps. + In your own app, follow the tutorial and keep these in the layer. */ +.step-custom-ui-components .custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/6-emoji-picker/index.html b/examples/tutorial/src/6-emoji-picker/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/6-emoji-picker/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/6-emoji-picker/layout.css b/examples/tutorial/src/6-emoji-picker/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/6-emoji-picker/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/6-emoji-picker/main.tsx b/examples/tutorial/src/6-emoji-picker/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/6-emoji-picker/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/6-emoji-picker/App.tsx b/examples/tutorial/src/7-emoji-picker/App.tsx similarity index 96% rename from examples/tutorial/src/6-emoji-picker/App.tsx rename to examples/tutorial/src/7-emoji-picker/App.tsx index 5f339501be..aeb7c27003 100644 --- a/examples/tutorial/src/6-emoji-picker/App.tsx +++ b/examples/tutorial/src/7-emoji-picker/App.tsx @@ -18,7 +18,7 @@ import { init, SearchIndex } from 'emoji-mart'; import data from '@emoji-mart/data'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/7-emoji-picker/layout.css b/examples/tutorial/src/7-emoji-picker/layout.css new file mode 100644 index 0000000000..16e5fbb0aa --- /dev/null +++ b/examples/tutorial/src/7-emoji-picker/layout.css @@ -0,0 +1,58 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* Scoped, and deliberately NOT in a layer - this differs from the published + tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. + The step browser (src/App.tsx) renders every step in one document, and the + steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS + outranks every @layer, so a layered override would silently do nothing here, + and an unscoped one would leak into the other steps. + In your own app, follow the tutorial and keep these in the layer. */ +.step-emoji-picker .custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/7-livestream/index.html b/examples/tutorial/src/7-livestream/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/7-livestream/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/7-livestream/layout.css b/examples/tutorial/src/7-livestream/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/7-livestream/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/7-livestream/main.tsx b/examples/tutorial/src/7-livestream/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/7-livestream/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/App.tsx b/examples/tutorial/src/App.tsx index 35a6b0092f..1629ac2e86 100644 --- a/examples/tutorial/src/App.tsx +++ b/examples/tutorial/src/App.tsx @@ -1,13 +1,14 @@ import { useEffect, useState } from 'react'; import type { ComponentType } from 'react'; -import ClientSetupStep from './1-client-setup/App'; -import CoreComponentSetupStep from './2-core-component-setup/App'; -import ChannelListStep from './3-channel-list/App'; -import CustomUiComponentsStep from './4-custom-ui-components/App'; -import CustomAttachmentTypeStep from './5-custom-attachment-type/App'; -import EmojiPickerStep from './6-emoji-picker/App'; -import LivestreamStep from './7-livestream/App'; +import ClientSetupStep from './2-client-setup/App'; +import CoreComponentSetupStep from './3-core-component-setup/App'; +import ChannelListStep from './4-channel-list/App'; +import ThemingStep from './5-theming/App'; +import CustomUiComponentsStep from './6-custom-ui-components/App'; +import EmojiPickerStep from './7-emoji-picker/App'; +import CustomAttachmentTypeStep from './optional-custom-attachment-type/App'; +import LivestreamStep from './optional-livestream/App'; import './tutorial-main.css'; type TutorialStep = { @@ -17,52 +18,64 @@ type TutorialStep = { Component: ComponentType; }; +// Titles and order mirror the published tutorial, so a step here maps 1:1 to a +// heading there: https://getstream.io/chat/sdk/react/tutorial/ +// +// The tutorial's Step 0 (environment) and Step 1 (project + credentials) have no +// runnable counterpart, so this browser starts at Step 2. const steps: TutorialStep[] = [ { id: 'client-setup', - title: '1. Client Setup', + title: 'Step 2. Connect the client', description: 'Connect the SDK to your Stream app and verify the chat client is ready.', Component: ClientSetupStep, }, { id: 'core-component-setup', - title: '2. Core Components', + title: 'Step 3. Get a working chat UI', description: 'Render the first complete chat UI with Channel, MessageList, MessageComposer, and Thread.', Component: CoreComponentSetupStep, }, { id: 'channel-list', - title: '3. Channel List', + title: 'Step 4. Add a channel list', description: 'Add channel navigation so the tutorial app feels like a real messaging experience.', Component: ChannelListStep, }, + { + id: 'theming', + title: 'Step 5. Theme it', + description: + 'Brand the default theme by overriding the SDK design tokens. Everything from here on carries the custom theme.', + Component: ThemingStep, + }, { id: 'custom-ui-components', - title: '4. Custom UI Components', + title: 'Step 6. Replace an SDK component', description: 'Use WithComponents to replace SDK-owned UI surfaces without rebuilding the whole app.', Component: CustomUiComponentsStep, }, { - id: 'custom-attachment-type', - title: '5. Custom Attachment Type', + id: 'emoji-picker', + title: 'Step 7. Emoji picker and autocomplete', description: - 'Render a branded product attachment while keeping the default attachment fallbacks.', - Component: CustomAttachmentTypeStep, + 'Wire the SDK EmojiPicker into MessageComposer with emoji-mart search support.', + Component: EmojiPickerStep, }, { - id: 'emoji-picker', - title: '6. Emoji Picker', + id: 'custom-attachment-type', + title: 'Optional. Custom attachment type', description: - 'Wire a custom EmojiPicker into MessageComposer with emoji-mart search support.', - Component: EmojiPickerStep, + 'Render a branded product attachment while keeping the default attachment fallbacks.', + Component: CustomAttachmentTypeStep, }, { id: 'livestream', - title: '7. Livestream', + title: 'Optional. Livestream-style chat', description: 'Switch the layout to a livestream-style experience with VirtualizedMessageList.', Component: LivestreamStep, @@ -137,7 +150,14 @@ const App = () => {
-
+ {/* The `step-` class scopes each step's theme overrides. Every step's + CSS is loaded into this one document, so without a scope the themed + steps would restyle the unthemed ones. See the README section + "One deliberate deviation: theme scoping". */} +
diff --git a/examples/tutorial/src/5-custom-attachment-type/App.tsx b/examples/tutorial/src/optional-custom-attachment-type/App.tsx similarity index 94% rename from examples/tutorial/src/5-custom-attachment-type/App.tsx rename to examples/tutorial/src/optional-custom-attachment-type/App.tsx index 3cd1483ee8..4229326028 100644 --- a/examples/tutorial/src/5-custom-attachment-type/App.tsx +++ b/examples/tutorial/src/optional-custom-attachment-type/App.tsx @@ -19,7 +19,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, @@ -93,7 +93,9 @@ const App = () => { await channel.watch(); const hasProductMessage = channel.state.messages.some((message) => - message.attachments?.some(isProductAttachment), + message.attachments?.some( + (attachment) => 'type' in attachment && attachment.type === 'product', + ), ); if (!hasProductMessage) { diff --git a/examples/tutorial/src/optional-custom-attachment-type/layout.css b/examples/tutorial/src/optional-custom-attachment-type/layout.css new file mode 100644 index 0000000000..bfc7f31b58 --- /dev/null +++ b/examples/tutorial/src/optional-custom-attachment-type/layout.css @@ -0,0 +1,58 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* Scoped, and deliberately NOT in a layer - this differs from the published + tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. + The step browser (src/App.tsx) renders every step in one document, and the + steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS + outranks every @layer, so a layered override would silently do nothing here, + and an unscoped one would leak into the other steps. + In your own app, follow the tutorial and keep these in the layer. */ +.step-custom-attachment-type .custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/5-custom-attachment-type/stream-chat.d.ts b/examples/tutorial/src/optional-custom-attachment-type/stream-chat.d.ts similarity index 100% rename from examples/tutorial/src/5-custom-attachment-type/stream-chat.d.ts rename to examples/tutorial/src/optional-custom-attachment-type/stream-chat.d.ts diff --git a/examples/tutorial/src/7-livestream/App.tsx b/examples/tutorial/src/optional-livestream/App.tsx similarity index 95% rename from examples/tutorial/src/7-livestream/App.tsx rename to examples/tutorial/src/optional-livestream/App.tsx index 5ab17f6cbb..afa2633026 100644 --- a/examples/tutorial/src/7-livestream/App.tsx +++ b/examples/tutorial/src/optional-livestream/App.tsx @@ -11,7 +11,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/optional-livestream/layout.css b/examples/tutorial/src/optional-livestream/layout.css new file mode 100644 index 0000000000..424ec464e1 --- /dev/null +++ b/examples/tutorial/src/optional-livestream/layout.css @@ -0,0 +1,58 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* Scoped, and deliberately NOT in a layer - this differs from the published + tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. + The step browser (src/App.tsx) renders every step in one document, and the + steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS + outranks every @layer, so a layered override would silently do nothing here, + and an unscoped one would leak into the other steps. + In your own app, follow the tutorial and keep these in the layer. */ +.step-livestream .custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/tutorial-main.css b/examples/tutorial/src/tutorial-main.css index c6076e0fc9..046ec58089 100644 --- a/examples/tutorial/src/tutorial-main.css +++ b/examples/tutorial/src/tutorial-main.css @@ -1,7 +1,21 @@ +/* Chrome only - deliberately NOT `.tutorial-browser *`, so the SDK's own + box-sizing is left alone. These panes are sized in viewport units *and* + padded, so with the default content-box the padding is added on top of 100vh + and pushes the chat UI (and its composer) below the fold. */ +.tutorial-browser, +.tutorial-browser__sidebar, +.tutorial-browser__main, +.tutorial-browser__header, +.tutorial-browser__preview-card, +.tutorial-browser__step-button { + box-sizing: border-box; +} + .tutorial-browser { - min-height: 100vh; + height: 100vh; width: 100%; display: flex; + overflow: hidden; background: linear-gradient(180deg, #eff5ff 0%, #f7fafc 32%, #eef7f6 100%); } @@ -11,10 +25,7 @@ background: rgba(255, 255, 255, 0.82); backdrop-filter: blur(16px); padding: 24px 20px; - position: sticky; - top: 0; - align-self: start; - height: 100vh; + height: 100%; overflow-y: auto; } @@ -84,7 +95,11 @@ flex-direction: column; padding: 20px; gap: 16px; - min-height: 100vh; + /* Exactly the viewport, not "at least" - the preview card below flexes into + whatever is left after the header, instead of overflowing the window. */ + height: 100%; + min-height: 0; + overflow: hidden; } .tutorial-browser__header { @@ -137,6 +152,14 @@ overflow: hidden; } +/* Step 2 renders bare text (`Chat with client is ready!`) with no + chat chrome, so it lands in the card's 28px corner arc and the first glyph + gets clipped. The other steps fill the corners with the channel header and + composer bars, which round cleanly, so they stay flush. */ +.tutorial-browser__step-shell.step-client-setup { + padding: 24px 28px; +} + .tutorial-browser__step-shell > * { flex: 1 1 auto; min-width: 0; From 4b865aabc6ce8a4210efb5e4a2629f6aff756e8c Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 28 Jul 2026 10:00:24 +0200 Subject: [PATCH 2/4] chore(tutorial): dedupe react so the example app boots `stream-chat-react` is consumed as a workspace dependency, so Vite serves its built output from outside the app's root and resolves that copy's `react` import separately from the app's. The SDK and the app end up on two React instances and the first hook call throws "Invalid hook call", which made the tutorial app fail to boot on a clean install regardless of how it was started. `resolve.dedupe` forces both onto a single copy. --- examples/tutorial/vite.config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/tutorial/vite.config.ts b/examples/tutorial/vite.config.ts index 0466183af6..d9e728778b 100644 --- a/examples/tutorial/vite.config.ts +++ b/examples/tutorial/vite.config.ts @@ -3,4 +3,11 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], + // `stream-chat-react` is consumed as a workspace dependency, so Vite serves + // its built output from outside this app's root and resolves that copy's + // `react` import separately from the app's. Without deduping, the SDK and the + // app end up on two React instances and every hook call throws. + resolve: { + dedupe: ['react', 'react-dom'], + }, }); From 6a67e4447297784cbea9256f7a9a1fe398fc60ef Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 28 Jul 2026 10:17:59 +0200 Subject: [PATCH 3/4] chore(tutorial): make the step stylesheets identical and self-contained Follow-up cleanup on the step CSS. No rendering change. - Drop the per-step `.step- .custom-theme` prefix for `.str-chat.custom-theme`. Channel.tsx and ChannelList.tsx both do `clsx('str-chat', theme, ...)`, so the theme class lands on the same element as `str-chat`; at 0,2,0 this beats the SDK's own `.str-chat` (0,1,0) regardless of source order, and it only matches the steps that actually pass `theme="custom-theme"`, so it still cannot leak into the unthemed steps. The layout.css copies are now byte-identical within each group - two distinct versions, matching the tutorial's two - so drift shows up in a diff. The CSS no longer depends on the browser's wrapper class. - Declare the html/body/#root rules in tutorial-main.css. They were coming only from the step stylesheets, so the chrome silently relied on a step's CSS for `body { margin: 0 }` and would pick up the UA margin if steps were ever loaded lazily or in isolation (2-client-setup has no layout.css at all). - Normalize steps 3 and 4 to the tutorial's 2-space indentation. - README documents which parts of each copy are inert inside the step browser (the custom-theme tokens in the two steps that don't opt in, and the .str-chat__* widths, which lose to tutorial-main.css) and why they stay anyway: the file has to remain a faithful copy of what the tutorial has readers write. --- examples/tutorial/README.md | 50 ++++++++++++++++--- .../src/3-core-component-setup/layout.css | 14 +++--- .../tutorial/src/4-channel-list/layout.css | 14 +++--- examples/tutorial/src/5-theming/layout.css | 18 ++++--- .../src/6-custom-ui-components/layout.css | 18 ++++--- .../tutorial/src/7-emoji-picker/layout.css | 18 ++++--- examples/tutorial/src/App.tsx | 6 +-- .../layout.css | 18 ++++--- .../src/optional-livestream/layout.css | 18 ++++--- examples/tutorial/src/tutorial-main.css | 16 ++++++ 10 files changed, 124 insertions(+), 66 deletions(-) diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index e20a99fda4..15af1b34ca 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -19,7 +19,40 @@ Folder names match the tutorial's step numbers, so `4-channel-list` is the tutor If you change a step's code here, update the matching code block in the tutorial too, and vice versa. -### One deliberate deviation: theme scoping +### `layout.css` is duplicated on purpose + +The tutorial has the reader create a single `src/layout.css` in Step 3 and +rewrite it in Step 5. Each step folder here carries its own copy so the folder is +a self-contained snapshot of the app at that step, which means there are only two +distinct versions of the file: + +| Version | In | +| -------- | -------------------------------------------------------------------------- | +| Step 3's | `3-core-component-setup`, `4-channel-list` | +| Step 5's | `5-theming`, `6-custom-ui-components`, `7-emoji-picker`, both `optional-*` | + +Every file in a group is byte-identical, so any drift shows up in a diff. If you +edit one, edit the whole group. + +Parts of each copy are inert inside the step browser. That is expected, and none +of it should be "cleaned up" here, because the file has to stay a faithful copy of +what the tutorial tells the reader to write: + +- The `custom-theme` tokens do nothing in `7-emoji-picker` and + `optional-livestream`, which don't pass `theme="custom-theme"` to ``. The + reader's single `layout.css` holds the tokens and leaves them unused for those + same two examples. +- The `.str-chat__channel-list` / `__channel` / `__thread` widths are overridden + by `.tutorial-browser__step-shell .str-chat__*` in `tutorial-main.css`, which + wins on specificity (0,2,0 against 0,1,0). The tutorial's widths assume the app + owns the whole page; here it is sized to fit a preview card. +- The `html` / `body` / `#root` rules are real, but `tutorial-main.css` declares + them too, so the chrome does not depend on a step's stylesheet. + +None of this costs bundle size: Vite collapses the identical copies, so the built +CSS contains one `width: 30%` and one `@layer stream`. + +### One deliberate deviation: unlayered theme tokens The tutorial puts the custom theme tokens in a CSS layer: @@ -34,26 +67,27 @@ The tutorial puts the custom theme tokens in a CSS layer: } ``` -The themed steps here instead use an unlayered, step-scoped selector: +The themed steps here declare them unlayered instead: ```css @layer stream; @import 'stream-chat-react/dist/css/index.css' layer(stream); -.step-theming .custom-theme { +.str-chat.custom-theme { /* tokens */ } ``` -Why: the step browser renders every step in a single document, so all eight +Why: the step browser renders every step in a single document, so all seven stylesheets are live at once. Steps 3 and 4 import the SDK stylesheet _unlayered_ (as the tutorial has them, since Step 5 is where you're taught to move it into a layer), and unlayered CSS outranks every `@layer` regardless of -specificity. A layered override would silently do nothing, and an unscoped one -would restyle the earlier steps. +specificity. A layered override would silently do nothing. -The `step-` class is applied by the browser in `src/App.tsx`, on the wrapper -around the active step. +`.str-chat.custom-theme` (specificity 0,2,0) also beats the SDK's own +`.str-chat` (0,1,0) regardless of source order, and it only matches the steps +that actually pass `theme="custom-theme"`, so the themed steps can't leak into +the unthemed ones. **This deviation exists only to make the step browser work. In your own app, follow the tutorial and keep the tokens in the layer.** diff --git a/examples/tutorial/src/3-core-component-setup/layout.css b/examples/tutorial/src/3-core-component-setup/layout.css index c3cf99687a..5fa14209f5 100644 --- a/examples/tutorial/src/3-core-component-setup/layout.css +++ b/examples/tutorial/src/3-core-component-setup/layout.css @@ -1,21 +1,21 @@ html, body, #root { - height: 100%; + height: 100%; } body { - margin: 0; + margin: 0; } #root { - display: flex; + display: flex; } .str-chat__channel-list { - width: 30%; + width: 30%; } .str-chat__channel { - width: 100%; + width: 100%; } .str-chat__thread { - width: 45%; -} \ No newline at end of file + width: 45%; +} diff --git a/examples/tutorial/src/4-channel-list/layout.css b/examples/tutorial/src/4-channel-list/layout.css index c3cf99687a..5fa14209f5 100644 --- a/examples/tutorial/src/4-channel-list/layout.css +++ b/examples/tutorial/src/4-channel-list/layout.css @@ -1,21 +1,21 @@ html, body, #root { - height: 100%; + height: 100%; } body { - margin: 0; + margin: 0; } #root { - display: flex; + display: flex; } .str-chat__channel-list { - width: 30%; + width: 30%; } .str-chat__channel { - width: 100%; + width: 100%; } .str-chat__thread { - width: 45%; -} \ No newline at end of file + width: 45%; +} diff --git a/examples/tutorial/src/5-theming/layout.css b/examples/tutorial/src/5-theming/layout.css index 30a5ae9f24..cacf7577dc 100644 --- a/examples/tutorial/src/5-theming/layout.css +++ b/examples/tutorial/src/5-theming/layout.css @@ -1,14 +1,16 @@ @layer stream; @import 'stream-chat-react/dist/css/index.css' layer(stream); -/* Scoped, and deliberately NOT in a layer - this differs from the published - tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. - The step browser (src/App.tsx) renders every step in one document, and the - steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS - outranks every @layer, so a layered override would silently do nothing here, - and an unscoped one would leak into the other steps. +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. In your own app, follow the tutorial and keep these in the layer. */ -.step-theming .custom-theme { +.str-chat.custom-theme { /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ --str-chat__accent-primary: #0d47a1; @@ -25,7 +27,7 @@ /* Panel backgrounds */ --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ + --str-chat__background-core-app: #c7dafc; /* message list background */ /* Focus ring */ --str-chat__border-utility-focused: #1e40af; diff --git a/examples/tutorial/src/6-custom-ui-components/layout.css b/examples/tutorial/src/6-custom-ui-components/layout.css index fce10f4427..cacf7577dc 100644 --- a/examples/tutorial/src/6-custom-ui-components/layout.css +++ b/examples/tutorial/src/6-custom-ui-components/layout.css @@ -1,14 +1,16 @@ @layer stream; @import 'stream-chat-react/dist/css/index.css' layer(stream); -/* Scoped, and deliberately NOT in a layer - this differs from the published - tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. - The step browser (src/App.tsx) renders every step in one document, and the - steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS - outranks every @layer, so a layered override would silently do nothing here, - and an unscoped one would leak into the other steps. +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. In your own app, follow the tutorial and keep these in the layer. */ -.step-custom-ui-components .custom-theme { +.str-chat.custom-theme { /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ --str-chat__accent-primary: #0d47a1; @@ -25,7 +27,7 @@ /* Panel backgrounds */ --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ + --str-chat__background-core-app: #c7dafc; /* message list background */ /* Focus ring */ --str-chat__border-utility-focused: #1e40af; diff --git a/examples/tutorial/src/7-emoji-picker/layout.css b/examples/tutorial/src/7-emoji-picker/layout.css index 16e5fbb0aa..cacf7577dc 100644 --- a/examples/tutorial/src/7-emoji-picker/layout.css +++ b/examples/tutorial/src/7-emoji-picker/layout.css @@ -1,14 +1,16 @@ @layer stream; @import 'stream-chat-react/dist/css/index.css' layer(stream); -/* Scoped, and deliberately NOT in a layer - this differs from the published - tutorial, which uses `@layer stream-overrides { .custom-theme { ... } }`. - The step browser (src/App.tsx) renders every step in one document, and the - steps before this one import the SDK stylesheet *unlayered*. Unlayered CSS - outranks every @layer, so a layered override would silently do nothing here, - and an unscoped one would leak into the other steps. +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. In your own app, follow the tutorial and keep these in the layer. */ -.step-emoji-picker .custom-theme { +.str-chat.custom-theme { /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ --str-chat__accent-primary: #0d47a1; @@ -25,7 +27,7 @@ /* Panel backgrounds */ --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ + --str-chat__background-core-app: #c7dafc; /* message list background */ /* Focus ring */ --str-chat__border-utility-focused: #1e40af; diff --git a/examples/tutorial/src/App.tsx b/examples/tutorial/src/App.tsx index 1629ac2e86..267f15f350 100644 --- a/examples/tutorial/src/App.tsx +++ b/examples/tutorial/src/App.tsx @@ -150,10 +150,8 @@ const App = () => {
- {/* The `step-` class scopes each step's theme overrides. Every step's - CSS is loaded into this one document, so without a scope the themed - steps would restyle the unthemed ones. See the README section - "One deliberate deviation: theme scoping". */} + {/* The `step-` class lets tutorial-main.css target an individual + step's chrome. Only `step-client-setup` needs it today. */}
Date: Tue, 28 Jul 2026 15:20:43 +0200 Subject: [PATCH 4/4] chore: use non-deprecated api --- examples/tutorial/src/6-custom-ui-components/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tutorial/src/6-custom-ui-components/App.tsx b/examples/tutorial/src/6-custom-ui-components/App.tsx index 2c212f76fb..064857d61b 100644 --- a/examples/tutorial/src/6-custom-ui-components/App.tsx +++ b/examples/tutorial/src/6-custom-ui-components/App.tsx @@ -148,7 +148,7 @@ const App = () => {