@stevederico/skateboard-ui 5.0.0 → 5.2.2
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 +21 -0
- package/dist/App.d.ts +20 -2
- package/dist/App.js +31 -5
- package/dist/components/core/Utilities.d.ts +28 -2
- package/dist/components/core/Utilities.js +26 -9
- package/dist/components/layout/Header.js +1 -1
- 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/styles.css +16 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
5.2.2
|
|
2
|
+
|
|
3
|
+
Fix CI install
|
|
4
|
+
|
|
5
|
+
5.2.1
|
|
6
|
+
|
|
7
|
+
Publish from CI
|
|
8
|
+
|
|
9
|
+
5.2.0
|
|
10
|
+
|
|
11
|
+
Add live list option
|
|
12
|
+
Reload stale builds
|
|
13
|
+
Raise dark contrast
|
|
14
|
+
Bolder page titles
|
|
15
|
+
|
|
16
|
+
5.1.0
|
|
17
|
+
|
|
18
|
+
Add loadLegal option
|
|
19
|
+
Lazy legal TextView routes
|
|
20
|
+
has* legal footer flags
|
|
21
|
+
|
|
1
22
|
# CHANGELOG
|
|
2
23
|
|
|
3
24
|
5.0.0
|
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,11 +107,27 @@ 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
|
|
104
114
|
initializeUtilities(constants);
|
|
115
|
+
// A deploy replaces the hashed chunk files. A page opened before it asks
|
|
116
|
+
// for chunks that are gone ("Importing a module script failed"); reload
|
|
117
|
+
// once to pick up the new build instead of showing a broken route.
|
|
118
|
+
window.addEventListener('vite:preloadError', (event) => {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
try {
|
|
121
|
+
const last = Number(sessionStorage.getItem('skateboard-chunk-reload') || 0);
|
|
122
|
+
if (Date.now() - last < 10_000)
|
|
123
|
+
return;
|
|
124
|
+
sessionStorage.setItem('skateboard-chunk-reload', String(Date.now()));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Storage can be unavailable (private mode, some web views); reload anyway
|
|
128
|
+
}
|
|
129
|
+
window.location.reload();
|
|
130
|
+
});
|
|
105
131
|
// Prevent theme flash by setting dark class before React hydrates
|
|
106
132
|
// This runs synchronously before render, reading from localStorage or system preference
|
|
107
133
|
const storageKey = 'theme';
|
|
@@ -112,5 +138,5 @@ export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRo
|
|
|
112
138
|
const container = document.getElementById('root');
|
|
113
139
|
const root = createRoot(container);
|
|
114
140
|
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 })] })) }) }) }));
|
|
141
|
+
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
142
|
}
|
|
@@ -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?: {
|
|
@@ -345,13 +360,24 @@ export declare function apiRequestWithParams<T = any>(endpoint: string, params?:
|
|
|
345
360
|
* @throws {Error} - If required fields are missing or invalid
|
|
346
361
|
*/
|
|
347
362
|
export declare function validateConstants(constants: SkateboardConstants): SkateboardConstants;
|
|
363
|
+
/** Options for {@link useListData}. */
|
|
364
|
+
export interface ListDataOptions {
|
|
365
|
+
/**
|
|
366
|
+
* Keep the list fresh: refetch quietly every this many milliseconds while
|
|
367
|
+
* the page is visible, and again when the user returns to it. `true`
|
|
368
|
+
* means 60 seconds. Quiet refetches skip the loading state and keep the
|
|
369
|
+
* last good rows on error, so new items just appear.
|
|
370
|
+
*/
|
|
371
|
+
live?: boolean | number;
|
|
372
|
+
}
|
|
348
373
|
/**
|
|
349
|
-
* Standard list data fetcher with optional sorting
|
|
374
|
+
* Standard list data fetcher with optional sorting and live refresh
|
|
350
375
|
* @param {string} endpoint - API endpoint to fetch from
|
|
351
376
|
* @param {function} sortFn - Optional sort function for results
|
|
377
|
+
* @param {ListDataOptions} options - `live` turns on quiet periodic refetch
|
|
352
378
|
* @returns {object} - { data, loading, error, refetch }
|
|
353
379
|
*/
|
|
354
|
-
export declare function useListData<T = any>(endpoint: string, sortFn?: ((a: T, b: T) => number) | null): {
|
|
380
|
+
export declare function useListData<T = any>(endpoint: string, sortFn?: ((a: T, b: T) => number) | null, options?: ListDataOptions): {
|
|
355
381
|
data: T[];
|
|
356
382
|
loading: boolean;
|
|
357
383
|
error: string | null;
|
|
@@ -838,22 +838,24 @@ export function validateConstants(constants) {
|
|
|
838
838
|
}
|
|
839
839
|
return constants;
|
|
840
840
|
}
|
|
841
|
-
// ===== REACT HOOKS =====
|
|
842
841
|
/**
|
|
843
|
-
* Standard list data fetcher with optional sorting
|
|
842
|
+
* Standard list data fetcher with optional sorting and live refresh
|
|
844
843
|
* @param {string} endpoint - API endpoint to fetch from
|
|
845
844
|
* @param {function} sortFn - Optional sort function for results
|
|
845
|
+
* @param {ListDataOptions} options - `live` turns on quiet periodic refetch
|
|
846
846
|
* @returns {object} - { data, loading, error, refetch }
|
|
847
847
|
*/
|
|
848
|
-
export function useListData(endpoint, sortFn = null) {
|
|
848
|
+
export function useListData(endpoint, sortFn = null, options = {}) {
|
|
849
849
|
const [data, setData] = useState([]);
|
|
850
850
|
const [loading, setLoading] = useState(true);
|
|
851
851
|
const [error, setError] = useState(null);
|
|
852
852
|
// Keep sortFn in a ref so an inline sort function doesn't retrigger the fetch effect every render
|
|
853
853
|
const sortFnRef = useRef(sortFn);
|
|
854
854
|
sortFnRef.current = sortFn;
|
|
855
|
-
const
|
|
856
|
-
|
|
855
|
+
const liveMs = options.live === true ? 60_000 : typeof options.live === 'number' ? options.live : 0;
|
|
856
|
+
const fetchData = async (signal, quiet = false) => {
|
|
857
|
+
if (!quiet)
|
|
858
|
+
setLoading(true);
|
|
857
859
|
try {
|
|
858
860
|
const result = await apiRequest(endpoint, { signal });
|
|
859
861
|
const sort = sortFnRef.current;
|
|
@@ -865,17 +867,32 @@ export function useListData(endpoint, sortFn = null) {
|
|
|
865
867
|
// Ignore abort errors (DOMException in the browser, not an Error)
|
|
866
868
|
if ((err instanceof DOMException || err instanceof Error) && err.name === 'AbortError')
|
|
867
869
|
return;
|
|
868
|
-
|
|
870
|
+
// A failed quiet refresh keeps showing the last good rows
|
|
871
|
+
if (!quiet)
|
|
872
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
869
873
|
}
|
|
870
874
|
finally {
|
|
871
|
-
|
|
875
|
+
if (!quiet)
|
|
876
|
+
setLoading(false);
|
|
872
877
|
}
|
|
873
878
|
};
|
|
874
879
|
useEffect(() => {
|
|
875
880
|
const controller = new AbortController();
|
|
876
881
|
fetchData(controller.signal);
|
|
877
|
-
|
|
878
|
-
|
|
882
|
+
if (liveMs <= 0)
|
|
883
|
+
return () => controller.abort();
|
|
884
|
+
const tick = () => {
|
|
885
|
+
if (document.visibilityState === 'visible')
|
|
886
|
+
fetchData(controller.signal, true);
|
|
887
|
+
};
|
|
888
|
+
const timer = setInterval(tick, liveMs);
|
|
889
|
+
document.addEventListener('visibilitychange', tick);
|
|
890
|
+
return () => {
|
|
891
|
+
controller.abort();
|
|
892
|
+
clearInterval(timer);
|
|
893
|
+
document.removeEventListener('visibilitychange', tick);
|
|
894
|
+
};
|
|
895
|
+
}, [endpoint, liveMs]);
|
|
879
896
|
return { data, loading, error, refetch: () => fetchData() };
|
|
880
897
|
}
|
|
881
898
|
// ===== UI VISIBILITY CONTROLS =====
|
|
@@ -4,6 +4,6 @@ import { Button } from "../../ui/button.js";
|
|
|
4
4
|
import { Badge } from "../../ui/badge.js";
|
|
5
5
|
import { cn } from "../../shadcn/lib/utils.js";
|
|
6
6
|
function Header({ title, buttonTitle, onButtonTitleClick, buttonClass, className, children, ...props }) {
|
|
7
|
-
return (_jsxs(_Fragment, { children: [_jsx("header", { className: cn("flex h-(--header-height) shrink-0 items-center gap-2", className), ...props, children: _jsxs("div", { className: "flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6", children: [_jsx("h1", { className: "text-
|
|
7
|
+
return (_jsxs(_Fragment, { children: [_jsx("header", { className: cn("flex h-(--header-height) shrink-0 items-center gap-2", className), ...props, children: _jsxs("div", { className: "flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6", children: [_jsx("h1", { className: "text-[1.625rem] leading-8 font-extrabold tracking-tight", children: title }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [typeof buttonTitle !== "undefined" && (_jsx(Button, { variant: "ghost", size: "sm", className: buttonClass || "", onClick: onButtonTitleClick, children: buttonTitle })), children] })] }) }), _jsx(Separator, {})] }));
|
|
8
8
|
}
|
|
9
9
|
export default Header;
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
package/styles.css
CHANGED
|
@@ -170,22 +170,22 @@
|
|
|
170
170
|
.dark {
|
|
171
171
|
--background: oklch(0.05 0 0);
|
|
172
172
|
--foreground: oklch(0.985 0 0);
|
|
173
|
-
--card: oklch(0.
|
|
173
|
+
--card: oklch(0.19 0 0);
|
|
174
174
|
--card-foreground: oklch(0.985 0 0);
|
|
175
|
-
--popover: oklch(0.
|
|
175
|
+
--popover: oklch(0.19 0 0);
|
|
176
176
|
--popover-foreground: oklch(0.985 0 0);
|
|
177
177
|
--primary: oklch(0.985 0 0);
|
|
178
178
|
--primary-foreground: oklch(0.05 0 0);
|
|
179
|
-
--secondary: oklch(0.
|
|
179
|
+
--secondary: oklch(0.28 0 0);
|
|
180
180
|
--secondary-foreground: oklch(0.985 0 0);
|
|
181
|
-
--muted: oklch(0.
|
|
182
|
-
--muted-foreground: oklch(0.
|
|
183
|
-
--accent: oklch(0.
|
|
181
|
+
--muted: oklch(0.28 0 0);
|
|
182
|
+
--muted-foreground: oklch(0.8 0 0);
|
|
183
|
+
--accent: oklch(0.28 0 0);
|
|
184
184
|
--accent-foreground: oklch(0.985 0 0);
|
|
185
185
|
--destructive: oklch(0.396 0.141 25.723);
|
|
186
186
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
187
|
-
--border: oklch(0.
|
|
188
|
-
--input: oklch(0.
|
|
187
|
+
--border: oklch(0.5 0 0);
|
|
188
|
+
--input: oklch(0.5 0 0);
|
|
189
189
|
--ring: oklch(0.439 0 0);
|
|
190
190
|
--chart-1: oklch(0.488 0.243 264.376);
|
|
191
191
|
--chart-2: oklch(0.696 0.17 162.48);
|
|
@@ -207,22 +207,22 @@
|
|
|
207
207
|
:root:not(.light) {
|
|
208
208
|
--background: oklch(0.05 0 0);
|
|
209
209
|
--foreground: oklch(0.985 0 0);
|
|
210
|
-
--card: oklch(0.
|
|
210
|
+
--card: oklch(0.19 0 0);
|
|
211
211
|
--card-foreground: oklch(0.985 0 0);
|
|
212
|
-
--popover: oklch(0.
|
|
212
|
+
--popover: oklch(0.19 0 0);
|
|
213
213
|
--popover-foreground: oklch(0.985 0 0);
|
|
214
214
|
--primary: oklch(0.985 0 0);
|
|
215
215
|
--primary-foreground: oklch(0.05 0 0);
|
|
216
|
-
--secondary: oklch(0.
|
|
216
|
+
--secondary: oklch(0.28 0 0);
|
|
217
217
|
--secondary-foreground: oklch(0.985 0 0);
|
|
218
|
-
--muted: oklch(0.
|
|
219
|
-
--muted-foreground: oklch(0.
|
|
220
|
-
--accent: oklch(0.
|
|
218
|
+
--muted: oklch(0.28 0 0);
|
|
219
|
+
--muted-foreground: oklch(0.8 0 0);
|
|
220
|
+
--accent: oklch(0.28 0 0);
|
|
221
221
|
--accent-foreground: oklch(0.985 0 0);
|
|
222
222
|
--destructive: oklch(0.396 0.141 25.723);
|
|
223
223
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
224
|
-
--border: oklch(0.
|
|
225
|
-
--input: oklch(0.
|
|
224
|
+
--border: oklch(0.5 0 0);
|
|
225
|
+
--input: oklch(0.5 0 0);
|
|
226
226
|
--ring: oklch(0.439 0 0);
|
|
227
227
|
--chart-1: oklch(0.488 0.243 264.376);
|
|
228
228
|
--chart-2: oklch(0.696 0.17 162.48);
|