@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 +21 -0
- package/LICENSE.upstream +21 -0
- package/README.md +76 -2
- package/UPSTREAM.md +91 -0
- package/package.json +67 -3
- package/src/components/checkmark.ts +18 -0
- package/src/components/error.ts +20 -0
- package/src/components/loader.ts +16 -0
- package/src/components/styled.tsrx +40 -0
- package/src/components/toast-bar.tsrx +81 -0
- package/src/components/toast-icon.tsrx +54 -0
- package/src/components/toaster.tsrx +135 -0
- package/src/core/store.ts +225 -0
- package/src/core/toast.ts +152 -0
- package/src/core/types.ts +94 -0
- package/src/core/use-toaster.ts +141 -0
- package/src/core/utils.ts +21 -0
- package/src/goober.d.ts +18 -0
- package/src/goober.js +6 -0
- package/src/headless/index.ts +27 -0
- package/src/index.ts +15 -0
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.
|
package/LICENSE.upstream
ADDED
|
@@ -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
|
-
#
|
|
1
|
+
# @octanejs/hot-toast
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|