first commit

This commit is contained in:
2026-07-17 21:48:37 +05:30
commit f85599dac5
124 changed files with 10775 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
# Architecture
## The one idea
Three product surfaces — **Messaging**, **Inbox**, **Support** — are built on **one kernel**:
a headless React SDK talking to a **BFF**. Everything a user sees is a projection of the same
data model, exactly like the IIOS "everything is an interaction" spine (see
[`IIOS_INTEGRATION.md`](IIOS_INTEGRATION.md)).
```
┌──────────────────────────────────────────────────────────────────────┐
│ Browser (Next.js app · apps/web) │
│ │
│ Components (nav / message / inbox / support / overlays) │
│ │ use hooks, never fetch directly │
│ ┌────▼───────────────────────────────────────────┐ │
│ │ @lynkd/messaging-inbox-sdk │ │
│ │ Provider · hooks · theme · feature flags │ │
│ │ BffClient ────────────────┐ │ │
│ └──────────────────────────────┼───────────────────┘ │
└──────────────────────────────────┼─────────────────────────────────────┘
│ fetch /api/bff/** (the ONLY seam)
┌──────────────────────────────────▼─────────────────────────────────────┐
│ BFF (Next.js route handlers · apps/web/app/api/bff/**) │
│ bootstrap · channels · messages · threads · reactions · inbox · │
│ support/tickets · search · notifications │
│ │ │
│ ┌────▼─────────────────────────┐ (swap this box for real) │
│ │ Mock store (in-memory) │ ──► IIOS service / NestJS / Supabase │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
```
## Layers
### 1. SDK — `packages/messaging-inbox-sdk`
Framework-agnostic-ish React layer (needs React 18/19). No knowledge of Next.js, no backend
coupling.
- **`types.ts`** — the domain model. `User`, `Channel` (channel/private/dm/group_dm),
`Message` (+ parts, reactions, attachments, thread metadata), `InboxItem`, `Ticket`,
`Notification`, `SearchResult`, `Bootstrap`.
- **`config.ts`** — `FeatureFlags` + `ThemeConfig` + `resolveConfig()` (defaults → env → props).
- **`theme.ts`** — turns a `ThemeConfig` into CSS variables and applies them to `<html>`.
- **`client.ts`** — `BffClient`: one typed method per BFF endpoint. **The only place that
does `fetch`.**
- **`provider.tsx`** — `<MessagingInboxProvider>` holds config, the client, bootstrap data,
and theme/scheme state. Exposes `useSdk`, `useFeature(s)`, `useTheme`.
- **`hooks.ts`** — data hooks: `useChannels`, `useMessages`, `useThread`, `useInbox`,
`useTickets`, `useTicket`, `useSearch`, `useNotifications`, `useAvailability`, `useTyping`.
### 2. BFF — `apps/web/app/api/bff/**`
Next.js **Route Handlers**. This *is* the Backend-For-Frontend: it shapes data specifically
for this UI, keeps secrets/tokens server-side, and decouples the client from the real backend.
Today each handler calls the in-memory **mock store**; each maps 1:1 to a `BffClient` method.
### 3. Mock store — `apps/web/lib/mock/`
`data.ts` is the seed (a full LynkedUp-flavoured workspace). `store.ts` is a mutable singleton
(persists across requests in one Node process) implementing send/react/pin/save/patch/reply/
search. **This is the box you replace to go real** — nothing above it changes.
### 4. App shell + components — `apps/web/components`
- `nav/``WorkspaceRail` (Slack-style far-left), `Sidebar` (sectioned channels + user card),
`Header` (title + search + theme + notifications + user).
- `message/``ChannelView`, `MessageItem`, `Composer`, `ThreadPanel`, `EmojiPicker`.
- `inbox/``InboxView`.
- `support/``SupportView` (master-detail console).
- `overlays/``CommandPalette`, `NotificationsPanel`, `ProfileDrawer`, `ThemingPanel`,
`HuddleBar`.
- `ui/primitives.tsx``Avatar`, `PresenceDot`, `CountBadge`, `Chip`, `IconButton`.
- `AppShell.tsx` composes rail + sidebar + main + thread panel + overlays.
## Request lifecycle (send a message)
1. `Composer` calls `send(body)` from `useMessages(channelId)`.
2. The hook calls `client.sendMessage(channelId, body)``POST /api/bff/channels/:id/messages`.
3. The route handler calls `store.send(...)`, which appends a `Message` and bumps thread/channel
metadata, and returns it.
4. The hook appends the returned message to local state → it renders instantly.
Reactions, pins, saves, inbox transitions, and ticket replies follow the same shape.
## Config lifecycle (flags + theme)
1. `app/layout.tsx` (server) calls `getServerConfig()``resolveConfig({ env: process.env })`.
2. The resolved **plain** `SdkConfig` is passed to `<Providers config=…>` (client) and the
theme is inlined into `<head>` for a flash-free first paint.
3. `MessagingInboxProvider` applies theme vars to `<html>`, manages light/dark, and exposes
flags via `useFeature`.
## Why these choices
See [`../.claude/memory/architecture-decisions.md`](../.claude/memory/architecture-decisions.md)
for the rationale (workspace layout, BFF seam, server-resolved config, CSS-variable theming,
data-driven flags, and why the NestJS/DB backend is documented rather than built for this demo).
+78
View File
@@ -0,0 +1,78 @@
# The BFF (Backend-For-Frontend)
The UI never talks to a real backend. It calls **its own BFF** — Next.js Route Handlers under
`apps/web/app/api/bff/**`. This is the seam that keeps the frontend independent of whatever
backend you eventually run (IIOS service, a NestJS BFF, Supabase, …).
## Why a BFF
- **One shaped API per frontend.** Endpoints return exactly what the UI needs, nothing more.
- **Secrets stay server-side.** Tokens/keys for the real backend live in the route handlers,
never in the browser bundle.
- **Backend independence.** Change the handlers; the SDK + components don't move.
## Endpoint contract
Base path: `/api/bff`. All JSON. (These mirror the `BffClient` methods in the SDK.)
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| GET | `/bootstrap` | — | `{ me, workspace, workspaces, users, sections, channels }` |
| GET | `/channels` | — | `{ channels }` |
| GET | `/channels/:id/messages` | — | `{ messages }` |
| POST | `/channels/:id/messages` | `{ body, parentId?, scheduledFor? }` | `{ message }` (201) |
| GET | `/channels/:id/threads/:parentId` | — | `{ parent, replies }` |
| POST | `/channels/:id/messages/:mid/reactions` | `{ emoji }` | `{ message }` |
| POST | `/channels/:id/messages/:mid/pin` | — | `{ message }` |
| POST | `/channels/:id/messages/:mid/save` | — | `{ message }` |
| GET | `/inbox` | `?state=OPEN\|SNOOZED\|DONE\|ARCHIVED` | `{ items }` |
| PATCH | `/inbox/:id` | `{ state }` | `{ item }` |
| GET | `/support/tickets` | `?scope=mine\|assigned\|all` | `{ tickets }` |
| GET | `/support/tickets/:id` | — | `{ ticket }` |
| PATCH | `/support/tickets/:id` | `{ state }` | `{ ticket }` |
| POST | `/support/tickets/:id/reply` | `{ body }` | `{ ticket }` |
| GET | `/support/availability` | — | `{ agents }` |
| GET | `/search` | `?q=` | `{ results }` |
| GET | `/notifications` | — | `{ notifications }` |
## The mock store
`apps/web/lib/mock/store.ts` is a module singleton seeded from `data.ts`. It persists across
requests in one Node process (resets on restart). Each handler is a thin adapter:
```ts
// app/api/bff/channels/[id]/messages/route.ts
export async function POST(req, { params }) {
const { id } = await params;
const { body, parentId, scheduledFor } = await req.json();
const message = store.send(id, body, { parentId, scheduledFor });
return NextResponse.json({ message }, { status: 201 });
}
```
## Swapping in a real backend
Two options:
**A. Keep the BFF inside Next (recommended).** Replace the `store.*` calls with `fetch()` to
your backend inside each handler. Add auth there. Example against IIOS:
```ts
// app/api/bff/channels/[id]/messages/route.ts
export async function GET(_req, { params }) {
const { id } = await params;
const token = await getIiosToken(); // server-side only
const res = await fetch(`${process.env.IIOS_URL}/v1/threads/${id}/messages`, {
headers: { authorization: `Bearer ${token}` },
});
const data = await res.json();
return NextResponse.json({ messages: data.messages.map(toSdkMessage) }); // map shapes
}
```
**B. Standalone NestJS BFF.** Point the SDK at it with `NEXT_PUBLIC_BFF_BASE_URL=https://bff…`
and implement the same 17 endpoints in NestJS. The `BffClient` prefixes `/api/bff`, so mount
your Nest routes under that prefix (or adjust `client.ts`).
Either way the SDK and every component are unchanged. See
[`IIOS_INTEGRATION.md`](IIOS_INTEGRATION.md) for the full mapping.
+83
View File
@@ -0,0 +1,83 @@
# Feature flags
Every feature is gated by a flag. Flags default **on**. Override per-flag with an env var, or
per-mount with a `flags` prop on `<MessagingInboxProvider>`.
## How it works
- Env var name = `NEXT_PUBLIC_FEATURE_` + the flag key in `UPPER_SNAKE_CASE`.
e.g. `scheduledSend``NEXT_PUBLIC_FEATURE_SCHEDULED_SEND`.
- Accepted truthy values: `true`, `1`, `on`, `yes`. Falsy: `false`, `0`, `off`, `no`.
- Resolution order (last wins): built-in defaults → env → Provider props.
- In components: `const on = useFeature("huddles")` or `const flags = useFeatureFlags()`.
## The flags
### Top-level surfaces
| Flag | Env | Effect when off |
|---|---|---|
| `messaging` | `NEXT_PUBLIC_FEATURE_MESSAGING` | Hides channels/DMs in the sidebar & rail |
| `inbox` | `NEXT_PUBLIC_FEATURE_INBOX` | Removes the Inbox nav + queue |
| `support` | `NEXT_PUBLIC_FEATURE_SUPPORT` | Removes the Support Center surface |
| `activity` | `NEXT_PUBLIC_FEATURE_ACTIVITY` | Reserved activity feed |
### Messaging sub-features
| Flag | Effect when off |
|---|---|
| `threads` | No "reply in thread", no thread panel/summaries |
| `reactions` | No emoji reactions or the reaction picker |
| `mentions` | No @-mention button/highlighting |
| `pins` | No pin action or pinned bar |
| `savedItems` | No save/bookmark action or Saved page |
| `fileUpload` | Hides the attach button |
| `richComposer` | Composer drops its formatting toolbar |
| `slashCommands` | Hides the `/` command affordance |
| `scheduledSend` | Removes schedule-send menu + Drafts |
| `drafts` | Hides Drafts & sent |
| `huddles` | Removes huddle/call buttons + the huddle bar |
| `presence` | Hides presence dots |
| `typingIndicator` | No "… is typing" |
| `readReceipts` | (reserved) |
| `emojiPicker` | Removes emoji picker buttons |
| `channelBrowser` | Hides "Add channels" / browse |
| `directMessages` | Hides the DM section |
| `groupDms` | (reserved) treats group DMs like channels |
### Support sub-features
| Flag | Effect when off |
|---|---|
| `supportTickets` | (reserved) core tickets always render in Support |
| `supportEscalate` | Removes the Escalate action |
| `supportCallback` | Removes "Request callback" |
| `supportAvailability` | Removes the online/offline availability pill |
### Global
| Flag | Effect when off |
|---|---|
| `search` | Removes the header search + palette trigger |
| `commandPalette` | (reserved) ⌘K behaviour |
| `notifications` | Removes the bell + panel |
| `themeToggle` | Removes the light/dark toggle |
| `themingPanel` | Removes the live Appearance panel |
| `workspaceSwitcher` | Removes the far-left workspace rail switcher |
| `aiAssist` | (reserved) AI assist affordances |
## Examples
```bash
# A lean messaging-only build
NEXT_PUBLIC_FEATURE_SUPPORT=false
NEXT_PUBLIC_FEATURE_INBOX=false
NEXT_PUBLIC_FEATURE_HUDDLES=false
NEXT_PUBLIC_FEATURE_SCHEDULED_SEND=false
```
```tsx
// Per-mount override (e.g. embed a read-only chat)
<MessagingInboxProvider flags={{ richComposer: false, huddles: false, support: false }}>
</MessagingInboxProvider>
```
Add a new flag: extend `FeatureFlags` + `defaultFlags` in
`packages/messaging-inbox-sdk/src/config.ts`; the env mapping is automatic.
+134
View File
@@ -0,0 +1,134 @@
# Wiring the demo to the real IIOS service
This app talks to a pluggable **BFF backend**. Today it defaults to the in-memory mock; an
**IIOS adapter is implemented** and switches on with one env var. This doc explains what's built,
how to turn it on, and what still needs IIOS-side endpoints.
> Reference: `D:\iios\docs\IIOS_API_AND_SDK_GUIDE.md` (endpoints, auth, DTOs).
## The seam
Every BFF route calls `getBackend()` — never the store directly. `BFF_BACKEND` picks the impl:
```
apps/web/lib/backend/
Backend.ts the interface every route uses (all async)
mock.ts MockBackend — wraps the in-memory store (default)
iios.ts IiosBackend — proxies the real IIOS service
index.ts getBackend() → mock | iios (from BFF_BACKEND)
apps/web/lib/iios/
client.ts server-only IIOS REST client (dev-token auth, lazy token mint)
map.ts IIOS → SDK type mappers (toMessage / toInboxItem / toTicket)
```
The SDK, hooks, and every UI component are unchanged between mock and IIOS — all translation lives
in `lib/iios/map.ts`.
## Turn it on
1. **Run IIOS** (from `D:\iios`, needs Docker for Postgres):
```bash
docker compose up -d # Postgres on :5434
pnpm install
pnpm -F @insignia/iios-service prisma:generate
pnpm -F @insignia/iios-service prisma:migrate
cd packages/iios-service && IIOS_DEV_TOKENS=1 pnpm start # → :3200
```
2. **Point this app at it** (`apps/web/.env.local`):
```bash
BFF_BACKEND="iios"
IIOS_URL="http://localhost:3200"
IIOS_APP_ID="portal-demo"
IIOS_USER_ID="james"
IIOS_ORG_ID="org_A"
```
3. `pnpm dev` → the BFF now mints a dev token and proxies IIOS. `IIOS_*` are **server-only**
(no `NEXT_PUBLIC_`), so the token never reaches the browser.
## ✅ Verified live (no Docker required)
The IIOS path has been run **end-to-end** against a real IIOS service + real Postgres, and the
round-trip through this app's BFF was confirmed (a real ingested message came back through
`GET /api/bff/channels/:threadId/messages`, and `/api/bff/support/tickets` returned IIOS's real
data instead of the mock set). Docker isn't needed — a native PostgreSQL works.
Exact steps that worked on Windows (PostgreSQL 18 already installed):
```bash
# 1. Create the iios role + database on the existing Postgres (port 5432)
# (as the postgres superuser)
psql -U postgres -c "CREATE ROLE iios LOGIN PASSWORD 'iios' CREATEDB;"
psql -U postgres -c "CREATE DATABASE iios OWNER iios;"
# 2. Point IIOS at it — packages/iios-service/.env
# DATABASE_URL="postgresql://iios:iios@localhost:5432/iios?schema=public"
# (the repo default is :5434 via docker-compose; :5432 is fine too)
# JWT_SECRET=... · APP_SECRETS={"portal-demo":"dev-secret"} · IIOS_DEV_TOKENS=1
# 3. From D:\iios — allow prisma/esbuild builds in pnpm-workspace.yaml, then:
pnpm install
pnpm -r build
node packages/iios-service/node_modules/prisma/build/index.js generate --schema packages/iios-service/prisma/schema.prisma
node packages/iios-service/node_modules/prisma/build/index.js migrate deploy --schema packages/iios-service/prisma/schema.prisma
node packages/iios-service/dist/main.js # → iios-service listening on :3200
# 4. Point THIS app at it — apps/web/.env.local
BFF_BACKEND=iios
IIOS_URL=http://localhost:3200
# then `pnpm dev` (restart) — the BFF now mints a dev token and proxies IIOS.
```
> The mapper in `map.ts` was validated against the real IIOS message shape
> (`{interactionId, actorId, occurredAt, parts:[{kind,bodyText}]}`). A **fresh IIOS DB is empty**
> and IIOS has no channel-directory endpoint, so `bootstrap`/`listChannels` stay on mock (the demo
> keeps its rich sidebar) while messages/inbox/tickets read from IIOS. Seed IIOS (via
> `POST /v1/interactions/ingest` or its smoke scripts) to see live data flow through.
## What's wired vs. delegated
| BFF method | IIOS backing | Notes |
|---|---|---|
| `channelMessages` / `thread` | `GET /v1/threads/:id/messages` | maps parts→body, parent links |
| `send` | `POST /v1/threads/:id/messages` (+ `idempotency-key`) | |
| `listInbox` / `patchInbox` | `GET`/`PATCH /v1/inbox/items` | |
| `listTickets` / `getTicket` / `patchTicket` | `/v1/support/tickets` | `getTicket` lists+finds (no single-GET documented) |
| `replyTicket` | `POST` to the ticket's linked thread | |
| `bootstrap` / `listChannels` / `createChannel` | **delegated to mock** | IIOS has no portal/channel-directory endpoint yet; build one, or compose from `/v1/threads` |
| `react` / `pin` / `save` | **delegated to mock** | IIOS has no reaction/pin/save endpoints; keep local or add them |
| `search` / `notifications` / `availability` | **delegated to mock** | add IIOS endpoints then swap |
Delegating keeps the UI fully functional while you fill the gaps — each is a small, isolated change
in `iios.ts`.
## Auth
`lib/iios/client.ts#mintToken()` calls `POST /v1/dev/token` and caches the JWT (~100 min). For
production, replace `mintToken()` with a real Session-Broker exchange (PPT), keep it server-side,
and attach `Authorization: Bearer …` exactly as now.
## Shape mapping
Keep this app's `types.ts` as the UI contract; translate in `map.ts` (already defensive about field
names). Example:
```ts
export function toMessage(m: any, channelId: string): Message {
const text = m.content ?? m.body ?? m.parts?.find(p => (p.kind ?? p.partType) === "TEXT")?.bodyText ?? "";
return { id: String(m.id ?? m.interactionId), channelId,
authorId: String(m.senderActorId ?? m.actorId ?? "unknown"),
ts: Date.parse(m.sentAt ?? m.occurredAt) || Date.now(),
body: String(text), parentId: m.parentInteractionId ?? null, reactions: [], deliveryState: "sent" };
}
```
## Tenancy & safety (carried by IIOS)
IIOS enforces tenant isolation (cross-tenant → 403), fail-closed policy/consent gates, and
idempotency. The BFF just forwards the scoped token — don't reimplement those client-side. The UI's
optimistic updates already tolerate a rejected write (surface an error, refetch).
## Realtime (optional upgrade)
Swap the mock `useTyping` + refetch for the IIOS Socket.IO client (`iios-kernel-client`'s
`MessageSocket`) and feed `message`/`receipt` events into the same hook state. The component tree
doesn't change.
+93
View File
@@ -0,0 +1,93 @@
# SDK reference — `@lynkd/messaging-inbox-sdk`
Headless React building blocks for messaging, inbox and support. Wrap your tree in the
provider, then use the hooks. Bring your own UI, or reuse the demo components in
`apps/web/components`.
## Install & provider
```tsx
import { MessagingInboxProvider } from "@lynkd/messaging-inbox-sdk";
<MessagingInboxProvider
bffBaseUrl="" // "" = same-origin /api/bff
brandName="LynkedUp Pro"
theme={{ mode: "dark", accent: "#FDA913", radiusCard: "20px" }}
flags={{ huddles: true, support: true }}
>
<App />
</MessagingInboxProvider>
```
Prefer resolving config on the server and passing a single `config` prop (as the demo does via
`getServerConfig()`), so env-driven flags/theme reach the client as plain JSON.
## Context hooks
| Hook | Returns |
|---|---|
| `useSdk()` | `{ config, flags, client, me, users, channels, userById, channelById, scheme, setScheme, toggleScheme, theme, setThemeOverrides, resetTheme, loading, refresh }` |
| `useFeature(name)` | `boolean` for one flag |
| `useFeatureFlags()` | the whole `FeatureFlags` object |
| `useTheme()` | `{ scheme, setScheme, toggleScheme, theme, setThemeOverrides, resetTheme }` |
## Data hooks
| Hook | Purpose |
|---|---|
| `useChannels()` | `{ all, starred, channels, dms, totalUnread, totalMentions, me }` |
| `useChannel(id)` | one `Channel` |
| `useMessages(channelId)` | `{ messages, pinned, send, react, pin, save, reload, loading }` |
| `useThread(channelId, parentId)` | `{ parent, replies, reply, reload }` |
| `useTyping(channelId)` | `ID[]` currently typing (mock) |
| `useInbox(state?)` | `{ items, state, setState, markDone, snooze, archive, reload }` |
| `useTickets(scope?)` | `{ tickets, patch, reload }` |
| `useTicket(id)` | `{ ticket, reply, setState, reload }` |
| `useAvailability()` | `AgentAvailability[]` |
| `useSearch()` | `{ query, setQuery, results, loading }` (debounced) |
| `useNotifications()` | `{ notifications, unread }` |
## Minimal chat example
```tsx
import { useChannels, useMessages } from "@lynkd/messaging-inbox-sdk";
function Chat() {
const { channels } = useChannels();
const active = channels[0]?.id ?? null;
const { messages, send } = useMessages(active);
return (
<>
<ul>{messages.map((m) => <li key={m.id}>{m.body}</li>)}</ul>
<form onSubmit={(e) => { e.preventDefault();
const f = e.currentTarget.elements.namedItem("t") as HTMLInputElement;
send(f.value); f.value = ""; }}>
<input name="t" placeholder="Message…" />
</form>
</>
);
}
```
## Direct client (no React)
```ts
import { BffClient } from "@lynkd/messaging-inbox-sdk";
const client = new BffClient(""); // base URL
const { channels } = await client.listChannels();
await client.sendMessage(channels[0].id, "Hi");
```
## Types
Import any domain type: `User, Channel, Message, RichBlock, Attachment, Reaction, InboxItem,
Ticket, CallbackRequest, AgentAvailability, Notification, SearchResult, Bootstrap, PresenceState,
ChannelKind, InboxKind, InboxState, TicketState, TicketPriority` — plus config types
`FeatureFlags, ThemeConfig, ThemeTokens, SdkConfig`.
## Publishing the SDK
The package is workspace-consumed as TS source. To publish standalone, add a build step (e.g.
`tsup src/index.ts --dts --format esm,cjs`) and point `main`/`module`/`types` at `dist/`. The
public surface is the `index.ts` barrel.
+101
View File
@@ -0,0 +1,101 @@
# Local setup — step by step
A complete, from-scratch guide to running this demo on your machine. No database, no external
services, no accounts. It's a static demo: mock data ships in code and every feature works.
## 1. Prerequisites
| Tool | Version | Check |
|---|---|---|
| Node.js | ≥ 20 (tested on 24) | `node -v` |
| pnpm | ≥ 8 (tested on 11) | `pnpm -v` |
| Git | any | `git --version` |
Don't have pnpm? `npm install -g pnpm` (or `corepack enable`).
## 2. Install
```bash
cd message-inbox-web-frontend-sdk
pnpm install
```
This installs the workspace (the SDK in `packages/` + the app in `apps/web`). First install pulls
Next.js/React/Tailwind — on a slow connection it can take a few minutes.
> **If install or `pnpm dev` fails with a corepack error** (e.g. "Server answered with HTTP 404 …
> pnpm-X.Y.Z.tgz"): this repo intentionally has **no** `packageManager` pin, so just use your local
> pnpm. If your shell forces corepack, run `corepack disable` once, then `pnpm install` again.
>
> **If install times out on a large tarball** (slow network), retry with a longer timeout:
> `pnpm install --fetch-timeout 2400000 --fetch-retries 10 --network-concurrency 2`.
## 3. Run
```bash
pnpm dev
```
Open **http://localhost:4300**. You land in `#general` with a full mock workspace.
To run only the web app: `pnpm --filter @lynkd/web dev`.
To change the port: edit `dev`/`start` scripts in `apps/web/package.json` (`next dev -p 4300`).
## 4. Configure (optional)
Everything works with defaults. To customize, copy the example env and edit:
```bash
cp apps/web/.env.example apps/web/.env.local
```
- **Feature flags** — `NEXT_PUBLIC_FEATURE_*` (all default on). e.g. `NEXT_PUBLIC_FEATURE_SUPPORT=false`.
- **Theme** — `NEXT_PUBLIC_THEME_MODE`, `NEXT_PUBLIC_THEME_ACCENT`, `NEXT_PUBLIC_THEME_RADIUS_CARD`, …
- **Branding** — `NEXT_PUBLIC_BRAND_NAME`.
Restart `pnpm dev` after changing env. Full references: [`FEATURE_FLAGS.md`](FEATURE_FLAGS.md),
[`THEMING.md`](THEMING.md).
## 5. Two-minute demo tour
1. **Send** a message in `#general` (rich composer: select text → **Bold**/Italic/Code; ⏎ to send).
2. **Hover** a message → react 🎉, reply in thread, pin, save, or the **⋯** menu.
3. **⌘K / Ctrl+K** → search channels, people, messages, tickets.
4. **Add channels** (sidebar) → create one; it appears and opens instantly.
5. **Inbox** → work the queue (done / snooze / archive).
6. **Support** → open **TCK-1042**, reply, **Resolve** (watch the list + stats update live).
7. **Theme** → top-right ☀️/🌙, or the **Appearance** panel (accent, radius, gradient, System mode).
8. **Start a huddle** (video icon) → floating call bar with working mute/video/share toggles.
9. Resize the window narrow — the layout stays responsive (sidebar → drawer, support → single pane).
## 6. Common tasks
| Task | Where |
|---|---|
| Add/seed mock data | `apps/web/lib/mock/data.ts` |
| Add a BFF endpoint | `apps/web/app/api/bff/<path>/route.ts` (+ `store` method + `BffClient` method) |
| Add a feature flag | `packages/messaging-inbox-sdk/src/config.ts` |
| Change default theme | `packages/messaging-inbox-sdk/src/config.ts` (`darkTokens`/`lightTokens`) |
| Add a page | `apps/web/app/(app)/<route>/page.tsx` |
## 7. Build for production
```bash
pnpm build
pnpm start # serves the built app on :4300
```
> The demo is not production-hardened, and the Next.js version is pinned for the demo (has a known
> CVE). Bump Next and wire a real backend (see [`IIOS_INTEGRATION.md`](IIOS_INTEGRATION.md)) before
> deploying.
## 8. Troubleshooting
| Symptom | Fix |
|---|---|
| corepack 404 on pnpm | `corepack disable`, re-run (no pin in this repo) |
| install times out | add `--fetch-timeout 2400000 --network-concurrency 2` |
| port 4300 in use | change the port in `apps/web/package.json` scripts |
| `store.X is not a function` after editing the store | bump `__lynkdStore_vN` in `lib/mock/store.ts` or restart `pnpm dev` (HMR singleton) |
| styles look unstyled | ensure `pnpm dev` compiled Tailwind (check the terminal); hard-reload |
| sent messages vanished | expected on server restart — the mock store is in-memory |
+73
View File
@@ -0,0 +1,73 @@
# Theming
The whole app is driven by **CSS variables** set on `<html>`. Tailwind colors map to those
variables, so switching light/dark or rebranding is just different values — no component edits.
Defaults match the LynkedUp Pro dashboard (dark + amber `#FDA913`).
## Three ways to theme
1. **Env (build/deploy time)**`NEXT_PUBLIC_THEME_*`.
2. **Provider props (per mount)**`<MessagingInboxProvider theme={{ … }}>`.
3. **Live in-app** — the **Appearance** panel (⚙️ by your name) sets accent/radius/gradient/mode
and persists to `localStorage`.
## Env variables
| Var | Default | Notes |
|---|---|---|
| `NEXT_PUBLIC_THEME_MODE` | `dark` | `light` \| `dark` \| `system` |
| `NEXT_PUBLIC_THEME_ACCENT` | `#FDA913` | any CSS color; overrides accent in both schemes |
| `NEXT_PUBLIC_THEME_ACCENT_FG` | `#1a1205` | text/icon color on accent surfaces |
| `NEXT_PUBLIC_THEME_RADIUS_CARD` | `20px` | card/panel radius |
| `NEXT_PUBLIC_THEME_RADIUS_CONTROL` | `10px` | button/input radius |
| `NEXT_PUBLIC_THEME_FONT_SANS` | Inter stack | any font-family string |
| `NEXT_PUBLIC_THEME_GRADIENT` | amber→coral | brand mark / hero gradient |
## Token reference
Semantic tokens (Tailwind class → CSS var). Use these, never raw hex.
| Purpose | Tailwind | CSS var | Dark | Light |
|---|---|---|---|---|
| App background | `bg-bg` | `--c-bg` | `#060608` | `#f6f7f9` |
| Sidebar / bars | `bg-surface` | `--c-surface` | `#08080b` | `#ffffff` |
| Panels / cards | `bg-panel` | `--c-panel` | `#0e0e13` | `#ffffff` |
| Elevated (menus) | `bg-elevated` | `--c-elevated` | `#15151c` | `#ffffff` |
| Hover wash | `bg-hover` | `--c-hover` | white 5.5% | ink 4.5% |
| Border | `border-border` | `--c-border` | white 8% | ink 10% |
| Strong border | `border-border-strong` | `--c-border-strong` | white 14% | ink 16% |
| Primary text | `text-ink` | `--c-ink` | `#ededf1` | `#14151a` |
| Muted text | `text-muted` | `--c-muted` | `#8c8c94` | `#5c6069` |
| Dim text | `text-dim` | `--c-dim` | `#63636b` | `#8b909a` |
| Accent | `bg-accent` / `text-accent` | `--c-accent` | `#FDA913` | `#e8920a` |
| Accent text | `text-accent-fg` | `--c-accent-fg` | `#1a1205` | `#ffffff` |
| Accent tint | `bg-accent-soft` | `--c-accent-soft` | amber 12% | amber 12% |
| Success/Warning/Danger/Info | `*-success/warning/danger/info` | `--c-success` … | — | — |
| Card radius | `rounded-card` | `--r-card` | 20px | 20px |
| Control radius | `rounded-control` | `--r-control` | 10px | 10px |
## Rebrand in 30 seconds
```bash
# apps/web/.env.local
NEXT_PUBLIC_THEME_MODE="light"
NEXT_PUBLIC_THEME_ACCENT="#6c5ce7"
NEXT_PUBLIC_THEME_RADIUS_CARD="14px"
NEXT_PUBLIC_THEME_RADIUS_CONTROL="8px"
NEXT_PUBLIC_THEME_GRADIENT="linear-gradient(135deg,#6c5ce7,#00cec9)"
NEXT_PUBLIC_BRAND_NAME="Acme"
```
Reload — sidebar, buttons, chips, gradient mark, focus rings, and both color schemes update.
## Editing the base palette
Change the `darkTokens` / `lightTokens` / `defaultTheme` objects in
`packages/messaging-inbox-sdk/src/config.ts`. Keep the matching fallbacks in
`apps/web/app/globals.css` (`:root { … }`) in sync so pre-hydration paint matches.
## How light/dark actually toggles
`theme.ts#applyTheme` writes the token vars to `document.documentElement.style` and toggles the
`dark` class (Tailwind `darkMode: 'class'`). The provider persists the chosen scheme to
`localStorage` (`lynkd.scheme`) and theme overrides to `lynkd.theme`.