@a-type/ui 3.0.26 → 3.0.28
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/dist/cjs/colors.stories.js +1 -8
- package/dist/cjs/colors.stories.js.map +1 -1
- package/dist/cjs/components/dialog/Dialog.js +30 -5
- package/dist/cjs/components/dialog/Dialog.js.map +1 -1
- package/dist/cjs/components/dialog/Dialog.stories.js +1 -1
- package/dist/cjs/components/dialog/Dialog.stories.js.map +1 -1
- package/dist/cjs/components/icon/Icon.js +1 -1
- package/dist/cjs/components/icon/Icon.js.map +1 -1
- package/dist/cjs/components/index.d.ts +2 -0
- package/dist/cjs/components/index.js +2 -0
- package/dist/cjs/components/index.js.map +1 -1
- package/dist/cjs/components/lightbox/Lightbox.d.ts +25 -0
- package/dist/cjs/components/lightbox/Lightbox.js +71 -0
- package/dist/cjs/components/lightbox/Lightbox.js.map +1 -0
- package/dist/cjs/components/lightbox/Lightbox.stories.d.ts +24 -0
- package/dist/cjs/components/lightbox/Lightbox.stories.js +21 -0
- package/dist/cjs/components/lightbox/Lightbox.stories.js.map +1 -0
- package/dist/cjs/components/lists/lists.d.ts +6 -0
- package/dist/cjs/components/lists/lists.js +16 -0
- package/dist/cjs/components/lists/lists.js.map +1 -0
- package/dist/cjs/components/lists/lists.stories.d.ts +14 -0
- package/dist/cjs/components/lists/lists.stories.js +25 -0
- package/dist/cjs/components/lists/lists.stories.js.map +1 -0
- package/dist/cjs/components/provider/Provider.d.ts +1 -4
- package/dist/cjs/components/provider/Provider.js +3 -3
- package/dist/cjs/components/provider/Provider.js.map +1 -1
- package/dist/cjs/components/pwaInstall/PlatformIcons.d.ts +5 -0
- package/dist/cjs/components/pwaInstall/PlatformIcons.js +25 -0
- package/dist/cjs/components/pwaInstall/PlatformIcons.js.map +1 -0
- package/dist/cjs/components/pwaInstall/PwaInstallTrigger.d.ts +5 -0
- package/dist/cjs/components/pwaInstall/PwaInstallTrigger.js +74 -0
- package/dist/cjs/components/pwaInstall/PwaInstallTrigger.js.map +1 -0
- package/dist/cjs/components/pwaInstall/PwaInstallTrigger.stories.d.ts +15 -0
- package/dist/cjs/components/pwaInstall/PwaInstallTrigger.stories.js +23 -0
- package/dist/cjs/components/pwaInstall/PwaInstallTrigger.stories.js.map +1 -0
- package/dist/cjs/components/pwaInstall/index.d.ts +3 -0
- package/dist/cjs/components/pwaInstall/index.js +21 -0
- package/dist/cjs/components/pwaInstall/index.js.map +1 -0
- package/dist/cjs/components/pwaInstall/useIsInstallReady.d.ts +3 -0
- package/dist/cjs/components/pwaInstall/useIsInstallReady.js +58 -0
- package/dist/cjs/components/pwaInstall/useIsInstallReady.js.map +1 -0
- package/dist/cjs/components/pwaInstall/useWebManifest.d.ts +2 -0
- package/dist/cjs/components/pwaInstall/useWebManifest.js +45 -0
- package/dist/cjs/components/pwaInstall/useWebManifest.js.map +1 -0
- package/dist/cjs/components/toasts/toasts.d.ts +4 -1
- package/dist/cjs/components/toasts/toasts.js +35 -1
- package/dist/cjs/components/toasts/toasts.js.map +1 -1
- package/dist/cjs/components/toasts/toasts.stories.d.ts +13 -0
- package/dist/cjs/components/toasts/toasts.stories.js +47 -0
- package/dist/cjs/components/toasts/toasts.stories.js.map +1 -0
- package/dist/cjs/platform.d.ts +11 -0
- package/dist/cjs/platform.js +95 -0
- package/dist/cjs/platform.js.map +1 -0
- package/dist/cjs/uno/logic/color.d.ts +0 -12
- package/dist/cjs/uno/logic/color.js +0 -35
- package/dist/cjs/uno/logic/color.js.map +1 -1
- package/dist/cjs/uno/logic/color.test.js +0 -12
- package/dist/cjs/uno/logic/color.test.js.map +1 -1
- package/dist/cjs/uno/theme/index.js +8 -0
- package/dist/cjs/uno/theme/index.js.map +1 -1
- package/dist/css/main.css +6 -5
- package/dist/esm/colors.stories.js +1 -8
- package/dist/esm/colors.stories.js.map +1 -1
- package/dist/esm/components/dialog/Dialog.js +30 -5
- package/dist/esm/components/dialog/Dialog.js.map +1 -1
- package/dist/esm/components/dialog/Dialog.stories.js +1 -1
- package/dist/esm/components/dialog/Dialog.stories.js.map +1 -1
- package/dist/esm/components/icon/Icon.js +1 -1
- package/dist/esm/components/icon/Icon.js.map +1 -1
- package/dist/esm/components/index.d.ts +2 -0
- package/dist/esm/components/index.js +2 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/esm/components/lightbox/Lightbox.d.ts +25 -0
- package/dist/esm/components/lightbox/Lightbox.js +32 -0
- package/dist/esm/components/lightbox/Lightbox.js.map +1 -0
- package/dist/esm/components/lightbox/Lightbox.stories.d.ts +24 -0
- package/dist/esm/components/lightbox/Lightbox.stories.js +18 -0
- package/dist/esm/components/lightbox/Lightbox.stories.js.map +1 -0
- package/dist/esm/components/lists/lists.d.ts +6 -0
- package/dist/esm/components/lists/lists.js +13 -0
- package/dist/esm/components/lists/lists.js.map +1 -0
- package/dist/esm/components/lists/lists.stories.d.ts +14 -0
- package/dist/esm/components/lists/lists.stories.js +22 -0
- package/dist/esm/components/lists/lists.stories.js.map +1 -0
- package/dist/esm/components/provider/Provider.d.ts +1 -4
- package/dist/esm/components/provider/Provider.js +3 -3
- package/dist/esm/components/provider/Provider.js.map +1 -1
- package/dist/esm/components/pwaInstall/PlatformIcons.d.ts +5 -0
- package/dist/esm/components/pwaInstall/PlatformIcons.js +18 -0
- package/dist/esm/components/pwaInstall/PlatformIcons.js.map +1 -0
- package/dist/esm/components/pwaInstall/PwaInstallTrigger.d.ts +5 -0
- package/dist/esm/components/pwaInstall/PwaInstallTrigger.js +71 -0
- package/dist/esm/components/pwaInstall/PwaInstallTrigger.js.map +1 -0
- package/dist/esm/components/pwaInstall/PwaInstallTrigger.stories.d.ts +15 -0
- package/dist/esm/components/pwaInstall/PwaInstallTrigger.stories.js +20 -0
- package/dist/esm/components/pwaInstall/PwaInstallTrigger.stories.js.map +1 -0
- package/dist/esm/components/pwaInstall/index.d.ts +3 -0
- package/dist/esm/components/pwaInstall/index.js +5 -0
- package/dist/esm/components/pwaInstall/index.js.map +1 -0
- package/dist/esm/components/pwaInstall/useIsInstallReady.d.ts +3 -0
- package/dist/esm/components/pwaInstall/useIsInstallReady.js +53 -0
- package/dist/esm/components/pwaInstall/useIsInstallReady.js.map +1 -0
- package/dist/esm/components/pwaInstall/useWebManifest.d.ts +2 -0
- package/dist/esm/components/pwaInstall/useWebManifest.js +42 -0
- package/dist/esm/components/pwaInstall/useWebManifest.js.map +1 -0
- package/dist/esm/components/toasts/toasts.d.ts +4 -1
- package/dist/esm/components/toasts/toasts.js +32 -1
- package/dist/esm/components/toasts/toasts.js.map +1 -1
- package/dist/esm/components/toasts/toasts.stories.d.ts +13 -0
- package/dist/esm/components/toasts/toasts.stories.js +44 -0
- package/dist/esm/components/toasts/toasts.stories.js.map +1 -0
- package/dist/esm/platform.d.ts +11 -0
- package/dist/esm/platform.js +84 -0
- package/dist/esm/platform.js.map +1 -0
- package/dist/esm/uno/logic/color.d.ts +0 -12
- package/dist/esm/uno/logic/color.js +0 -35
- package/dist/esm/uno/logic/color.js.map +1 -1
- package/dist/esm/uno/logic/color.test.js +0 -12
- package/dist/esm/uno/logic/color.test.js.map +1 -1
- package/dist/esm/uno/theme/index.js +8 -0
- package/dist/esm/uno/theme/index.js.map +1 -1
- package/package.json +5 -7
- package/src/colors.stories.tsx +0 -12
- package/src/components/dialog/Dialog.stories.tsx +20 -6
- package/src/components/dialog/Dialog.tsx +49 -4
- package/src/components/icon/Icon.tsx +6 -1
- package/src/components/index.ts +2 -0
- package/src/components/lightbox/Lightbox.stories.tsx +28 -0
- package/src/components/lightbox/Lightbox.tsx +87 -0
- package/src/components/lists/lists.stories.tsx +38 -0
- package/src/components/lists/lists.tsx +21 -0
- package/src/components/provider/Provider.tsx +2 -9
- package/src/components/pwaInstall/PlatformIcons.tsx +75 -0
- package/src/components/pwaInstall/PwaInstallTrigger.stories.tsx +29 -0
- package/src/components/pwaInstall/PwaInstallTrigger.tsx +227 -0
- package/src/components/pwaInstall/index.ts +3 -0
- package/src/components/pwaInstall/useIsInstallReady.ts +56 -0
- package/src/components/pwaInstall/useWebManifest.ts +55 -0
- package/src/components/toasts/toasts.stories.tsx +83 -0
- package/src/components/toasts/toasts.tsx +72 -1
- package/src/platform.ts +103 -0
- package/src/uno/logic/color.test.ts +0 -27
- package/src/uno/logic/color.ts +0 -56
- package/src/uno/theme/index.ts +8 -0
- package/dist/cjs/uno/logic/oklch.d.ts +0 -3
- package/dist/cjs/uno/logic/oklch.js +0 -96
- package/dist/cjs/uno/logic/oklch.js.map +0 -1
- package/dist/esm/uno/logic/oklch.d.ts +0 -3
- package/dist/esm/uno/logic/oklch.js +0 -90
- package/dist/esm/uno/logic/oklch.js.map +0 -1
- package/src/uno/logic/oklch.ts +0 -120
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { DialogTriggerProps } from '@radix-ui/react-dialog';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
getIsSafari,
|
|
5
|
+
getOS,
|
|
6
|
+
getSupportsPWAInstallPrompt,
|
|
7
|
+
} from '../../platform.js';
|
|
8
|
+
import { Box } from '../box/Box.js';
|
|
9
|
+
import { Button } from '../button/Button.js';
|
|
10
|
+
import { Dialog } from '../dialog/Dialog.js';
|
|
11
|
+
import { Icon } from '../icon/Icon.js';
|
|
12
|
+
import { Lightbox } from '../lightbox/Lightbox.js';
|
|
13
|
+
import { Ol } from '../lists/lists.js';
|
|
14
|
+
import { P } from '../typography/typography.js';
|
|
15
|
+
import {
|
|
16
|
+
AndroidAddToHomeIcon,
|
|
17
|
+
SafariAddToDockIcon,
|
|
18
|
+
SafariIcon,
|
|
19
|
+
SafariPlusSquareIcon,
|
|
20
|
+
SafariShareIcon,
|
|
21
|
+
} from './PlatformIcons.js';
|
|
22
|
+
import {
|
|
23
|
+
triggerDeferredInstall,
|
|
24
|
+
useIsInstalled,
|
|
25
|
+
useIsInstallReady,
|
|
26
|
+
} from './useIsInstallReady.js';
|
|
27
|
+
import { useWebManifest } from './useWebManifest.js';
|
|
28
|
+
|
|
29
|
+
export interface PwaInstallTriggerProps extends DialogTriggerProps {
|
|
30
|
+
manifestPath?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PwaInstallTrigger({
|
|
34
|
+
children,
|
|
35
|
+
manifestPath,
|
|
36
|
+
asChild: _,
|
|
37
|
+
...rest
|
|
38
|
+
}: PwaInstallTriggerProps) {
|
|
39
|
+
const installed = useIsInstalled();
|
|
40
|
+
const manifest = useWebManifest(manifestPath);
|
|
41
|
+
|
|
42
|
+
const [showInstructions, setShowInstructions] = useState(false);
|
|
43
|
+
|
|
44
|
+
if (installed) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const primaryIcon = manifest?.icons?.[0];
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Dialog>
|
|
52
|
+
<Dialog.Trigger {...rest} asChild>
|
|
53
|
+
{children ?? (
|
|
54
|
+
<Button color="primary" emphasis="light">
|
|
55
|
+
<Icon name="star" /> Install
|
|
56
|
+
</Button>
|
|
57
|
+
)}
|
|
58
|
+
</Dialog.Trigger>
|
|
59
|
+
<Dialog.Content className="flex flex-col gap-xs">
|
|
60
|
+
<Dialog.Title className="flex flex-row gap-md items-center">
|
|
61
|
+
{primaryIcon && (
|
|
62
|
+
<img
|
|
63
|
+
src={primaryIcon.src}
|
|
64
|
+
alt={
|
|
65
|
+
primaryIcon.label ??
|
|
66
|
+
manifest?.short_name ??
|
|
67
|
+
manifest?.name ??
|
|
68
|
+
'App Icon'
|
|
69
|
+
}
|
|
70
|
+
className="inline-block w-1em h-1em rounded"
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
Install {manifest?.short_name ?? manifest?.name ?? 'App'}
|
|
74
|
+
</Dialog.Title>
|
|
75
|
+
{showInstructions ? (
|
|
76
|
+
<>
|
|
77
|
+
<Dialog.Description>
|
|
78
|
+
Follow the instructions below to install this app on your device.
|
|
79
|
+
</Dialog.Description>
|
|
80
|
+
<InstallInstructions />
|
|
81
|
+
<Dialog.Actions>
|
|
82
|
+
<Dialog.Close asChild>
|
|
83
|
+
<Button emphasis="ghost">Close</Button>
|
|
84
|
+
</Dialog.Close>
|
|
85
|
+
<Button onClick={() => setShowInstructions(false)}>
|
|
86
|
+
<Icon name="arrowLeft" /> Back
|
|
87
|
+
</Button>
|
|
88
|
+
</Dialog.Actions>
|
|
89
|
+
</>
|
|
90
|
+
) : (
|
|
91
|
+
<>
|
|
92
|
+
<Dialog.Description>
|
|
93
|
+
This site is also an app. You can install it right now for easier
|
|
94
|
+
access and more features.
|
|
95
|
+
</Dialog.Description>
|
|
96
|
+
{manifest?.description && (
|
|
97
|
+
<P className="mb-sm">{manifest.description}</P>
|
|
98
|
+
)}
|
|
99
|
+
<ManifestImageGallery manifestPath={manifestPath} />
|
|
100
|
+
<Dialog.Actions>
|
|
101
|
+
<Dialog.Close asChild>
|
|
102
|
+
<Button emphasis="ghost">Close</Button>
|
|
103
|
+
</Dialog.Close>
|
|
104
|
+
<InstallDeviceActions
|
|
105
|
+
showInstructions={() => setShowInstructions(true)}
|
|
106
|
+
/>
|
|
107
|
+
</Dialog.Actions>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</Dialog.Content>
|
|
111
|
+
</Dialog>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const supportsDirectInstall = getSupportsPWAInstallPrompt();
|
|
116
|
+
|
|
117
|
+
function InstallInstructions() {
|
|
118
|
+
const os = getOS();
|
|
119
|
+
if (os === 'iOS' || os === 'Mac OS') {
|
|
120
|
+
if (getIsSafari()) {
|
|
121
|
+
if (os === 'iOS') {
|
|
122
|
+
return (
|
|
123
|
+
<Ol>
|
|
124
|
+
<Ol.Item>
|
|
125
|
+
<SafariShareIcon /> Tap the Share button in the toolbar.
|
|
126
|
+
</Ol.Item>
|
|
127
|
+
<Ol.Item>
|
|
128
|
+
<SafariPlusSquareIcon /> Scroll down and tap "Add to Home Screen".
|
|
129
|
+
</Ol.Item>
|
|
130
|
+
</Ol>
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
return (
|
|
134
|
+
<Ol>
|
|
135
|
+
<Ol.Item>
|
|
136
|
+
<SafariShareIcon /> Tap the Share button in the toolbar.
|
|
137
|
+
</Ol.Item>
|
|
138
|
+
<Ol.Item>
|
|
139
|
+
<SafariAddToDockIcon /> Tap "Add to Dock".
|
|
140
|
+
</Ol.Item>
|
|
141
|
+
</Ol>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
return (
|
|
146
|
+
<>
|
|
147
|
+
<Ol>
|
|
148
|
+
<Ol.Item>
|
|
149
|
+
<SafariIcon /> Open this site in Safari to continue.
|
|
150
|
+
</Ol.Item>
|
|
151
|
+
</Ol>
|
|
152
|
+
<Box surface p>
|
|
153
|
+
Apple does not allow non-Safari browsers to install web apps.
|
|
154
|
+
</Box>
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Ol>
|
|
162
|
+
<Ol.Item>
|
|
163
|
+
<Icon name="dots" className="rotate-90" /> Tap the menu button in the
|
|
164
|
+
toolbar.
|
|
165
|
+
</Ol.Item>
|
|
166
|
+
<Ol.Item>
|
|
167
|
+
<AndroidAddToHomeIcon /> Tap "Add to Home Screen."
|
|
168
|
+
</Ol.Item>
|
|
169
|
+
</Ol>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ManifestImageGallery({ manifestPath }: { manifestPath?: string }) {
|
|
174
|
+
const manifest = useWebManifest(manifestPath);
|
|
175
|
+
|
|
176
|
+
if (!manifest?.screenshots?.length) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Box overflow="auto-x" p="sm" gap className="h-240px">
|
|
182
|
+
{manifest.screenshots?.map((screenshot, index) => (
|
|
183
|
+
<Lightbox.Root key={screenshot.src}>
|
|
184
|
+
<Lightbox.Trigger asChild>
|
|
185
|
+
<Lightbox.Image
|
|
186
|
+
tabIndex={0}
|
|
187
|
+
key={index}
|
|
188
|
+
src={screenshot.src}
|
|
189
|
+
alt={screenshot.label || `Screenshot ${index + 1}`}
|
|
190
|
+
className="border border-default rounded-xs"
|
|
191
|
+
/>
|
|
192
|
+
</Lightbox.Trigger>
|
|
193
|
+
<Lightbox.Portal>
|
|
194
|
+
<Lightbox.Overlay className="z-10000" />
|
|
195
|
+
<Lightbox.Content className="z-10001">
|
|
196
|
+
<Lightbox.Image
|
|
197
|
+
src={screenshot.src}
|
|
198
|
+
alt={screenshot.label || `Screenshot ${index + 1}`}
|
|
199
|
+
/>
|
|
200
|
+
</Lightbox.Content>
|
|
201
|
+
</Lightbox.Portal>
|
|
202
|
+
</Lightbox.Root>
|
|
203
|
+
))}
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function InstallDeviceActions({
|
|
209
|
+
showInstructions,
|
|
210
|
+
}: {
|
|
211
|
+
showInstructions: () => void;
|
|
212
|
+
}) {
|
|
213
|
+
const ready = useIsInstallReady();
|
|
214
|
+
if (supportsDirectInstall && ready) {
|
|
215
|
+
return (
|
|
216
|
+
<Button onClick={() => triggerDeferredInstall()} emphasis="primary">
|
|
217
|
+
<Icon name="download" /> Install now
|
|
218
|
+
</Button>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<Button onClick={() => showInstructions()} emphasis="primary">
|
|
224
|
+
<Icon name="arrowRight" /> Add to Home
|
|
225
|
+
</Button>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { proxy, useSnapshot } from 'valtio';
|
|
2
|
+
import { getIsPWAInstalled, PRETEND_INSTALLABLE } from '../../platform.js';
|
|
3
|
+
|
|
4
|
+
let deferredPrompt: BeforeInstallPromptEvent | null = null;
|
|
5
|
+
const installState = proxy({
|
|
6
|
+
installReady: false,
|
|
7
|
+
installed: getIsPWAInstalled(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
if (typeof window !== 'undefined') {
|
|
11
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
12
|
+
// Prevent the mini-infobar from appearing on mobile
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
e.stopImmediatePropagation();
|
|
15
|
+
e.stopPropagation();
|
|
16
|
+
// Stash the event so it can be triggered later.
|
|
17
|
+
deferredPrompt = e;
|
|
18
|
+
// Update UI notify the user they can install the PWA
|
|
19
|
+
installState.installReady = true;
|
|
20
|
+
// Optionally, send analytics event that PWA install promo was shown.
|
|
21
|
+
console.log(`Ready to show custom install prompt`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (PRETEND_INSTALLABLE) {
|
|
26
|
+
installState.installReady = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useIsInstallReady() {
|
|
30
|
+
return useSnapshot(installState).installReady;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useIsInstalled() {
|
|
34
|
+
return useSnapshot(installState).installed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function triggerDeferredInstall() {
|
|
38
|
+
if (!deferredPrompt) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Show the install prompt
|
|
42
|
+
deferredPrompt.prompt();
|
|
43
|
+
// Wait for the user to respond to the prompt
|
|
44
|
+
deferredPrompt.userChoice.then((choiceResult) => {
|
|
45
|
+
if (choiceResult.outcome === 'accepted') {
|
|
46
|
+
console.log('User accepted the install prompt');
|
|
47
|
+
installState.installed = true;
|
|
48
|
+
} else {
|
|
49
|
+
console.log('User dismissed the install prompt');
|
|
50
|
+
}
|
|
51
|
+
// Clear the deferredPrompt so it can only be used once.
|
|
52
|
+
deferredPrompt = null;
|
|
53
|
+
installState.installReady = false;
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { proxy, useSnapshot } from 'valtio';
|
|
3
|
+
import { WebAppManifest } from 'web-app-manifest';
|
|
4
|
+
|
|
5
|
+
async function fetchManifest(
|
|
6
|
+
manifestPath: string = '/manifest.json',
|
|
7
|
+
controller?: AbortController,
|
|
8
|
+
): Promise<WebAppManifest | undefined> {
|
|
9
|
+
const response = await fetch(manifestPath, {
|
|
10
|
+
signal: controller?.signal,
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
console.error(
|
|
14
|
+
`Failed to fetch web manifest: ${response.status} ${response.statusText}`,
|
|
15
|
+
);
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const manifest = await response.json();
|
|
20
|
+
return manifest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function abortableManifestSync(manifestPath: string) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
fetchManifest(manifestPath, controller).then((manifest) => {
|
|
26
|
+
if (manifest) {
|
|
27
|
+
manifestState.value = manifest;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return controller;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const manifestState = proxy({
|
|
34
|
+
value: {} as WebAppManifest,
|
|
35
|
+
});
|
|
36
|
+
let abortController: AbortController | null = null;
|
|
37
|
+
|
|
38
|
+
export function useWebManifest(
|
|
39
|
+
manifestPath = '/manifest.json',
|
|
40
|
+
): WebAppManifest {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (abortController) {
|
|
43
|
+
abortController.abort();
|
|
44
|
+
}
|
|
45
|
+
abortController = abortableManifestSync(manifestPath);
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
if (abortController) {
|
|
49
|
+
abortController.abort();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}, [manifestPath]);
|
|
53
|
+
|
|
54
|
+
return useSnapshot(manifestState).value as WebAppManifest;
|
|
55
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { toast } from 'react-hot-toast';
|
|
3
|
+
import { Box } from '../box/Box.js';
|
|
4
|
+
import { Button } from '../button/Button.js';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/toasts',
|
|
8
|
+
argTypes: {},
|
|
9
|
+
parameters: {
|
|
10
|
+
controls: { expanded: true },
|
|
11
|
+
},
|
|
12
|
+
} satisfies Meta;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render(args) {
|
|
20
|
+
return (
|
|
21
|
+
<Box col gap>
|
|
22
|
+
<Button
|
|
23
|
+
onClick={() => {
|
|
24
|
+
toast(
|
|
25
|
+
'This is a default toast! With a lot of text. Enough to wrap around.',
|
|
26
|
+
{
|
|
27
|
+
duration: 10_000,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
Show Default Toast
|
|
33
|
+
</Button>
|
|
34
|
+
<Button
|
|
35
|
+
color="success"
|
|
36
|
+
onClick={() => {
|
|
37
|
+
toast.success('This is a success toast!', {
|
|
38
|
+
duration: 10_000,
|
|
39
|
+
});
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
Show Success Toast
|
|
43
|
+
</Button>
|
|
44
|
+
<Button
|
|
45
|
+
color="attention"
|
|
46
|
+
onClick={() => {
|
|
47
|
+
toast.error('This is an error toast!', {
|
|
48
|
+
duration: 10_000,
|
|
49
|
+
});
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
Show Error Toast
|
|
53
|
+
</Button>
|
|
54
|
+
<Button
|
|
55
|
+
onClick={() => {
|
|
56
|
+
const id = toast.loading('This is a loading toast!');
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
toast.success('Loading complete!', { id, duration: 5000 });
|
|
59
|
+
}, 3000);
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
Show Loading Toast
|
|
63
|
+
</Button>
|
|
64
|
+
<Button
|
|
65
|
+
onClick={() => {
|
|
66
|
+
toast.promise(
|
|
67
|
+
(async () => {
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
69
|
+
})(),
|
|
70
|
+
{
|
|
71
|
+
loading: 'Promise is loading...',
|
|
72
|
+
success: 'Promise resolved!',
|
|
73
|
+
error: 'Promise rejected.',
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
Show Promise Toast
|
|
79
|
+
</Button>
|
|
80
|
+
</Box>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -1,2 +1,73 @@
|
|
|
1
1
|
export type * from 'react-hot-toast';
|
|
2
|
-
export { toast
|
|
2
|
+
export { toast } from 'react-hot-toast';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { AnimatePresence, motion } from 'motion/react';
|
|
5
|
+
import { DefaultToastOptions, useToaster } from 'react-hot-toast';
|
|
6
|
+
import { useResolvedColorMode } from '../../colorMode.js';
|
|
7
|
+
import { Icon } from '../icon/Icon.js';
|
|
8
|
+
|
|
9
|
+
const toastOptions: DefaultToastOptions = {};
|
|
10
|
+
|
|
11
|
+
export const Toaster = (props: { className?: string }) => {
|
|
12
|
+
const mode = useResolvedColorMode();
|
|
13
|
+
const { toasts, handlers } = useToaster(toastOptions);
|
|
14
|
+
const { startPause, endPause } = handlers;
|
|
15
|
+
const visibleToasts = toasts.filter((t) => t.visible);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={clsx(
|
|
20
|
+
'fixed z-toast flex flex-col items-center gap-xs left-1/2 center-x top-sm max-w-400px',
|
|
21
|
+
mode === 'dark' ? 'override-light' : 'override-dark',
|
|
22
|
+
props.className,
|
|
23
|
+
)}
|
|
24
|
+
onMouseEnter={startPause}
|
|
25
|
+
onMouseLeave={endPause}
|
|
26
|
+
>
|
|
27
|
+
<AnimatePresence>
|
|
28
|
+
{visibleToasts.map((toast) => {
|
|
29
|
+
const message =
|
|
30
|
+
typeof toast.message === 'function'
|
|
31
|
+
? toast.message(toast)
|
|
32
|
+
: toast.message;
|
|
33
|
+
return (
|
|
34
|
+
<motion.div
|
|
35
|
+
key={toast.id}
|
|
36
|
+
className={clsx(
|
|
37
|
+
{
|
|
38
|
+
'palette-success': toast.type === 'success',
|
|
39
|
+
'palette-attention': toast.type === 'error',
|
|
40
|
+
'palette-info': toast.type === 'blank',
|
|
41
|
+
},
|
|
42
|
+
'bg-main-wash color-black rounded-md shadow-md px-md py-sm',
|
|
43
|
+
'flex flex-row gap-sm',
|
|
44
|
+
)}
|
|
45
|
+
{...toast.ariaProps}
|
|
46
|
+
initial={{ scale: 0.8, opacity: 0, y: -20 }}
|
|
47
|
+
exit={{ scale: 0.8, opacity: 0, y: -20 }}
|
|
48
|
+
animate={{
|
|
49
|
+
scale: 1,
|
|
50
|
+
opacity: 1,
|
|
51
|
+
y: 0,
|
|
52
|
+
}}
|
|
53
|
+
layout
|
|
54
|
+
>
|
|
55
|
+
<Icon
|
|
56
|
+
className="mt-2px"
|
|
57
|
+
loading={toast.type === 'loading'}
|
|
58
|
+
name={
|
|
59
|
+
toast.type === 'success'
|
|
60
|
+
? 'check'
|
|
61
|
+
: toast.type === 'error'
|
|
62
|
+
? 'warning'
|
|
63
|
+
: 'info'
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
{message}
|
|
67
|
+
</motion.div>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
</AnimatePresence>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const PRETEND_PWA =
|
|
2
|
+
(typeof localStorage !== 'undefined' &&
|
|
3
|
+
localStorage.getItem('pretendPWA')) === 'true';
|
|
4
|
+
|
|
5
|
+
export const PRETEND_INSTALLABLE =
|
|
6
|
+
typeof localStorage !== 'undefined' &&
|
|
7
|
+
localStorage.getItem('pretendInstallable') === 'true';
|
|
8
|
+
|
|
9
|
+
export const PRETEND_OS =
|
|
10
|
+
(typeof localStorage !== 'undefined' && localStorage.getItem('pretendOS')) ||
|
|
11
|
+
null;
|
|
12
|
+
|
|
13
|
+
export function getIsPWAInstalled() {
|
|
14
|
+
return (
|
|
15
|
+
(typeof window !== 'undefined' && PRETEND_PWA) ||
|
|
16
|
+
window.matchMedia('(display-mode: standalone)').matches
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getOS() {
|
|
21
|
+
if (PRETEND_OS) {
|
|
22
|
+
return PRETEND_OS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof window === 'undefined') {
|
|
26
|
+
return 'Server';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const userAgent = window.navigator.userAgent;
|
|
30
|
+
const platform = window.navigator.platform;
|
|
31
|
+
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'];
|
|
32
|
+
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
|
|
33
|
+
const iosPlatforms = ['iPhone', 'iPad', 'iPod'];
|
|
34
|
+
|
|
35
|
+
if (macosPlatforms.indexOf(platform) !== -1) {
|
|
36
|
+
return 'Mac OS';
|
|
37
|
+
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
|
38
|
+
return 'iOS';
|
|
39
|
+
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
|
40
|
+
return 'Windows';
|
|
41
|
+
} else if (/Android/.test(userAgent)) {
|
|
42
|
+
return 'Android';
|
|
43
|
+
} else if (!platform && /Linux/.test(userAgent)) {
|
|
44
|
+
return 'Linux';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return 'Other';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getIsSafari() {
|
|
51
|
+
if (typeof window === 'undefined') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
56
|
+
return !!ua.match(/WebKit/i) && !ua.match(/CriOS/i);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getIsFirefox() {
|
|
60
|
+
if (typeof window === 'undefined') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
65
|
+
return !!ua.match(/Firefox/i);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getIsEdge() {
|
|
69
|
+
if (typeof window === 'undefined') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
74
|
+
return !!ua.match(/Edge/i);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getIsMobile() {
|
|
78
|
+
return (
|
|
79
|
+
typeof window !== 'undefined' &&
|
|
80
|
+
(/Mobi/.test(navigator.userAgent) ||
|
|
81
|
+
/Android/i.test(navigator.userAgent) ||
|
|
82
|
+
/iPhone/i.test(navigator.userAgent) ||
|
|
83
|
+
/iPad/i.test(navigator.userAgent))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getSupportsPWAInstallPrompt() {
|
|
88
|
+
if (PRETEND_INSTALLABLE) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return typeof window !== 'undefined' && 'BeforeInstallPromptEvent' in window;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let isUsingTouch = false;
|
|
95
|
+
if (typeof window !== 'undefined') {
|
|
96
|
+
window.addEventListener('touchstart', () => {
|
|
97
|
+
isUsingTouch = true;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getIsTouch() {
|
|
102
|
+
return isUsingTouch;
|
|
103
|
+
}
|
|
@@ -50,36 +50,18 @@ describe('oklch color evaluation tools', () => {
|
|
|
50
50
|
});
|
|
51
51
|
it('should evaluate a valid oklch color value using element property values', () => {
|
|
52
52
|
const context = snapshotColorContext('leek');
|
|
53
|
-
expect(wash.computeSrgb(context)).toMatchInlineSnapshot(
|
|
54
|
-
`"rgb(100% 100% 100%)"`,
|
|
55
|
-
);
|
|
56
|
-
expect(wash.computeHex(context)).toMatchInlineSnapshot(`"#fff"`);
|
|
57
53
|
expect(wash.computeOklch(context)).toMatchInlineSnapshot(
|
|
58
54
|
`"oklch(100% 10% 165.88)"`,
|
|
59
55
|
);
|
|
60
|
-
|
|
61
|
-
expect(DEFAULT.computeSrgb(context)).toMatchInlineSnapshot(
|
|
62
|
-
`"rgb(40.747% 99.344% 77.98%)"`,
|
|
63
|
-
);
|
|
64
|
-
expect(DEFAULT.computeHex(context)).toMatchInlineSnapshot(`"#68fdc7"`);
|
|
65
56
|
expect(DEFAULT.computeOklch(context)).toMatchInlineSnapshot(
|
|
66
57
|
`"oklch(90% 37.5% 165.88)"`,
|
|
67
58
|
);
|
|
68
59
|
});
|
|
69
60
|
it('should evaluate different named palettes in addition to main', () => {
|
|
70
61
|
const context = snapshotColorContext('attention');
|
|
71
|
-
expect(wash.computeSrgb(context)).toMatchInlineSnapshot(
|
|
72
|
-
`"rgb(100% 100% 100%)"`,
|
|
73
|
-
);
|
|
74
|
-
expect(wash.computeHex(context)).toMatchInlineSnapshot(`"#fff"`);
|
|
75
62
|
expect(wash.computeOklch(context)).toMatchInlineSnapshot(
|
|
76
63
|
`"oklch(100% 10% 30)"`,
|
|
77
64
|
);
|
|
78
|
-
|
|
79
|
-
expect(DEFAULT.computeSrgb(context)).toMatchInlineSnapshot(
|
|
80
|
-
`"rgb(100% 80.069% 75.637%)"`,
|
|
81
|
-
);
|
|
82
|
-
expect(DEFAULT.computeHex(context)).toMatchInlineSnapshot(`"#ffccc1"`);
|
|
83
65
|
expect(DEFAULT.computeOklch(context)).toMatchInlineSnapshot(
|
|
84
66
|
`"oklch(90% 37.5% 30)"`,
|
|
85
67
|
);
|
|
@@ -89,18 +71,9 @@ describe('oklch color evaluation tools', () => {
|
|
|
89
71
|
darkMode.classList.add('override-dark', 'palette-leek');
|
|
90
72
|
document.body.appendChild(darkMode);
|
|
91
73
|
const context = snapshotColorContext('leek', 'dark');
|
|
92
|
-
expect(wash.computeSrgb(context)).toMatchInlineSnapshot(
|
|
93
|
-
`"rgb(0% 14.06% 7.3466%)"`,
|
|
94
|
-
);
|
|
95
|
-
expect(wash.computeHex(context)).toMatchInlineSnapshot(`"#002413"`);
|
|
96
74
|
expect(wash.computeOklch(context)).toMatchInlineSnapshot(
|
|
97
75
|
`"oklch(22% 20% 165.88)"`,
|
|
98
76
|
);
|
|
99
|
-
|
|
100
|
-
expect(DEFAULT.computeSrgb(context)).toMatchInlineSnapshot(
|
|
101
|
-
`"rgb(0% 60.697% 41.999%)"`,
|
|
102
|
-
);
|
|
103
|
-
expect(DEFAULT.computeHex(context)).toMatchInlineSnapshot(`"#009b6b"`);
|
|
104
77
|
expect(DEFAULT.computeOklch(context)).toMatchInlineSnapshot(
|
|
105
78
|
`"oklch(60% 40% 165.88)"`,
|
|
106
79
|
);
|