@stevederico/skateboard-ui 2.21.0 → 2.23.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,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ 2.23.0
4
+
5
+ Rename auth view functions
6
+ Harden PaymentView redirect check
7
+ Replace UNSAFE_NavigationContext usage
8
+ Consolidate app-key derivation
9
+ Export createSkateboardApp from root
10
+ Bump lucide-react to 1.x
11
+ Bump dependencies
12
+
13
+ 2.22.0
14
+
15
+ Fix Layout sidebar override
16
+
3
17
  2.21.0
4
18
 
5
19
  Reduce sidebar width to 12rem
package/core/Utilities.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useEffect, useState, useContext } from 'react';
2
- import { UNSAFE_NavigationContext } from 'react-router-dom';
1
+ import { useEffect, useState } from 'react';
2
+ import { useInRouterContext, useNavigate } from 'react-router-dom';
3
3
  import { getDispatch } from './Context.jsx';
4
4
 
5
5
  // Constants will be initialized by the app shell
@@ -131,8 +131,7 @@ export function getCookie(name) {
131
131
  // For token cookies, use app-specific name
132
132
  let cookieName = name;
133
133
  if (name === 'token') {
134
- const appName = getConstants().appName || 'skateboard';
135
- cookieName = `${appName.toLowerCase().replace(/\s+/g, '-')}_token`;
134
+ cookieName = getAppKey('token');
136
135
  }
137
136
 
138
137
  const value = `; ${document.cookie}`;
@@ -162,9 +161,7 @@ export function getCSRFToken() {
162
161
  if (csrfCookie) return csrfCookie;
163
162
 
164
163
  // Fallback to localStorage (for backwards compatibility)
165
- const appName = getConstants().appName || 'skateboard';
166
- const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
167
- return safeGetItem(csrfKey);
164
+ return safeGetItem(getAppKey('csrf'));
168
165
  }
169
166
 
170
167
  /**
@@ -523,9 +520,7 @@ export async function trackUsage(action) {
523
520
  */
524
521
  export async function showUpgradeSheet(upgradeSheetRef) {
525
522
  // Check subscription from user data in localStorage instead of API call
526
- const appName = getConstants().appName || 'skateboard';
527
- const storageKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_user`;
528
- const storedUser = safeGetItem(storageKey);
523
+ const storedUser = safeGetItem(getAppKey('user'));
529
524
 
530
525
  let subscriber = false;
531
526
  if (storedUser && storedUser !== "undefined") {
@@ -1010,9 +1005,8 @@ export function setUIVisibility({ sidebar, tabBar }) {
1010
1005
  /**
1011
1006
  * Safe navigation hook that works with or without Router context.
1012
1007
  *
1013
- * Uses UNSAFE_NavigationContext directly instead of useNavigate() to avoid
1014
- * throwing when rendered outside Router (e.g., in Portals or module duplication).
1015
- * Falls back to window.location.href if no Router context is available.
1008
+ * Falls back to window.location.href if no Router context is available
1009
+ * (e.g., rendered in Portals or via module duplication).
1016
1010
  *
1017
1011
  * @returns {function} Navigate function (path, options?) => void
1018
1012
  *
@@ -1022,17 +1016,16 @@ export function setUIVisibility({ sidebar, tabBar }) {
1022
1016
  * navigate('/app', { replace: true });
1023
1017
  */
1024
1018
  export function useSafeNavigate() {
1025
- const ctx = useContext(UNSAFE_NavigationContext);
1019
+ const inRouter = useInRouterContext();
1020
+ // Router presence is stable for a given component instance, so the
1021
+ // conditional useNavigate call respects the rules of hooks at runtime.
1022
+ const navigate = inRouter ? useNavigate() : null;
1026
1023
 
1027
- if (!ctx) {
1024
+ if (!navigate) {
1028
1025
  return (path) => { window.location.href = path; };
1029
1026
  }
1030
1027
 
1031
1028
  return (path, options = {}) => {
1032
- if (options.replace) {
1033
- ctx.navigator.replace(path, options.state);
1034
- } else {
1035
- ctx.navigator.push(path, options.state);
1036
- }
1029
+ navigate(path, { replace: !!options.replace, state: options.state });
1037
1030
  };
1038
1031
  }
package/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as ProtectedRoute } from "./components/ProtectedRoute";
2
2
  export { isAuthenticated, getAppKey, getCSRFToken, getCurrentUser, useAppSetup } from "./core/Utilities";
3
+ export { createSkateboardApp } from "./App.jsx";
package/layout/Layout.jsx CHANGED
@@ -46,7 +46,7 @@ export default function Layout({ children }) {
46
46
  <SidebarProvider
47
47
  defaultOpen={!constants.sidebarCollapsed}
48
48
  style={{
49
- '--sidebar-width': 'calc(var(--spacing) * 72)',
49
+ '--sidebar-width': '12rem',
50
50
  '--header-height': '3.5rem',
51
51
  }}>
52
52
  {showSidebar && <Sidebar variant="inset" />}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "2.21.0",
4
+ "version": "2.23.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./Sidebar": {
@@ -122,19 +122,19 @@
122
122
  "url": "git+https://github.com/stevederico/skateboard-ui.git"
123
123
  },
124
124
  "dependencies": {
125
- "@base-ui/react": "^1.1.0",
125
+ "@base-ui/react": "^1.4.1",
126
126
  "use-sync-external-store": "^1.6.0",
127
127
  "class-variance-authority": "^0.7.1",
128
128
  "clsx": "^2.1.1",
129
129
  "cmdk": "^1.1.1",
130
130
  "embla-carousel-react": "^8.6.0",
131
- "lucide-react": "^0.563.0",
131
+ "lucide-react": "^1.11.0",
132
132
  "next-themes": "^0.4.6",
133
- "react-day-picker": "^9.13.0",
134
- "react-resizable-panels": "^4.5.1",
135
- "recharts": "^3.7.0",
133
+ "react-day-picker": "^9.14.0",
134
+ "react-resizable-panels": "^4.10.0",
135
+ "recharts": "^3.8.1",
136
136
  "sonner": "^2.0.7",
137
- "tailwind-merge": "^3.4.0",
137
+ "tailwind-merge": "^3.5.0",
138
138
  "tailwindcss-animate": "^1.0.7",
139
139
  "vaul": "^1.1.2"
140
140
  },
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useCallback } from 'react';
2
2
  import { useNavigate, useSearchParams } from 'react-router-dom';
3
3
  import { getState } from '../core/Context.jsx';
4
- import { getCurrentUser } from '../core/Utilities.js'
4
+ import { getCurrentUser, getAppKey } from '../core/Utilities.js'
5
5
  import { Spinner } from '../shadcn/ui/spinner.jsx';
6
6
 
7
7
  // Whitelist of allowed redirect paths to prevent open redirect vulnerabilities
@@ -9,9 +9,14 @@ const ALLOWED_REDIRECT_PREFIXES = ['/app/', '/'];
9
9
  const DEFAULT_REDIRECT = '/app/home';
10
10
 
11
11
  function isAllowedRedirect(path) {
12
- // Must start with allowed prefix and not contain protocol
13
- if (path.includes('://') || path.includes('//')) return false;
14
- return ALLOWED_REDIRECT_PREFIXES.some(prefix => path.startsWith(prefix));
12
+ try {
13
+ // Resolve relative to current origin; reject anything that escapes it.
14
+ const url = new URL(path, window.location.origin);
15
+ if (url.origin !== window.location.origin) return false;
16
+ return ALLOWED_REDIRECT_PREFIXES.some(prefix => url.pathname.startsWith(prefix));
17
+ } catch {
18
+ return false;
19
+ }
15
20
  }
16
21
 
17
22
  /**
@@ -28,8 +33,7 @@ function isAllowedRedirect(path) {
28
33
  * <Route path="payment" element={<PaymentView />} />
29
34
  */
30
35
  export default function PaymentView() {
31
- const { state, dispatch } = getState();
32
- const constants = state.constants;
36
+ const { dispatch } = getState();
33
37
  const navigate = useNavigate();
34
38
  const [searchParams] = useSearchParams();
35
39
 
@@ -45,10 +49,6 @@ export default function PaymentView() {
45
49
  }, [dispatch]);
46
50
 
47
51
  useEffect(() => {
48
- // Get app-specific localStorage keys
49
- const appName = constants.appName || 'skateboard';
50
- const getAppKey = (suffix) => `${appName.toLowerCase().replace(/\s+/g, '-')}_${suffix}`;
51
-
52
52
  // Get query parameters
53
53
  const success = searchParams.get('success') === 'true';
54
54
  const canceled = searchParams.get('canceled') === 'true';
@@ -30,7 +30,7 @@ import { getBackendURL, useSafeNavigate } from '../core/Utilities'
30
30
  * // Embedded in dialog
31
31
  * <SignInView embedded onSuccess={handleSuccess} onSwitchMode={() => setMode('signup')} />
32
32
  */
33
- export default function LoginForm({
33
+ export default function SignInView({
34
34
  className,
35
35
  embedded = false,
36
36
  onSuccess,
@@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader } from "../shadcn/ui/card"
7
7
  import { Alert, AlertDescription } from "../shadcn/ui/alert"
8
8
  import DynamicIcon from '../core/DynamicIcon';
9
9
  import { getState } from "../core/Context.jsx";
10
- import { getBackendURL, useSafeNavigate } from '../core/Utilities'
10
+ import { getBackendURL, useSafeNavigate, getAppKey } from '../core/Utilities'
11
11
 
12
12
  /**
13
13
  * Sign-up form component.
@@ -31,7 +31,7 @@ import { getBackendURL, useSafeNavigate } from '../core/Utilities'
31
31
  * // Embedded in dialog
32
32
  * <SignUpView embedded onSuccess={handleSuccess} onSwitchMode={() => setMode('signin')} />
33
33
  */
34
- export default function LoginForm({
34
+ export default function SignUpView({
35
35
  className,
36
36
  embedded = false,
37
37
  onSuccess,
@@ -79,9 +79,7 @@ export default function LoginForm({
79
79
  const csrfCookie = document.cookie.split('; ').find(row => row.startsWith('csrf_token='));
80
80
  const csrfToken = csrfCookie ? csrfCookie.split('=')[1] : data.csrfToken;
81
81
  if (csrfToken) {
82
- const appName = constants.appName || 'skateboard';
83
- const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
84
- localStorage.setItem(csrfKey, csrfToken);
82
+ localStorage.setItem(getAppKey('csrf'), csrfToken);
85
83
  }
86
84
  dispatch({ type: 'SET_USER', payload: data });
87
85
  if (embedded && onSuccess) {