@stevederico/skateboard-ui 0.9.8 → 1.0.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/App.jsx ADDED
@@ -0,0 +1,75 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import {
3
+ BrowserRouter as Router,
4
+ Routes,
5
+ Route,
6
+ Navigate,
7
+ useNavigate,
8
+ useLocation,
9
+ } from 'react-router-dom';
10
+ import { useEffect } from 'react';
11
+ import Layout from './Layout.jsx';
12
+ import LandingView from './LandingView.jsx';
13
+ import TextView from './TextView.jsx';
14
+ import SignUpView from './SignUpView.jsx';
15
+ import SignInView from './SignInView.jsx';
16
+ import SignOutView from './SignOutView.jsx';
17
+ import PaymentView from './PaymentView.jsx';
18
+ import SettingsView from './SettingsView.jsx';
19
+ import NotFound from './NotFound.jsx';
20
+ import ProtectedRoute from './ProtectedRoute.jsx';
21
+ import { useAppSetup, initializeUtilities } from './Utilities.js';
22
+ import { ContextProvider, getState } from './Context.jsx';
23
+
24
+ function App({ constants, appRoutes, defaultRoute }) {
25
+ const location = useLocation();
26
+ const navigate = useNavigate();
27
+ const { dispatch } = getState();
28
+
29
+ useEffect(() => {
30
+ document.title = constants.appName;
31
+ }, [constants.appName]);
32
+
33
+ useAppSetup(location, navigate, dispatch);
34
+
35
+ return (
36
+ <Routes>
37
+ <Route element={<Layout />}>
38
+ <Route path="/console" element={<Navigate to="/app" replace />} />
39
+ <Route path="/app" element={<ProtectedRoute />}>
40
+ <Route index element={<Navigate to={defaultRoute} replace />} />
41
+ {appRoutes.map(({ path, element }) => (
42
+ <Route key={path} path={path} element={element} />
43
+ ))}
44
+ <Route path="settings" element={<SettingsView />} />
45
+ <Route path="payment" element={<PaymentView />} />
46
+ </Route>
47
+ </Route>
48
+ <Route path="/" element={<LandingView />} />
49
+ <Route path="/signin" element={<SignInView />} />
50
+ <Route path="/signup" element={<SignUpView />} />
51
+ <Route path="/signout" element={<SignOutView />} />
52
+ <Route path="/terms" element={<TextView details={constants.termsOfService} />} />
53
+ <Route path="/privacy" element={<TextView details={constants.privacyPolicy} />} />
54
+ <Route path="/eula" element={<TextView details={constants.EULA} />} />
55
+ <Route path="/subs" element={<TextView details={constants.subscriptionDetails} />} />
56
+ <Route path="*" element={<NotFound />} />
57
+ </Routes>
58
+ );
59
+ }
60
+
61
+ export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRoutes[0]?.path || 'home' }) {
62
+ // Initialize utilities with constants
63
+ initializeUtilities(constants);
64
+
65
+ const container = document.getElementById('root');
66
+ const root = createRoot(container);
67
+
68
+ root.render(
69
+ <ContextProvider constants={constants}>
70
+ <Router>
71
+ <App constants={constants} appRoutes={appRoutes} defaultRoute={defaultRoute} />
72
+ </Router>
73
+ </ContextProvider>
74
+ );
75
+ }
package/CHANGELOG.md CHANGED
@@ -1,4 +1,22 @@
1
1
  # CHANGELOG
2
+ 1.0.1
3
+
4
+ Remove Vite build plugins
5
+
6
+ 1.0.0
7
+
8
+ Add API request utilities
9
+ Add constants validation
10
+ Add React hooks
11
+ Export App component
12
+ Export Context component
13
+ Export styles.css
14
+
15
+ 0.9.9
16
+
17
+ Add optional chaining safeguards
18
+ Fix features rendering logic
19
+
2
20
  0.9.8
3
21
 
4
22
  Add ProtectedRoute component
package/Context.jsx ADDED
@@ -0,0 +1,57 @@
1
+ import React, { createContext, useContext, useReducer } from 'react';
2
+
3
+ const context = createContext();
4
+
5
+ export function ContextProvider({ children, constants }) {
6
+ const getStorageKey = () => {
7
+ const appName = constants.appName || 'skateboard';
8
+ return `${appName.toLowerCase().replace(/\s+/g, '-')}_user`;
9
+ };
10
+
11
+ const getInitialUser = () => {
12
+ try {
13
+ const storageKey = getStorageKey();
14
+ const storedUser = localStorage.getItem(storageKey);
15
+ if (!storedUser || storedUser === "undefined") return null;
16
+ return JSON.parse(storedUser);
17
+ } catch (e) {
18
+ return null;
19
+ }
20
+ };
21
+
22
+ const initialState = { user: getInitialUser() };
23
+
24
+ function reducer(state, action) {
25
+ try {
26
+ const storageKey = getStorageKey();
27
+ const appName = constants.appName || 'skateboard';
28
+ const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
29
+
30
+ switch (action.type) {
31
+ case 'SET_USER':
32
+ localStorage.setItem(storageKey, JSON.stringify(action.payload));
33
+ return { ...state, user: action.payload };
34
+ case 'CLEAR_USER':
35
+ localStorage.removeItem(storageKey);
36
+ localStorage.removeItem(csrfKey);
37
+ return { ...state, user: null };
38
+ default:
39
+ return state;
40
+ }
41
+ } catch (e) {
42
+ return state;
43
+ }
44
+ }
45
+
46
+ const [state, dispatch] = useReducer(reducer, initialState);
47
+
48
+ return (
49
+ <context.Provider value={{ state, dispatch }}>
50
+ {children}
51
+ </context.Provider>
52
+ );
53
+ }
54
+
55
+ export function getState() {
56
+ return useContext(context);
57
+ }
package/LandingView.jsx CHANGED
@@ -252,11 +252,11 @@ export default function LandingView() {
252
252
  {/* Features Section */}
253
253
  <section id="features" className="bg-slate-100 dark:bg-gray-800 py-12 md:py-20 transition-colors duration-300">
254
254
  <div className="max-w-7xl mx-auto px-6">
255
- <h2 className="text-center text-4xl md:text-5xl font-bold mb-16 text-gray-900 dark:text-white">{constants.features.title}</h2>
256
-
255
+ <h2 className="text-center text-4xl md:text-5xl font-bold mb-16 text-gray-900 dark:text-white">{constants.features?.title || 'Features'}</h2>
256
+
257
257
  {/* Feature Grid */}
258
258
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
259
- {constants.features.items.map((feature, index) => (
259
+ {(constants.features?.items || []).map((feature, index) => (
260
260
  <div key={index} className="bg-white dark:bg-gray-700 rounded-2xl p-8 shadow-lg text-center transition-colors duration-300">
261
261
  <div className="text-4xl mb-6">{feature.icon}</div>
262
262
  <h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">{feature.title}</h3>
@@ -296,7 +296,7 @@ export default function LandingView() {
296
296
  >{constants.stripeProducts[0]?.price || '$5.00'}</div>
297
297
  <p className="text-gray-600 dark:text-gray-300 mb-8">per month</p>
298
298
  <ul className="text-left space-y-4 mb-8">
299
- {constants.features.items.map((feature, index) => (
299
+ {(constants.features?.items || []).map((feature, index) => (
300
300
  <li key={index} className="flex items-center">
301
301
  ✅ {feature.title}
302
302
  </li>
package/Utilities.js CHANGED
@@ -1,11 +1,24 @@
1
- import { useEffect } from 'react';
2
- import constants from "@/constants.json";
1
+ import { useEffect, useState } from 'react';
2
+
3
+ // Constants will be initialized by the app shell
4
+ let _constants = null;
5
+
6
+ export function initializeUtilities(constants) {
7
+ _constants = constants;
8
+ }
9
+
10
+ function getConstants() {
11
+ if (!_constants) {
12
+ throw new Error('Utilities not initialized. Call initializeUtilities(constants) first.');
13
+ }
14
+ return _constants;
15
+ }
3
16
 
4
17
  export function getCookie(name) {
5
18
  // For token cookies, use app-specific name
6
19
  let cookieName = name;
7
20
  if (name === 'token') {
8
- const appName = constants.appName || 'skateboard';
21
+ const appName = getConstants().appName || 'skateboard';
9
22
  cookieName = `${appName.toLowerCase().replace(/\s+/g, '-')}_token`;
10
23
  }
11
24
 
@@ -16,18 +29,18 @@ export function getCookie(name) {
16
29
  }
17
30
 
18
31
  export function getCSRFToken() {
19
- const appName = constants.appName || 'skateboard';
32
+ const appName = getConstants().appName || 'skateboard';
20
33
  const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
21
34
  return localStorage.getItem(csrfKey);
22
35
  }
23
36
 
24
37
  export function getAppKey(suffix) {
25
- const appName = constants.appName || 'skateboard';
38
+ const appName = getConstants().appName || 'skateboard';
26
39
  return `${appName.toLowerCase().replace(/\s+/g, '-')}_${suffix}`;
27
40
  }
28
41
 
29
42
  export function isAuthenticated() {
30
- if (constants.noLogin === true) {
43
+ if (getConstants().noLogin === true) {
31
44
  return true;
32
45
  }
33
46
  const csrfKey = getAppKey('csrf');
@@ -35,7 +48,7 @@ export function isAuthenticated() {
35
48
  }
36
49
 
37
50
  export function getBackendURL() {
38
- let result = import.meta.env.DEV ? constants.devBackendURL : constants.backendURL;
51
+ let result = import.meta.env.DEV ? getConstants().devBackendURL : getConstants().backendURL;
39
52
  return result
40
53
  }
41
54
 
@@ -50,7 +63,7 @@ export function isAppMode() {
50
63
 
51
64
  export async function getCurrentUser() {
52
65
 
53
- if (constants.noLogin == true) {
66
+ if (getConstants().noLogin == true) {
54
67
  return {}
55
68
  }
56
69
 
@@ -78,11 +91,11 @@ export async function getCurrentUser() {
78
91
  }
79
92
 
80
93
  export async function isSubscriber() {
81
- if (constants.noLogin == true) {
94
+ if (getConstants().noLogin == true) {
82
95
  return false
83
96
  }
84
97
 
85
- try {
98
+ try{
86
99
  const csrfToken = getCSRFToken();
87
100
  const response = await fetch(`${getBackendURL()}/isSubscriber`, {
88
101
  method: 'GET',
@@ -150,7 +163,7 @@ export async function showCheckout(email, productIndex = 0) {
150
163
  const csrfToken = getCSRFToken();
151
164
 
152
165
  const params = {
153
- lookup_key: constants.stripeProducts[productIndex].lookup_key,
166
+ lookup_key: getConstants().stripeProducts[productIndex].lookup_key,
154
167
  email: email
155
168
  };
156
169
 
@@ -194,7 +207,7 @@ const FREE_LIMITS = {
194
207
  };
195
208
 
196
209
  export async function getRemainingUsage(action) {
197
- if (constants.noLogin === true) {
210
+ if (getConstants().noLogin === true) {
198
211
  return { remaining: -1, total: -1, isSubscriber: true };
199
212
  }
200
213
 
@@ -223,7 +236,7 @@ export async function getRemainingUsage(action) {
223
236
  }
224
237
 
225
238
  export async function trackUsage(action) {
226
- if (constants.noLogin === true) {
239
+ if (getConstants().noLogin === true) {
227
240
  return { remaining: -1, total: -1, isSubscriber: true };
228
241
  }
229
242
 
@@ -259,7 +272,7 @@ export async function trackUsage(action) {
259
272
 
260
273
  export async function showUpgradeSheet(upgradeSheetRef) {
261
274
  // Check subscription from user data in localStorage instead of API call
262
- const appName = constants.appName || 'skateboard';
275
+ const appName = getConstants().appName || 'skateboard';
263
276
  const storageKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_user`;
264
277
  const storedUser = localStorage.getItem(storageKey);
265
278
 
@@ -384,7 +397,7 @@ export function timestampToString(input, format = "DOB") {
384
397
 
385
398
  export function useAppSetup(location) {
386
399
  useEffect(() => {
387
- document.title = constants.appName;
400
+ document.title = getConstants().appName;
388
401
  if (!location.pathname.toLowerCase().includes('app')) {
389
402
  document.documentElement.classList.remove('dark');
390
403
  document.body.classList.remove('dark');
@@ -392,3 +405,417 @@ export function useAppSetup(location) {
392
405
  }, [location.pathname]);
393
406
  }
394
407
 
408
+ // ===== API REQUEST UTILITIES =====
409
+
410
+ /**
411
+ * Unified API request utility
412
+ * Handles credentials, CSRF tokens, 401 redirects, and error handling
413
+ *
414
+ * @param {string} endpoint - API endpoint (e.g., '/deals')
415
+ * @param {RequestInit} options - Fetch options (method, body, headers, etc.)
416
+ * @returns {Promise<any>} - Parsed JSON response
417
+ */
418
+ export async function apiRequest(endpoint, options = {}) {
419
+ const csrfToken = getCSRFToken();
420
+ const needsCSRF = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(
421
+ (options.method || 'GET').toUpperCase()
422
+ );
423
+
424
+ const response = await fetch(`${getBackendURL()}${endpoint}`, {
425
+ ...options,
426
+ credentials: 'include',
427
+ headers: {
428
+ 'Content-Type': 'application/json',
429
+ ...(needsCSRF && csrfToken && { 'X-CSRF-Token': csrfToken }),
430
+ ...options.headers
431
+ }
432
+ });
433
+
434
+ // Handle 401 (redirect to signout)
435
+ if (response.status === 401) {
436
+ window.location.href = '/signout';
437
+ throw new Error('Unauthorized - Redirecting to Sign Out');
438
+ }
439
+
440
+ // Handle other errors
441
+ if (!response.ok) {
442
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
443
+ }
444
+
445
+ return response.json();
446
+ }
447
+
448
+ /**
449
+ * API request with query parameters
450
+ *
451
+ * @param {string} endpoint - API endpoint
452
+ * @param {object} params - Query parameters
453
+ * @param {RequestInit} options - Fetch options
454
+ * @returns {Promise<any>}
455
+ */
456
+ export async function apiRequestWithParams(endpoint, params = {}, options = {}) {
457
+ const queryString = new URLSearchParams(params).toString();
458
+ const url = queryString ? `${endpoint}?${queryString}` : endpoint;
459
+ return apiRequest(url, options);
460
+ }
461
+
462
+ // ===== CONSTANTS VALIDATION =====
463
+
464
+ /**
465
+ * Validate constants.json has all required fields
466
+ * @param {object} constants - Constants object to validate
467
+ * @returns {object} - Same constants if valid
468
+ * @throws {Error} - If required fields are missing
469
+ */
470
+ export function validateConstants(constants) {
471
+ const required = [
472
+ 'appName',
473
+ 'appIcon',
474
+ 'tagline',
475
+ 'cta',
476
+ 'backendURL',
477
+ 'devBackendURL',
478
+ 'features.title',
479
+ 'features.items',
480
+ 'companyName',
481
+ 'companyWebsite',
482
+ 'companyEmail',
483
+ ];
484
+
485
+ const missing = required.filter(key => {
486
+ const value = key.split('.').reduce((obj, k) => obj?.[k], constants);
487
+ return !value;
488
+ });
489
+
490
+ if (missing.length > 0) {
491
+ throw new Error(`Missing required constants: ${missing.join(', ')}`);
492
+ }
493
+
494
+ return constants;
495
+ }
496
+
497
+
498
+ // ===== REACT HOOKS =====
499
+
500
+ /**
501
+ * Standard list data fetcher with optional sorting
502
+ * @param {string} endpoint - API endpoint to fetch from
503
+ * @param {function} sortFn - Optional sort function for results
504
+ * @returns {object} - { data, loading, error, refetch }
505
+ */
506
+ export function useListData(endpoint, sortFn = null) {
507
+ const [data, setData] = useState([]);
508
+ const [loading, setLoading] = useState(true);
509
+ const [error, setError] = useState(null);
510
+
511
+ const fetchData = async () => {
512
+ setLoading(true);
513
+ try {
514
+ const result = await apiRequest(endpoint);
515
+ const sorted = sortFn ? result.sort(sortFn) : result;
516
+ setData(sorted);
517
+ setError(null);
518
+ } catch (err) {
519
+ setError(err.message);
520
+ } finally {
521
+ setLoading(false);
522
+ }
523
+ };
524
+
525
+ useEffect(() => {
526
+ fetchData();
527
+ }, [endpoint]);
528
+
529
+ return { data, loading, error, refetch: fetchData };
530
+ }
531
+
532
+ /**
533
+ * Standard form state management
534
+ * @param {object} initialValues - Initial form values
535
+ * @param {function} onSubmit - Submit handler function
536
+ * @returns {object} - { values, handleChange, handleSubmit, reset, submitting, error }
537
+ */
538
+ export function useForm(initialValues, onSubmit) {
539
+ const [values, setValues] = useState(initialValues);
540
+ const [submitting, setSubmitting] = useState(false);
541
+ const [error, setError] = useState(null);
542
+
543
+ const handleChange = (field) => (e) => {
544
+ setValues(prev => ({ ...prev, [field]: e.target.value }));
545
+ };
546
+
547
+ const handleSubmit = async (e) => {
548
+ e?.preventDefault();
549
+ setSubmitting(true);
550
+ setError(null);
551
+ try {
552
+ await onSubmit(values);
553
+ setValues(initialValues);
554
+ } catch (err) {
555
+ setError(err.message);
556
+ } finally {
557
+ setSubmitting(false);
558
+ }
559
+ };
560
+
561
+ const reset = () => setValues(initialValues);
562
+
563
+ return { values, handleChange, handleSubmit, reset, submitting, error };
564
+ }
565
+
566
+ // ===== VITE BUILD CONFIG UTILITIES =====
567
+
568
+ /**
569
+ * Custom logger plugin for Vite
570
+ */
571
+ export const customLoggerPlugin = () => {
572
+ return {
573
+ name: 'custom-logger',
574
+ configureServer(server) {
575
+ server.printUrls = () => {
576
+ console.log(`🖥️ React is running on http://localhost:${server.config.server.port || 5173}`);
577
+ };
578
+ }
579
+ };
580
+ };
581
+
582
+ /**
583
+ * HTML replacement plugin
584
+ * Replaces {{APP_NAME}}, {{TAGLINE}}, {{COMPANY_WEBSITE}} in index.html
585
+ */
586
+ export const htmlReplacePlugin = () => {
587
+ return {
588
+ name: 'html-replace',
589
+ async transformIndexHtml(html) {
590
+ const { readFileSync } = await import('node:fs');
591
+ const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
592
+
593
+ return html
594
+ .replace(/{{APP_NAME}}/g, constants.appName)
595
+ .replace(/{{TAGLINE}}/g, constants.tagline)
596
+ .replace(/{{COMPANY_WEBSITE}}/g, constants.companyWebsite);
597
+ }
598
+ };
599
+ };
600
+
601
+ /**
602
+ * Dynamic robots.txt plugin
603
+ */
604
+ export const dynamicRobotsPlugin = () => {
605
+ return {
606
+ name: 'dynamic-robots',
607
+ async generateBundle() {
608
+ const { readFileSync } = await import('node:fs');
609
+ const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
610
+ const website = constants.companyWebsite.startsWith('http')
611
+ ? constants.companyWebsite
612
+ : `https://${constants.companyWebsite}`;
613
+
614
+ const robotsContent = `User-agent: Googlebot
615
+ Disallow: /app/
616
+ Disallow: /console/
617
+ Disallow: /signin/
618
+ Disallow: /signup/
619
+
620
+ User-agent: Bingbot
621
+ Disallow: /app/
622
+ Disallow: /console/
623
+ Disallow: /signin/
624
+ Disallow: /signup/
625
+
626
+ User-agent: Applebot
627
+ Disallow: /app/
628
+ Disallow: /console/
629
+ Disallow: /signin/
630
+ Disallow: /signup/
631
+
632
+ User-agent: facebookexternalhit
633
+ Disallow: /app/
634
+ Disallow: /console/
635
+ Disallow: /signin/
636
+ Disallow: /signup/
637
+
638
+ User-agent: Facebot
639
+ Disallow: /app/
640
+ Disallow: /console/
641
+ Disallow: /signin/
642
+ Disallow: /signup/
643
+
644
+ User-agent: Twitterbot
645
+ Disallow: /app/
646
+ Disallow: /console/
647
+ Disallow: /signin/
648
+ Disallow: /signup/
649
+
650
+ User-agent: *
651
+ Disallow: /
652
+
653
+ Sitemap: ${website}/sitemap.xml
654
+ `;
655
+
656
+ this.emitFile({
657
+ type: 'asset',
658
+ fileName: 'robots.txt',
659
+ source: robotsContent
660
+ });
661
+ }
662
+ };
663
+ };
664
+
665
+ /**
666
+ * Dynamic sitemap.xml plugin
667
+ */
668
+ export const dynamicSitemapPlugin = () => {
669
+ return {
670
+ name: 'dynamic-sitemap',
671
+ async generateBundle() {
672
+ const { readFileSync } = await import('node:fs');
673
+ const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
674
+ const website = constants.companyWebsite.startsWith('http')
675
+ ? constants.companyWebsite
676
+ : `https://${constants.companyWebsite}`;
677
+
678
+ const currentDate = new Date().toISOString().split('T')[0];
679
+
680
+ const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
681
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
682
+ <url>
683
+ <loc>${website}/</loc>
684
+ <lastmod>${currentDate}</lastmod>
685
+ <changefreq>weekly</changefreq>
686
+ <priority>1.0</priority>
687
+ </url>
688
+ <url>
689
+ <loc>${website}/terms</loc>
690
+ <lastmod>${currentDate}</lastmod>
691
+ <changefreq>monthly</changefreq>
692
+ <priority>0.8</priority>
693
+ </url>
694
+ <url>
695
+ <loc>${website}/privacy</loc>
696
+ <lastmod>${currentDate}</lastmod>
697
+ <changefreq>monthly</changefreq>
698
+ <priority>0.8</priority>
699
+ </url>
700
+ <url>
701
+ <loc>${website}/subs</loc>
702
+ <lastmod>${currentDate}</lastmod>
703
+ <changefreq>monthly</changefreq>
704
+ <priority>0.7</priority>
705
+ </url>
706
+ <url>
707
+ <loc>${website}/eula</loc>
708
+ <lastmod>${currentDate}</lastmod>
709
+ <changefreq>monthly</changefreq>
710
+ <priority>0.7</priority>
711
+ </url>
712
+ </urlset>`;
713
+
714
+ this.emitFile({
715
+ type: 'asset',
716
+ fileName: 'sitemap.xml',
717
+ source: sitemapContent
718
+ });
719
+ }
720
+ };
721
+ };
722
+
723
+ /**
724
+ * Dynamic manifest.json plugin
725
+ */
726
+ export const dynamicManifestPlugin = () => {
727
+ return {
728
+ name: 'dynamic-manifest',
729
+ async generateBundle() {
730
+ const { readFileSync } = await import('node:fs');
731
+ const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
732
+
733
+ const manifestContent = {
734
+ short_name: constants.appName,
735
+ name: constants.appName,
736
+ description: constants.tagline,
737
+ icons: [
738
+ {
739
+ src: "/icons/icon.svg",
740
+ sizes: "192x192",
741
+ type: "image/svg+xml"
742
+ }
743
+ ],
744
+ start_url: "./app",
745
+ display: "standalone",
746
+ theme_color: "#000000",
747
+ background_color: "#ffffff"
748
+ };
749
+
750
+ this.emitFile({
751
+ type: 'asset',
752
+ fileName: 'manifest.json',
753
+ source: JSON.stringify(manifestContent, null, 2)
754
+ });
755
+ }
756
+ };
757
+ };
758
+
759
+ /**
760
+ * Complete Vite config generator
761
+ * Returns standard skateboard config with optional overrides
762
+ */
763
+ export async function getSkateboardViteConfig(customConfig = {}) {
764
+ const [react, tailwindcss, { resolve }, path] = await Promise.all([
765
+ import('@vitejs/plugin-react-swc').then(m => m.default),
766
+ import('@tailwindcss/vite').then(m => m.default),
767
+ import('node:path'),
768
+ import('node:path')
769
+ ]);
770
+
771
+ return {
772
+ plugins: [
773
+ react(),
774
+ tailwindcss(),
775
+ customLoggerPlugin(),
776
+ htmlReplacePlugin(),
777
+ dynamicRobotsPlugin(),
778
+ dynamicSitemapPlugin(),
779
+ dynamicManifestPlugin(),
780
+ ...(customConfig.plugins || [])
781
+ ],
782
+ esbuild: {
783
+ drop: []
784
+ },
785
+ resolve: {
786
+ alias: {
787
+ '@': resolve(process.cwd(), './src'),
788
+ '@package': path.resolve(process.cwd(), 'package.json'),
789
+ '@root': path.resolve(process.cwd()),
790
+ 'react/jsx-runtime': path.resolve(process.cwd(), 'node_modules/react/jsx-runtime.js'),
791
+ ...(customConfig.resolve?.alias || {})
792
+ }
793
+ },
794
+ optimizeDeps: {
795
+ include: ['react-dom', '@radix-ui/react-slot'],
796
+ esbuildOptions: {
797
+ define: {
798
+ global: 'globalThis',
799
+ },
800
+ },
801
+ },
802
+ server: {
803
+ host: 'localhost',
804
+ open: false,
805
+ port: 5173,
806
+ strictPort: false,
807
+ hmr: {
808
+ port: 5173,
809
+ overlay: false,
810
+ },
811
+ watch: {
812
+ usePolling: false,
813
+ ignored: ['**/node_modules/**', '**/.git/**']
814
+ },
815
+ ...(customConfig.server || {})
816
+ },
817
+ logLevel: 'error',
818
+ ...customConfig
819
+ };
820
+ }
821
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "0.9.8",
4
+ "version": "1.0.2",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./AppSidebar": {
@@ -76,6 +76,18 @@
76
76
  "import": "./ProtectedRoute.jsx",
77
77
  "default": "./ProtectedRoute.jsx"
78
78
  },
79
+ "./styles.css": {
80
+ "import": "./styles.css",
81
+ "default": "./styles.css"
82
+ },
83
+ "./Context": {
84
+ "import": "./Context.jsx",
85
+ "default": "./Context.jsx"
86
+ },
87
+ "./App": {
88
+ "import": "./App.jsx",
89
+ "default": "./App.jsx"
90
+ },
79
91
  "./shadcn/ui/*": "./shadcn/ui/*.jsx",
80
92
  "./shadcn/lib/utils": {
81
93
  "import": "./shadcn/lib/utils.js",
package/styles.css ADDED
@@ -0,0 +1,174 @@
1
+ @import "tailwindcss";
2
+
3
+ @source '../';
4
+
5
+ @plugin 'tailwindcss-animate';
6
+
7
+ @custom-variant dark (&:is(.dark *));
8
+
9
+ @theme {
10
+ --color-app: var(--color-blue-500);
11
+ }
12
+
13
+ :root {
14
+ --background: oklch(0.985 0 0);
15
+ --foreground: oklch(0.145 0 0);
16
+ --card: oklch(0.985 0 0);
17
+ --card-foreground: oklch(0.145 0 0);
18
+ --popover: oklch(1 0 0);
19
+ --popover-foreground: oklch(0.145 0 0);
20
+ --primary: oklch(0.205 0 0);
21
+ --primary-foreground: oklch(0.985 0 0);
22
+ --secondary: oklch(0.97 0 0);
23
+ --secondary-foreground: oklch(0.205 0 0);
24
+ --muted: oklch(0.97 0 0);
25
+ --muted-foreground: oklch(0.556 0 0);
26
+ --accent: oklch(0.96 0 0);
27
+ --accent-foreground: oklch(0.205 0 0);
28
+ --destructive: oklch(0.577 0.245 27.325);
29
+ --destructive-foreground: oklch(0.577 0.245 27.325);
30
+ --border: oklch(0.922 0 0);
31
+ --input: oklch(0.922 0 0);
32
+ --ring: oklch(0.708 0 0);
33
+ --chart-1: oklch(0.646 0.222 41.116);
34
+ --chart-2: oklch(0.6 0.118 184.704);
35
+ --chart-3: oklch(0.398 0.07 227.392);
36
+ --chart-4: oklch(0.828 0.189 84.429);
37
+ --chart-5: oklch(0.769 0.188 70.08);
38
+ --radius: 0.625rem;
39
+ --sidebar: oklch(0.985 0 0);
40
+ --sidebar-foreground: oklch(0.145 0 0);
41
+ --sidebar-primary: oklch(0.205 0 0);
42
+ --sidebar-primary-foreground: oklch(0.985 0 0);
43
+ --sidebar-accent: oklch(0.96 0 0);
44
+ --sidebar-accent-foreground: oklch(0.205 0 0);
45
+ --sidebar-border: oklch(0.922 0 0);
46
+ --sidebar-ring: oklch(0.708 0 0);
47
+ }
48
+
49
+ .dark {
50
+ --background: oklch(0.205 0 0);
51
+ --foreground: oklch(0.985 0 0);
52
+ --card: oklch(0.205 0 0);
53
+ --card-foreground: oklch(0.985 0 0);
54
+ --popover: oklch(0.145 0 0);
55
+ --popover-foreground: oklch(0.985 0 0);
56
+ --primary: oklch(0.985 0 0);
57
+ --primary-foreground: oklch(0.205 0 0);
58
+ --secondary: oklch(0.269 0 0);
59
+ --secondary-foreground: oklch(0.985 0 0);
60
+ --muted: oklch(0.269 0 0);
61
+ --muted-foreground: oklch(0.708 0 0);
62
+ --accent: oklch(0.28 0 0);
63
+ --accent-foreground: oklch(0.985 0 0);
64
+ --destructive: oklch(0.396 0.141 25.723);
65
+ --destructive-foreground: oklch(0.637 0.237 25.331);
66
+ --border: oklch(0.269 0 0);
67
+ --input: oklch(0.269 0 0);
68
+ --ring: oklch(0.439 0 0);
69
+ --chart-1: oklch(0.488 0.243 264.376);
70
+ --chart-2: oklch(0.696 0.17 162.48);
71
+ --chart-3: oklch(0.769 0.188 70.08);
72
+ --chart-4: oklch(0.627 0.265 303.9);
73
+ --chart-5: oklch(0.645 0.246 16.439);
74
+ --sidebar: oklch(0.205 0 0);
75
+ --sidebar-foreground: oklch(0.985 0 0);
76
+ --sidebar-primary: oklch(0.488 0.243 264.376);
77
+ --sidebar-primary-foreground: oklch(0.985 0 0);
78
+ --sidebar-accent: var(--accent);
79
+ --sidebar-accent-foreground: oklch(0.985 0 0);
80
+ --sidebar-border: oklch(0.269 0 0);
81
+ --sidebar-ring: oklch(0.439 0 0);
82
+ }
83
+
84
+ @theme inline {
85
+ --color-background: var(--background);
86
+ --color-foreground: var(--foreground);
87
+ --color-card: var(--card);
88
+ --color-card-foreground: var(--card-foreground);
89
+ --color-popover: var(--popover);
90
+ --color-popover-foreground: var(--popover-foreground);
91
+ --color-primary: var(--primary);
92
+ --color-primary-foreground: var(--primary-foreground);
93
+ --color-secondary: var(--secondary);
94
+ --color-secondary-foreground: var(--secondary-foreground);
95
+ --color-muted: var(--muted);
96
+ --color-muted-foreground: var(--muted-foreground);
97
+ --color-accent: var(--accent);
98
+ --color-accent-foreground: var(--accent-foreground);
99
+ --color-destructive: var(--destructive);
100
+ --color-destructive-foreground: var(--destructive-foreground);
101
+ --color-border: var(--border);
102
+ --color-input: var(--input);
103
+ --color-ring: var(--ring);
104
+ --color-chart-1: var(--chart-1);
105
+ --color-chart-2: var(--chart-2);
106
+ --color-chart-3: var(--chart-3);
107
+ --color-chart-4: var(--chart-4);
108
+ --color-chart-5: var(--chart-5);
109
+ --radius-sm: calc(var(--radius) - 4px);
110
+ --radius-md: calc(var(--radius) - 2px);
111
+ --radius-lg: var(--radius);
112
+ --radius-xl: calc(var(--radius) + 4px);
113
+ --color-sidebar: var(--sidebar);
114
+ --color-sidebar-foreground: var(--sidebar-foreground);
115
+ --color-sidebar-primary: var(--sidebar-primary);
116
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
117
+ --color-sidebar-accent: var(--sidebar-accent);
118
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
119
+ --color-sidebar-border: var(--sidebar-border);
120
+ --color-sidebar-ring: var(--sidebar-ring);
121
+ --animate-accordion-down: accordion-down 0.2s ease-out;
122
+ --animate-accordion-up: accordion-up 0.2s ease-out;
123
+
124
+ @keyframes accordion-down {
125
+ from {
126
+ height: 0;
127
+ }
128
+ to {
129
+ height: var(--radix-accordion-content-height);
130
+ }
131
+ }
132
+
133
+ @keyframes accordion-up {
134
+ from {
135
+ height: var(--radix-accordion-content-height);
136
+ }
137
+ to {
138
+ height: 0;
139
+ }
140
+ }
141
+ }
142
+
143
+ @layer base {
144
+ * {
145
+ @apply border-border outline-ring/50;
146
+ }
147
+ body {
148
+ @apply bg-white dark:bg-black text-foreground;
149
+ }
150
+ }
151
+
152
+ @layer base {
153
+ :root {
154
+ --sidebar-background: 0 0% 98%;
155
+ --sidebar-foreground: 240 5.3% 26.1%;
156
+ --sidebar-primary: 240 5.9% 10%;
157
+ --sidebar-primary-foreground: 0 0% 98%;
158
+ --sidebar-accent: 240 4.8% 95.9%;
159
+ --sidebar-accent-foreground: 240 5.9% 10%;
160
+ --sidebar-border: 220 13% 91%;
161
+ --sidebar-ring: 217.2 91.2% 59.8%;
162
+ }
163
+
164
+ .dark {
165
+ --sidebar-background: 240 5.9% 10%;
166
+ --sidebar-foreground: 240 4.8% 95.9%;
167
+ --sidebar-primary: 224.3 76.3% 48%;
168
+ --sidebar-primary-foreground: 0 0% 100%;
169
+ --sidebar-accent: 240 3.7% 15.9%;
170
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
171
+ --sidebar-border: 240 3.7% 15.9%;
172
+ --sidebar-ring: 217.2 91.2% 59.8%;
173
+ }
174
+ }