|
| 1 | +--- |
| 2 | +name: react-ui-cookbook |
| 3 | +description: |
| 4 | + Write, port or update a recipe for the React v15 docs' UI Cookbooks group, with its code in |
| 5 | + examples/cookbook and its screenshots taken from that app. Use whenever a task adds or changes a |
| 6 | + UI Cookbook recipe, its CSS, its screenshots, the cookbook base app or its demo data, so every |
| 7 | + recipe keeps the same look, cast and style rules. |
| 8 | +--- |
| 9 | + |
| 10 | +# React UI Cookbook |
| 11 | + |
| 12 | +Every UI Cookbook recipe in the React v15 docs (getstream.io, `content/docs/chat-sdk/react/v15-rc/`, |
| 13 | +URLs under `/chat/docs/sdk/react/v15/ui-cookbooks/`) is built on one base app, follows one set of |
| 14 | +style rules, and is shot in one frame with one cast. The runnable code lives here, in |
| 15 | +`examples/cookbook`; the docs page shows that same code. |
| 16 | + |
| 17 | +## The look |
| 18 | + |
| 19 | +- **Theme:** the SDK's default theme, light, with its default font: the base app loads no theme |
| 20 | + of its own, and no recipe changes the theme's colors or font. A recipe's CSS reads the |
| 21 | + `--str-chat__*` tokens, so it follows whatever theme an app sets. Only the Theming recipe |
| 22 | + (`src/recipes/theming/brand-theme-and-dark-mode/`, shown on the UI › Customizing › Theming docs |
| 23 | + page) loads a theme, from its own variants: `theme.css`, a green brand theme in light and dark |
| 24 | + (an emerald accent, `#047857`, on green-tinted surfaces, solid green own bubbles, `#1a7f5a`, |
| 25 | + with white text, links and mentions, rounder bubbles and cards, Plus Jakarta Sans and soft |
| 26 | + green-tinted shadows), with every text at 4.5:1 and every border and icon at 3:1. Change the |
| 27 | + green theme only there. Recipes are shot in the light theme; only the Theming recipe adds dark |
| 28 | + shots. |
| 29 | +- **Frame:** desktop 1280×720 at DPR 1 (`ChatView`: channel list 320px on the left, the open |
| 30 | + channel in the middle, a thread on the right when the recipe needs one). Phone recipes use |
| 31 | + 390×844. A shot with four columns (the channel list, the channel, a thread and the side panel) |
| 32 | + uses 1600×720, set with `viewport` in `shots.json`, so the messages don't wrap word by word. |
| 33 | + Crop each shot to the part the recipe changes, with a little surrounding UI. |
| 34 | +- **Side panel:** a recipe that shows content in a panel (details, a profile, info) opens it in the |
| 35 | + base app's one side panel (`src/base/SidePanel.tsx`), never in a column of its own: call |
| 36 | + `useSidePanel().openSidePanel({ id, title, body })`, or `openUserProfile(userId)` for a profile |
| 37 | + (the SDK's `userProfile` kind). The panel draws the title row and the close button; the recipe |
| 38 | + writes only the body, and its section headings use `.cookbook-side-panel__section-title`. It |
| 39 | + never closes the thread, and opening another panel replaces it: other content and closing |
| 40 | + release the whole slot, so an earlier profile never comes back. The panel moves focus to its |
| 41 | + title on open and gives it back to the opener on close; a recipe adds no focus code of its own. |
| 42 | + A recipe that only changes the message lists or the profile body sets `RecipeConfig.MessageList` |
| 43 | + and `RecipeConfig.sidePanelProps` instead of copying the channels view. The Overview shows its |
| 44 | + code. Likewise, a recipe that adds a view beside the channels view sets `RecipeConfig.views` and |
| 45 | + `RecipeConfig.layouts`, which the base app merges into its own. |
| 46 | +- **Cast:** the user is Cleo; the others are Bob, Cara and Dan, with initials avatars and no |
| 47 | + photos. Channels: Design review, Weekend trip, Book club, Garden plot, General, and a direct |
| 48 | + message with Bob. All of it, with fixed message ids, is in `src/base/demo-data.json`. |
| 49 | +- **Content:** neutral and friendly. The demo app is public: no real names, no personal data, no |
| 50 | + brand names. |
| 51 | +- **Read-only:** the base app blocks every request that would change the demo app (sending, |
| 52 | + editing, deleting, reacting, flagging, pinning, adding members, updating channels); a blocked |
| 53 | + request never settles, so the UI keeps its optimistic state. Reads, watching, typing and the |
| 54 | + user's read state still go through (`src/base/readOnly.ts`). Data changes go in the seed, never |
| 55 | + in a recipe or a screenshot helper. `?writes=on` turns the guard off, for an app of your own. |
| 56 | + |
| 57 | +## Style rules |
| 58 | + |
| 59 | +`yarn workspace @stream-io/stream-chat-react-cookbook check:css` checks them: literal colors and |
| 60 | +sizes, `var()`s of anything but `--str-chat__` tokens, CSS outside the layer, the prefix, and |
| 61 | +layout properties on SDK markup. It can't judge whether a token fits; review that by eye. |
| 62 | + |
| 63 | +1. **Tokens only.** Colors, spacing, radii and type come from `--str-chat__*` semantic tokens: |
| 64 | + `--str-chat__text-secondary`, `--str-chat__spacing-sm`, `--str-chat__radius-md`, |
| 65 | + `--str-chat__font-caption-default`, … No hex, `rgb()` or named colors, and no `px`/`rem`/`em` |
| 66 | + for spacing, radius or type. Border widths and component sizes may use `px`. |
| 67 | +2. **One layer.** All recipe CSS sits inside `@layer stream-overrides { … }`. The base app declares |
| 68 | + the order (`modern-normalize, stream, cookbook-base, stream-overrides`), so a recipe never needs |
| 69 | + extra specificity or `!important`. |
| 70 | +3. **One prefix.** Classes a recipe adds start with `.cookbook-`, followed by a name for the part it |
| 71 | + styles, for example `.cookbook-pinned-by` or `.cookbook-channel-header__title`. SDK classes |
| 72 | + (`.str-chat…`) may be restyled. |
| 73 | +4. **Leave the SDK's layout alone.** Never set `display`, `position`, `flex`, `grid`, `float` or |
| 74 | + offsets on a rule whose target is an SDK class. Style your own markup, or change sizes through a |
| 75 | + component variable such as `--str-chat__channel-list-width`. Where the SDK has no variable, a |
| 76 | + comment starting `cookbook-allow-layout:` right above the rule says why; known case: the width of |
| 77 | + a thread beside a channel. |
| 78 | + |
| 79 | +## Where things are |
| 80 | + |
| 81 | +``` |
| 82 | +examples/cookbook/ |
| 83 | + src/base/ the base app (BaseApp.tsx), its CSS (base.css), client, demo data, |
| 84 | + the side panel (SidePanel.tsx), URL helpers (OpenFromUrl: |
| 85 | + ?channel=&thread=; Simulate: ?simulate=typing&as=), and useClickInTurn |
| 86 | + for screenshot helpers that click through the UI |
| 87 | + src/recipes/<group>/<recipe>/ |
| 88 | + *.tsx, *.css exactly the recipe code the docs page shows |
| 89 | + <variant>.recipe.tsx default-exports a RecipeConfig: what the recipe changes in the base app |
| 90 | + (ChannelHeader, Wrapper, MessageList, sidePanelProps, views and |
| 91 | + layouts merged into the base ones, chatViewProps, chatViewChildren, |
| 92 | + chatProps, openFromUrl, theme), and imports the recipe's CSS |
| 93 | + shots.json the screenshots the docs page uses (format in scripts/shots.mjs) |
| 94 | + demo-data.json optional: users and channels only this recipe needs, in the shape of |
| 95 | + src/base/demo-data.json; seeded and listed with the base data |
| 96 | + scripts/seed.mjs creates or brings back in line the cast, channels, members, messages, |
| 97 | + replies and pins; sets a message's attachments, mentions and custom |
| 98 | + fields after sending it; sets read state and receipts, invites, and |
| 99 | + the current user's pinned and archived channels, reminders, saved |
| 100 | + messages and drafts (fields below) |
| 101 | + scripts/shots.mjs takes every shot from the running app, writes them to --out |
| 102 | + scripts/check-css.mjs the style-rule check |
| 103 | +``` |
| 104 | + |
| 105 | +`<group>` and `<recipe>` match the docs URL: `/ui-cookbooks/headers/channel-header/` lives in |
| 106 | +`src/recipes/headers/channel-header/`. A page with two solutions has two variants, for example |
| 107 | +`css.recipe.tsx` and `component.recipe.tsx`. Open a recipe at |
| 108 | +`http://localhost:5176/?recipe=<group>/<recipe>&variant=<variant>`; `?recipe=base` is the base app |
| 109 | +alone, and no parameter lists every recipe. |
| 110 | + |
| 111 | +## Adding or changing a recipe |
| 112 | + |
| 113 | +1. Read the recipe's docs page and the pages it links to. Check every API it uses against |
| 114 | + `stream-chat-react` `master` and `stream-chat` `release-v10`. |
| 115 | +2. Write the code in `src/recipes/<group>/<recipe>/`, following the style rules, and add a |
| 116 | + `<variant>.recipe.tsx` for each solution the page shows. |
| 117 | +3. Put demo data only this recipe needs in its folder's `demo-data.json` (new ids, never reuse |
| 118 | + one); change `src/base/demo-data.json` only for data every recipe shares. A message may carry |
| 119 | + `attachments` (for example image URLs), which the seed sets after sending, since the demo app |
| 120 | + refuses uploads. A message may also carry `mentions` (user ids), which the seed sets after |
| 121 | + sending, since the demo app refuses a new message with mentions. A message may carry |
| 122 | + `reminderForCurrentUser`: `{}` saves it for later, `{ "remindAt": "<ISO time>" }` sets a fixed |
| 123 | + time, and `{ "remindAt": "+2m" }` a time that many minutes after the first seed, kept on later |
| 124 | + runs, which is how a reminder becomes overdue (the server refuses a past time). The demo app's |
| 125 | + `messaging` type has `user_message_reminders` off, so the seed reports that and skips them. |
| 126 | + A channel may carry `draftsForCurrentUser`, the current user's unsent drafts in it: |
| 127 | + `{ "text" }` for the channel's draft, with `"parent"` (a main-list message id) for a thread's |
| 128 | + draft and `"quotedMessage"` (a message id) for one that quotes a message. Each is saved only |
| 129 | + when the stored draft differs. A draft in a base channel would show in that channel's composer |
| 130 | + in every recipe, so drafts go in the recipe's own channels. |
| 131 | + |
| 132 | + The other fields the seed reads, each set only when the stored state differs: |
| 133 | + - On a channel: `createdBy` (a cast id that creates it instead of the current user), |
| 134 | + `invites` (user ids left with a pending invite), `readBy` and `deliveredTo` (members who |
| 135 | + read or received its last message), `readUpTo` (`{ userId: messageId }`), `unreadFrom` (a |
| 136 | + message id the current user's unread state starts at), and `pinnedForCurrentUser` / |
| 137 | + `archivedForCurrentUser`. |
| 138 | + - On a message: `pinnedBy` (its author), `custom` (custom fields, such as |
| 139 | + `{ "ai_generated": true }`), and `unreadForCurrentUser` on a thread's parent (its latest |
| 140 | + reply stays unread). |
| 141 | + |
| 142 | + Then run `seed`. |
| 143 | +4. List the shots in `shots.json`, reusing the file names the page already references. |
| 144 | +5. Run the checks and the shots (below), then look at every image. |
| 145 | +6. Copy the code into the docs page so it matches the folder exactly, formatted with the docs' |
| 146 | + prettier (double quotes, width 80), apart from the base app's setup, which the page either |
| 147 | + shows in full or links to on the UI Cookbooks Overview. Two rewrites are allowed: imports of |
| 148 | + the shared side panel and the New conversation dialog become `./SidePanel` and |
| 149 | + `./NewConversationDialog`, and the `cookbook-allow-layout:` prefix is dropped from comments. |
| 150 | +7. In getstream.io, copy the images into `content/docs/chat-sdk/react/v15-rc/_assets/` and run the |
| 151 | + docs checks. |
| 152 | + |
| 153 | +## The docs page |
| 154 | + |
| 155 | +Every recipe page has the same shape: |
| 156 | + |
| 157 | +1. What it builds: a one- or two-sentence intro, then the features in a note: `<Admonition |
| 158 | + type="note">` that opens with the bold line **What you'll build**, then one short bullet per |
| 159 | + feature: what the user gets, never how it's done (the steps cover that). Then the main |
| 160 | + screenshot, captioned **Preview**. |
| 161 | +2. Which level it uses, from the Overview's order: restyle, replace one part, compose, or change |
| 162 | + behaviour in the state layer. |
| 163 | +3. "Start from the base app", linking to the Overview. |
| 164 | +4. The steps, each with its code and, where it helps, a screenshot next to it. |
| 165 | +5. How it works. |
| 166 | +6. Related pages. |
| 167 | + |
| 168 | +Every screenshot has a caption: a short bold line right above the image, **Preview** for the main |
| 169 | +one and a few words saying what the image shows for the others (the site can't render image |
| 170 | +captions below an image). Each captioned screenshot sits between two horizontal rules (`---`, with a |
| 171 | +blank line on each side); consecutive screenshots share the rule between them. |
| 172 | + |
| 173 | +A component and its stylesheet go in one `<Tabs>` block, the code tab labelled `React` and the |
| 174 | +stylesheet tab `CSS`, each with its `title` (see the Pin indicator page). |
| 175 | + |
| 176 | +Docs prose follows the v15 docs conventions: name methods as `Class.method()`, name every field with |
| 177 | +its owner (`channel.state.typing`), say "paginator" for the object that loads items, and put example |
| 178 | +scenarios in an `<Admonition type="info">` titled **Example**. |
| 179 | + |
| 180 | +## Commands |
| 181 | + |
| 182 | +Run from the repository root unless noted. |
| 183 | + |
| 184 | +```bash |
| 185 | +yarn install && yarn build # the SDK the cookbook imports |
| 186 | +yarn workspace @stream-io/stream-chat-react-cookbook seed # demo data (idempotent) |
| 187 | +yarn workspace @stream-io/stream-chat-react-cookbook dev # http://localhost:5176 |
| 188 | +yarn workspace @stream-io/stream-chat-react-cookbook shots --out <getstream.io>/content/docs/chat-sdk/react/v15-rc/_assets [--only <group>/<recipe>] |
| 189 | +yarn workspace @stream-io/stream-chat-react-cookbook check:css |
| 190 | +yarn workspace @stream-io/stream-chat-react-cookbook build # type-check + build, as CI does |
| 191 | +``` |
| 192 | + |
| 193 | +`shots` needs the dev server running and Playwright's Chromium (`npx playwright install chromium`). |
| 194 | +A relative `--out` is resolved from the repository root; an absolute path is safest. Shots use the `en-US` locale |
| 195 | +and the UTC time zone, so dates look the same on every machine. File names must be unique across |
| 196 | +all recipes. |
| 197 | + |
| 198 | +## Done when |
| 199 | + |
| 200 | +- [ ] `check:css` passes. |
| 201 | +- [ ] `build` passes, and `npx eslint --max-warnings 0` and `npx prettier --check` pass for the |
| 202 | + changed files. |
| 203 | +- [ ] Every shot in `shots.json` is taken, looked at, and shows what the page says. |
| 204 | +- [ ] The docs page's code matches the recipe folder. |
| 205 | +- [ ] In getstream.io: the docs checks and the sample checker pass on the page, and the site builds. |
| 206 | +- [ ] Anything that only renders wrong because of the SDK is reported, not hidden with CSS. |
0 commit comments