@stevederico/skateboard-ui 4.12.1 → 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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ 4.13.0
4
+
5
+ Add content skeletons
6
+ Skeleton in ProtectedRoute
7
+
3
8
  4.12.1
4
9
 
5
10
  Republish built dist
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:
@@ -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 { Spinner } from '../shadcn/ui/spinner.js';
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 (_jsx("div", { className: "flex items-center justify-center min-h-screen bg-background", children: _jsx(Spinner, { className: "size-6" }) }));
55
+ return _jsx(PageSkeleton, {});
53
56
  }
54
57
  return status === 'valid' ? _jsx(Outlet, {}) : _jsx(Navigate, { to: "/signin", replace: true });
55
58
  };
@@ -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
- export { Skeleton };
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 };
@@ -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
- export { Skeleton };
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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "4.12.1",
4
+ "version": "4.13.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {