Skip to content

Commit 4f27401

Browse files
committed
docs: add sonner in playground
1 parent a0daa5a commit 4f27401

File tree

6 files changed

+375
-0
lines changed

6 files changed

+375
-0
lines changed

.cursor/mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"playwright": {
4+
"command": "npx",
5+
"args": ["@playwright/mcp@latest"]
6+
}
7+
}
8+
}

.cursor/rules/general.mdc

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
- Ignore reading files mentioned in [.gitignore](mdc:.gitignore)
7+
8+
# General Cursor rules
9+
10+
- use `declarative` over `imperative` pattern
11+
- always add type signature to functions
12+
- modules are not allowed, only standlaone components
13+
- Use Angular version 19
14+
15+
## Typescript
16+
17+
- use `type` over `interface`
18+
- if a type is used in a single file, declare it at the top of the same file, under the imports section. Otherwise create a a dedicated `*.model.ts` file.
19+
- do not prefix types with `I`
20+
21+
## Dependancy Injection
22+
23+
- use `inject()` over `contructor`
24+
25+
## Components
26+
27+
- do not specify `standalone: true`
28+
- do not add empty Angular lifecycle hooks
29+
- do not include a `.css` file
30+
- use `afterNextRender` over `afterViewInit`
31+
- use `[class]` over `[ngClass]`
32+
- use Angular Material components wherever possible
33+
- do not use *ngIf and *ngFor, instead use @if and @for
34+
35+
## Services
36+
37+
- do not create a service if only ment for functions without a shared state or DI. Prefer utility functions in a `*.utils.ts` file.
38+
39+
## Rxjs
40+
41+
- on component destruction, unsubscribe to observables meant to query data (such as a state or a GET endpoint), not to change it (such as a POST, PUT or DELETE endpoints).
42+
- do not nest subscriptions
43+
- use `subscribe()` callback rather than `tap()` to update local state if `.subscribe()` is used
44+
- use the `async` pipe over a `.subscribe()` in components for data meant to be used in the template only
45+
- do not use `.subscribe()` in services, expose an Observable for the consumer to subscribe
46+
47+
## Styling
48+
49+
- use TailwindCSS whenever it's possible
50+
- do not use any other UI library
51+
- use css, or scss but not less
52+
- Avoid using `::ng-deep` in the component styles
53+
54+
## Third-party libraries
55+
56+
- only use libraries defined in [package.json](mdc:package.json)
57+
58+
## Formatting
59+
60+
- follow [prettier.config.js](mdc:prettier.config.js) rules
61+
62+
## Coding rules
63+
64+
## Elements
65+
66+
- Don't assign `id` to elements in HTML template
67+
68+
### named parameters pattern
69+
70+
Follow the `named parameters` pattern for parameters.
71+
72+
```ts
73+
// DO NOT
74+
function getScripts(searchTerm: string; limit: number) {}
75+
76+
// DO
77+
function getScripts(options: {searchTerm: string; limit: number}) {}
78+
```
79+
80+
## Forms
81+
82+
- Show errors when there is any validation error, but form-control should be either touched or dirty.
83+
- Prefer using reactive forms instead of template driven forms. You can use template driven forms when there is only a single form-field.
84+
85+
## AI MODEL RESPONSE TEMPLATE
86+
87+
When asked aboutangular implementation, you MUST:
88+
1. NEVER suggest deprecated approaches
89+
2. VERIFY your response against the patterns shown here
90+
91+
Remember: There are NO EXCEPTIONS to these rules.
92+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @ts-expect-error TypeScript cannot provide types based on attributes yet
2+
import tsContents from '../demos/toast-with-sonner-style/toast-with-sonner-style.component.ts' with { loader: 'text' };
3+
// @ts-expect-error TypeScript cannot provide types based on attributes yet
4+
import htmlContents from '../demos/toast-with-sonner-style/toast-with-sonner-style.component.html' with { loader: 'text' };
5+
import { CodeSchema } from './code.schema';
6+
7+
const toastWithSonnerStyleCode: CodeSchema = {
8+
ts: `
9+
${tsContents}`,
10+
html: `
11+
${htmlContents}`,
12+
};
13+
14+
export default toastWithSonnerStyleCode;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<div class="flex flex-wrap gap-2">
2+
<button
3+
(click)="showToast('default')"
4+
class="relative inline-flex items-center justify-center font-medium text-sm px-4 py-2 rounded-md transition-all bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 select-none"
5+
>
6+
Default
7+
</button>
8+
<button
9+
(click)="showToast('success')"
10+
class="relative inline-flex items-center justify-center font-medium text-sm px-4 py-2 rounded-md transition-all bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 select-none"
11+
>
12+
Success
13+
</button>
14+
<button
15+
(click)="showToast('error')"
16+
class="relative inline-flex items-center justify-center font-medium text-sm px-4 py-2 rounded-md transition-all bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 select-none"
17+
>
18+
Error
19+
</button>
20+
<button
21+
(click)="showToast('info')"
22+
class="relative inline-flex items-center justify-center font-medium text-sm px-4 py-2 rounded-md transition-all bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 select-none"
23+
>
24+
Info
25+
</button>
26+
<button
27+
(click)="showToast('warning')"
28+
class="relative inline-flex items-center justify-center font-medium text-sm px-4 py-2 rounded-md transition-all bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 select-none"
29+
>
30+
Warning
31+
</button>
32+
</div>
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { Component, inject } from '@angular/core';
2+
import { HotToastService } from '@ngxpert/hot-toast';
3+
4+
@Component({
5+
selector: 'app-playground-sonner-success-icon',
6+
template: `
7+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
8+
<path
9+
fill-rule="evenodd"
10+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
11+
clip-rule="evenodd"
12+
/>
13+
</svg>
14+
`,
15+
})
16+
export class SuccessIconComponent {}
17+
18+
@Component({
19+
selector: 'app-playground-sonner-error-icon',
20+
template: `
21+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
22+
<path
23+
fill-rule="evenodd"
24+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
25+
clip-rule="evenodd"
26+
/>
27+
</svg>
28+
`,
29+
})
30+
export class ErrorIconComponent {}
31+
32+
@Component({
33+
selector: 'app-playground-sonner-info-icon',
34+
template: `
35+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
36+
<path
37+
fill-rule="evenodd"
38+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
39+
clip-rule="evenodd"
40+
/>
41+
</svg>
42+
`,
43+
})
44+
export class InfoIconComponent {}
45+
46+
@Component({
47+
selector: 'app-playground-sonner-warning-icon',
48+
template: `
49+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" height="20" width="20">
50+
<path
51+
fill-rule="evenodd"
52+
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
53+
clip-rule="evenodd"
54+
/>
55+
</svg>
56+
`,
57+
})
58+
export class WarningIconComponent {}
59+
60+
@Component({
61+
selector: 'app-playground-sonner-close-icon',
62+
template: `
63+
<svg
64+
xmlns="http://www.w3.org/2000/svg"
65+
width="12"
66+
height="12"
67+
viewBox="0 0 24 24"
68+
fill="none"
69+
stroke="currentColor"
70+
stroke-width="1.5"
71+
stroke-linecap="round"
72+
stroke-linejoin="round"
73+
>
74+
<line x1="18" y1="6" x2="6" y2="18"></line>
75+
<line x1="6" y1="6" x2="18" y2="18"></line>
76+
</svg>
77+
`,
78+
})
79+
export class CloseIconComponent {}
80+
81+
type ToastType = 'default' | 'success' | 'error' | 'info' | 'warning';
82+
83+
@Component({
84+
selector: 'app-toast-with-sonner-style',
85+
templateUrl: './toast-with-sonner-style.component.html',
86+
styles: `
87+
button {
88+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
89+
}
90+
91+
button:active {
92+
transform: translateY(1px);
93+
}
94+
`,
95+
})
96+
export class ToastWithSonnerStyleComponent {
97+
private toast = inject(HotToastService);
98+
99+
showToast(type: ToastType = 'default'): void {
100+
const messages = {
101+
default: 'An opinionated toast message',
102+
success: 'Event has been created',
103+
error: 'Something went wrong',
104+
info: 'This is an informational message',
105+
warning: 'Warning: This action may be irreversible',
106+
};
107+
108+
const baseOptions = {
109+
duration: 4000,
110+
dismissible: true,
111+
style: {
112+
padding: '16px',
113+
background: 'white',
114+
color: 'hsl(0, 0%, 9%)',
115+
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
116+
border: '1px solid hsl(0, 0%, 93%)',
117+
borderRadius: '8px',
118+
fontSize: '13px',
119+
fontFamily:
120+
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif',
121+
},
122+
className: 'sonner-toast',
123+
iconTheme: {
124+
primary: 'hsl(0, 0%, 9%)',
125+
secondary: 'white',
126+
},
127+
autoClose: true,
128+
};
129+
130+
// Add global styles for toast layout
131+
this.addSonnerGlobalStyles();
132+
133+
switch (type) {
134+
case 'success':
135+
this.toast.success(messages[type], {
136+
...baseOptions,
137+
style: {
138+
...baseOptions.style,
139+
background: 'hsl(143, 85%, 96%)',
140+
borderColor: 'hsl(145, 92%, 87%)',
141+
color: 'hsl(140, 100%, 27%)',
142+
},
143+
iconTheme: {
144+
primary: 'hsl(140, 100%, 27%)',
145+
secondary: 'hsl(143, 85%, 96%)',
146+
},
147+
icon: SuccessIconComponent,
148+
});
149+
break;
150+
case 'error':
151+
this.toast.error(messages[type], {
152+
...baseOptions,
153+
style: {
154+
...baseOptions.style,
155+
background: 'hsl(359, 100%, 97%)',
156+
borderColor: 'hsl(359, 100%, 94%)',
157+
color: 'hsl(360, 100%, 45%)',
158+
},
159+
iconTheme: {
160+
primary: 'hsl(360, 100%, 45%)',
161+
secondary: 'hsl(359, 100%, 97%)',
162+
},
163+
icon: ErrorIconComponent,
164+
});
165+
break;
166+
case 'info':
167+
this.toast.info(messages[type], {
168+
...baseOptions,
169+
style: {
170+
...baseOptions.style,
171+
background: 'hsl(208, 100%, 97%)',
172+
borderColor: 'hsl(221, 91%, 93%)',
173+
color: 'hsl(210, 92%, 45%)',
174+
},
175+
iconTheme: {
176+
primary: 'hsl(210, 92%, 45%)',
177+
secondary: 'hsl(208, 100%, 97%)',
178+
},
179+
icon: InfoIconComponent,
180+
});
181+
break;
182+
case 'warning':
183+
this.toast.warning(messages[type], {
184+
...baseOptions,
185+
style: {
186+
...baseOptions.style,
187+
background: 'hsl(49, 100%, 97%)',
188+
borderColor: 'hsl(49, 91%, 84%)',
189+
color: 'hsl(31, 92%, 45%)',
190+
},
191+
iconTheme: {
192+
primary: 'hsl(31, 92%, 45%)',
193+
secondary: 'hsl(49, 100%, 97%)',
194+
},
195+
icon: WarningIconComponent,
196+
});
197+
break;
198+
default:
199+
this.toast.show(messages[type], baseOptions);
200+
break;
201+
}
202+
}
203+
204+
private addSonnerGlobalStyles(): void {
205+
if (!document.getElementById('sonner-styles')) {
206+
const styleSheet = document.createElement('style');
207+
styleSheet.id = 'sonner-styles';
208+
styleSheet.textContent = `
209+
.sonner-toast {
210+
display: flex !important;
211+
align-items: center !important;
212+
gap: 6px !important;
213+
max-width: 350px !important;
214+
}
215+
`;
216+
document.head.appendChild(styleSheet);
217+
}
218+
}
219+
}

src/app/features/playground/playground.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ToastWithLineIndicatorComponent } from './demos/toast-with-line-indicat
77
import toastWithLineIndicatorCode from './codes/toast-with-line-indicator';
88
import { ToastWithConfettiComponent } from './demos/toast-with-confetti/toast-with-confetti.component';
99
import toastWithConfettiCode from './codes/toast-with-confetti';
10+
import { ToastWithSonnerStyleComponent } from './demos/toast-with-sonner-style/toast-with-sonner-style.component';
11+
import toastWithSonnerStyleCode from './codes/toast-with-sonner-style';
1012

1113
export const PLAYGROUND_ITEMS: PlaygroundSchema[] = [
1214
{
@@ -38,6 +40,14 @@ export const PLAYGROUND_ITEMS: PlaygroundSchema[] = [
3840
code: toastWithConfettiCode,
3941
component: ToastWithConfettiComponent,
4042
},
43+
{
44+
title: 'Toast with Sonner style',
45+
id: 'toast-with-sonner-style',
46+
description:
47+
'A toast with styling inspired by the <a href="https://github.com/emilkowalski/sonner" target="_blank">Sonner</a> React toast library.',
48+
code: toastWithSonnerStyleCode,
49+
component: ToastWithSonnerStyleComponent,
50+
},
4151
];
4252

4353
// Validate no duplicate IDs exist

0 commit comments

Comments
 (0)