create-near-app 5.3.1 → 6.0.0-beta.0

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.
Files changed (112) hide show
  1. package/dist/make.js +19 -15
  2. package/dist/messages.js +2 -2
  3. package/dist/package-json.js +17 -1
  4. package/dist/types.js +1 -1
  5. package/dist/user-input.js +16 -15
  6. package/package.json +4 -1
  7. package/templates/contracts/js/deploy.sh +6 -1
  8. package/templates/contracts/js/package-lock.json +5966 -0
  9. package/templates/contracts/js/src/contract.ts +5 -5
  10. package/templates/contracts/rust/Cargo.toml +1 -1
  11. package/templates/contracts/rust/README.md +2 -2
  12. package/templates/contracts/rust/deploy.sh +5 -0
  13. package/templates/contracts/rust/src/lib.rs +11 -11
  14. package/templates/frontend/gateway/.nvmrc +1 -0
  15. package/templates/frontend/gateway/.prettierrc +6 -0
  16. package/templates/frontend/gateway/next.config.js +15 -0
  17. package/templates/frontend/gateway/package.json +74 -0
  18. package/templates/frontend/gateway/public/apple-touch-icon.png +0 -0
  19. package/templates/frontend/gateway/public/bos-meta.png +0 -0
  20. package/templates/frontend/gateway/public/favicon.ico +0 -0
  21. package/templates/frontend/gateway/public/favicon.png +0 -0
  22. package/templates/frontend/gateway/public/fonts/FKGrotesk.woff2 +0 -0
  23. package/templates/frontend/gateway/public/fonts/Mona-Sans.woff2 +0 -0
  24. package/templates/frontend/gateway/public/logo192.png +0 -0
  25. package/templates/frontend/gateway/public/next.svg +1 -0
  26. package/templates/frontend/gateway/public/robots.txt +3 -0
  27. package/templates/frontend/gateway/public/site.webmanifest +8 -0
  28. package/templates/frontend/gateway/public/vercel.svg +1 -0
  29. package/templates/frontend/gateway/src/assets/images/near-icon.svg +3 -0
  30. package/templates/frontend/gateway/src/components/MetaTags.tsx +22 -0
  31. package/templates/frontend/gateway/src/components/component/ComponentWrapperPage.tsx +27 -0
  32. package/templates/frontend/gateway/src/components/layouts/DefaultLayout.tsx +16 -0
  33. package/templates/frontend/gateway/src/components/layouts/SimpleLayout.tsx +9 -0
  34. package/templates/frontend/gateway/src/components/lib/Button/Button.tsx +379 -0
  35. package/templates/frontend/gateway/src/components/lib/Button/index.tsx +1 -0
  36. package/templates/frontend/gateway/src/components/lib/Spinner/Spinner.tsx +33 -0
  37. package/templates/frontend/gateway/src/components/lib/Spinner/index.ts +1 -0
  38. package/templates/frontend/gateway/src/components/lib/Text/Text.tsx +14 -0
  39. package/templates/frontend/gateway/src/components/lib/Text/index.tsx +1 -0
  40. package/templates/frontend/gateway/src/components/lib/Toast/README.md +83 -0
  41. package/templates/frontend/gateway/src/components/lib/Toast/Toast.tsx +25 -0
  42. package/templates/frontend/gateway/src/components/lib/Toast/Toaster.tsx +48 -0
  43. package/templates/frontend/gateway/src/components/lib/Toast/api.ts +6 -0
  44. package/templates/frontend/gateway/src/components/lib/Toast/index.ts +3 -0
  45. package/templates/frontend/gateway/src/components/lib/Toast/store.ts +83 -0
  46. package/templates/frontend/gateway/src/components/lib/Toast/styles.ts +126 -0
  47. package/templates/frontend/gateway/src/components/navigation/Navigation.tsx +23 -0
  48. package/templates/frontend/gateway/src/components/navigation/UserDropdownMenu.tsx +209 -0
  49. package/templates/frontend/gateway/src/components/navigation/desktop/DesktopNavigation.tsx +109 -0
  50. package/templates/frontend/gateway/src/components/navigation/desktop/MainNavigationMenu.tsx +170 -0
  51. package/templates/frontend/gateway/src/components/navigation/icons/close.svg +22 -0
  52. package/templates/frontend/gateway/src/components/navigation/icons/near-icon.svg +3 -0
  53. package/templates/frontend/gateway/src/components/navigation/icons/near-logo.svg +14 -0
  54. package/templates/frontend/gateway/src/components/navigation/icons/return.svg +29 -0
  55. package/templates/frontend/gateway/src/components/navigation/icons/search.svg +3 -0
  56. package/templates/frontend/gateway/src/components/navigation/mobile/AccordionMenu.tsx +141 -0
  57. package/templates/frontend/gateway/src/components/navigation/mobile/Menu.tsx +86 -0
  58. package/templates/frontend/gateway/src/components/navigation/mobile/MobileNavigation.tsx +131 -0
  59. package/templates/frontend/gateway/src/components/navigation/navigation-categories.ts +75 -0
  60. package/templates/frontend/gateway/src/components/vm/VmCommitButton.tsx +20 -0
  61. package/templates/frontend/gateway/src/components/vm/VmComponent.tsx +29 -0
  62. package/templates/frontend/gateway/src/components/vm/VmInitializer.tsx +124 -0
  63. package/templates/frontend/gateway/src/data/components.ts +50 -0
  64. package/templates/frontend/gateway/src/data/web3.ts +292 -0
  65. package/templates/frontend/gateway/src/hooks/useBosComponents.ts +14 -0
  66. package/templates/frontend/gateway/src/hooks/useBosLoaderInitializer.ts +60 -0
  67. package/templates/frontend/gateway/src/hooks/useFlags.ts +40 -0
  68. package/templates/frontend/gateway/src/hooks/useLayout.tsx +12 -0
  69. package/templates/frontend/gateway/src/index.d.ts +9 -0
  70. package/templates/frontend/gateway/src/pages/_app.tsx +47 -0
  71. package/templates/frontend/gateway/src/pages/_document.tsx +27 -0
  72. package/templates/frontend/gateway/src/pages/dig.tsx +25 -0
  73. package/templates/frontend/gateway/src/pages/flags.tsx +83 -0
  74. package/templates/frontend/gateway/src/pages/hello-api.tsx +105 -0
  75. package/templates/frontend/gateway/src/pages/hello-component.tsx +23 -0
  76. package/templates/frontend/gateway/src/pages/hello-ethereum.tsx +23 -0
  77. package/templates/frontend/gateway/src/pages/hello-social.tsx +31 -0
  78. package/templates/frontend/gateway/src/pages/index.tsx +51 -0
  79. package/templates/frontend/gateway/src/pages/nui.tsx +25 -0
  80. package/templates/frontend/gateway/src/stores/auth.ts +29 -0
  81. package/templates/frontend/gateway/src/stores/bos-loader.ts +20 -0
  82. package/templates/frontend/gateway/src/stores/current-component.ts +11 -0
  83. package/templates/frontend/gateway/src/stores/vm.ts +24 -0
  84. package/templates/frontend/gateway/src/styles/globals.css +68 -0
  85. package/templates/frontend/gateway/src/styles/theme.css +129 -0
  86. package/templates/frontend/gateway/src/utils/config.ts +49 -0
  87. package/templates/frontend/gateway/src/utils/form-validation.ts +18 -0
  88. package/templates/frontend/gateway/src/utils/keypom-options.ts +59 -0
  89. package/templates/frontend/gateway/src/utils/route/privateRoute.tsx +20 -0
  90. package/templates/frontend/gateway/src/utils/route/signedOutRoute.tsx +21 -0
  91. package/templates/frontend/gateway/src/utils/types.ts +33 -0
  92. package/templates/frontend/gateway/tsconfig.json +24 -0
  93. package/templates/frontend/vanilla/.env +1 -0
  94. package/templates/frontend/vanilla/index.html +15 -6
  95. package/templates/frontend/vanilla/index.js +1 -1
  96. package/templates/frontend/{shared → vanilla}/near-wallet.js +2 -2
  97. package/templates/frontend/vanilla/package.json +9 -8
  98. package/templates/integration-tests/js-tests/package-lock.json +5014 -0
  99. package/templates/integration-tests/js-tests/package.json +3 -0
  100. package/templates/integration-tests/js-tests/src/main.ava.ts +6 -6
  101. package/templates/integration-tests/rust-tests/src/tests.rs +7 -7
  102. package/templates/frontend/react/App.js +0 -75
  103. package/templates/frontend/react/assets/favicon.ico +0 -0
  104. package/templates/frontend/react/assets/global.css +0 -231
  105. package/templates/frontend/react/assets/logo-black.svg +0 -1
  106. package/templates/frontend/react/assets/logo-white.svg +0 -1
  107. package/templates/frontend/react/index.html +0 -15
  108. package/templates/frontend/react/index.js +0 -23
  109. package/templates/frontend/react/package.json +0 -52
  110. package/templates/frontend/react/ui-components.js +0 -59
  111. package/templates/frontend/shared/start.sh +0 -26
  112. /package/templates/{shared → contracts/rust}/rust-toolchain.toml +0 -0
@@ -0,0 +1,379 @@
1
+ import type { ButtonHTMLAttributes } from 'react';
2
+ import { forwardRef } from 'react';
3
+ import styled from 'styled-components';
4
+
5
+ type Fill = 'solid' | 'outline' | 'ghost';
6
+ type Size = 'small' | 'default' | 'large';
7
+ type Variant = 'primary' | 'secondary' | 'affirmative' | 'destructive';
8
+
9
+ type Props = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'size'> & {
10
+ disabled?: boolean;
11
+ fill?: Fill;
12
+ href?: string;
13
+ icon?: string;
14
+ iconLeft?: string;
15
+ iconRight?: string;
16
+ label: string;
17
+ loading?: boolean;
18
+ size?: Size;
19
+ type?: 'button' | 'submit';
20
+ variant?: Variant;
21
+ };
22
+
23
+ type StyledProps = {
24
+ disabled?: boolean;
25
+ fill: Fill;
26
+ icon?: string;
27
+ loading?: boolean;
28
+ size: Size;
29
+ variant: Variant;
30
+ };
31
+
32
+ const variants: Record<Variant, any> = {
33
+ primary: {
34
+ outline: {
35
+ background: 'var(--sand1)',
36
+ border: 'var(--sand6)',
37
+ color: 'var(--violet8)',
38
+ iconColor: 'var(--violet9)',
39
+ hover: {
40
+ border: 'var(--violet6)',
41
+ },
42
+ focus: {
43
+ border: 'var(--violet9)',
44
+ },
45
+ active: {
46
+ background: 'var(--violet2)',
47
+ border: 'var(--violet7)',
48
+ },
49
+ },
50
+ solid: {
51
+ background: 'var(--sand12)',
52
+ border: 'var(--sand12)',
53
+ color: 'var(--sand1)',
54
+ iconColor: 'var(--sand9)',
55
+ hover: {
56
+ background: 'var(--sand11)',
57
+ border: 'var(--sand11)',
58
+ },
59
+ focus: {},
60
+ active: {},
61
+ },
62
+ },
63
+ secondary: {
64
+ outline: {
65
+ background: 'var(--sand1)',
66
+ border: 'var(--sand6)',
67
+ color: 'var(--sand12)',
68
+ iconColor: 'var(--sand10)',
69
+ hover: {
70
+ border: 'var(--sand8)',
71
+ },
72
+ focus: {
73
+ border: 'var(--violet8)',
74
+ },
75
+ active: {
76
+ background: 'var(--sand3)',
77
+ border: 'var(--sand8)',
78
+ },
79
+ },
80
+ solid: {
81
+ background: 'var(--sand3)',
82
+ border: 'var(--sand6)',
83
+ color: 'var(--sand12)',
84
+ iconColor: 'var(--sand11)',
85
+ hover: {
86
+ background: 'var(--sand4)',
87
+ },
88
+ focus: {
89
+ border: 'var(--violet8)',
90
+ },
91
+ active: {
92
+ background: 'var(--sand5)',
93
+ },
94
+ },
95
+ },
96
+ destructive: {
97
+ outline: {
98
+ background: 'var(--sand1)',
99
+ border: 'var(--sand6)',
100
+ color: 'var(--red8)',
101
+ iconColor: 'var(--red9)',
102
+ hover: {
103
+ border: 'var(--red6)',
104
+ },
105
+ focus: {
106
+ border: 'var(--violet8)',
107
+ },
108
+ active: {
109
+ background: 'var(--red2)',
110
+ border: 'var(--red7)',
111
+ },
112
+ },
113
+ solid: {
114
+ background: 'var(--red9)',
115
+ border: 'var(--red8)',
116
+ color: 'var(--red12)',
117
+ iconColor: 'var(--red11)',
118
+ hover: {
119
+ background: 'var(--red10)',
120
+ },
121
+ focus: {
122
+ border: 'var(--red11)',
123
+ },
124
+ active: {
125
+ background: 'var(--red8)',
126
+ },
127
+ },
128
+ },
129
+ affirmative: {
130
+ outline: {
131
+ background: 'var(--sand1)',
132
+ border: 'var(--sand6)',
133
+ color: 'var(--green11)',
134
+ iconColor: 'var(--green10)',
135
+ hover: {
136
+ border: 'var(--green9)',
137
+ },
138
+ focus: {
139
+ border: 'var(--violet8)',
140
+ },
141
+ active: {
142
+ background: 'var(--green2)',
143
+ border: 'var(--green8)',
144
+ },
145
+ },
146
+ solid: {
147
+ background: 'var(--green9)',
148
+ border: 'var(--green8)',
149
+ color: 'var(--green12)',
150
+ iconColor: 'var(--green11)',
151
+ hover: {
152
+ background: 'var(--green10)',
153
+ },
154
+ focus: {
155
+ border: 'var(--green11)',
156
+ },
157
+ active: {
158
+ background: 'var(--green8)',
159
+ },
160
+ },
161
+ },
162
+ };
163
+ variants.primary.ghost = {
164
+ ...variants.primary.outline,
165
+ border: 'hsla(0, 0%, 100%, 0)',
166
+ background: 'hsla(0, 0%, 100%, 0)',
167
+ };
168
+ variants.secondary.ghost = {
169
+ ...variants.secondary.outline,
170
+ border: 'hsla(0, 0%, 100%, 0)',
171
+ background: 'hsla(0, 0%, 100%, 0)',
172
+ };
173
+ variants.destructive.ghost = {
174
+ ...variants.destructive.outline,
175
+ border: 'hsla(0, 0%, 100%, 0)',
176
+ background: 'hsla(0, 0%, 100%, 0)',
177
+ };
178
+ variants.affirmative.ghost = {
179
+ ...variants.affirmative.outline,
180
+ border: 'hsla(0, 0%, 100%, 0)',
181
+ background: 'hsla(0, 0%, 100%, 0)',
182
+ };
183
+
184
+ const sizes: Record<Size, any> = {
185
+ small: {
186
+ font: 'var(--text-xs)',
187
+ gap: '6px',
188
+ height: '32px',
189
+ icon: '14px',
190
+ paddingX: '16px',
191
+ },
192
+ default: {
193
+ font: 'var(--text-s)',
194
+ gap: '8px',
195
+ height: '40px',
196
+ icon: '18px',
197
+ paddingX: '20px',
198
+ },
199
+ large: {
200
+ font: 'var(--text-base)',
201
+ gap: '8px',
202
+ height: '48px',
203
+ icon: '18px',
204
+ paddingX: '24px',
205
+ },
206
+ };
207
+
208
+ function returnColor(variant: Variant, fill: string, state: string, key: string) {
209
+ if (state === 'default') return variants[variant][fill][key];
210
+ return variants[variant][fill][state][key] || variants[variant][fill][key];
211
+ }
212
+
213
+ const StyledButton = styled.button<StyledProps>`
214
+ all: unset;
215
+ box-sizing: border-box;
216
+ position: relative;
217
+ display: inline-flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ flex-shrink: 0;
221
+ width: ${(p) => (p.icon ? sizes[p.size].height : undefined)};
222
+ height: ${(p) => sizes[p.size].height};
223
+ padding: ${(p) => (p.icon ? '0' : `0 ${sizes[p.size].paddingX}`)};
224
+ font: ${(p) => sizes[p.size].font};
225
+ font-weight: 600;
226
+ line-height: 1;
227
+ border-radius: 100px;
228
+ background: ${(p) => returnColor(p.variant, p.fill, 'default', 'background')};
229
+ color: ${(p) => returnColor(p.variant, p.fill, 'default', 'color')};
230
+ border: 1px solid ${(p) => returnColor(p.variant, p.fill, 'default', 'border')};
231
+ box-shadow: 0 0 0 0px var(--violet4);
232
+ cursor: pointer;
233
+ transition: all 200ms;
234
+ text-decoration: none !important;
235
+
236
+ &:hover {
237
+ background: ${(p) => returnColor(p.variant, p.fill, 'hover', 'background')};
238
+ color: ${(p) => returnColor(p.variant, p.fill, 'hover', 'color')};
239
+ border: 1px solid ${(p) => returnColor(p.variant, p.fill, 'hover', 'border')};
240
+ }
241
+ &:focus {
242
+ background: ${(p) => returnColor(p.variant, p.fill, 'focus', 'background')};
243
+ color: ${(p) => returnColor(p.variant, p.fill, 'focus', 'color')};
244
+ border: 1px solid ${(p) => returnColor(p.variant, p.fill, 'focus', 'border')};
245
+ box-shadow: 0 0 0 4px var(--violet4);
246
+ }
247
+ &:active {
248
+ background: ${(p) => returnColor(p.variant, p.fill, 'active', 'background')};
249
+ color: ${(p) => returnColor(p.variant, p.fill, 'active', 'color')};
250
+ border: 1px solid ${(p) => returnColor(p.variant, p.fill, 'active', 'border')};
251
+ }
252
+
253
+ ${(p) =>
254
+ p.loading &&
255
+ `
256
+ pointer-events: none;
257
+ `}
258
+
259
+ ${(p) =>
260
+ p.disabled &&
261
+ `
262
+ opacity: 1;
263
+ background: ${p.fill === 'ghost' ? 'hsla(0, 0%, 100%, 0)' : 'var(--sand3)'};
264
+ border-color: var(--sand3);
265
+ color: var(--sand8);
266
+ pointer-events: none;
267
+
268
+ i {
269
+ color: var(--sand8) !important;
270
+ }
271
+ `}
272
+ `;
273
+
274
+ const Inner = styled.span<StyledProps>`
275
+ display: inline-flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ gap: ${(p) => sizes[p.size].gap};
279
+
280
+ i {
281
+ font-size: ${(p) => sizes[p.size].icon};
282
+ line-height: ${(p) => sizes[p.size].icon};
283
+ color: ${(p) => (p.icon ? undefined : returnColor(p.variant, p.fill, 'default', 'iconColor'))};
284
+ }
285
+
286
+ ${(p) =>
287
+ p.loading &&
288
+ `
289
+ opacity: 0;
290
+ `}
291
+ `;
292
+
293
+ const Label = styled.span``;
294
+
295
+ const Spinner = styled.i<StyledProps>`
296
+ position: absolute;
297
+ top: 50%;
298
+ left: 0;
299
+ right: 0;
300
+ margin: calc(${(p) => sizes[p.size].icon} * -0.5) auto 0;
301
+ width: ${(p) => sizes[p.size].icon};
302
+ height: ${(p) => sizes[p.size].icon};
303
+ font-size: ${(p) => sizes[p.size].icon};
304
+ line-height: ${(p) => sizes[p.size].icon};
305
+ animation: spin 800ms infinite linear;
306
+
307
+ @keyframes spin {
308
+ from {
309
+ transform: rotate(0deg);
310
+ }
311
+ to {
312
+ transform: rotate(360deg);
313
+ }
314
+ }
315
+ `;
316
+
317
+ export const Button = forwardRef<HTMLButtonElement, Props>(
318
+ (
319
+ {
320
+ disabled,
321
+ fill = 'solid',
322
+ href,
323
+ icon,
324
+ iconLeft,
325
+ iconRight,
326
+ label,
327
+ loading,
328
+ size = 'default',
329
+ type = 'button',
330
+ variant = 'primary',
331
+ ...forwardedProps
332
+ },
333
+ ref,
334
+ ) => {
335
+ const conditionalAttributes: Record<string, unknown> = href
336
+ ? {
337
+ as: 'a',
338
+ href,
339
+ }
340
+ : {
341
+ type,
342
+ disabled: disabled || loading,
343
+ };
344
+
345
+ if (icon) {
346
+ conditionalAttributes['aria-label'] = label;
347
+ }
348
+
349
+ const styledProps: StyledProps = {
350
+ disabled,
351
+ fill,
352
+ icon,
353
+ loading,
354
+ size,
355
+ variant,
356
+ };
357
+
358
+ return (
359
+ <StyledButton ref={ref} {...styledProps} {...conditionalAttributes} {...forwardedProps}>
360
+ <>
361
+ {loading && <Spinner {...styledProps} className="ph-bold ph-circle-notch" />}
362
+ <Inner {...styledProps}>
363
+ {icon ? (
364
+ <i className={icon} />
365
+ ) : (
366
+ <>
367
+ {iconLeft && <i className={iconLeft} />}
368
+ <Label>{label}</Label>
369
+ {iconRight && <i className={iconRight} />}
370
+ </>
371
+ )}
372
+ </Inner>
373
+ </>
374
+ </StyledButton>
375
+ );
376
+ },
377
+ );
378
+
379
+ Button.displayName = 'Button';
@@ -0,0 +1 @@
1
+ export * from './Button';
@@ -0,0 +1,33 @@
1
+ import styled from 'styled-components';
2
+
3
+ const Wrapper = styled.span`
4
+ display: inline-flex;
5
+ width: 100%;
6
+ align-items: center;
7
+ justify-content: center;
8
+ padding: 12px;
9
+ animation: spin 1200ms infinite linear;
10
+
11
+ i {
12
+ color: currentColor;
13
+ font-size: 16px;
14
+ line-height: 16px;
15
+ }
16
+
17
+ @keyframes spin {
18
+ from {
19
+ transform: rotate(0deg);
20
+ }
21
+ to {
22
+ transform: rotate(360deg);
23
+ }
24
+ }
25
+ `;
26
+
27
+ export function Spinner() {
28
+ return (
29
+ <Wrapper>
30
+ <i className="ph ph-spinner"></i>
31
+ </Wrapper>
32
+ );
33
+ }
@@ -0,0 +1 @@
1
+ export * from './Spinner';
@@ -0,0 +1,14 @@
1
+ import styled from 'styled-components';
2
+
3
+ type Props = {
4
+ color?: string;
5
+ font?: string;
6
+ weight?: string;
7
+ };
8
+
9
+ export const Text = styled.p<Props>`
10
+ font: ${(p) => (p.font ? `var(--${p.font})` : 'var(--text-base)')};
11
+ font-weight: ${(p) => p.weight ?? '400'};
12
+ color: ${(p) => (p.color ? `var(--${p.color})` : 'currentColor')};
13
+ margin: 0;
14
+ `;
@@ -0,0 +1 @@
1
+ export * from './Text';
@@ -0,0 +1,83 @@
1
+ # Toast
2
+
3
+ Implemented via Radix primitives: https://www.radix-ui.com/docs/primitives/components/toast
4
+
5
+ _If the current props and Stitches style overrides aren't enough to cover your use case, feel free to implement your own component using the Radix primitives directly._
6
+
7
+ ## Example
8
+
9
+ Using the `openToast` API allows you to easily open a toast from any context:
10
+
11
+ ```tsx
12
+ import { openToast } from '@/components/lib/Toast';
13
+
14
+ ...
15
+
16
+ <Button
17
+ onClick={() =>
18
+ openToast({
19
+ type: 'ERROR',
20
+ title: 'Toast Title',
21
+ description: 'This is a great toast description.',
22
+ })
23
+ }
24
+ >
25
+ Open a Toast
26
+ </Button>
27
+ ```
28
+
29
+ You can pass other options too:
30
+
31
+ ```tsx
32
+ <Button
33
+ onClick={() =>
34
+ openToast({
35
+ type: 'SUCCESS', // SUCCESS | INFO | ERROR
36
+ title: 'Toast Title',
37
+ description: 'This is a great toast description.',
38
+ icon: 'ph-bold ph-pizza', // https://phosphoricons.com/
39
+ duration: 20000, // milliseconds (pass Infinity to disable auto close)
40
+ })
41
+ }
42
+ >
43
+ Open a Toast
44
+ </Button>
45
+ ```
46
+
47
+ ## Deduplicate
48
+
49
+ If you need to ensure only a single instance of a toast is ever displayed at once, you can deduplicate by passing a unique `id` key. If a toast with the passed `id` is currently open, a new toast will not be opened:
50
+
51
+ ```tsx
52
+ <Button
53
+ onClick={() =>
54
+ openToast({
55
+ id: 'my-unique-toast',
56
+ title: 'Toast Title',
57
+ description: 'This is a great toast description.',
58
+ })
59
+ }
60
+ >
61
+ Deduplicated Toast
62
+ </Button>
63
+ ```
64
+
65
+ ## Custom Toast
66
+
67
+ If you need something more custom, you can render a custom toast using `lib/Toast/Toaster.tsx` as an example like so:
68
+
69
+ ```tsx
70
+ import * as Toast from '@/components/lib/Toast';
71
+
72
+ ...
73
+
74
+ <Toast.Provider duration={5000}>
75
+ <Toast.Root open={isOpen} onOpenChange={setIsOpen}>
76
+ <Toast.Title>My Title</Toast.Title>
77
+ <Toast.Description>My Description</Toast.Description>
78
+ <Toast.CloseButton />
79
+ </Toast.Root>
80
+
81
+ <Toast.Viewport />
82
+ </Toast.Provider>
83
+ ```
@@ -0,0 +1,25 @@
1
+ import * as ToastPrimitive from '@radix-ui/react-toast';
2
+ import type { ComponentProps } from 'react';
3
+ import { forwardRef } from 'react';
4
+
5
+ import * as S from './styles';
6
+
7
+ type CloseButtonProps = ComponentProps<typeof S.CloseButton>;
8
+
9
+ export const Root = S.Root;
10
+ export const Title = S.Title;
11
+ export const Description = S.Description;
12
+ export const Content = S.Content;
13
+ export const Viewport = S.Viewport;
14
+ export const Action = ToastPrimitive.Action;
15
+ export const Provider = ToastPrimitive.Provider;
16
+ export const Close = ToastPrimitive.Close;
17
+
18
+ export const CloseButton = forwardRef<HTMLButtonElement, CloseButtonProps>((props, ref) => {
19
+ return (
20
+ <S.CloseButton aria-label="Close" ref={ref} {...props}>
21
+ <i className="ph-bold ph-x"></i>
22
+ </S.CloseButton>
23
+ );
24
+ });
25
+ CloseButton.displayName = 'CloseButton';
@@ -0,0 +1,48 @@
1
+ import type { Toast, ToastType } from './store';
2
+ import { useToasterStore } from './store';
3
+ import * as T from './Toast';
4
+
5
+ export function Toaster() {
6
+ const toaster = useToasterStore();
7
+
8
+ const iconsByType: Record<ToastType, string> = {
9
+ INFO: 'ph ph-info',
10
+ WARNING: 'ph ph-warning',
11
+ ERROR: 'ph ph-warning-circle',
12
+ SUCCESS: 'ph ph-check-circle',
13
+ };
14
+
15
+ function onOpenChange(open: boolean, toast: Toast) {
16
+ if (!open) toaster.close(toast);
17
+ }
18
+
19
+ return (
20
+ <T.Provider duration={5000}>
21
+ {toaster.toasts.map((toast) => {
22
+ const type = toast.type || 'INFO';
23
+ const icon = toast.icon || iconsByType[type];
24
+
25
+ return (
26
+ <T.Root
27
+ data-type={type}
28
+ duration={toast.duration}
29
+ open={toast.isOpen}
30
+ onOpenChange={(open) => onOpenChange(open, toast)}
31
+ key={toast.id}
32
+ >
33
+ <i className={icon} />
34
+
35
+ <T.Content>
36
+ <T.Title>{toast.title}</T.Title>
37
+ {toast.description && <T.Description>{toast.description}</T.Description>}
38
+ </T.Content>
39
+
40
+ <T.CloseButton data-type={type} />
41
+ </T.Root>
42
+ );
43
+ })}
44
+
45
+ <T.Viewport />
46
+ </T.Provider>
47
+ );
48
+ }
@@ -0,0 +1,6 @@
1
+ import type { OpenToastOptions } from './store';
2
+ import { useToasterStore } from './store';
3
+
4
+ export function openToast(options: OpenToastOptions) {
5
+ useToasterStore.getState().open(options);
6
+ }
@@ -0,0 +1,3 @@
1
+ export * from './api';
2
+ export * from './Toast';
3
+ export * from './Toaster';
@@ -0,0 +1,83 @@
1
+ import { create } from 'zustand';
2
+
3
+ export type ToastType = 'INFO' | 'ERROR' | 'SUCCESS' | 'WARNING';
4
+
5
+ export interface Toast {
6
+ description?: string;
7
+ duration?: number;
8
+ icon?: string;
9
+ id: string;
10
+ isOpen: boolean;
11
+ title: string;
12
+ type?: ToastType;
13
+ }
14
+
15
+ export interface OpenToastOptions {
16
+ description?: string;
17
+ duration?: number;
18
+ icon?: string;
19
+ id?: string;
20
+ title: string;
21
+ type?: ToastType;
22
+ }
23
+
24
+ interface ToasterStore {
25
+ toasts: Toast[];
26
+ close: (toast: Toast) => void;
27
+ destroy: (toast: Toast) => void;
28
+ open: (options: OpenToastOptions) => void;
29
+ }
30
+
31
+ export const useToasterStore = create<ToasterStore>((set) => ({
32
+ toasts: [],
33
+
34
+ close: (toast) => {
35
+ set((state) => {
36
+ const toasts = state.toasts.map((t) => {
37
+ if (t.id === toast.id) {
38
+ return {
39
+ ...t,
40
+ isOpen: false,
41
+ };
42
+ }
43
+
44
+ return t;
45
+ });
46
+
47
+ setTimeout(() => {
48
+ state.destroy(toast);
49
+ }, 5000);
50
+
51
+ return {
52
+ toasts,
53
+ };
54
+ });
55
+ },
56
+
57
+ destroy: (toast) => {
58
+ set((state) => {
59
+ const toasts = state.toasts.filter((t) => t.id !== toast.id);
60
+
61
+ return {
62
+ toasts,
63
+ };
64
+ });
65
+ },
66
+
67
+ open: (options) => {
68
+ const newToast = {
69
+ ...options,
70
+ isOpen: true,
71
+ id: options.id || Date.now().toString(),
72
+ type: options.type || 'INFO',
73
+ };
74
+
75
+ set((state) => {
76
+ const toasts = state.toasts.filter((t) => t.id !== newToast.id);
77
+
78
+ return {
79
+ toasts: [...toasts, newToast],
80
+ };
81
+ });
82
+ },
83
+ }));