@stevederico/skateboard-ui 5.0.0 → 5.1.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 +6 -0
- package/dist/App.d.ts +20 -2
- package/dist/App.js +15 -5
- package/dist/components/core/Utilities.d.ts +15 -0
- package/dist/components/views/LandingView.js +3 -3
- package/dist/components/views/LegalRoute.d.ts +22 -0
- package/dist/components/views/LegalRoute.js +48 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/App.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
-
import type { SkateboardConstants } from './components/core/Utilities.js';
|
|
2
|
+
import type { SkateboardConstants, LegalTexts } from './components/core/Utilities.js';
|
|
3
|
+
/** Re-export for consumers that type `loadLegal`. */
|
|
4
|
+
export type { LegalTexts } from './components/core/Utilities.js';
|
|
3
5
|
/** A route rendered under /app (config.appRoutes). */
|
|
4
6
|
export interface AppRoute {
|
|
5
7
|
path: string;
|
|
@@ -26,6 +28,12 @@ export interface CreateSkateboardAppConfig {
|
|
|
26
28
|
children?: ReactNode;
|
|
27
29
|
}>;
|
|
28
30
|
overrides?: AppOverrides;
|
|
31
|
+
/**
|
|
32
|
+
* Lazy-load legal document bodies so they stay out of the main JS chunk.
|
|
33
|
+
* When set, `/terms` `/privacy` `/eula` `/subs` call this on first visit.
|
|
34
|
+
* Eager `constants.termsOfService` etc. remain supported as a fallback.
|
|
35
|
+
*/
|
|
36
|
+
loadLegal?: () => Promise<LegalTexts>;
|
|
29
37
|
}
|
|
30
38
|
/**
|
|
31
39
|
* Bootstrap and render a skateboard-ui application.
|
|
@@ -49,6 +57,8 @@ export interface CreateSkateboardAppConfig {
|
|
|
49
57
|
* @param {React.ComponentType} [config.overrides.notFound] - Replace NotFound
|
|
50
58
|
* @param {React.ComponentType} [config.overrides.authOverlay] - Replace AuthOverlay
|
|
51
59
|
*
|
|
60
|
+
* @param {() => Promise<LegalTexts>} [config.loadLegal] - Lazy-load legal document bodies
|
|
61
|
+
*
|
|
52
62
|
* @example
|
|
53
63
|
* import { createSkateboardApp } from '@stevederico/skateboard-ui/App';
|
|
54
64
|
* import constants from './constants.json';
|
|
@@ -59,6 +69,14 @@ export interface CreateSkateboardAppConfig {
|
|
|
59
69
|
* });
|
|
60
70
|
*
|
|
61
71
|
* @example
|
|
72
|
+
* // Keep legal text out of the main chunk
|
|
73
|
+
* createSkateboardApp({
|
|
74
|
+
* constants,
|
|
75
|
+
* appRoutes: [{ path: 'home', element: <HomeView /> }],
|
|
76
|
+
* loadLegal: () => import('./legal.json'),
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
62
80
|
* // Override built-in views
|
|
63
81
|
* createSkateboardApp({
|
|
64
82
|
* constants,
|
|
@@ -69,4 +87,4 @@ export interface CreateSkateboardAppConfig {
|
|
|
69
87
|
* }
|
|
70
88
|
* });
|
|
71
89
|
*/
|
|
72
|
-
export declare function createSkateboardApp({ constants, appRoutes, defaultRoute, landingPage, wrapper: Wrapper, overrides }: CreateSkateboardAppConfig): void;
|
|
90
|
+
export declare function createSkateboardApp({ constants, appRoutes, defaultRoute, landingPage, wrapper: Wrapper, overrides, loadLegal }: CreateSkateboardAppConfig): void;
|
package/dist/App.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getState } from './components/core/Context.js';
|
|
|
6
6
|
import { ThemeProvider } from './components/core/ThemeProvider.js';
|
|
7
7
|
import Layout from './components/layout/Layout.js';
|
|
8
8
|
import LandingView from './components/views/LandingView.js';
|
|
9
|
-
import
|
|
9
|
+
import LegalRoute from './components/views/LegalRoute.js';
|
|
10
10
|
import SignUpView from './components/views/SignUpView.js';
|
|
11
11
|
import SignInView from './components/views/SignInView.js';
|
|
12
12
|
import SignOutView from './components/views/SignOutView.js';
|
|
@@ -39,7 +39,7 @@ function AuthRedirect() {
|
|
|
39
39
|
}, [dispatch, navigate, location.state]);
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {} }) {
|
|
42
|
+
function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {}, loadLegal }) {
|
|
43
43
|
const location = useLocation();
|
|
44
44
|
// document.title is set by useAppSetup (on navigation) — single source of truth.
|
|
45
45
|
useAppSetup(location);
|
|
@@ -53,7 +53,7 @@ function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {} }
|
|
|
53
53
|
// An explicit override always wins, so consumers can supply custom auth pages.
|
|
54
54
|
const SignInComponent = overrides.signIn || (isAuthOverlayEnabled() ? AuthRedirect : SignInView);
|
|
55
55
|
const SignUpComponent = overrides.signUp || (isAuthOverlayEnabled() ? AuthRedirect : SignUpView);
|
|
56
|
-
return (_jsxs(Routes, { children: [_jsxs(Route, { element: _jsx(LayoutComponent, {}), children: [_jsx(Route, { path: "/console", element: _jsx(Navigate, { to: "/app", replace: true }) }), _jsxs(Route, { path: "/app", element: _jsx(ProtectedRoute, {}), children: [_jsx(Route, { index: true, element: _jsx(Navigate, { to: defaultRoute, replace: true }) }), appRoutes.map(({ path, element }) => (_jsx(Route, { path: path, element: element }, path))), _jsx(Route, { path: "settings", element: _jsx(SettingsComponent, {}) }), _jsx(Route, { path: "payment", element: _jsx(PaymentComponent, {}) })] })] }), _jsx(Route, { path: "/", element: landingPage || _jsx(LandingView, {}) }), _jsx(Route, { path: "/signin", element: _jsx(SignInComponent, {}) }), _jsx(Route, { path: "/signup", element: _jsx(SignUpComponent, {}) }), _jsx(Route, { path: "/signout", element: _jsx(SignOutComponent, {}) }), _jsx(Route, { path: "/terms", element: _jsx(
|
|
56
|
+
return (_jsxs(Routes, { children: [_jsxs(Route, { element: _jsx(LayoutComponent, {}), children: [_jsx(Route, { path: "/console", element: _jsx(Navigate, { to: "/app", replace: true }) }), _jsxs(Route, { path: "/app", element: _jsx(ProtectedRoute, {}), children: [_jsx(Route, { index: true, element: _jsx(Navigate, { to: defaultRoute, replace: true }) }), appRoutes.map(({ path, element }) => (_jsx(Route, { path: path, element: element }, path))), _jsx(Route, { path: "settings", element: _jsx(SettingsComponent, {}) }), _jsx(Route, { path: "payment", element: _jsx(PaymentComponent, {}) })] })] }), _jsx(Route, { path: "/", element: landingPage || _jsx(LandingView, {}) }), _jsx(Route, { path: "/signin", element: _jsx(SignInComponent, {}) }), _jsx(Route, { path: "/signup", element: _jsx(SignUpComponent, {}) }), _jsx(Route, { path: "/signout", element: _jsx(SignOutComponent, {}) }), _jsx(Route, { path: "/terms", element: _jsx(LegalRoute, { field: "termsOfService", fallback: constants.termsOfService, loadLegal: loadLegal }) }), _jsx(Route, { path: "/privacy", element: _jsx(LegalRoute, { field: "privacyPolicy", fallback: constants.privacyPolicy, loadLegal: loadLegal }) }), _jsx(Route, { path: "/eula", element: _jsx(LegalRoute, { field: "EULA", fallback: constants.EULA, loadLegal: loadLegal }) }), _jsx(Route, { path: "/subs", element: _jsx(LegalRoute, { field: "subscriptionDetails", fallback: constants.subscriptionDetails, loadLegal: loadLegal }) }), _jsx(Route, { path: "*", element: _jsx(NotFoundComponent, {}) })] }));
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Bootstrap and render a skateboard-ui application.
|
|
@@ -77,6 +77,8 @@ function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {} }
|
|
|
77
77
|
* @param {React.ComponentType} [config.overrides.notFound] - Replace NotFound
|
|
78
78
|
* @param {React.ComponentType} [config.overrides.authOverlay] - Replace AuthOverlay
|
|
79
79
|
*
|
|
80
|
+
* @param {() => Promise<LegalTexts>} [config.loadLegal] - Lazy-load legal document bodies
|
|
81
|
+
*
|
|
80
82
|
* @example
|
|
81
83
|
* import { createSkateboardApp } from '@stevederico/skateboard-ui/App';
|
|
82
84
|
* import constants from './constants.json';
|
|
@@ -87,6 +89,14 @@ function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {} }
|
|
|
87
89
|
* });
|
|
88
90
|
*
|
|
89
91
|
* @example
|
|
92
|
+
* // Keep legal text out of the main chunk
|
|
93
|
+
* createSkateboardApp({
|
|
94
|
+
* constants,
|
|
95
|
+
* appRoutes: [{ path: 'home', element: <HomeView /> }],
|
|
96
|
+
* loadLegal: () => import('./legal.json'),
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
90
100
|
* // Override built-in views
|
|
91
101
|
* createSkateboardApp({
|
|
92
102
|
* constants,
|
|
@@ -97,7 +107,7 @@ function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {} }
|
|
|
97
107
|
* }
|
|
98
108
|
* });
|
|
99
109
|
*/
|
|
100
|
-
export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRoutes[0]?.path || 'home', landingPage, wrapper: Wrapper, overrides }) {
|
|
110
|
+
export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRoutes[0]?.path || 'home', landingPage, wrapper: Wrapper, overrides, loadLegal }) {
|
|
101
111
|
// Validate constants before initialization
|
|
102
112
|
validateConstants(constants);
|
|
103
113
|
// Initialize utilities with constants
|
|
@@ -112,5 +122,5 @@ export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRo
|
|
|
112
122
|
const container = document.getElementById('root');
|
|
113
123
|
const root = createRoot(container);
|
|
114
124
|
const AuthOverlayComponent = overrides?.authOverlay || AuthOverlay;
|
|
115
|
-
root.render(_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { attribute: "class", defaultTheme: "system", enableSystem: true, children: _jsx(ContextProvider, { constants: constants, children: Wrapper ? (_jsx(Wrapper, { children: _jsxs(Router, { children: [_jsx(AuthOverlayComponent, {}), _jsx(App, { constants: constants, appRoutes: appRoutes, defaultRoute: defaultRoute, landingPage: landingPage, overrides: overrides })] }) })) : (_jsxs(Router, { children: [_jsx(AuthOverlayComponent, {}), _jsx(App, { constants: constants, appRoutes: appRoutes, defaultRoute: defaultRoute, landingPage: landingPage, overrides: overrides })] })) }) }) }));
|
|
125
|
+
root.render(_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { attribute: "class", defaultTheme: "system", enableSystem: true, children: _jsx(ContextProvider, { constants: constants, children: Wrapper ? (_jsx(Wrapper, { children: _jsxs(Router, { children: [_jsx(AuthOverlayComponent, {}), _jsx(App, { constants: constants, appRoutes: appRoutes, defaultRoute: defaultRoute, landingPage: landingPage, overrides: overrides, loadLegal: loadLegal })] }) })) : (_jsxs(Router, { children: [_jsx(AuthOverlayComponent, {}), _jsx(App, { constants: constants, appRoutes: appRoutes, defaultRoute: defaultRoute, landingPage: landingPage, overrides: overrides, loadLegal: loadLegal })] })) }) }) }));
|
|
116
126
|
}
|
|
@@ -28,6 +28,13 @@ export interface StripeProduct {
|
|
|
28
28
|
features?: string[];
|
|
29
29
|
[key: string]: unknown;
|
|
30
30
|
}
|
|
31
|
+
/** Legal document bodies that can be loaded lazily via `loadLegal`. */
|
|
32
|
+
export interface LegalTexts {
|
|
33
|
+
termsOfService?: string;
|
|
34
|
+
privacyPolicy?: string;
|
|
35
|
+
EULA?: string;
|
|
36
|
+
subscriptionDetails?: string;
|
|
37
|
+
}
|
|
31
38
|
/**
|
|
32
39
|
* App configuration object passed to createSkateboardApp / initializeUtilities.
|
|
33
40
|
* Required fields are enforced at runtime by validateConstants(); everything
|
|
@@ -56,6 +63,14 @@ export interface SkateboardConstants {
|
|
|
56
63
|
privacyPolicy?: string;
|
|
57
64
|
EULA?: string;
|
|
58
65
|
subscriptionDetails?: string;
|
|
66
|
+
/** Show Privacy footer link when the body is loaded via `loadLegal` instead of constants. */
|
|
67
|
+
hasPrivacyPolicy?: boolean;
|
|
68
|
+
/** Show Terms footer link when the body is loaded via `loadLegal` instead of constants. */
|
|
69
|
+
hasTermsOfService?: boolean;
|
|
70
|
+
/** Show EULA footer link when the body is loaded via `loadLegal` instead of constants. */
|
|
71
|
+
hasEULA?: boolean;
|
|
72
|
+
/** Show subscription-details link when the body is loaded via `loadLegal`. */
|
|
73
|
+
hasSubscriptionDetails?: boolean;
|
|
59
74
|
navLinks?: ConstantsLink[];
|
|
60
75
|
footerLinks?: ConstantsLink[];
|
|
61
76
|
pricing?: {
|
|
@@ -54,9 +54,9 @@ export default function LandingView() {
|
|
|
54
54
|
...((constants.stripeProducts?.length ?? 0) > 0 ? [{ label: 'Pricing', href: '#pricing' }] : []),
|
|
55
55
|
];
|
|
56
56
|
const footerLinks = constants.footerLinks || [
|
|
57
|
-
...(constants.privacyPolicy ? [PRIVACY_LINK] : []),
|
|
58
|
-
...(constants.termsOfService ? [TERMS_LINK] : []),
|
|
59
|
-
...(constants.EULA ? [EULA_LINK] : []),
|
|
57
|
+
...(constants.privacyPolicy || constants.hasPrivacyPolicy ? [PRIVACY_LINK] : []),
|
|
58
|
+
...(constants.termsOfService || constants.hasTermsOfService ? [TERMS_LINK] : []),
|
|
59
|
+
...(constants.EULA || constants.hasEULA ? [EULA_LINK] : []),
|
|
60
60
|
];
|
|
61
61
|
const items = constants.features?.items || [];
|
|
62
62
|
const sp = (constants.stripeProducts || [])[0];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LegalTexts } from '../core/Utilities.js';
|
|
2
|
+
/** Which legal blob a route should render. */
|
|
3
|
+
export type LegalField = keyof LegalTexts;
|
|
4
|
+
export interface LegalRouteProps {
|
|
5
|
+
/** Field to read from the loaded (or eager) legal texts. */
|
|
6
|
+
field: LegalField;
|
|
7
|
+
/** Eager text from constants — used when `loadLegal` is absent. */
|
|
8
|
+
fallback?: string;
|
|
9
|
+
/** Optional lazy loader; preferred over embedding legal text in the main chunk. */
|
|
10
|
+
loadLegal?: () => Promise<LegalTexts>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Route element for `/terms`, `/privacy`, `/eula`, and `/subs`.
|
|
14
|
+
*
|
|
15
|
+
* When `loadLegal` is provided the body is fetched on first visit under a
|
|
16
|
+
* spinner; otherwise `fallback` (eager constants) is rendered immediately so
|
|
17
|
+
* skateboard-ui 5.0 apps keep working unchanged.
|
|
18
|
+
*
|
|
19
|
+
* @param props - Field name plus optional loader / eager fallback
|
|
20
|
+
* @returns TextView once the document is available
|
|
21
|
+
*/
|
|
22
|
+
export default function LegalRoute({ field, fallback, loadLegal }: LegalRouteProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import TextView from './TextView.js';
|
|
4
|
+
import { Spinner } from '../../ui/spinner.js';
|
|
5
|
+
/**
|
|
6
|
+
* Route element for `/terms`, `/privacy`, `/eula`, and `/subs`.
|
|
7
|
+
*
|
|
8
|
+
* When `loadLegal` is provided the body is fetched on first visit under a
|
|
9
|
+
* spinner; otherwise `fallback` (eager constants) is rendered immediately so
|
|
10
|
+
* skateboard-ui 5.0 apps keep working unchanged.
|
|
11
|
+
*
|
|
12
|
+
* @param props - Field name plus optional loader / eager fallback
|
|
13
|
+
* @returns TextView once the document is available
|
|
14
|
+
*/
|
|
15
|
+
export default function LegalRoute({ field, fallback, loadLegal }) {
|
|
16
|
+
const [details, setDetails] = useState(loadLegal ? null : (fallback ?? ''));
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!loadLegal) {
|
|
20
|
+
setDetails(fallback ?? '');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let cancelled = false;
|
|
24
|
+
loadLegal()
|
|
25
|
+
.then((legal) => {
|
|
26
|
+
if (cancelled)
|
|
27
|
+
return;
|
|
28
|
+
const value = legal[field];
|
|
29
|
+
setDetails(typeof value === 'string' ? value : '');
|
|
30
|
+
})
|
|
31
|
+
.catch((err) => {
|
|
32
|
+
if (cancelled)
|
|
33
|
+
return;
|
|
34
|
+
console.error('Failed to load legal text', err);
|
|
35
|
+
setError('Could not load this document. Try again.');
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, [field, fallback, loadLegal]);
|
|
41
|
+
if (error) {
|
|
42
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center p-8 text-sm text-muted-foreground", role: "alert", children: error }));
|
|
43
|
+
}
|
|
44
|
+
if (details === null) {
|
|
45
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center p-8", role: "status", "aria-label": "Loading", children: _jsx(Spinner, {}) }));
|
|
46
|
+
}
|
|
47
|
+
return _jsx(TextView, { details: details });
|
|
48
|
+
}
|