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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,19 @@ jobs:
with:
version: 9
- name: Install dependencies
run: pnpm i
run: pnpm i
# - name: Lint
# run: yarn lint
- name: Setup
run: pnpm run setup
- name: Build
run: pnpm build
- name: Test
run: pnpm run test:run
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-videos
Expand Down
102 changes: 102 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

Package manager is **pnpm** (CI uses pnpm 9, Node 22). Do not use npm or yarn.

| Task | Command |
| ------------------------------------------------------------------------------ | --------------- |
| Install deps (also wires the playground to the local source via the workspace) | `pnpm install` |
| Run the playground (interactive demo / dev harness) | `pnpm start` |
| Open Cypress component-test UI | `pnpm test` |
| Run Cypress component tests headlessly (CI uses this) | `pnpm test:run` |
| Build the library (`tsup` + copy `style.css` → `dist/ReactToastify.css`) | `pnpm build` |
| Format `src/` with Prettier | `pnpm prettier` |

### Running a single test

Tests are **Cypress component tests**, not Jest. Files live next to source as `*.cy.tsx` (e.g. `src/components/Toast.cy.tsx`). To run one file headlessly:

```sh
pnpm exec cypress run --component -b chrome --spec src/components/Toast.cy.tsx
```

Or use `pnpm test` to open the interactive runner and pick a spec.

### Lint / formatting

There is **no ESLint**. Code style is Prettier only (config inline in `package.json`: `printWidth: 120`, single quotes, no trailing commas, avoid arrow parens). A **lefthook** pre-commit hook runs `lint-staged`, which runs Prettier on staged `*.{js,jsx,ts,tsx,md,html,css}` files. The CI workflow's lint step is currently commented out — Prettier is the only gate.

## Architecture

### Two public entry points + one addon

`tsup.config.ts` produces three independent bundles:

1. **`react-toastify`** (default) — `src/index.ts`. Its `ToastContainer` export is the **`StyledToastContainer`** wrapper at `src/components/StyledToastContainer.tsx`, which imports `src/style.css` as a string (via Vite's `?raw` suffix; tsup uses `loader: { '.css': 'text' }` plus a small esbuild plugin that strips the `?raw` query) and injects it via `useStyleSheet(css, props.nonce)` on mount. Users can pass a `nonce` prop for Content Security Policy compliance. All bundles are prefixed with `"use client";` for React Server Components.
2. **`react-toastify/unstyled`** — `src/unstyled.ts`. Re-exports the **raw** `ToastContainer` from `./components` without the styled wrapper, so nothing is injected at runtime. Use this subpath when consumers want to ship their own CSS (from `react-toastify/dist/ReactToastify.css` or elsewhere).
3. **`react-toastify/addons/use-notification-center`** — built from the internal workspace package at `packages/use-notification-center/` into the `/addons` directory (not `/dist`); declared in `package.json#exports`.

The raw stylesheet is also exposed as `react-toastify/dist/ReactToastify.css`.

### Runtime model: imperative global store bridged by `useSyncExternalStore`

The library is **not** Context- or Redux-based. The flow is:

- `src/core/store.ts` — a module-level singleton. Holds a `Map<containerId, ContainerObserver>` and a `renderQueue` that buffers `toast()` calls issued before any `<ToastContainer>` mounts.
- `src/core/toast.ts` — the public imperative API (`toast()`, `toast.success`, `toast.update`, `toast.promise`, `toast.onChange`, …). Dispatches through the store.
- `src/core/containerObserver.ts` — one instance per `<ToastContainer>`. Owns per-container state: toast `Map`, waiting `queue` (when `limit > 0`), snapshot for `useSyncExternalStore`, prop validation, and lifecycle callbacks (`onOpen` / `onClose`).
- `src/hooks/useToastContainer.ts` — the **only** bridge to React. `<ToastContainer>` subscribes to its observer via `useSyncExternalStore`, which is why nothing in the tree needs to re-render just because a toast was pushed.

Consequence: multiple `<ToastContainer>` instances are supported via `containerId`; the store routes toasts to the right container (default id is `1`).

### Components (`src/components/`)

- `ToastContainer.tsx` — positions the portal, manages stacking (CSS-variable transforms computed in a layout effect), keyboard focus / `Alt+T` hotkey, collapse state.
- `Toast.tsx` — per-toast wrapper; consumes `useToast` (drag-to-dismiss, pause-on-hover, pause-on-blur, timer).
- `ProgressBar.tsx`, `CloseButton.tsx`, `Icons.tsx` — the pieces a toast renders.
- `Transitions.tsx` — `Bounce` / `Flip` / `Slide` / `Zoom` built with `cssTransition` from `src/utils/cssTransition.tsx`.

### Hooks (`src/hooks/`)

- `useToastContainer.ts` — store → React bridge (see above). The most important file in the repo.
- `useToast.ts` — per-toast UX behavior.
- `useIsomorphicLayoutEffect.ts` — SSR-safe layout effect.
- `useStyleSheet.ts` — runtime CSS injection (replaces the old tsup shim). Per-document `Map` so multiple `<ToastContainer>` instances only inject once per document (shadow DOM / iframe safe). If a later mount supplies a nonce and the previous injection had none, the attribute is updated on the existing `<style>` tag — see `src/hooks/useStyleSheet.ts`.

### Stacked / limit / queue semantics

- `ToastContainerProps.limit` — excess toasts are buffered in a per-container queue inside `containerObserver.ts`; as toasts close, the next queued toast is emitted. `toast.clearWaitingQueue()` drains it.
- `ToastContainerProps.stacked` — `ToastContainer.tsx` measures toast heights in a layout effect and sets CSS custom properties (`--y`, `--g`, `--s`) that drive `translate3d`-based stacking and a collapsed state.

### Types

Public types are centralized in `src/types.ts` and re-exported from `src/index.ts`. Notable: `ToastOptions<Data>`, `UpdateOptions<Data>`, `ToastContainerProps`, `ToastContent<T>`, `ToastItem<Data>` (payload for `toast.onChange`), `TypeOptions`, `Theme`.

### Addon: `use-notification-center`

Lives at `packages/use-notification-center/` as a **private pnpm workspace package** (see its `package.json` — `"private": true`). It exports a `useNotificationCenter()` hook — a **persistent** notification store independent from the transient toast queue. It subscribes to `toast.onChange()` to capture lifecycle events and provides filtering, read/unread, and sort.

The addon declares `"react-toastify": "workspace:*"` as a dependency, so its source can `import 'react-toastify'` and pnpm resolves it via a symlink at `packages/use-notification-center/node_modules/react-toastify → <repo root>`. No Vite alias or tsconfig `paths` entry is needed.

It is **not published separately**. The root `tsup.config.ts` builds it into `/addons/use-notification-center/` at the repo root, which the main `react-toastify` package includes via `package.json#files` and exposes via the `react-toastify/addons/use-notification-center` and `react-toastify/notification-center` subpath exports. Consumers still install only `react-toastify`.

## Playground

`/playground` is a standalone Vite app used as the demo and manual-QA harness. It is declared as a workspace in `pnpm-workspace.yaml` and depends on the root package via `"react-toastify": "workspace:*"`, so `pnpm install` symlinks the root into `playground/node_modules/react-toastify`. `pnpm start` runs it (via `pnpm --filter playground dev`). Most changes should be verified in the playground before running Cypress.

### Workspace layout

`pnpm-workspace.yaml` lists:

- **root** (`react-toastify`) — the published library.
- **`packages/*`** — currently just `packages/use-notification-center/` (private internal addon package, see Addon section above).
- **`playground`** — dev harness.

Both `playground` and `packages/use-notification-center` depend on the root via `workspace:*`. `pnpm install` is the only setup step — no `pnpm link` required.

## CI

`.github/workflows/build.yaml` runs on every push and PR: install → `pnpm build` → `pnpm test:run` → upload Cypress screenshots/videos on failure → report coverage to Coveralls. Coverage is instrumented at runtime by `vite-plugin-istanbul` (see `vite.config.mts`) and collected by `@cypress/code-coverage`.
15 changes: 8 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Contributing
# Contributing

:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:

When contributing to this repository, please first discuss the change you wish to make via issue before making a change.
When contributing to this repository, please first discuss the change you wish to make via issue before making a change.

Please note we have a code of conduct, please follow it in all your interactions with the project.

Expand All @@ -20,8 +20,8 @@ Please note we have a code of conduct, please follow it in all your interactions

### Pre-requisites

- *Node:* `^18.0.0`
- *Yarn*
- _Node:_ `^18.0.0`
- _Yarn_

### Install

Expand All @@ -37,11 +37,11 @@ git checkout -b my-branch
Install dependencies:

```sh
pnpm install
// then
pnpm setup
pnpm install
```

This is a pnpm workspace — the playground and the `use-notification-center` addon are workspace packages wired to the root via `workspace:*`, so no extra linking step is needed.

## Developing

```sh
Expand All @@ -64,4 +64,5 @@ The playground let you test your changes, it's like the demo of react-toastify.
- [toast:](https://github.com/fkhadra/react-toastify/blob/main/src/core/toast.ts) Contain the exposed api (`toast.success...`).

## License

By contributing, you agree that your contributions will be licensed under its [MIT License](https://github.com/fkhadra/react-toastify/blob/main/LICENSE).
4 changes: 3 additions & 1 deletion cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import '@cypress/code-coverage/support';
import './commands';
import './style.css';
// Inject library CSS into the Cypress iframe so raw <ToastContainer> tests (which don't
// go through StyledToastContainer's useStyleSheet hook) still see the default styles.
import '../../src/style.css';

// Alternatively you can use CommonJS syntax:
// require('./commands')

import { mount } from 'cypress/react18';
import { mount } from 'cypress/react';

// Augment the Cypress namespace to include type definitions for
// your custom command.
Expand Down
41 changes: 20 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@
],
"scripts": {
"prepare": "lefthook install",
"setup": "pnpm link .",
"start": "cd playground && pnpm dev",
"start": "pnpm --filter playground dev",
"test": "cypress open --component",
"test:run": "cypress run --component -b chrome",
"prettier": "prettier --write src",
"build": "tsup && cp src/style.css dist/ReactToastify.css && rm dist/unstyled.css*"
"build": "tsup && cp src/style.css dist/ReactToastify.css"
},
"peerDependencies": {
"react": "^18 || ^19",
Expand All @@ -48,26 +47,26 @@
},
"homepage": "https://github.com/fkhadra/react-toastify#readme",
"devDependencies": {
"@4tw/cypress-drag-drop": "^2.2.5",
"@cypress/code-coverage": "^3.13.9",
"@4tw/cypress-drag-drop": "^2.3.1",
"@cypress/code-coverage": "^4.0.3",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@testing-library/cypress": "^10.0.2",
"@types/node": "^22.10.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"@testing-library/cypress": "^10.1.0",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"coveralls": "^3.1.1",
"cypress": "^13.16.1",
"lefthook": "^1.9.2",
"lint-staged": "^15.2.11",
"postcss": "^8.4.49",
"prettier": "3.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vite-plugin-istanbul": "^6.0.2"
"cypress": "^15.14.0",
"lefthook": "^2.1.6",
"lint-staged": "^16.4.0",
"postcss": "^8.5.10",
"prettier": "3.8.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
"vite": "^8.0.8",
"vite-plugin-istanbul": "^8.0.0"
},
"dependencies": {
"clsx": "^2.1.1"
Expand Down
14 changes: 14 additions & 0 deletions packages/use-notification-center/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "use-notification-center",
"private": true,
"version": "0.0.0",
"description": "Internal workspace package for the useNotificationCenter addon. Not published separately — built into react-toastify's /addons directory by tsup at the repo root.",
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"react-toastify": "workspace:*"
},
"peerDependencies": {
"react": "^18 || ^19"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,21 @@ describe('NotificationCenter', () => {
});

it('update', () => {
const id = toast('msg');
let id: ReturnType<typeof toast>;
cy.then(() => {
id = toast('msg');
});

cy.resolveEntranceAnimation();
cy.findByRole('alert').should('exist');

setTimeout(() => {
toast.update(id, {
render: 'msg updated'
});
}, 0);
cy.then(() => {
setTimeout(() => {
toast.update(id, {
render: 'msg updated'
});
}, 0);
});

cy.findAllByText('msg updated').should('exist');
});
Expand Down
15 changes: 8 additions & 7 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-toastify": "workspace:*"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.1"
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.3",
"vite": "^8.0.8"
}
}
Loading
Loading