@ponti-studios/ui 1.5.1 → 1.7.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.
- package/README.md +7 -11
- package/package.json +6 -31
- package/src/components/feedback/alert.tsx +8 -4
- package/src/components/feedback/index.ts +0 -4
- package/src/components/feedback/progress.stories.tsx +15 -0
- package/src/components/feedback/progress.tsx +14 -3
- package/src/components/feedback/update-guard.stories.tsx +4 -8
- package/src/components/feedback/update-guard.tsx +41 -272
- package/src/components/filters/filter-select.tsx +3 -5
- package/src/components/forms/date-picker.stories.tsx +158 -18
- package/src/components/forms/date-picker.tsx +286 -51
- package/src/components/forms/date-utils.ts +7 -2
- package/src/components/forms/drop-zone.tsx +11 -12
- package/src/components/forms/index.ts +2 -6
- package/src/components/forms/passkey-management.stories.tsx +8 -4
- package/src/components/forms/passkey-management.tsx +78 -42
- package/src/components/forms/radio-group.stories.tsx +3 -5
- package/src/components/forms/radio-group.tsx +2 -2
- package/src/components/forms/select.stories.tsx +16 -13
- package/src/components/forms/textarea.stories.tsx +1 -1
- package/src/components/layout/index.ts +2 -0
- package/src/components/layout/particle-background.stories.tsx +38 -0
- package/src/components/layout/particle-background.tsx +378 -0
- package/src/components/layout/scroll-area.stories.tsx +12 -4
- package/src/components/layout/scroll-area.tsx +41 -4
- package/src/components/navigation/tabs.stories.tsx +0 -21
- package/src/components/overlays/dialog.stories.tsx +38 -1
- package/src/components/overlays/dialog.tsx +53 -12
- package/src/components/overlays/index.ts +0 -11
- package/src/components/primitives/avatar-placeholder.svg +13 -0
- package/src/components/primitives/avatar.stories.tsx +2 -1
- package/src/components/primitives/badge.stories.tsx +13 -3
- package/src/components/primitives/badge.tsx +1 -1
- package/src/components/primitives/button.tsx +1 -1
- package/src/components/primitives/card.tsx +6 -2
- package/src/components/primitives/index.ts +0 -3
- package/src/index.ts +1 -3
- package/src/lib/index.ts +2 -3
- package/src/styles/animations.css +62 -0
- package/src/styles/index.css +27 -0
- package/src/styles/tokens/generated/dark.colors.generated.ts +15 -12
- package/src/styles/tokens/generated/dark.css +15 -12
- package/src/styles/tokens/generated/light.colors.generated.ts +14 -11
- package/src/styles/tokens/generated/light.css +14 -11
- package/src/styles/tokens/index.ts +0 -3
- package/src/styles/tokens/source/dark.tokens.json +24 -12
- package/src/styles/tokens/source/light.tokens.json +23 -11
- package/src/styles/tokens/typography.stories.tsx +157 -0
- package/src/components/feedback/file-upload-status.stories.tsx +0 -57
- package/src/components/feedback/file-upload-status.tsx +0 -160
- package/src/components/feedback/progress-bar.stories.tsx +0 -39
- package/src/components/feedback/progress-bar.tsx +0 -22
- package/src/components/forms/calendar.stories.tsx +0 -30
- package/src/components/forms/calendar.tsx +0 -70
- package/src/components/forms/date-field.stories.tsx +0 -164
- package/src/components/forms/date-field.tsx +0 -174
- package/src/components/overlays/alert-dialog.stories.tsx +0 -45
- package/src/components/overlays/alert-dialog.tsx +0 -116
- package/src/components/primitives/label.stories.tsx +0 -29
- package/src/components/primitives/label.tsx +0 -20
- package/src/components/primitives/status-badge.stories.tsx +0 -52
- package/src/components/primitives/status-badge.tsx +0 -33
- package/src/lib/create-memory-storage.ts +0 -24
- package/src/native/card.tsx +0 -36
- package/src/native/icon-button.tsx +0 -41
- package/src/native/index.ts +0 -38
- package/src/native/list-row.tsx +0 -67
- package/src/native/make-styles.ts +0 -32
- package/src/native/shadows.ts +0 -38
- package/src/native/text-field.tsx +0 -34
- package/src/native/uniwind.d.ts +0 -1
- package/src/native/use-theme-colors.ts +0 -19
- package/src/styles/tokens/foundations.stories.tsx +0 -109
- package/src/styles/tokens/generated/native.generated.ts +0 -266
- package/src/styles/tokens/normalize-native.ts +0 -88
- package/src/styles/tokens/source/native.tokens.json +0 -204
- package/src/styles/tokens/style-dictionary.native.mjs +0 -26
package/README.md
CHANGED
|
@@ -16,13 +16,13 @@ The design system is documented in `docs/`:
|
|
|
16
16
|
Common development workflows are available through the `Makefile`:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
make
|
|
20
|
-
make
|
|
21
|
-
make
|
|
22
|
-
make
|
|
19
|
+
make build # regenerate token artifacts and typecheck
|
|
20
|
+
make check # build, then verify tokens/source contracts/lint
|
|
21
|
+
make test # run light and dark Storybook/vitest tests (implies build)
|
|
22
|
+
make storybook # build tokens and start Storybook
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The
|
|
25
|
+
The `pnpm run` scripts (`check`, `build`, `test`, `storybook`, `format`) delegate to these targets for compatibility.
|
|
26
26
|
|
|
27
27
|
## Release
|
|
28
28
|
|
|
@@ -37,11 +37,11 @@ Releases are owned by [release-please](https://github.com/googleapis/release-ple
|
|
|
37
37
|
2. Once on `main`, find or wait for the `chore(main): release ui X.Y.Z` PR that Release Please opens or updates.
|
|
38
38
|
3. Review and merge it. That merge creates the `ui-v<version>` tag; after validation succeeds, the release workflow publishes that exact tag automatically.
|
|
39
39
|
|
|
40
|
-
`prepack` (`
|
|
40
|
+
`prepack` (`make check`) runs as part of `pnpm install`/publish tooling and is the last gate before anything reaches npm — see "Generated files" below for the failure mode that most commonly trips it.
|
|
41
41
|
|
|
42
42
|
### Generated files must never be hand-edited
|
|
43
43
|
|
|
44
|
-
`src/styles/tokens/generated/**` and `.storybook/generated/**` are produced only by `
|
|
44
|
+
`src/styles/tokens/generated/**` and `.storybook/generated/**` are produced only by `make build` (style-dictionary) from the DTCG source in `src/styles/tokens/source/*.tokens.json`. `make check` enforces this with `git diff --exit-code -- src/styles/tokens/generated .storybook/generated` after a fresh rebuild — if the committed file doesn't byte-for-byte match what the tool just produced, the check (and therefore `prepack` and the publish) fails. This has happened in practice when a repo-wide formatter (oxfmt) reformatted a generated CSS file directly; `.oxfmtrc.json`'s `ignorePatterns` now excludes both generated directories specifically to prevent that recurring. If `make check` fails on a file you didn't intend to touch, run `make build` and commit the regenerated output — don't hand-format it back into place.
|
|
45
45
|
|
|
46
46
|
## Consumers
|
|
47
47
|
|
|
@@ -70,7 +70,3 @@ This package has no build output — every `exports` entry resolves to a file un
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
**Important:** `./styles.css` uses `@reference "tailwindcss"` (it does not import Tailwind itself). Your project owns the `@import "tailwindcss"` — place it before the UI stylesheet. Omitting it will cause missing CSS variables (e.g. `--tw-shadow-color`) and unresolved utilities.
|
|
73
|
-
|
|
74
|
-
### `./native` subpath
|
|
75
|
-
|
|
76
|
-
`@ponti-studios/ui/native` is the only part of the package that imports `react-native`. Web consumers never pull it in; native consumers get `createMakeStyles`, `nativeShadows`, `useColorMode`, and `useThemeColors`. `react-native` is an optional peer dependency — only declare it in your own `package.json` if you actually import from this subpath.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ponti-studios/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared UI component library for Ponti Studios",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,10 +66,6 @@
|
|
|
66
66
|
"types": "./src/styles/tokens/css-vars.ts",
|
|
67
67
|
"import": "./src/styles/tokens/css-vars.ts"
|
|
68
68
|
},
|
|
69
|
-
"./native": {
|
|
70
|
-
"types": "./src/native/index.ts",
|
|
71
|
-
"import": "./src/native/index.ts"
|
|
72
|
-
},
|
|
73
69
|
"./hooks": {
|
|
74
70
|
"types": "./src/hooks/index.ts",
|
|
75
71
|
"import": "./src/hooks/index.ts"
|
|
@@ -82,29 +78,19 @@
|
|
|
82
78
|
"registry": "https://registry.npmjs.org"
|
|
83
79
|
},
|
|
84
80
|
"scripts": {
|
|
81
|
+
"check": "make check",
|
|
85
82
|
"build": "make build",
|
|
86
|
-
"dev": "vite",
|
|
87
|
-
"storybook": "make storybook",
|
|
88
|
-
"check:source": "make check-source",
|
|
89
83
|
"test": "make test",
|
|
90
|
-
"
|
|
91
|
-
"test:a11y:light": "make test-a11y-light",
|
|
92
|
-
"test:a11y:dark": "make test-a11y-dark",
|
|
93
|
-
"tokens:build": "make tokens-build",
|
|
94
|
-
"tokens:check": "make tokens-check",
|
|
95
|
-
"typecheck": "make typecheck",
|
|
96
|
-
"lint": "make lint",
|
|
84
|
+
"storybook": "make storybook",
|
|
97
85
|
"format": "make format",
|
|
98
|
-
"prepack": "make
|
|
86
|
+
"prepack": "make check"
|
|
99
87
|
},
|
|
100
88
|
"dependencies": {
|
|
101
89
|
"@base-ui/react": "^1.6.0",
|
|
102
90
|
"@radix-ui/colors": "^3.0.0",
|
|
103
91
|
"class-variance-authority": "^0.7.1",
|
|
104
92
|
"clsx": "^2.1.1",
|
|
105
|
-
"date-fns": "^4.1.0",
|
|
106
93
|
"lucide-react": "^1.24.0",
|
|
107
|
-
"react-day-picker": "^10.0.1",
|
|
108
94
|
"tailwind-merge": "^3.6.0"
|
|
109
95
|
},
|
|
110
96
|
"devDependencies": {
|
|
@@ -120,38 +106,27 @@
|
|
|
120
106
|
"@types/node": "^26.1.1",
|
|
121
107
|
"@types/react": "^19.2.17",
|
|
122
108
|
"@types/react-dom": "^19.2.3",
|
|
123
|
-
"@vitejs/plugin-react": "^6.0.3",
|
|
124
109
|
"@vitest/browser": "4.1.10",
|
|
125
110
|
"@vitest/browser-playwright": "4.1.10",
|
|
126
|
-
"oxfmt": "0.
|
|
111
|
+
"oxfmt": "0.64.0",
|
|
127
112
|
"oxlint": "^1.74.0",
|
|
128
113
|
"playwright": "^1.61.1",
|
|
129
114
|
"react": "^19.2.7",
|
|
130
115
|
"react-dom": "^19.2.7",
|
|
131
|
-
"react-native": "0.86.0",
|
|
132
116
|
"storybook": "10.5.6",
|
|
133
117
|
"style-dictionary": "5.5.0",
|
|
134
118
|
"tailwindcss": "^4.3.3",
|
|
135
119
|
"typescript": "6.0.3",
|
|
136
|
-
"uniwind": "^1.10.1",
|
|
137
120
|
"vite": "^8.1.5",
|
|
138
121
|
"vitest": "^4.1.10"
|
|
139
122
|
},
|
|
140
123
|
"peerDependencies": {
|
|
141
124
|
"react": "^18.2.0 || ^19.0.0",
|
|
142
|
-
"react-dom": "^18.2.0 || ^19.0.0"
|
|
143
|
-
"react-native": ">=0.74",
|
|
144
|
-
"uniwind": ">=1.10.0"
|
|
125
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
145
126
|
},
|
|
146
127
|
"peerDependenciesMeta": {
|
|
147
128
|
"react-dom": {
|
|
148
129
|
"optional": true
|
|
149
|
-
},
|
|
150
|
-
"react-native": {
|
|
151
|
-
"optional": true
|
|
152
|
-
},
|
|
153
|
-
"uniwind": {
|
|
154
|
-
"optional": true
|
|
155
130
|
}
|
|
156
131
|
},
|
|
157
132
|
"packageManager": "pnpm@11.10.0"
|
|
@@ -10,7 +10,7 @@ const alertVariants = cva(
|
|
|
10
10
|
variant: {
|
|
11
11
|
default: "bg-card text-card-foreground",
|
|
12
12
|
destructive:
|
|
13
|
-
"border-destructive
|
|
13
|
+
"border-destructive text-destructive-text bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-text",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
defaultVariants: {
|
|
@@ -36,11 +36,13 @@ function Alert({
|
|
|
36
36
|
|
|
37
37
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
38
|
return (
|
|
39
|
-
<
|
|
39
|
+
<h2
|
|
40
40
|
data-slot="alert-title"
|
|
41
41
|
className={cn("col-start-2 line-clamp-1 min-h-4 text-sm font-medium", className)}
|
|
42
42
|
{...props}
|
|
43
|
-
|
|
43
|
+
>
|
|
44
|
+
{props.children}
|
|
45
|
+
</h2>
|
|
44
46
|
);
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -53,7 +55,9 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
|
|
|
53
55
|
className,
|
|
54
56
|
)}
|
|
55
57
|
{...props}
|
|
56
|
-
|
|
58
|
+
>
|
|
59
|
+
{props.children}
|
|
60
|
+
</div>
|
|
57
61
|
);
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
export { Alert, AlertDescription, AlertTitle } from "./alert";
|
|
2
2
|
export { EmptyState } from "./empty-state";
|
|
3
3
|
export type { EmptyStateProps } from "./empty-state";
|
|
4
|
-
export { FileUploadStatus } from "./file-upload-status";
|
|
5
|
-
export type { FileUploadState, FileUploadStats } from "./file-upload-status";
|
|
6
4
|
export { Progress } from "./progress";
|
|
7
|
-
export { ProgressBar } from "./progress-bar";
|
|
8
|
-
export type { ProgressBarProps } from "./progress-bar";
|
|
9
5
|
export { Skeleton } from "./skeleton";
|
|
10
6
|
export { Spinner } from "./spinner";
|
|
11
7
|
export type { SpinnerProps } from "./spinner";
|
|
@@ -16,14 +16,29 @@ export const Default: Story = {
|
|
|
16
16
|
value: 60,
|
|
17
17
|
"aria-label": "Upload progress",
|
|
18
18
|
},
|
|
19
|
+
render: (args) => (
|
|
20
|
+
<div className="w-64">
|
|
21
|
+
<Progress {...args} />
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
export const Empty: Story = {
|
|
22
27
|
args: { value: 0, "aria-label": "Upload progress" },
|
|
28
|
+
render: (args) => (
|
|
29
|
+
<div className="w-64">
|
|
30
|
+
<Progress {...args} />
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
export const Complete: Story = {
|
|
26
36
|
args: { value: 100, "aria-label": "Upload progress" },
|
|
37
|
+
render: (args) => (
|
|
38
|
+
<div className="w-64">
|
|
39
|
+
<Progress {...args} />
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
27
42
|
};
|
|
28
43
|
|
|
29
44
|
export const Partial: Story = {
|
|
@@ -11,18 +11,29 @@ function Progress({
|
|
|
11
11
|
}: React.ComponentProps<typeof ProgressPrimitive.Root> & {
|
|
12
12
|
indicatorClassName?: string;
|
|
13
13
|
}) {
|
|
14
|
+
const isFilling = typeof value === "number" && value > 0 && value < 100;
|
|
14
15
|
return (
|
|
15
16
|
<ProgressPrimitive.Root
|
|
16
17
|
data-slot="progress"
|
|
17
|
-
className={cn("bg-
|
|
18
|
+
className={cn("bg-muted relative h-2 w-full overflow-hidden rounded-full", className)}
|
|
18
19
|
value={value}
|
|
19
20
|
{...props}
|
|
20
21
|
>
|
|
21
22
|
<ProgressPrimitive.Indicator
|
|
22
23
|
data-slot="progress-indicator"
|
|
23
|
-
className={cn(
|
|
24
|
+
className={cn(
|
|
25
|
+
"bg-primary relative h-full w-full flex-1 overflow-hidden transition-transform duration-300 ease-out",
|
|
26
|
+
indicatorClassName,
|
|
27
|
+
)}
|
|
24
28
|
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
|
25
|
-
|
|
29
|
+
>
|
|
30
|
+
{isFilling && (
|
|
31
|
+
<span
|
|
32
|
+
aria-hidden
|
|
33
|
+
className="animate-progress-shimmer absolute inset-0 bg-[linear-gradient(90deg,transparent_35%,rgba(255,255,255,0.75)_50%,transparent_65%)]"
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
</ProgressPrimitive.Indicator>
|
|
26
37
|
</ProgressPrimitive.Root>
|
|
27
38
|
);
|
|
28
39
|
}
|
|
@@ -10,7 +10,7 @@ const meta = {
|
|
|
10
10
|
docs: {
|
|
11
11
|
description: {
|
|
12
12
|
component:
|
|
13
|
-
"
|
|
13
|
+
"Surfaces an update-available notification. Service-worker registration and update activation are owned by the consuming application.",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
},
|
|
@@ -21,12 +21,8 @@ type Story = StoryObj<typeof meta>;
|
|
|
21
21
|
|
|
22
22
|
export const Default: Story = {
|
|
23
23
|
args: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
App content renders here. Offline/update prompts appear fixed to the bottom of the viewport
|
|
28
|
-
when the service worker reports a change.
|
|
29
|
-
</div>
|
|
30
|
-
),
|
|
24
|
+
needRefresh: true,
|
|
25
|
+
onRefresh: () => undefined,
|
|
26
|
+
onDismiss: () => undefined,
|
|
31
27
|
},
|
|
32
28
|
};
|
|
@@ -1,288 +1,57 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
|
|
4
|
-
interface RegisterSWOptions {
|
|
5
|
-
onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void;
|
|
6
|
-
onRegisterError?: (error: Error) => void;
|
|
7
|
-
serviceWorkerPath?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface RegisterSWResult {
|
|
11
|
-
offlineReady: [boolean, (value: boolean) => void];
|
|
12
|
-
needRefresh: [boolean, (value: boolean) => void];
|
|
13
|
-
updateServiceWorker: (reload?: boolean) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function useRegisterSW(options?: RegisterSWOptions): RegisterSWResult {
|
|
17
|
-
const [offlineReady, setOfflineReady] = useState(false);
|
|
18
|
-
const [needRefresh, setNeedRefresh] = useState(false);
|
|
19
|
-
const registrationRef = useRef<ServiceWorkerRegistration | undefined>(undefined);
|
|
20
|
-
const shouldReloadRef = useRef(false);
|
|
21
|
-
const hasReloadedRef = useRef(false);
|
|
22
|
-
const onRegisteredRef = useRef(options?.onRegistered);
|
|
23
|
-
const onRegisterErrorRef = useRef(options?.onRegisterError);
|
|
24
|
-
const serviceWorkerPathRef = useRef(options?.serviceWorkerPath ?? "/sw.js");
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
onRegisteredRef.current = options?.onRegistered;
|
|
28
|
-
onRegisterErrorRef.current = options?.onRegisterError;
|
|
29
|
-
serviceWorkerPathRef.current = options?.serviceWorkerPath ?? "/sw.js";
|
|
30
|
-
}, [options?.onRegistered, options?.onRegisterError, options?.serviceWorkerPath]);
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (
|
|
34
|
-
typeof window === "undefined" ||
|
|
35
|
-
!("serviceWorker" in navigator) ||
|
|
36
|
-
!window.isSecureContext
|
|
37
|
-
) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let isMounted = true;
|
|
42
|
-
|
|
43
|
-
const handleControllerChange = () => {
|
|
44
|
-
if (!shouldReloadRef.current || hasReloadedRef.current) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
hasReloadedRef.current = true;
|
|
49
|
-
window.location.reload();
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
navigator.serviceWorker.addEventListener("controllerchange", handleControllerChange);
|
|
53
|
-
|
|
54
|
-
const handleWaitingServiceWorker = (registration: ServiceWorkerRegistration) => {
|
|
55
|
-
registrationRef.current = registration;
|
|
56
|
-
if (!isMounted) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
setNeedRefresh(true);
|
|
61
|
-
setOfflineReady(false);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const handleInstalledServiceWorker = (registration: ServiceWorkerRegistration) => {
|
|
65
|
-
registrationRef.current = registration;
|
|
66
|
-
if (!isMounted) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (navigator.serviceWorker.controller) {
|
|
71
|
-
setNeedRefresh(true);
|
|
72
|
-
setOfflineReady(false);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
setOfflineReady(true);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const trackRegistration = (registration: ServiceWorkerRegistration) => {
|
|
80
|
-
registrationRef.current = registration;
|
|
81
|
-
onRegisteredRef.current?.(registration);
|
|
82
|
-
|
|
83
|
-
if (registration.waiting) {
|
|
84
|
-
handleWaitingServiceWorker(registration);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
registration.addEventListener("updatefound", () => {
|
|
88
|
-
const installingWorker = registration.installing;
|
|
89
|
-
if (!installingWorker) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
installingWorker.addEventListener("statechange", () => {
|
|
94
|
-
if (installingWorker.state === "installed") {
|
|
95
|
-
handleInstalledServiceWorker(registration);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
void navigator.serviceWorker
|
|
102
|
-
.register(serviceWorkerPathRef.current)
|
|
103
|
-
.then((registration) => {
|
|
104
|
-
if (!isMounted) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
trackRegistration(registration);
|
|
109
|
-
return navigator.serviceWorker.ready.then(() => {
|
|
110
|
-
if (!isMounted || navigator.serviceWorker.controller) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
setOfflineReady(true);
|
|
115
|
-
});
|
|
116
|
-
})
|
|
117
|
-
.catch((error: Error) => {
|
|
118
|
-
if (isMounted) {
|
|
119
|
-
onRegisterErrorRef.current?.(error);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return () => {
|
|
124
|
-
isMounted = false;
|
|
125
|
-
navigator.serviceWorker.removeEventListener("controllerchange", handleControllerChange);
|
|
126
|
-
};
|
|
127
|
-
}, []);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
offlineReady: [offlineReady, setOfflineReady],
|
|
131
|
-
needRefresh: [needRefresh, setNeedRefresh],
|
|
132
|
-
updateServiceWorker: (reload = true) => {
|
|
133
|
-
shouldReloadRef.current = reload;
|
|
134
|
-
registrationRef.current?.waiting?.postMessage({ type: "SKIP_WAITING" });
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function subscribeOnline(onStoreChange: () => void) {
|
|
140
|
-
if (typeof window === "undefined") {
|
|
141
|
-
return () => {};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
window.addEventListener("online", onStoreChange);
|
|
145
|
-
window.addEventListener("offline", onStoreChange);
|
|
146
|
-
|
|
147
|
-
return () => {
|
|
148
|
-
window.removeEventListener("online", onStoreChange);
|
|
149
|
-
window.removeEventListener("offline", onStoreChange);
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function getOnlineSnapshot() {
|
|
154
|
-
if (typeof navigator === "undefined") {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return navigator.onLine;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function getOnlineServerSnapshot() {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
1
|
+
import { LucideRefreshCcw, LucideX } from "lucide-react";
|
|
164
2
|
|
|
165
3
|
export interface UpdateGuardCopy {
|
|
166
|
-
offlineReady?: string;
|
|
167
4
|
newContentAvailable?: string;
|
|
168
5
|
refreshButton?: string;
|
|
169
6
|
closeButton?: string;
|
|
170
|
-
offlineMessage?: string;
|
|
171
|
-
offlineStaleMessage?: string;
|
|
172
7
|
}
|
|
173
8
|
|
|
174
9
|
export interface UpdateGuardProps {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
10
|
+
needRefresh: boolean;
|
|
11
|
+
onRefresh: () => void;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
copyText?: string;
|
|
14
|
+
refreshText?: string;
|
|
180
15
|
}
|
|
181
16
|
|
|
182
|
-
function UpdateGuardClient({
|
|
183
|
-
serviceWorkerPath,
|
|
184
|
-
hasStaleData = false,
|
|
185
|
-
hideInDev = true,
|
|
186
|
-
copy,
|
|
187
|
-
}: Omit<UpdateGuardProps, "children">) {
|
|
188
|
-
const isDev =
|
|
189
|
-
typeof import.meta !== "undefined" &&
|
|
190
|
-
typeof import.meta.env !== "undefined" &&
|
|
191
|
-
Boolean(import.meta.env.DEV);
|
|
192
|
-
|
|
193
|
-
const {
|
|
194
|
-
offlineReady: [offlineReady, setOfflineReady],
|
|
195
|
-
needRefresh: [needRefresh, setNeedRefresh],
|
|
196
|
-
updateServiceWorker,
|
|
197
|
-
} = useRegisterSW({ serviceWorkerPath });
|
|
198
|
-
|
|
199
|
-
const isOnline = useSyncExternalStore(
|
|
200
|
-
subscribeOnline,
|
|
201
|
-
getOnlineSnapshot,
|
|
202
|
-
getOnlineServerSnapshot,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const closePrompt = () => {
|
|
206
|
-
setOfflineReady(false);
|
|
207
|
-
setNeedRefresh(false);
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const offlineMessage = useMemo(() => {
|
|
211
|
-
if (isOnline) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return hasStaleData
|
|
216
|
-
? (copy?.offlineStaleMessage ?? "Offline - showing cached data where available")
|
|
217
|
-
: (copy?.offlineMessage ?? "Offline - data may be unavailable");
|
|
218
|
-
}, [copy?.offlineMessage, copy?.offlineStaleMessage, hasStaleData, isOnline]);
|
|
219
|
-
|
|
220
|
-
if (hideInDev && isDev) {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<>
|
|
226
|
-
{offlineMessage ? (
|
|
227
|
-
<div className="fixed inset-x-0 bottom-16 z-50 flex justify-center px-4">
|
|
228
|
-
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
229
|
-
<span className="text-foreground text-sm">{offlineMessage}</span>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
) : null}
|
|
233
|
-
|
|
234
|
-
{offlineReady || needRefresh ? (
|
|
235
|
-
<div className="fixed inset-x-0 bottom-4 z-50 flex justify-center px-4">
|
|
236
|
-
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
237
|
-
<span className="text-foreground text-sm">
|
|
238
|
-
{offlineReady
|
|
239
|
-
? (copy?.offlineReady ?? "App ready to work offline")
|
|
240
|
-
: (copy?.newContentAvailable ?? "New content available")}
|
|
241
|
-
</span>
|
|
242
|
-
{needRefresh ? (
|
|
243
|
-
<button
|
|
244
|
-
type="button"
|
|
245
|
-
onClick={() => updateServiceWorker(true)}
|
|
246
|
-
className="text-accent text-sm font-semibold"
|
|
247
|
-
>
|
|
248
|
-
{copy?.refreshButton ?? "Refresh"}
|
|
249
|
-
</button>
|
|
250
|
-
) : null}
|
|
251
|
-
<button type="button" onClick={closePrompt} className="text-muted-foreground text-sm">
|
|
252
|
-
{copy?.closeButton ?? "Close"}
|
|
253
|
-
</button>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
) : null}
|
|
257
|
-
</>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Registers a service worker and surfaces "offline ready" / "update
|
|
263
|
-
* available" prompts around `children`. No-op (renders only `children`) when
|
|
264
|
-
* rendered on the server.
|
|
265
|
-
*/
|
|
266
17
|
export function UpdateGuard({
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
18
|
+
needRefresh,
|
|
19
|
+
onRefresh,
|
|
20
|
+
onDismiss,
|
|
21
|
+
copyText = "New content available",
|
|
22
|
+
refreshText = "Refresh",
|
|
272
23
|
}: UpdateGuardProps) {
|
|
273
|
-
|
|
24
|
+
if (!needRefresh) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
274
27
|
|
|
275
28
|
return (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
29
|
+
<output
|
|
30
|
+
aria-live="polite"
|
|
31
|
+
aria-atomic="true"
|
|
32
|
+
className="fixed inset-x-0 bottom-0 z-50 flex justify-center px-4"
|
|
33
|
+
>
|
|
34
|
+
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
35
|
+
<span className="text-foreground text-sm">{copyText}</span>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-label={refreshText}
|
|
39
|
+
onClick={onRefresh}
|
|
40
|
+
className="text-accent-text font-semibold border-muted-foreground rounded-md border px-2 py-1 hover:bg-accent hover:text-accent-foreground"
|
|
41
|
+
>
|
|
42
|
+
<LucideRefreshCcw className="size-4" />
|
|
43
|
+
</button>
|
|
44
|
+
{onDismiss ? (
|
|
45
|
+
<button
|
|
46
|
+
aria-label="Close"
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onDismiss}
|
|
49
|
+
className="text-accent-text font-semibold border-muted-foreground rounded-md border px-2 py-1 hover:bg-accent hover:text-accent-foreground"
|
|
50
|
+
>
|
|
51
|
+
<LucideX className="size-4" />
|
|
52
|
+
</button>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
</output>
|
|
287
56
|
);
|
|
288
57
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useId, useMemo } from "react";
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../lib/utils";
|
|
4
|
+
import { Spinner } from "../feedback/spinner";
|
|
4
5
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../forms/select";
|
|
5
|
-
import { Label } from "../primitives/label";
|
|
6
6
|
|
|
7
7
|
export interface FilterSelectOption {
|
|
8
8
|
value: string;
|
|
@@ -14,7 +14,6 @@ export interface FilterSelectProps {
|
|
|
14
14
|
onChange: (value: string | null) => void;
|
|
15
15
|
options: FilterSelectOption[];
|
|
16
16
|
label: string;
|
|
17
|
-
loadingText?: string;
|
|
18
17
|
placeholder?: string;
|
|
19
18
|
disabled?: boolean;
|
|
20
19
|
isLoading?: boolean;
|
|
@@ -28,7 +27,6 @@ export function FilterSelect({
|
|
|
28
27
|
value,
|
|
29
28
|
options,
|
|
30
29
|
onChange,
|
|
31
|
-
loadingText = "Loading...",
|
|
32
30
|
placeholder = "All",
|
|
33
31
|
disabled = false,
|
|
34
32
|
isLoading = false,
|
|
@@ -45,7 +43,7 @@ export function FilterSelect({
|
|
|
45
43
|
|
|
46
44
|
return (
|
|
47
45
|
<div className={cn("space-y-2 min-w-36", className)}>
|
|
48
|
-
<
|
|
46
|
+
<label htmlFor={selectId}>{label}</label>
|
|
49
47
|
<Select value={value} onValueChange={onChange} items={items}>
|
|
50
48
|
<SelectTrigger id={selectId} disabled={disabled} aria-label={label} className="w-full">
|
|
51
49
|
<SelectValue placeholder={placeholder} />
|
|
@@ -53,7 +51,7 @@ export function FilterSelect({
|
|
|
53
51
|
<SelectContent className="bg-background max-h-[250px] min-w-36 overflow-y-auto">
|
|
54
52
|
<SelectItem>{placeholder}</SelectItem>
|
|
55
53
|
{isLoading ? (
|
|
56
|
-
<
|
|
54
|
+
<Spinner presentation="centered" size="sm" />
|
|
57
55
|
) : options.length === 0 ? (
|
|
58
56
|
<div className="text-muted-foreground px-2 py-1.5 text-sm">{emptyLabel}</div>
|
|
59
57
|
) : (
|