@stevederico/skateboard-ui 4.12.0 → 4.13.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/CHANGELOG.md +9 -0
- package/README.md +24 -0
- package/dist/App.js +2 -2
- package/dist/components/ProtectedRoute.d.ts +3 -0
- package/dist/components/ProtectedRoute.js +5 -2
- package/dist/components/views/PaymentView.js +1 -1
- package/dist/ui/drawer.js +1 -1
- package/dist/ui/skeleton.d.ts +17 -1
- package/dist/ui/skeleton.js +22 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -124,6 +124,30 @@ Sets up routing, auth, theming, state, and error handling.
|
|
|
124
124
|
| `/terms`, `/privacy`, `/eula`, `/subscription` | TextView | No |
|
|
125
125
|
| `*` | NotFound | No |
|
|
126
126
|
|
|
127
|
+
## Loading skeletons
|
|
128
|
+
|
|
129
|
+
Prefer **skeletons** for content waits (lists, page data, auth gate inside layout). Keep **spinners** for actions (submit, redirect).
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
import {
|
|
133
|
+
Skeleton,
|
|
134
|
+
PageSkeleton,
|
|
135
|
+
CardListSkeleton,
|
|
136
|
+
SettingsSkeleton,
|
|
137
|
+
} from '@stevederico/skateboard-ui/ui/skeleton';
|
|
138
|
+
|
|
139
|
+
// Generic main-content placeholder (also used by ProtectedRoute while validating /me)
|
|
140
|
+
if (loading) return <PageSkeleton />;
|
|
141
|
+
|
|
142
|
+
// List/grid of avatar+text cards
|
|
143
|
+
if (loading) return <CardListSkeleton count={9} />;
|
|
144
|
+
|
|
145
|
+
// Settings-shaped stacked cards
|
|
146
|
+
if (loading) return <SettingsSkeleton />;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`ProtectedRoute` renders `PageSkeleton` inside the layout main area while checking the session — sidebar and tab bar stay put.
|
|
150
|
+
|
|
127
151
|
## Authentication
|
|
128
152
|
|
|
129
153
|
Hybrid cookie + localStorage auth:
|
package/dist/App.js
CHANGED
|
@@ -22,7 +22,7 @@ import AuthOverlay from './components/AuthOverlay.js';
|
|
|
22
22
|
* Redirect component for auth routes when authOverlay mode is enabled.
|
|
23
23
|
*
|
|
24
24
|
* Dispatches SHOW_AUTH_OVERLAY on mount and redirects to the previous page
|
|
25
|
-
* (from location state) or /app
|
|
25
|
+
* (from location state) or /app as fallback (which routes to the app's defaultRoute).
|
|
26
26
|
*/
|
|
27
27
|
function AuthRedirect() {
|
|
28
28
|
const { dispatch } = getState();
|
|
@@ -34,7 +34,7 @@ function AuthRedirect() {
|
|
|
34
34
|
if (!isAuthenticated()) {
|
|
35
35
|
dispatch({ type: 'SHOW_AUTH_OVERLAY' });
|
|
36
36
|
}
|
|
37
|
-
const returnTo = location.state?.from || '/app
|
|
37
|
+
const returnTo = location.state?.from || '/app';
|
|
38
38
|
navigate(returnTo, { replace: true });
|
|
39
39
|
}, [dispatch, navigate, location.state]);
|
|
40
40
|
return null;
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Bypassed for lazy auth (the default) unless constants.authOverlay is false.
|
|
7
7
|
* Bypassed when constants.noLogin is true (no auth required).
|
|
8
8
|
*
|
|
9
|
+
* While checking, renders PageSkeleton inside the layout main content
|
|
10
|
+
* (ProtectedRoute is already nested under Layout / SidebarInset).
|
|
11
|
+
*
|
|
9
12
|
* @returns {JSX.Element} Outlet if authenticated, Navigate to /signin otherwise
|
|
10
13
|
*
|
|
11
14
|
* @example
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Navigate, Outlet } from 'react-router';
|
|
4
4
|
import { isAuthenticated, apiRequest, getAppKey, getConstants, isAuthOverlayEnabled } from './core/Utilities.js';
|
|
5
|
-
import {
|
|
5
|
+
import { PageSkeleton } from '../ui/skeleton.js';
|
|
6
6
|
/**
|
|
7
7
|
* Route guard that validates authentication before rendering child routes.
|
|
8
8
|
*
|
|
@@ -11,6 +11,9 @@ import { Spinner } from '../shadcn/ui/spinner.js';
|
|
|
11
11
|
* Bypassed for lazy auth (the default) unless constants.authOverlay is false.
|
|
12
12
|
* Bypassed when constants.noLogin is true (no auth required).
|
|
13
13
|
*
|
|
14
|
+
* While checking, renders PageSkeleton inside the layout main content
|
|
15
|
+
* (ProtectedRoute is already nested under Layout / SidebarInset).
|
|
16
|
+
*
|
|
14
17
|
* @returns {JSX.Element} Outlet if authenticated, Navigate to /signin otherwise
|
|
15
18
|
*
|
|
16
19
|
* @example
|
|
@@ -49,7 +52,7 @@ const ProtectedRoute = () => {
|
|
|
49
52
|
});
|
|
50
53
|
}, []);
|
|
51
54
|
if (status === 'checking') {
|
|
52
|
-
return
|
|
55
|
+
return _jsx(PageSkeleton, {});
|
|
53
56
|
}
|
|
54
57
|
return status === 'valid' ? _jsx(Outlet, {}) : _jsx(Navigate, { to: "/signin", replace: true });
|
|
55
58
|
};
|
|
@@ -6,7 +6,7 @@ import { getCurrentUser, getAppKey } from '../core/Utilities.js';
|
|
|
6
6
|
import { Spinner } from '../../shadcn/ui/spinner.js';
|
|
7
7
|
// Whitelist of allowed redirect paths to prevent open redirect vulnerabilities
|
|
8
8
|
const ALLOWED_REDIRECT_PREFIXES = ['/app/', '/'];
|
|
9
|
-
const DEFAULT_REDIRECT = '/app
|
|
9
|
+
const DEFAULT_REDIRECT = '/app';
|
|
10
10
|
function isAllowedRedirect(path) {
|
|
11
11
|
try {
|
|
12
12
|
// Resolve relative to current origin; reject anything that escapes it.
|
package/dist/ui/drawer.js
CHANGED
|
@@ -229,7 +229,7 @@ function DrawerContent({ className, children, onPointerDown, onPointerMove, onPo
|
|
|
229
229
|
// browser claiming it for native scroll (which would fire pointercancel).
|
|
230
230
|
// top-auto/m-0/w-full/max-w-none clear the native <dialog> UA inset and
|
|
231
231
|
// sizing so the drawer pins to the bottom edge at full width.
|
|
232
|
-
"fixed inset-x-0 top-auto bottom-0 z-50 m-0 flex max-h-[80vh] w-full max-w-none touch-none flex-col rounded-t-xl border-t border-border bg-background p-0 text-sm outline-none backdrop:bg-black/10 backdrop:transition-opacity backdrop:duration-250 supports-backdrop-filter:backdrop:backdrop-blur-xs", open
|
|
232
|
+
"fixed inset-x-0 top-auto bottom-0 z-50 m-0 flex max-h-[80vh] w-full max-w-none touch-none flex-col rounded-t-xl border-t border-border bg-background p-0 text-sm text-foreground outline-none backdrop:bg-black/10 backdrop:transition-opacity backdrop:duration-250 supports-backdrop-filter:backdrop:backdrop-blur-xs", open
|
|
233
233
|
? "animate-in fade-in-0 slide-in-from-bottom-52 duration-300"
|
|
234
234
|
: dismissingByDrag.current
|
|
235
235
|
? ""
|
package/dist/ui/skeleton.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
declare function Skeleton({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Generic main-content loading placeholder (header bar + text + card grid).
|
|
5
|
+
* Use inside SidebarInset / route content — not as a full-app takeover.
|
|
6
|
+
*/
|
|
7
|
+
declare function PageSkeleton({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
8
|
+
/**
|
|
9
|
+
* List/grid of bordered cards with avatar + text lines.
|
|
10
|
+
* Match count to typical list size so layout stays stable.
|
|
11
|
+
*/
|
|
12
|
+
declare function CardListSkeleton({ count, className, ...props }: React.ComponentProps<"div"> & {
|
|
13
|
+
count?: number;
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
/**
|
|
16
|
+
* Settings page placeholder — header strip + stacked max-w-lg cards.
|
|
17
|
+
*/
|
|
18
|
+
declare function SettingsSkeleton({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
19
|
+
export { Skeleton, PageSkeleton, CardListSkeleton, SettingsSkeleton };
|
package/dist/ui/skeleton.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cn } from "../shadcn/lib/utils.js";
|
|
4
4
|
function Skeleton({ className, ...props }) {
|
|
5
5
|
return (_jsx("div", { "data-slot": "skeleton", className: cn("animate-pulse rounded-md bg-muted", className), ...props }));
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Generic main-content loading placeholder (header bar + text + card grid).
|
|
9
|
+
* Use inside SidebarInset / route content — not as a full-app takeover.
|
|
10
|
+
*/
|
|
11
|
+
function PageSkeleton({ className, ...props }) {
|
|
12
|
+
return (_jsxs("div", { "data-slot": "page-skeleton", className: cn("flex flex-1 flex-col gap-4 p-4 lg:p-6", className), "aria-busy": "true", "aria-live": "polite", ...props, children: [_jsx(Skeleton, { className: "h-8 w-48" }), _jsx(Skeleton, { className: "h-4 w-full max-w-xl" }), _jsx(Skeleton, { className: "h-4 w-4/5 max-w-lg" }), _jsx("div", { className: "grid grid-cols-1 gap-4 pt-4 md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Skeleton, { className: "h-32 w-full" }, i))) })] }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* List/grid of bordered cards with avatar + text lines.
|
|
16
|
+
* Match count to typical list size so layout stays stable.
|
|
17
|
+
*/
|
|
18
|
+
function CardListSkeleton({ count = 6, className, ...props }) {
|
|
19
|
+
return (_jsx("div", { "data-slot": "card-list-skeleton", className: cn("flex flex-1 flex-col gap-4 p-4 lg:p-6", className), "aria-busy": "true", "aria-live": "polite", ...props, children: _jsx("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: count }).map((_, i) => (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-border p-4", children: [_jsx(Skeleton, { className: "size-12 shrink-0 rounded-full" }), _jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [_jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-1/3" }), _jsx(Skeleton, { className: "mt-1 h-3 w-full" }), _jsx(Skeleton, { className: "h-3 w-4/5" })] })] }, i))) }) }));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Settings page placeholder — header strip + stacked max-w-lg cards.
|
|
23
|
+
*/
|
|
24
|
+
function SettingsSkeleton({ className, ...props }) {
|
|
25
|
+
return (_jsxs("div", { "data-slot": "settings-skeleton", className: cn("flex-1", className), "aria-busy": "true", "aria-live": "polite", ...props, children: [_jsx("div", { className: "flex h-(--header-height) shrink-0 items-center px-4 lg:px-6", children: _jsx(Skeleton, { className: "h-6 w-24" }) }), _jsx("div", { className: "h-px bg-border" }), _jsx("div", { className: "flex flex-col items-center gap-4 p-4", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "flex w-full max-w-lg items-center justify-between gap-4 rounded-xl border border-border p-6", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [_jsx(Skeleton, { className: "h-5 w-28" }), _jsx(Skeleton, { className: "h-4 w-48" })] }), _jsx(Skeleton, { className: "h-8 w-20 shrink-0" })] }, i))) })] }));
|
|
26
|
+
}
|
|
27
|
+
export { Skeleton, PageSkeleton, CardListSkeleton, SettingsSkeleton };
|