@octanejs/hot-toast 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Timo Lins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # @octanejs/hot-toast
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [react-hot-toast](https://react-hot-toast.com/) ported to the
4
+ [Octane](https://github.com/octanejs/octane) renderer. The package tracks the
5
+ published `react-hot-toast@2.6.1` API and behavior: the callable `toast` API
6
+ and its `success`, `error`, `loading`, `custom`, `promise`, `dismiss` and
7
+ `remove` variants, `Toaster`, `ToastBar`, `ToastIcon`, the icon components,
8
+ multiple toasters, pausing on hover, and the headless `useToaster` hook.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @octanejs/hot-toast
14
+ pnpm add @octanejs/hot-toast
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Mount one toaster near the root of the application, then call `toast` from any
20
+ event handler or module:
21
+
22
+ ```tsrx
23
+ import toast, { Toaster } from '@octanejs/hot-toast';
24
+
25
+ export function App() {
26
+ return (
27
+ <main>
28
+ <button onClick={() => toast.success('Saved')}>Save</button>
29
+ <Toaster position="top-right" />
30
+ </main>
31
+ );
32
+ }
33
+ ```
34
+
35
+ `toast.promise` follows a promise through loading, success and error:
36
+
37
+ ```ts
38
+ toast.promise(saveSettings(settings), {
39
+ loading: 'Saving…',
40
+ success: 'Settings saved',
41
+ error: (error) => `Could not save: ${error.message}`,
42
+ });
43
+ ```
44
+
45
+ Render your own toast UI with the `Toaster` render prop and `ToastBar`, or drop
46
+ the bundled UI entirely and build on the headless entry:
47
+
48
+ ```ts
49
+ import { toast, useToaster } from '@octanejs/hot-toast/headless';
50
+ ```
51
+
52
+ ## Differences from react-hot-toast
53
+
54
+ - The `Toaster` pauses its timers on the native `mouseenter` and `mouseleave`
55
+ events of its container. A real pointer behaves as in React. In tests, fire
56
+ those events on the `[data-rht-toaster]` container rather than on a toast.
57
+ - The port never calls goober's `setup()`, so an application that also uses
58
+ goober keeps its own pragma.
59
+ - The root and `/headless` entries share one toast store. Upstream bundles
60
+ them separately, each with its own store.
61
+ - Renderables, styles and refs use Octane's types (`ElementDescriptor`,
62
+ `CSSProperties`, refs as props).
63
+
64
+ Class names and the injected stylesheet are the ones `react-hot-toast@2.6.1`
65
+ ships, so CSS that targets them keeps working.
66
+
67
+ ## Server rendering
68
+
69
+ `Toaster` renders on the server without browser globals, and toasts already in
70
+ the store render with it. goober collects the generated rules on the server;
71
+ inline them with `extractCss()` from `goober`. On the client, `hydrateRoot`
72
+ adopts the server markup.
73
+
74
+ ## Provenance
75
+
76
+ See [`UPSTREAM.md`](./UPSTREAM.md). Ported code keeps the upstream MIT license
77
+ in `LICENSE.upstream`.
package/UPSTREAM.md ADDED
@@ -0,0 +1,91 @@
1
+ # Upstream
2
+
3
+ - Package: `react-hot-toast@2.6.1`
4
+ - Repository: https://github.com/timolins/react-hot-toast
5
+ - Release tag: `v2.6.1`, which resolves to commit
6
+ `e725d38e0faec05f7b8fb10644b616ef978d2f4d`.
7
+ - npm tarball: `https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.1.tgz`,
8
+ integrity `sha512-gjFsf05dxcku0FkyMtFH+XmYFdA2odXfBVknEr/IZ+XpUjwsgvtY2616sOpUynK+l9ykQDRNpaJ1hYfuximUiA==`,
9
+ SHA-256 `336bae521480e2f77425cf7c993ffbb58ca0b61407df9f99c63ca954b8099360`, kept as
10
+ `upstream-artifact/react-hot-toast-2.6.1.tgz`.
11
+ - License: MIT. The exact upstream text ships as `LICENSE.upstream`.
12
+
13
+ ## Pinning the commit
14
+
15
+ npm publishes no `gitHead` and no provenance attestation for this release, so the
16
+ repository preflight cannot derive the commit from the registry. The pin comes from the
17
+ `v2.6.1` tag instead and is proven by content: the tag commit's `package.json` declares
18
+ `react-hot-toast@2.6.1`, and every file in the tarball's `src/`, plus `LICENSE`, is
19
+ byte-identical to the commit's. `audit/upstream.lock.json` was written in pin mode
20
+ (`pnpm react-port:materialize lock --pin react-hot-toast@2.6.1 --repo
21
+ timolins/react-hot-toast --commit e725d38e…`).
22
+
23
+ ## Source boundary
24
+
25
+ The lock pins `LICENSE`, `package.json`, `jest.config.js`, `tsconfig.json`, `src/` and
26
+ `test/`. `upstream/` holds those files byte for byte as unpublished evidence, verified
27
+ offline with `pnpm react-port:materialize run --check --package-dir packages/hot-toast`.
28
+ `tests/upstream/` is regenerated from them using the lock's mechanical rewrites (React
29
+ imports to `octane`, `@testing-library/react` to `@octanejs/testing-library`, `../src`
30
+ to the port, and Jest `done` callbacks to plain functions), plus the one divergence patch
31
+ in `audit/upstream-patches/`.
32
+
33
+ `src/` mirrors upstream's layout. The core (`types`, `utils`, `store`, `toast`,
34
+ `use-toaster`) is ported nearly unchanged with Octane hooks. The components are
35
+ re-authored in `.tsrx`. goober is reused unchanged as a dependency for `css` and
36
+ `keyframes`. Its `styled()` builds React elements through a global pragma, so
37
+ `src/components/styled.tsrx` authors those elements instead. `src/goober.js` re-exports
38
+ goober's runtime with its own declaration file, because goober 2.1.19's `goober.d.ts`
39
+ fails TypeScript's library check in ESM projects (cristianbote/goober#606).
40
+
41
+ The published build collapses every whitespace run in the source's template literals
42
+ (`esbuild-minify-templates`), and goober names classes by hashing that text. The port's
43
+ templates carry the collapsed text, so its class names and stylesheet match the
44
+ published package. This was checked against every template literal in the tarball's
45
+ `dist/index.mjs`.
46
+
47
+ ## Export crosswalk
48
+
49
+ | Upstream export | Disposition |
50
+ | --- | --- |
51
+ | `toast` (default and named) with `success`, `error`, `loading`, `custom`, `promise`, `dismiss`, `dismissAll`, `remove`, `removeAll` | ported (`src/core/toast.ts`) |
52
+ | `Toaster` | ported (`src/components/toaster.tsrx`) |
53
+ | `ToastBar` | ported (`src/components/toast-bar.tsrx`) |
54
+ | `ToastIcon` | ported (`src/components/toast-icon.tsrx`) |
55
+ | `CheckmarkIcon`, `ErrorIcon`, `LoaderIcon` | ported (`src/components/{checkmark,error,loader}.ts`) |
56
+ | `useToaster` | ported (`src/core/use-toaster.ts`) |
57
+ | `useToasterStore` | ported (`useStore` in `src/core/store.ts`) |
58
+ | `resolveValue` | ported (`src/core/types.ts`) |
59
+ | types `DefaultToastOptions`, `IconTheme`, `Renderable`, `Toast`, `ToasterProps`, `ToastOptions`, `ToastPosition`, `ToastType`, `ValueFunction`, `ValueOrFunction` | ported with Octane renderable and style types |
60
+ | `./headless` entry: `toast`, `resolveValue`, `useToaster`, `useToasterStore` and the types | ported (`src/headless/index.ts`) |
61
+
62
+ ## Divergences
63
+
64
+ Each is declared in `audit/react-parity.json` and pinned by a test.
65
+
66
+ - `native-mouseenter`: the Toaster's pause handlers are native `mouseenter`/`mouseleave`
67
+ listeners on its container. React synthesizes both from `mouseover`/`mouseout`, so its
68
+ `fireEvent.mouseEnter(toast)` reaches the container. The adapted `pause toast` case fires
69
+ on the container instead.
70
+ - `goober-pragma`: upstream calls goober's `setup(React.createElement)` at load. The port
71
+ never calls `setup()`.
72
+ - `shared-headless-store`: upstream bundles the root and `/headless` entries separately, each
73
+ with its own store. The port publishes its source, so both entries share one store.
74
+ - Types: renderables, styles and refs use Octane's `ElementDescriptor`, `OctaneNode`,
75
+ `CSSProperties` and ref-as-prop types. The icon components are typed as Octane components
76
+ over `div` props rather than goober's `StyledVNode`.
77
+
78
+ ## Behavioral oracle
79
+
80
+ - `hot-toast-pristine-upstream`: all 14 upstream Jest registrations run byte for byte against
81
+ the pinned React source (`jest.pristine.config.cjs`; Babel strips the TypeScript that
82
+ upstream's ts-jest does).
83
+ - `hot-toast-adapted-full`: the same 14 registrations run against the port.
84
+ - `hot-toast-differential`: one fixture runs through the port and through the published
85
+ `react-hot-toast@2.6.1` on React, comparing HTML after every step. It covers every toast
86
+ type, updates, the promise lifecycle, dismissal, removal, both render props, the icons and
87
+ headless `useToaster`.
88
+
89
+ Octane-only conformance, hydration and SSR tests live in `tests/conformance` and
90
+ `tests/ssr`. The SSR test compares the server HTML with React's `renderToString` of the
91
+ published package. Upstream ships no type tests.
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@octanejs/hot-toast",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.22.2"
8
+ },
9
+ "description": "react-hot-toast for Octane: the toast store, Toaster, ToastBar, icons, and headless useToaster hook, ported from react-hot-toast 2.6.1.",
10
+ "author": {
11
+ "name": "Dominic Gannaway",
12
+ "email": "dg@domgan.com"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/octanejs/octane.git",
20
+ "directory": "packages/hot-toast"
21
+ },
22
+ "main": "src/index.ts",
23
+ "module": "src/index.ts",
24
+ "types": "src/index.ts",
25
+ "files": [
26
+ "src",
27
+ "README.md",
28
+ "UPSTREAM.md",
29
+ "LICENSE",
30
+ "LICENSE.upstream"
31
+ ],
32
+ "sideEffects": false,
33
+ "exports": {
34
+ ".": "./src/index.ts",
35
+ "./headless": "./src/headless/index.ts",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "dependencies": {
39
+ "goober": "2.1.19"
40
+ },
41
+ "peerDependencies": {
42
+ "octane": "^0.16.0"
43
+ },
44
+ "devDependencies": {
45
+ "@babel/preset-env": "7.29.7",
46
+ "@babel/preset-react": "7.29.7",
47
+ "@babel/preset-typescript": "7.29.7",
48
+ "@octanejs/testing-library": "0.1.66",
49
+ "@testing-library/jest-dom": "^6.9.1",
50
+ "@testing-library/react": "16.3.2",
51
+ "@tsrx/react": "^0.4.5",
52
+ "@types/react": "^19.2.17",
53
+ "@types/react-dom": "^19.2.3",
54
+ "babel-jest": "30.4.1",
55
+ "esbuild": "^0.28.1",
56
+ "jest": "30.4.2",
57
+ "jest-environment-jsdom": "30.4.1",
58
+ "octane": "0.16.4",
59
+ "react": "19.2.7",
60
+ "react-dom": "19.2.7",
61
+ "react-hot-toast": "2.6.1",
62
+ "vitest": "^4.1.10"
63
+ },
64
+ "scripts": {
65
+ "test": "vitest run --root ../.. --project hot-toast --project hot-toast-adapted --project hot-toast-ssr",
66
+ "test:pristine": "CI=true jest --config jest.pristine.config.cjs --runInBand",
67
+ "typecheck": "octane-tsc -p tsconfig.json",
68
+ "verify:upstream": "node ../../scripts/react-parity/verify-provenance.mjs"
69
+ }
6
70
  }
@@ -0,0 +1,18 @@
1
+ // Ported from react-hot-toast@2.6.1 src/components/checkmark.tsx
2
+ // (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
3
+ //
4
+ // Octane adaptations: goober's styled('div') becomes styledDiv, and templates
5
+ // carry the published build's collapsed whitespace (see styled.tsrx).
6
+ import { keyframes } from '../goober.js';
7
+ import { styledDiv } from './styled.tsrx';
8
+
9
+ const circleAnimation = keyframes` from { transform: scale(0) rotate(45deg); opacity: 0; } to { transform: scale(1) rotate(45deg); opacity: 1; }`;
10
+
11
+ const checkmarkAnimation = keyframes` 0% { height: 0; width: 0; opacity: 0; } 40% { height: 0; width: 6px; opacity: 1; } 100% { opacity: 1; height: 10px; }`;
12
+
13
+ export interface CheckmarkTheme {
14
+ primary?: string;
15
+ secondary?: string;
16
+ }
17
+
18
+ export const CheckmarkIcon = styledDiv<CheckmarkTheme>` width: 20px; opacity: 0; height: 20px; border-radius: 10px; background: ${(p) => p.primary || '#61d345'}; position: relative; transform: rotate(45deg); animation: ${circleAnimation} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; animation-delay: 100ms; &:after { content: ''; box-sizing: border-box; animation: ${checkmarkAnimation} 0.2s ease-out forwards; opacity: 0; animation-delay: 200ms; position: absolute; border-right: 2px solid; border-bottom: 2px solid; border-color: ${(p) => p.secondary || '#fff'}; bottom: 6px; left: 6px; height: 10px; width: 6px; } `;
@@ -0,0 +1,20 @@
1
+ // Ported from react-hot-toast@2.6.1 src/components/error.tsx
2
+ // (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
3
+ //
4
+ // Octane adaptations: goober's styled('div') becomes styledDiv, and templates
5
+ // carry the published build's collapsed whitespace (see styled.tsrx).
6
+ import { keyframes } from '../goober.js';
7
+ import { styledDiv } from './styled.tsrx';
8
+
9
+ const circleAnimation = keyframes` from { transform: scale(0) rotate(45deg); opacity: 0; } to { transform: scale(1) rotate(45deg); opacity: 1; }`;
10
+
11
+ const firstLineAnimation = keyframes` from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; }`;
12
+
13
+ const secondLineAnimation = keyframes` from { transform: scale(0) rotate(90deg); opacity: 0; } to { transform: scale(1) rotate(90deg); opacity: 1; }`;
14
+
15
+ export interface ErrorTheme {
16
+ primary?: string;
17
+ secondary?: string;
18
+ }
19
+
20
+ export const ErrorIcon = styledDiv<ErrorTheme>` width: 20px; opacity: 0; height: 20px; border-radius: 10px; background: ${(p) => p.primary || '#ff4b4b'}; position: relative; transform: rotate(45deg); animation: ${circleAnimation} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; animation-delay: 100ms; &:after, &:before { content: ''; animation: ${firstLineAnimation} 0.15s ease-out forwards; animation-delay: 150ms; position: absolute; border-radius: 3px; opacity: 0; background: ${(p) => p.secondary || '#fff'}; bottom: 9px; left: 4px; height: 2px; width: 12px; } &:before { animation: ${secondLineAnimation} 0.15s ease-out forwards; animation-delay: 180ms; transform: rotate(90deg); } `;
@@ -0,0 +1,16 @@
1
+ // Ported from react-hot-toast@2.6.1 src/components/loader.tsx
2
+ // (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
3
+ //
4
+ // Octane adaptations: goober's styled('div') becomes styledDiv, and templates
5
+ // carry the published build's collapsed whitespace (see styled.tsrx).
6
+ import { keyframes } from '../goober.js';
7
+ import { styledDiv } from './styled.tsrx';
8
+
9
+ const rotate = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); } `;
10
+
11
+ export interface LoaderTheme {
12
+ primary?: string;
13
+ secondary?: string;
14
+ }
15
+
16
+ export const LoaderIcon = styledDiv<LoaderTheme>` width: 12px; height: 12px; box-sizing: border-box; border: 2px solid; border-radius: 100%; border-color: ${(p) => p.secondary || '#e0e0e0'}; border-right-color: ${(p) => p.primary || '#616161'}; animation: ${rotate} 1s linear infinite; `;
@@ -0,0 +1,40 @@
1
+ // Octane stand-in for the goober `styled('div')` components react-hot-toast@2.6.1
2
+ // builds after `setup(React.createElement)`. Element construction is authored
3
+ // here instead of going through goober's global pragma, so the port never calls
4
+ // goober's `setup()` and leaves any application pragma in place. goober's own
5
+ // `css` still compiles and injects every rule.
6
+ //
7
+ // goober names a class by hashing its template text, and react-hot-toast's
8
+ // published build (esbuild-minify-templates) collapses every whitespace run in
9
+ // its template literals. The port's templates carry that collapsed text, so its
10
+ // class names and injected stylesheet are the ones react-hot-toast@2.6.1 ships.
11
+ import { css } from '../goober.js';
12
+ import type { ComponentProps } from 'octane';
13
+
14
+ type Interpolation<T> = string | number | ((props: StyledDivProps<T>) => string | number);
15
+
16
+ // goober types className as a string, so the clsx-style forms Octane's hosts
17
+ // accept are not part of this contract.
18
+ export type StyledDivProps<T = {}> = Omit<ComponentProps<'div'>, 'className'> & {
19
+ className?: string;
20
+ } & T;
21
+
22
+ // As goober's styled: interpolations read the element's props, the generated
23
+ // class comes before a caller's className (and its rules are prepended when that
24
+ // className already carries a goober class), and every prop reaches the div.
25
+ export function styledDiv<T extends object = {}>(
26
+ template: TemplateStringsArray,
27
+ ...interpolations: Interpolation<T>[]
28
+ ) {
29
+ return function Styled(props: StyledDivProps<T>) @{
30
+ const { className: previous, children, ...rest } = props;
31
+ const generated = css.call(
32
+ { p: props, o: /go\d/.test(previous ?? '') },
33
+ template,
34
+ ...(interpolations as (string | number)[]),
35
+ );
36
+ <div {...rest} className={previous ? generated + ' ' + previous : generated}>
37
+ {children}
38
+ </div>
39
+ };
40
+ }
@@ -0,0 +1,81 @@
1
+ // Ported from react-hot-toast@2.6.1 src/components/toast-bar.tsx
2
+ // (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
3
+ //
4
+ // Octane adaptations: memo and the style types come from octane, goober's
5
+ // styled('div') becomes styledDiv, templates carry the published build's
6
+ // collapsed whitespace (see styled.tsrx), and the render-prop branch is an @if
7
+ // block.
8
+ import { keyframes } from '../goober.js';
9
+ import { type CSSProperties, type FC, memo } from 'octane';
10
+ import { type Renderable, type Toast, type ToastPosition, resolveValue } from '../core/types';
11
+ import { prefersReducedMotion } from '../core/utils';
12
+ import { styledDiv } from './styled.tsrx';
13
+ import { ToastIcon } from './toast-icon.tsrx';
14
+
15
+ const enterAnimation = (factor: number) =>
16
+ ` 0% {transform: translate3d(0,${factor * -200}%,0) scale(.6); opacity:.5;} 100% {transform: translate3d(0,0,0) scale(1); opacity:1;} `;
17
+
18
+ const exitAnimation = (factor: number) =>
19
+ ` 0% {transform: translate3d(0,0,-1px) scale(1); opacity:1;} 100% {transform: translate3d(0,${factor * -150}%,-1px) scale(.6); opacity:0;} `;
20
+
21
+ const fadeInAnimation = `0%{opacity:0;} 100%{opacity:1;}`;
22
+ const fadeOutAnimation = `0%{opacity:1;} 100%{opacity:0;}`;
23
+
24
+ // Use :where() for zero specificity - allows Tailwind to override easily
25
+ const ToastBarBase = styledDiv` :where(&) { display: flex; align-items: center; background: #fff; color: #363636; line-height: 1.3; will-change: transform; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05); max-width: 350px; pointer-events: auto; padding: 8px 10px; border-radius: 8px; } `;
26
+
27
+ const Message = styledDiv` display: flex; justify-content: center; margin: 4px 10px; color: inherit; flex: 1 1 auto; white-space: pre-line; `;
28
+
29
+ interface ToastBarProps {
30
+ toast: Toast;
31
+ position?: ToastPosition;
32
+ style?: CSSProperties;
33
+ children?: (components: { icon: Renderable; message: Renderable }) => Renderable;
34
+ }
35
+
36
+ const getAnimationStyle = (position: ToastPosition, visible: boolean): CSSProperties => {
37
+ const top = position.includes('top');
38
+ const factor = top ? 1 : -1;
39
+
40
+ const [enter, exit] = prefersReducedMotion()
41
+ ? [fadeInAnimation, fadeOutAnimation]
42
+ : [enterAnimation(factor), exitAnimation(factor)];
43
+
44
+ return {
45
+ animation: visible
46
+ ? `${keyframes(enter)} 0.35s cubic-bezier(.21,1.02,.73,1) forwards`
47
+ : `${keyframes(exit)} 0.4s forwards cubic-bezier(.06,.71,.55,1)`,
48
+ };
49
+ };
50
+
51
+ function ToastBarBody(props: ToastBarProps) @{
52
+ const { toast, position, style, children } = props;
53
+ const animationStyle: CSSProperties = toast.height
54
+ ? getAnimationStyle(toast.position || position || 'top-center', toast.visible)
55
+ : { opacity: 0 };
56
+
57
+ const icon = <ToastIcon toast={toast} />;
58
+ const message = <Message {...toast.ariaProps}>{resolveValue(toast.message, toast)}</Message>;
59
+
60
+ <ToastBarBase
61
+ className={toast.className}
62
+ style={{
63
+ ...animationStyle,
64
+ ...style,
65
+ ...toast.style,
66
+ }}
67
+ >
68
+ @if (typeof children === 'function') {
69
+ <>{children({ icon, message })}</>
70
+ } @else {
71
+ <>
72
+ {icon}
73
+ {message}
74
+ </>
75
+ }
76
+ </ToastBarBase>
77
+ }
78
+
79
+ // TypeScript 5.9 resolves `FC = memo(function … )` to memo's ComponentBody
80
+ // overload, so the body is a declaration.
81
+ export const ToastBar: FC<ToastBarProps> = memo(ToastBarBody);
@@ -0,0 +1,54 @@
1
+ // Ported from react-hot-toast@2.6.1 src/components/toast-icon.tsx
2
+ // (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
3
+ //
4
+ // Octane adaptations: goober's styled('div') becomes styledDiv, templates carry
5
+ // the published build's collapsed whitespace (see styled.tsrx), and the
6
+ // conditional status icon is an @if block.
7
+ import { keyframes } from '../goober.js';
8
+ import type { Toast } from '../core/types';
9
+ import { type CheckmarkTheme, CheckmarkIcon } from './checkmark';
10
+ import { type ErrorTheme, ErrorIcon } from './error';
11
+ import { type LoaderTheme, LoaderIcon } from './loader';
12
+ import { styledDiv } from './styled.tsrx';
13
+
14
+ const StatusWrapper = styledDiv` position: absolute; `;
15
+
16
+ const IndicatorWrapper = styledDiv` position: relative; display: flex; justify-content: center; align-items: center; min-width: 20px; min-height: 20px; `;
17
+
18
+ const enter = keyframes` from { transform: scale(0.6); opacity: 0.4; } to { transform: scale(1); opacity: 1; }`;
19
+
20
+ export const AnimatedIconWrapper = styledDiv` position: relative; transform: scale(0.6); opacity: 0.4; min-width: 20px; animation: ${enter} 0.3s 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; `;
21
+
22
+ export type IconThemes = Partial<{
23
+ success: CheckmarkTheme;
24
+ error: ErrorTheme;
25
+ loading: LoaderTheme;
26
+ }>;
27
+
28
+ export function ToastIcon(props: { toast: Toast }) @{
29
+ const { icon, type, iconTheme } = props.toast;
30
+ if (icon !== undefined) {
31
+ if (typeof icon === 'string') {
32
+ return <AnimatedIconWrapper>{icon as string}</AnimatedIconWrapper>;
33
+ } else {
34
+ return icon;
35
+ }
36
+ }
37
+
38
+ if (type === 'blank') {
39
+ return null;
40
+ }
41
+
42
+ <IndicatorWrapper>
43
+ <LoaderIcon {...iconTheme} />
44
+ @if (type !== 'loading') {
45
+ <StatusWrapper>
46
+ @if (type === 'error') {
47
+ <ErrorIcon {...iconTheme} />
48
+ } @else {
49
+ <CheckmarkIcon {...iconTheme} />
50
+ }
51
+ </StatusWrapper>
52
+ }
53
+ </IndicatorWrapper>
54
+ }
@@ -0,0 +1,135 @@
1
+ // Ported from react-hot-toast@2.6.1 src/components/toaster.tsx
2
+ // (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
3
+ //
4
+ // Octane adaptations: hooks and style types come from octane, the toast list is
5
+ // a keyed @for block, and the template carries the published build's collapsed
6
+ // whitespace (see styled.tsrx).
7
+ //
8
+ // OCTANE DIVERGENCE[goober-pragma][conformance:hot-toast-goober-pragma]: upstream
9
+ // calls goober's setup(React.createElement) here. The port renders its own
10
+ // elements (see styled.tsrx) and never calls setup().
11
+ import { css } from '../goober.js';
12
+ import { type CSSProperties, useCallback } from 'octane';
13
+ import {
14
+ type ToasterProps,
15
+ type ToastPosition,
16
+ type ToastWrapperProps,
17
+ resolveValue,
18
+ } from '../core/types';
19
+ import { useToaster } from '../core/use-toaster';
20
+ import { prefersReducedMotion } from '../core/utils';
21
+ import { ToastBar } from './toast-bar.tsrx';
22
+
23
+ function ToastWrapper(props: ToastWrapperProps) @{
24
+ const { id, className, style, onHeightUpdate, children } = props;
25
+ const ref = useCallback(
26
+ (el: HTMLElement | null) => {
27
+ if (el) {
28
+ const updateHeight = () => {
29
+ const height = el.getBoundingClientRect().height;
30
+ onHeightUpdate(id, height);
31
+ };
32
+ updateHeight();
33
+ new MutationObserver(updateHeight).observe(el, {
34
+ subtree: true,
35
+ childList: true,
36
+ characterData: true,
37
+ });
38
+ }
39
+ },
40
+ [id, onHeightUpdate],
41
+ );
42
+
43
+ <div ref={ref} className={className} style={style}>
44
+ {children}
45
+ </div>
46
+ }
47
+
48
+ const getPositionStyle = (position: ToastPosition, offset: number): CSSProperties => {
49
+ const top = position.includes('top');
50
+ const verticalStyle: CSSProperties = top ? { top: 0 } : { bottom: 0 };
51
+ const horizontalStyle: CSSProperties = position.includes('center')
52
+ ? {
53
+ justifyContent: 'center',
54
+ }
55
+ : position.includes('right')
56
+ ? {
57
+ justifyContent: 'flex-end',
58
+ }
59
+ : {};
60
+ return {
61
+ left: 0,
62
+ right: 0,
63
+ display: 'flex',
64
+ position: 'absolute',
65
+ transition: prefersReducedMotion() ? undefined : `all 230ms cubic-bezier(.21,1.02,.73,1)`,
66
+ transform: `translateY(${offset * (top ? 1 : -1)}px)`,
67
+ ...verticalStyle,
68
+ ...horizontalStyle,
69
+ };
70
+ };
71
+
72
+ // Prettier formats `css` templates; goober hashes the published text verbatim.
73
+ // prettier-ignore
74
+ const activeClass = css` z-index: 9999; > * { pointer-events: auto; } `;
75
+
76
+ const DEFAULT_OFFSET = 16;
77
+
78
+ export function Toaster(props: ToasterProps) @{
79
+ const {
80
+ reverseOrder,
81
+ position = 'top-center',
82
+ toastOptions,
83
+ gutter,
84
+ children,
85
+ toasterId,
86
+ containerStyle,
87
+ containerClassName,
88
+ } = props;
89
+ const { toasts, handlers } = useToaster(toastOptions, toasterId);
90
+
91
+ // OCTANE DIVERGENCE[native-mouseenter][conformance:hot-toast-native-pause]: the
92
+ // pause handlers are native mouseenter/mouseleave listeners, which a pointer
93
+ // entering any toast dispatches on this container.
94
+ <div
95
+ data-rht-toaster={toasterId || ''}
96
+ style={{
97
+ position: 'fixed',
98
+ zIndex: 9999,
99
+ top: DEFAULT_OFFSET,
100
+ left: DEFAULT_OFFSET,
101
+ right: DEFAULT_OFFSET,
102
+ bottom: DEFAULT_OFFSET,
103
+ pointerEvents: 'none',
104
+ ...containerStyle,
105
+ }}
106
+ className={containerClassName}
107
+ onMouseEnter={handlers.startPause}
108
+ onMouseLeave={handlers.endPause}
109
+ >
110
+ @for (const t of toasts; key t.id) {
111
+ const toastPosition = t.position || position;
112
+ const offset = handlers.calculateOffset(t, {
113
+ reverseOrder,
114
+ gutter,
115
+ defaultPosition: position,
116
+ });
117
+ const positionStyle = getPositionStyle(toastPosition, offset);
118
+
119
+ <ToastWrapper
120
+ id={t.id}
121
+ onHeightUpdate={handlers.updateHeight}
122
+ className={t.visible ? activeClass : ''}
123
+ style={positionStyle}
124
+ >
125
+ @if (t.type === 'custom') {
126
+ <>{resolveValue(t.message, t)}</>
127
+ } @else if (children) {
128
+ <>{children(t)}</>
129
+ } @else {
130
+ <ToastBar toast={t} position={toastPosition} />
131
+ }
132
+ </ToastWrapper>
133
+ }
134
+ </div>
135
+ }