@stevederico/skateboard-ui 4.6.0 → 4.9.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 +16 -0
- package/README.md +143 -1100
- package/dist/App.d.ts +2 -0
- package/dist/App.js +3 -1
- package/dist/ui/command.js +1 -1
- package/dist/ui/context-menu.js +2 -2
- package/dist/ui/dropdown-menu.js +2 -2
- package/dist/ui/hover-card.js +1 -1
- package/dist/ui/menubar.js +2 -2
- package/dist/ui/popover.js +1 -1
- package/dist/ui/select.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
# skateboard-ui
|
|
2
2
|
|
|
3
|
-
React component library for rapid application development. Built with TypeScript, TailwindCSS v4, and shadcn
|
|
3
|
+
React component library for rapid application development. Built with TypeScript, TailwindCSS v4, and shadcn-style primitives.
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
**Self-contained components (v4.0).** Every component is now hand-written with no primitive framework underneath — the previously-vendored `@base-ui/react` bundle (~31k lines) is gone. Floating positioning, focus trapping, dismiss layers, and animations are small in-house hooks; modals use the native `<dialog>` element. The legacy `shadcn/ui/*` import paths still work — they re-export the new `ui/*` components — so existing apps need no changes.
|
|
8
|
-
|
|
9
|
-
**Accessibility-hardened (v4.1–4.3).** The self-contained tier was put through three rounds of a11y work: menu/select typeahead, correct keyboard focus management and tab-stops (menus, tabs, radios, popovers, navigation menu), ARIA wiring (`aria-controls`, labelled-by, indeterminate checkbox mixed state, named popover/slider dialogs), scroll-lock and exit-animation fixes, plus matching DOMException error names. Each release adds to a Playwright a11y regression suite that runs `workers: 1`.
|
|
10
|
-
|
|
11
|
-
**TypeScript with full type declarations.** Source is TypeScript, compiled to plain JavaScript + `.d.ts` in `dist/` (`npm run build`). JavaScript apps consume the compiled JS exactly as before — import paths are unchanged — and TypeScript apps get real types for every export (`SkateboardConstants`, `User`, component props, `VariantProps`, typed icons).
|
|
12
|
-
|
|
13
|
-
**Node.js 24+** is required in the app repo for build tooling and backend (see `engines` in `package.json`). The UI package itself runs in the browser.
|
|
5
|
+
Requires **Node.js 24+** in the app repo (see `engines` in `package.json`).
|
|
14
6
|
|
|
15
7
|
## Installation
|
|
16
8
|
|
|
@@ -37,150 +29,21 @@ const appRoutes = [
|
|
|
37
29
|
createSkateboardApp({ constants, appRoutes });
|
|
38
30
|
```
|
|
39
31
|
|
|
40
|
-
That's it
|
|
41
|
-
|
|
42
|
-
## Dependency Footprint
|
|
43
|
-
|
|
44
|
-
Across the v3.x series, every npm runtime dep was either vendored, recreated, or dropped. v3.5 finished the job by vendoring `@base-ui/react`; **v4.0 removes that vendored bundle entirely** by rewriting every component to be self-contained.
|
|
45
|
-
|
|
46
|
-
| | Hard deps | Optional peer deps | Total |
|
|
47
|
-
|---|---|---|---|
|
|
48
|
-
| Before (v2.23) | 15 | 0 | 15 |
|
|
49
|
-
| v3.0 | 4 | 3 | 7 |
|
|
50
|
-
| v3.1 | 3 | 1 | 4 |
|
|
51
|
-
| v3.2 | 2 | 1 | 3 |
|
|
52
|
-
| v3.3 | 2 | 0 | 2 |
|
|
53
|
-
| v3.4 | 1 | 0 | 1 |
|
|
54
|
-
| v3.5 | 0 | 0 | 0 |
|
|
55
|
-
| **Now (v4.0+)** | **0** | **0** | **0** |
|
|
56
|
-
|
|
57
|
-
v3.10 converts the package to TypeScript — compiled JS + `.d.ts` ship in `dist/`; the dependency count is unchanged (`typescript` is a devDependency only).
|
|
58
|
-
|
|
59
|
-
**No hard deps remain.** React + React-DOM + React Router are the only `peerDependencies`. Every other piece of code that runs in a consumer browser is either authored here, recreated as a drop-in, or vendored from upstream.
|
|
60
|
-
|
|
61
|
-
### Vendored packages
|
|
62
|
-
|
|
63
|
-
Pre-built copies live in this repo so consumers don't pull them from npm. Refresh scripts in `scripts/` pin a version and re-bundle.
|
|
64
|
-
|
|
65
|
-
| Source | Lives at | Refresh script |
|
|
66
|
-
|---|---|---|
|
|
67
|
-
| `lucide` icons (1700+) | `icons/*.tsx` | `node scripts/vendor-icons.js` (bump `LUCIDE_TAG`) |
|
|
68
|
-
| `tailwind-merge` | `shadcn/lib/tailwind-merge.js` | `node scripts/vendor-tailwind-merge.js` (bump `TM_VERSION`) |
|
|
69
|
-
|
|
70
|
-
`@base-ui/react` was vendored in v3.5 and **removed in v4.0** — the 47 components are now self-contained (see _Self-contained components_ above).
|
|
71
|
-
|
|
72
|
-
### Ported / recreated / inlined
|
|
73
|
-
|
|
74
|
-
| Replaces | Lives at | Approach |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| `@base-ui/react` (behavior) | `ui/*.tsx` + `ui/{use-floating,use-dismiss,use-presence,use-controllable-state,slot,portal}.tsx` | rewritten — small in-house hooks for positioning, dismiss, presence, focus; native `<dialog>` for modals |
|
|
77
|
-
| `vaul` (drag gesture) | `ui/drawer.tsx` | ported — drag math copied from vaul `src/index.tsx` (MIT, Emil Kowalski) on a native `<dialog>` shell |
|
|
78
|
-
| `cmdk` | `components/core/Command.tsx` | rewritten drop-in |
|
|
79
|
-
| `react-day-picker` | `components/core/Calendar.tsx` | rewritten drop-in |
|
|
80
|
-
| `next-themes` | `components/core/ThemeProvider.tsx` | rewritten drop-in |
|
|
81
|
-
| `class-variance-authority` | `shadcn/lib/cva.ts` | rewritten drop-in |
|
|
82
|
-
| `clsx` | `shadcn/lib/clsx.ts` | rewritten drop-in |
|
|
83
|
-
| `tailwindcss-animate` | `styles.css` | inlined as plain CSS utilities |
|
|
84
|
-
| `sonner` | — | removed; use `Dialog` / `Alert` |
|
|
85
|
-
|
|
86
|
-
### Removed components (by version)
|
|
87
|
-
|
|
88
|
-
| Version | Dropped | Reason |
|
|
89
|
-
|---|---|---|
|
|
90
|
-
| v3.1 | `Carousel`, `Resizable` | unused; consumers can install `embla-carousel-react` / `react-resizable-panels` directly |
|
|
91
|
-
| v3.3 | `Chart` | unused; consumers can install `recharts` directly |
|
|
92
|
-
| v3.4 | `vaul` | drag physics ported; `Drawer` rebuilt on base-ui Dialog |
|
|
93
|
-
| v3.5 | `@base-ui/react` | vendored; see table above |
|
|
94
|
-
| v4.0 | `@base-ui/react` (vendored bundle) | removed; components rewritten self-contained on `ui/*` |
|
|
95
|
-
|
|
96
|
-
## Migrating to 3.0
|
|
97
|
-
|
|
98
|
-
Version 3.0 vendors all 1700+ lucide icons into the package and drops the `lucide-react` npm dependency. Icons are pin-versioned, auditable, and re-runnable via `node scripts/vendor-icons.js`.
|
|
99
|
-
|
|
100
|
-
**One change in your app:**
|
|
101
|
-
|
|
102
|
-
```diff
|
|
103
|
-
- import { ArrowUp, X } from 'lucide-react';
|
|
104
|
-
+ import { ArrowUp, X } from '@stevederico/skateboard-ui/icons';
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
One-line migration:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
find src -type f -name "*.jsx" -exec sed -i '' \
|
|
111
|
-
"s|from 'lucide-react'|from '@stevederico/skateboard-ui/icons'|g" {} +
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Then remove `lucide-react` from your app's `package.json`.
|
|
115
|
-
|
|
116
|
-
**API is identical** — same component names (`ArrowUp`, `XIcon`, etc.), same props (`size`, `color`, `strokeWidth`, `className`), same `Icon`-suffix aliases shadcn uses. Legacy lucide aliases (e.g., `Loader2` → `LoaderCircle`) are preserved.
|
|
117
|
-
|
|
118
|
-
To refresh the icon set against a newer lucide release: bump `LUCIDE_TAG` in `scripts/vendor-icons.js` and re-run.
|
|
119
|
-
|
|
120
|
-
The vendored icons keep their original [Lucide ISC license](icons/LICENSE) (some legacy icons inherit Feather's MIT license — both notices are in that file).
|
|
121
|
-
|
|
122
|
-
## Refreshing vendored packages
|
|
123
|
-
|
|
124
|
-
Each vendored library has a script that pins a version, fetches the upstream release, and re-emits the local copy. Trust model: vendoring upgrades from "trust npm on every install" to "trust npm at refresh time." Audit the diff between refreshes.
|
|
125
|
-
|
|
126
|
-
| Library | Bump | Command | Host requirement |
|
|
127
|
-
|---|---|---|---|
|
|
128
|
-
| `lucide` icons | `LUCIDE_TAG` in `scripts/vendor-icons.js` | `node scripts/vendor-icons.js` | Node |
|
|
129
|
-
| `tailwind-merge` | `TM_VERSION` in `scripts/vendor-tailwind-merge.js` | `node scripts/vendor-tailwind-merge.js` | Node |
|
|
130
|
-
|
|
131
|
-
## Dark Mode Setup
|
|
132
|
-
|
|
133
|
-
To prevent flash of unstyled content (FOUC) when using dark mode, add this script to your `index.html` **before** your app loads:
|
|
134
|
-
|
|
135
|
-
```html
|
|
136
|
-
<!DOCTYPE html>
|
|
137
|
-
<html lang="en">
|
|
138
|
-
<head>
|
|
139
|
-
<meta charset="UTF-8" />
|
|
140
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
141
|
-
|
|
142
|
-
<!-- Prevent dark mode FOUC -->
|
|
143
|
-
<script>
|
|
144
|
-
try {
|
|
145
|
-
const theme = localStorage.getItem('theme');
|
|
146
|
-
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
147
|
-
if (theme === 'dark' || (!theme && systemDark)) {
|
|
148
|
-
document.documentElement.classList.add('dark');
|
|
149
|
-
}
|
|
150
|
-
} catch (e) {}
|
|
151
|
-
</script>
|
|
152
|
-
|
|
153
|
-
<title>Your App</title>
|
|
154
|
-
</head>
|
|
155
|
-
<body>
|
|
156
|
-
<div id="root"></div>
|
|
157
|
-
<script type="module" src="/src/main.jsx"></script>
|
|
158
|
-
</body>
|
|
159
|
-
</html>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
This ensures the correct theme is applied before React renders, eliminating any flash between light and dark modes.
|
|
32
|
+
That's it — routing, auth, layout, landing page, settings, and payments are wired up.
|
|
163
33
|
|
|
164
34
|
## Configuration
|
|
165
35
|
|
|
166
|
-
skateboard-ui requires a `constants` object
|
|
36
|
+
skateboard-ui requires a `constants` object:
|
|
167
37
|
|
|
168
38
|
```javascript
|
|
169
|
-
// constants.json or constants.js
|
|
170
39
|
const constants = {
|
|
171
|
-
// Required
|
|
40
|
+
// Required
|
|
172
41
|
devBackendURL: "http://localhost:8000/api",
|
|
173
42
|
backendURL: "https://api.myapp.com/api",
|
|
174
|
-
|
|
175
|
-
// Required: App identity
|
|
176
43
|
appName: "MyApp",
|
|
177
|
-
appIcon: "sparkles",
|
|
178
|
-
|
|
179
|
-
// Required: Landing page content
|
|
44
|
+
appIcon: "sparkles",
|
|
180
45
|
tagline: "Build apps faster with skateboard-ui",
|
|
181
46
|
cta: "Get Started",
|
|
182
|
-
|
|
183
|
-
// Required: Features section
|
|
184
47
|
features: {
|
|
185
48
|
title: "Everything you need",
|
|
186
49
|
items: [
|
|
@@ -188,29 +51,19 @@ const constants = {
|
|
|
188
51
|
{ icon: "Shield", title: "Secure", description: "Authentication included" }
|
|
189
52
|
]
|
|
190
53
|
},
|
|
191
|
-
|
|
192
|
-
// Required: Company information
|
|
193
54
|
companyName: "Your Company",
|
|
194
55
|
companyWebsite: "https://yourcompany.com",
|
|
195
56
|
companyEmail: "hello@yourcompany.com",
|
|
196
57
|
|
|
197
|
-
// Optional
|
|
58
|
+
// Optional
|
|
198
59
|
pages: [
|
|
199
60
|
{ title: "Home", icon: "home", url: "home" },
|
|
200
61
|
{ title: "Search", icon: "search", url: "search" }
|
|
201
62
|
],
|
|
202
|
-
|
|
203
|
-
// Optional: Authentication
|
|
204
|
-
noLogin: false, // Set true to disable authentication entirely
|
|
205
|
-
// authOverlay defaults to true: lazy auth via the sign-in overlay.
|
|
206
|
-
// Set `authOverlay: false` to require eager sign-in before /app access.
|
|
207
|
-
|
|
208
|
-
// Optional: UI visibility
|
|
63
|
+
noLogin: false,
|
|
209
64
|
hideSidebar: false,
|
|
210
65
|
hideTabBar: false,
|
|
211
66
|
hideSidebarHeader: false,
|
|
212
|
-
|
|
213
|
-
// Optional: Payments (Stripe)
|
|
214
67
|
stripeProducts: [
|
|
215
68
|
{
|
|
216
69
|
name: "Pro Plan",
|
|
@@ -219,749 +72,240 @@ const constants = {
|
|
|
219
72
|
lookup_key: "pro_plan",
|
|
220
73
|
title: "Go Pro",
|
|
221
74
|
interval: "month",
|
|
222
|
-
features: ["Unlimited usage", "Priority support"
|
|
75
|
+
features: ["Unlimited usage", "Priority support"]
|
|
223
76
|
}
|
|
224
77
|
],
|
|
225
|
-
|
|
226
|
-
// Optional: Landing page customization
|
|
227
|
-
navLinks: [ // Override header nav links
|
|
78
|
+
navLinks: [
|
|
228
79
|
{ label: "Features", href: "#features" },
|
|
229
|
-
{ label: "Pricing", href: "#pricing" }
|
|
230
|
-
{ label: "Blog", href: "/blog" }
|
|
80
|
+
{ label: "Pricing", href: "#pricing" }
|
|
231
81
|
],
|
|
232
|
-
pricing: {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
},
|
|
236
|
-
ctaHeading: "Ready To Build?", // CTA section heading
|
|
237
|
-
footerLinks: [ // Override footer links
|
|
82
|
+
pricing: { title: "Simple Pricing", extras: ["Priority support", "Cancel anytime"] },
|
|
83
|
+
ctaHeading: "Ready To Build?",
|
|
84
|
+
footerLinks: [
|
|
238
85
|
{ label: "Privacy", href: "/privacy" },
|
|
239
|
-
{ label: "Terms", href: "/terms" }
|
|
240
|
-
{ label: "EULA", href: "/eula" }
|
|
86
|
+
{ label: "Terms", href: "/terms" }
|
|
241
87
|
],
|
|
242
|
-
copyrightText: "All rights reserved.",
|
|
243
|
-
|
|
244
|
-
// Optional: Legal documents (plain text, supports _COMPANY_, _WEBSITE_, _EMAIL_ placeholders)
|
|
88
|
+
copyrightText: "All rights reserved.",
|
|
245
89
|
termsOfService: "Terms of Service for _COMPANY_...",
|
|
246
90
|
privacyPolicy: "Privacy Policy for _COMPANY_...",
|
|
247
91
|
EULA: "End User License Agreement...",
|
|
248
92
|
subscriptionDetails: "Subscription details...",
|
|
249
|
-
|
|
250
|
-
// Optional: App metadata
|
|
251
93
|
version: "1.0.0"
|
|
252
94
|
}
|
|
253
95
|
```
|
|
254
96
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
The `devBackendURL` and `backendURL` should include your full API base path (including the `/api` prefix):
|
|
258
|
-
|
|
259
|
-
```javascript
|
|
260
|
-
const constants = {
|
|
261
|
-
devBackendURL: "http://localhost:8000/api", // Include /api prefix
|
|
262
|
-
backendURL: "https://api.myapp.com/api",
|
|
263
|
-
}
|
|
264
|
-
```
|
|
97
|
+
`devBackendURL` and `backendURL` should include the full API base path (e.g. `/api`). Endpoints are relative to that base:
|
|
265
98
|
|
|
266
|
-
Endpoints are relative to this base URL:
|
|
267
99
|
- `${getBackendURL()}/signup` → `http://localhost:8000/api/signup`
|
|
268
100
|
- `${getBackendURL()}/me` → `http://localhost:8000/api/me`
|
|
269
|
-
- `${getBackendURL()}/deals` → `http://localhost:8000/api/deals`
|
|
270
|
-
|
|
271
|
-
**Tip:** Include API versioning in the base URL (e.g., `/api/v2`) rather than in each endpoint path.
|
|
272
101
|
|
|
273
102
|
## createSkateboardApp
|
|
274
103
|
|
|
275
|
-
The bootstrap function that sets up routing, auth, theming, state, toasts, and error handling.
|
|
276
|
-
|
|
277
104
|
```javascript
|
|
278
105
|
import { createSkateboardApp } from '@stevederico/skateboard-ui/App';
|
|
279
106
|
|
|
280
107
|
createSkateboardApp({
|
|
281
|
-
constants, // Required
|
|
108
|
+
constants, // Required
|
|
282
109
|
appRoutes, // Required: [{ path: string, element: JSX.Element }]
|
|
283
|
-
defaultRoute, // Optional:
|
|
284
|
-
landingPage, // Optional:
|
|
285
|
-
wrapper, // Optional:
|
|
110
|
+
defaultRoute, // Optional: defaults to first appRoute path
|
|
111
|
+
landingPage, // Optional: custom landing page element
|
|
112
|
+
wrapper, // Optional: component to wrap the router
|
|
286
113
|
});
|
|
287
114
|
```
|
|
288
115
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
- **Routes:** Landing, signin, signup, signout, app routes, settings, payment, legal pages (terms, privacy, EULA, subscription)
|
|
292
|
-
- **Authentication:** ProtectedRoute wrapping `/app/*`, AuthOverlay for lazy auth
|
|
293
|
-
- **Theming:** in-house `ThemeProvider` (system / light / dark) — `components/core/ThemeProvider.tsx`
|
|
294
|
-
- **State:** ContextProvider with user, UI, and auth overlay state
|
|
295
|
-
- **Error Boundary:** Catches render errors, unhandled rejections, and global errors
|
|
296
|
-
|
|
297
|
-
### Generated Routes
|
|
116
|
+
Sets up routing, auth, theming, state, and error handling.
|
|
298
117
|
|
|
299
118
|
| Route | Component | Protected |
|
|
300
119
|
|-------|-----------|-----------|
|
|
301
120
|
| `/` | LandingView (or custom `landingPage`) | No |
|
|
302
|
-
| `/signin` |
|
|
303
|
-
| `/signup` | SignUpView | No |
|
|
304
|
-
| `/signout` | SignOutView | No |
|
|
121
|
+
| `/signin`, `/signup`, `/signout` | SignIn/SignUp/SignOutView | No |
|
|
305
122
|
| `/app/:path` | Your appRoutes | Yes |
|
|
306
|
-
| `/app/settings` | SettingsView | Yes |
|
|
307
|
-
| `/
|
|
308
|
-
| `/terms` | TextView | No |
|
|
309
|
-
| `/privacy` | TextView | No |
|
|
310
|
-
| `/eula` | TextView | No |
|
|
311
|
-
| `/subscription` | TextView | No |
|
|
123
|
+
| `/app/settings`, `/app/payment` | SettingsView, PaymentView | Yes |
|
|
124
|
+
| `/terms`, `/privacy`, `/eula`, `/subscription` | TextView | No |
|
|
312
125
|
| `*` | NotFound | No |
|
|
313
126
|
|
|
314
127
|
## Authentication
|
|
315
128
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
skateboard-ui uses a **hybrid cookie + localStorage authentication system** that combines security with performance:
|
|
319
|
-
|
|
320
|
-
```
|
|
321
|
-
Frontend Backend Storage
|
|
322
|
-
──────── ─────── ───────
|
|
323
|
-
|
|
324
|
-
1. POST /signin → Validate credentials
|
|
325
|
-
credentials
|
|
326
|
-
← Set-Cookie: {appName}_token (HttpOnly)
|
|
327
|
-
← Set-Cookie: csrf_token
|
|
328
|
-
← Response: { csrfToken, ...user }
|
|
329
|
-
|
|
330
|
-
2. Extract tokens → localStorage:
|
|
331
|
-
- CSRF from cookie {appName}_csrf
|
|
332
|
-
- User from response {appName}_user
|
|
333
|
-
|
|
334
|
-
3. isAuthenticated() → Check localStorage
|
|
335
|
-
(client-side) (fast, no network)
|
|
336
|
-
|
|
337
|
-
4. ProtectedRoute → GET /me
|
|
338
|
-
(server validation) Validate cookies
|
|
339
|
-
← 200 OK or 401 Unauthorized
|
|
340
|
-
|
|
341
|
-
5. API requests → Protected endpoints
|
|
342
|
-
+ cookies (automatic) Validate {appName}_token
|
|
343
|
-
+ X-CSRF-Token header Validate CSRF header
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
### Cookie-Based Session Management
|
|
347
|
-
- **Session token** stored in `{appName}_token` cookie (HttpOnly, Secure, SameSite=Strict)
|
|
348
|
-
- Automatically sent with every request via browser
|
|
349
|
-
- Cannot be accessed by JavaScript (XSS protection)
|
|
350
|
-
- Backend validates cookie on each protected endpoint
|
|
351
|
-
|
|
352
|
-
### localStorage for Client-Side Validation
|
|
353
|
-
- **CSRF token** and **user data** stored in localStorage
|
|
354
|
-
- Enables instant `isAuthenticated()` checks without network calls
|
|
355
|
-
- Used by client-side routing logic (ProtectedRoute initial check)
|
|
356
|
-
- Not used for actual authentication (cookies handle that)
|
|
357
|
-
|
|
358
|
-
### CSRF Protection
|
|
359
|
-
- Dual-token system prevents CSRF attacks
|
|
360
|
-
- **CSRF token** sent in `X-CSRF-Token` header with state-changing requests
|
|
361
|
-
- Backend validates header matches stored session CSRF token
|
|
362
|
-
- Separate from session cookie to prevent cookie-based CSRF
|
|
363
|
-
|
|
364
|
-
### CSRF Error Handling
|
|
365
|
-
|
|
366
|
-
The `apiRequest` utility automatically handles CSRF token failures:
|
|
367
|
-
|
|
368
|
-
1. **Auto-Regeneration**: Backend auto-regenerates tokens after server restart
|
|
369
|
-
2. **Retry Logic**: Frontend automatically retries failed requests once after refreshing the session
|
|
370
|
-
3. **User Experience**: Transparent recovery without forcing sign-out or page refresh
|
|
371
|
-
|
|
372
|
-
**Error Flow**:
|
|
373
|
-
```
|
|
374
|
-
POST /api/keys → 403 CSRF error
|
|
375
|
-
↓
|
|
376
|
-
Fetch /me (triggers backend auto-regeneration)
|
|
377
|
-
↓
|
|
378
|
-
Retry POST /api/keys with fresh token
|
|
379
|
-
↓
|
|
380
|
-
Success
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Required Backend Endpoints
|
|
384
|
-
|
|
385
|
-
#### POST /signup
|
|
386
|
-
Create new user account.
|
|
387
|
-
|
|
388
|
-
**Request:**
|
|
389
|
-
```json
|
|
390
|
-
{
|
|
391
|
-
"email": "user@example.com",
|
|
392
|
-
"password": "securePassword123",
|
|
393
|
-
"name": "John Doe"
|
|
394
|
-
}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
**Response:**
|
|
398
|
-
- Status: 201 Created
|
|
399
|
-
- Headers:
|
|
400
|
-
- `Set-Cookie: {appName}_token={sessionToken}; HttpOnly; Secure; SameSite=Strict; Path=/`
|
|
401
|
-
- `Set-Cookie: csrf_token={csrfToken}; Secure; SameSite=Lax; Path=/`
|
|
402
|
-
- Body:
|
|
403
|
-
```json
|
|
404
|
-
{
|
|
405
|
-
"csrfToken": "csrf_abc123...",
|
|
406
|
-
"user": {
|
|
407
|
-
"id": "user123",
|
|
408
|
-
"email": "user@example.com",
|
|
409
|
-
"name": "John Doe"
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
#### POST /signin
|
|
415
|
-
Authenticate existing user.
|
|
416
|
-
|
|
417
|
-
**Request:**
|
|
418
|
-
```json
|
|
419
|
-
{
|
|
420
|
-
"email": "user@example.com",
|
|
421
|
-
"password": "securePassword123"
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
**Response:**
|
|
426
|
-
- Status: 200 OK
|
|
427
|
-
- Headers: Same as /signup
|
|
428
|
-
- Body: Same as /signup
|
|
429
|
-
|
|
430
|
-
#### GET /me
|
|
431
|
-
Validate current session and return user data.
|
|
432
|
-
|
|
433
|
-
**Request:**
|
|
434
|
-
- Headers: Cookies automatically sent by browser
|
|
435
|
-
|
|
436
|
-
**Response (authenticated):**
|
|
437
|
-
- Status: 200 OK
|
|
438
|
-
- Body:
|
|
439
|
-
```json
|
|
440
|
-
{
|
|
441
|
-
"user": {
|
|
442
|
-
"id": "user123",
|
|
443
|
-
"email": "user@example.com",
|
|
444
|
-
"name": "John Doe",
|
|
445
|
-
"subscription": {
|
|
446
|
-
"status": "active",
|
|
447
|
-
"expires": 1735689600,
|
|
448
|
-
"stripeID": "cus_abc123"
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
**Response (not authenticated):**
|
|
455
|
-
- Status: 401 Unauthorized
|
|
456
|
-
|
|
457
|
-
#### POST /signout
|
|
458
|
-
End current session.
|
|
459
|
-
|
|
460
|
-
**Request:**
|
|
461
|
-
- Headers:
|
|
462
|
-
- Cookies automatically sent
|
|
463
|
-
- `X-CSRF-Token: {csrfToken}`
|
|
129
|
+
Hybrid cookie + localStorage auth:
|
|
464
130
|
|
|
465
|
-
**
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
- `Set-Cookie: csrf_token=; Max-Age=0; Path=/` (clear cookie)
|
|
131
|
+
1. **Sign in** — backend sets `{appName}_token` (HttpOnly) and `csrf_token` cookies; frontend stores CSRF + user in localStorage
|
|
132
|
+
2. **Client check** — `isAuthenticated()` reads localStorage (fast, no network)
|
|
133
|
+
3. **Route guard** — `ProtectedRoute` validates via `GET /me`
|
|
134
|
+
4. **API calls** — cookies sent automatically; `X-CSRF-Token` header on state-changing requests
|
|
470
135
|
|
|
471
|
-
###
|
|
136
|
+
### Required backend endpoints
|
|
472
137
|
|
|
473
|
-
|
|
474
|
-
Track and check feature usage limits.
|
|
138
|
+
**POST /signup** and **POST /signin**
|
|
475
139
|
|
|
476
|
-
**Request:**
|
|
477
140
|
```json
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
or
|
|
481
|
-
```json
|
|
482
|
-
{ "operation": "track" }
|
|
483
|
-
```
|
|
141
|
+
// Request
|
|
142
|
+
{ "email": "user@example.com", "password": "securePassword123", "name": "John Doe" }
|
|
484
143
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
144
|
+
// Response (201 / 200)
|
|
145
|
+
// Set-Cookie: {appName}_token=...; HttpOnly; Secure; SameSite=Strict
|
|
146
|
+
// Set-Cookie: csrf_token=...
|
|
147
|
+
{ "csrfToken": "...", "user": { "id": "...", "email": "...", "name": "..." } }
|
|
488
148
|
```
|
|
489
149
|
|
|
490
|
-
|
|
491
|
-
Check subscription status.
|
|
150
|
+
**GET /me** — validate session, return user (401 if not authenticated)
|
|
492
151
|
|
|
493
|
-
**
|
|
494
|
-
```json
|
|
495
|
-
{ "isSubscriber": true }
|
|
496
|
-
```
|
|
152
|
+
**POST /signout** — requires `X-CSRF-Token`, clears cookies
|
|
497
153
|
|
|
498
|
-
|
|
499
|
-
Create a Stripe checkout session.
|
|
154
|
+
### Optional endpoints
|
|
500
155
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
156
|
+
| Endpoint | Purpose |
|
|
157
|
+
|----------|---------|
|
|
158
|
+
| `POST /usage` | Track/check feature usage limits |
|
|
159
|
+
| `GET /isSubscriber` | Subscription status |
|
|
160
|
+
| `POST /checkout` | Stripe checkout session |
|
|
161
|
+
| `POST /portal` | Stripe billing portal |
|
|
505
162
|
|
|
506
|
-
|
|
507
|
-
```json
|
|
508
|
-
{ "url": "https://checkout.stripe.com/..." }
|
|
509
|
-
```
|
|
163
|
+
### Lazy auth (default)
|
|
510
164
|
|
|
511
|
-
|
|
512
|
-
Open Stripe billing portal.
|
|
165
|
+
Users can browse `/app` without signing in. Protected actions trigger the auth overlay via `useAuthGate`:
|
|
513
166
|
|
|
514
|
-
**Request:**
|
|
515
|
-
```json
|
|
516
|
-
{ "customerID": "cus_abc123" }
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
**Response:**
|
|
520
|
-
```json
|
|
521
|
-
{ "url": "https://billing.stripe.com/..." }
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### Cookie Configuration
|
|
525
|
-
|
|
526
|
-
**Session Token Cookie:**
|
|
527
167
|
```javascript
|
|
528
|
-
{
|
|
529
|
-
name: '{appName}_token',
|
|
530
|
-
httpOnly: true, // Prevents JavaScript access (XSS protection)
|
|
531
|
-
secure: true, // HTTPS only (production)
|
|
532
|
-
sameSite: 'Strict', // Strongest CSRF protection
|
|
533
|
-
path: '/',
|
|
534
|
-
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days (configurable)
|
|
535
|
-
}
|
|
536
|
-
```
|
|
168
|
+
import { useAuthGate } from '@stevederico/skateboard-ui/useAuthGate';
|
|
537
169
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
{
|
|
541
|
-
name: 'csrf_token',
|
|
542
|
-
httpOnly: false, // Must be readable by JavaScript
|
|
543
|
-
secure: true, // HTTPS only (production)
|
|
544
|
-
sameSite: 'Lax', // Allow top-level navigation
|
|
545
|
-
path: '/',
|
|
546
|
-
maxAge: 7 * 24 * 60 * 60 * 1000 // Match session token
|
|
170
|
+
function SaveButton() {
|
|
171
|
+
const requireAuth = useAuthGate();
|
|
172
|
+
return <button onClick={() => requireAuth(() => saveThing())}>Save</button>;
|
|
547
173
|
}
|
|
548
174
|
```
|
|
549
175
|
|
|
550
|
-
|
|
551
|
-
All authenticated endpoints must:
|
|
552
|
-
1. Validate `{appName}_token` cookie exists and is valid
|
|
553
|
-
2. For state-changing operations (POST, PUT, DELETE), validate `X-CSRF-Token` header
|
|
554
|
-
3. Return 401 if authentication fails
|
|
555
|
-
4. Return 403 if CSRF validation fails
|
|
556
|
-
|
|
557
|
-
### Security Considerations
|
|
176
|
+
Set `authOverlay: false` in constants to require sign-in before `/app` access.
|
|
558
177
|
|
|
559
|
-
-
|
|
560
|
-
- **CSRF Protection**: Dual-token pattern prevents cookie-based CSRF attacks
|
|
561
|
-
- **SameSite Policy**: Session token (Strict), CSRF token (Lax)
|
|
562
|
-
- **HTTPS Requirement**: All cookies marked `Secure` in production
|
|
563
|
-
- **localStorage Trade-offs**: Acceptable for CSRF token (cannot authenticate alone), never store session token
|
|
564
|
-
|
|
565
|
-
### Example Backend Implementation (Express.js)
|
|
178
|
+
### No-login mode
|
|
566
179
|
|
|
567
180
|
```javascript
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
import crypto from 'crypto';
|
|
571
|
-
|
|
572
|
-
const app = express();
|
|
573
|
-
app.use(express.json());
|
|
574
|
-
app.use(cookieParser());
|
|
575
|
-
|
|
576
|
-
// In-memory session store (use Redis in production)
|
|
577
|
-
const sessions = new Map();
|
|
578
|
-
|
|
579
|
-
function generateToken() {
|
|
580
|
-
return crypto.randomBytes(32).toString('hex');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function requireAuth(req, res, next) {
|
|
584
|
-
const sessionToken = req.cookies.myapp_token;
|
|
585
|
-
const session = sessions.get(sessionToken);
|
|
586
|
-
if (!session) return res.status(401).json({ error: 'Not authenticated' });
|
|
587
|
-
req.session = session;
|
|
588
|
-
next();
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function requireCSRF(req, res, next) {
|
|
592
|
-
const csrfToken = req.headers['x-csrf-token'];
|
|
593
|
-
if (!req.session || req.session.csrfToken !== csrfToken) {
|
|
594
|
-
return res.status(403).json({ error: 'Invalid CSRF token' });
|
|
595
|
-
}
|
|
596
|
-
next();
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
app.post('/api/signup', async (req, res) => {
|
|
600
|
-
const { email, password, name } = req.body;
|
|
601
|
-
if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
|
|
602
|
-
|
|
603
|
-
const user = await createUser(email, password, name);
|
|
604
|
-
const sessionToken = generateToken();
|
|
605
|
-
const csrfToken = generateToken();
|
|
181
|
+
const constants = { noLogin: true };
|
|
182
|
+
```
|
|
606
183
|
|
|
607
|
-
|
|
184
|
+
### Troubleshooting
|
|
608
185
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
httpOnly: false, secure: process.env.NODE_ENV === 'production',
|
|
615
|
-
sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000
|
|
616
|
-
});
|
|
186
|
+
| Problem | Fix |
|
|
187
|
+
|---------|-----|
|
|
188
|
+
| "Not authenticated" after signin | Ensure `credentials: 'include'` in fetch calls |
|
|
189
|
+
| CSRF 403 errors | Verify `X-CSRF-Token` header and `csrf_token` cookie |
|
|
190
|
+
| Cookies not persisting (dev) | Set `secure: false` in dev, check domain match |
|
|
617
191
|
|
|
618
|
-
|
|
619
|
-
});
|
|
192
|
+
## Dark Mode
|
|
620
193
|
|
|
621
|
-
|
|
622
|
-
const user = await getUserById(req.session.userId);
|
|
623
|
-
res.json({ user: { id: user.id, email: user.email, name: user.name } });
|
|
624
|
-
});
|
|
194
|
+
Add this script to `index.html` **before** your app loads to prevent FOUC:
|
|
625
195
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
196
|
+
```html
|
|
197
|
+
<script>
|
|
198
|
+
try {
|
|
199
|
+
const theme = localStorage.getItem('theme');
|
|
200
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
201
|
+
if (theme === 'dark' || (!theme && systemDark)) {
|
|
202
|
+
document.documentElement.classList.add('dark');
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {}
|
|
205
|
+
</script>
|
|
632
206
|
```
|
|
633
207
|
|
|
634
|
-
|
|
208
|
+
## Styling
|
|
635
209
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
| "Not authenticated" after signin | Cookies not sent | Verify `credentials: 'include'` in fetch calls |
|
|
639
|
-
| CSRF 403 errors | Token mismatch | Check `X-CSRF-Token` header, verify cookie exists |
|
|
640
|
-
| Cookies not persisting | SameSite/Secure flags | Set `secure: false` in dev, check domain match |
|
|
641
|
-
| `isAuthenticated()` false but cookie exists | localStorage cleared | Re-fetch from `/me` endpoint |
|
|
210
|
+
```css
|
|
211
|
+
@import "@stevederico/skateboard-ui/styles.css";
|
|
642
212
|
|
|
643
|
-
|
|
213
|
+
@source '../../node_modules/@stevederico/skateboard-ui';
|
|
644
214
|
|
|
645
|
-
|
|
646
|
-
|
|
215
|
+
@theme {
|
|
216
|
+
--color-app: var(--color-purple-500);
|
|
217
|
+
}
|
|
647
218
|
```
|
|
648
219
|
|
|
649
|
-
|
|
220
|
+
Key variables: `--color-app` (brand), `--background`, `--foreground`, `--accent`, `--radius`.
|
|
650
221
|
|
|
651
222
|
## Components
|
|
652
223
|
|
|
653
|
-
###
|
|
224
|
+
### App components
|
|
654
225
|
|
|
655
226
|
| Component | Import | Description |
|
|
656
227
|
|-----------|--------|-------------|
|
|
657
|
-
| Sidebar | `@stevederico/skateboard-ui/Sidebar` | Desktop navigation sidebar
|
|
658
|
-
| Header | `@stevederico/skateboard-ui/Header` |
|
|
659
|
-
| Layout | `@stevederico/skateboard-ui/Layout` |
|
|
660
|
-
| TabBar | `@stevederico/skateboard-ui/TabBar` | Mobile bottom navigation
|
|
228
|
+
| Sidebar | `@stevederico/skateboard-ui/Sidebar` | Desktop navigation sidebar |
|
|
229
|
+
| Header | `@stevederico/skateboard-ui/Header` | Page header with optional action button |
|
|
230
|
+
| Layout | `@stevederico/skateboard-ui/Layout` | Sidebar (desktop) + tabbar (mobile) |
|
|
231
|
+
| TabBar | `@stevederico/skateboard-ui/TabBar` | Mobile bottom navigation |
|
|
661
232
|
| DynamicIcon | `@stevederico/skateboard-ui/DynamicIcon` | Lucide icon by name string |
|
|
662
|
-
| ThemeToggle | `@stevederico/skateboard-ui/ThemeToggle` | Dark/light mode toggle
|
|
233
|
+
| ThemeToggle | `@stevederico/skateboard-ui/ThemeToggle` | Dark/light mode toggle |
|
|
663
234
|
| Sheet | `@stevederico/skateboard-ui/Sheet` | Slide-out panel |
|
|
664
235
|
| UpgradeSheet | `@stevederico/skateboard-ui/UpgradeSheet` | Premium upgrade drawer |
|
|
665
236
|
| ErrorBoundary | `@stevederico/skateboard-ui/ErrorBoundary` | Error boundary wrapper |
|
|
666
237
|
|
|
667
|
-
###
|
|
668
|
-
|
|
669
|
-
| Component | Import | Description |
|
|
670
|
-
|-----------|--------|-------------|
|
|
671
|
-
| LandingView | `@stevederico/skateboard-ui/LandingView` | Landing page — sticky header, hero, features, pricing, CTA, footer |
|
|
672
|
-
| SignInView | `@stevederico/skateboard-ui/SignInView` | Sign in form with Card layout |
|
|
673
|
-
| SignUpView | `@stevederico/skateboard-ui/SignUpView` | Sign up form with password validation |
|
|
674
|
-
| SignOutView | `@stevederico/skateboard-ui/SignOutView` | Sign out handler with redirect |
|
|
675
|
-
| SettingsView | `@stevederico/skateboard-ui/SettingsView` | User settings, billing, theme |
|
|
676
|
-
| PaymentView | `@stevederico/skateboard-ui/PaymentView` | Stripe payment redirect handler |
|
|
677
|
-
| TextView | `@stevederico/skateboard-ui/TextView` | Legal document viewer with placeholder replacement |
|
|
678
|
-
| NotFound | `@stevederico/skateboard-ui/NotFound` | 404 page |
|
|
679
|
-
|
|
680
|
-
### Auth Components
|
|
681
|
-
|
|
682
|
-
| Export | Import | Description |
|
|
683
|
-
|--------|--------|-------------|
|
|
684
|
-
| AuthOverlay | `@stevederico/skateboard-ui/AuthOverlay` | Modal sign-in/sign-up dialog |
|
|
685
|
-
| useAuthGate | `@stevederico/skateboard-ui/useAuthGate` | Hook to gate actions behind auth |
|
|
686
|
-
|
|
687
|
-
### State & Utilities
|
|
688
|
-
|
|
689
|
-
| Export | Import | Description |
|
|
690
|
-
|--------|--------|-------------|
|
|
691
|
-
| Context | `@stevederico/skateboard-ui/Context` | App state provider and accessor |
|
|
692
|
-
| Utilities | `@stevederico/skateboard-ui/Utilities` | API, auth, formatting, and UI utilities |
|
|
693
|
-
| App | `@stevederico/skateboard-ui/App` | createSkateboardApp bootstrap function |
|
|
694
|
-
| ProtectedRoute | `@stevederico/skateboard-ui/ProtectedRoute` | Route guard with server validation |
|
|
238
|
+
### Views
|
|
695
239
|
|
|
696
|
-
|
|
240
|
+
| Component | Import |
|
|
241
|
+
|-----------|--------|
|
|
242
|
+
| LandingView | `@stevederico/skateboard-ui/LandingView` |
|
|
243
|
+
| SignInView | `@stevederico/skateboard-ui/SignInView` |
|
|
244
|
+
| SignUpView | `@stevederico/skateboard-ui/SignUpView` |
|
|
245
|
+
| SignOutView | `@stevederico/skateboard-ui/SignOutView` |
|
|
246
|
+
| SettingsView | `@stevederico/skateboard-ui/SettingsView` |
|
|
247
|
+
| PaymentView | `@stevederico/skateboard-ui/PaymentView` |
|
|
248
|
+
| TextView | `@stevederico/skateboard-ui/TextView` |
|
|
249
|
+
| NotFound | `@stevederico/skateboard-ui/NotFound` |
|
|
697
250
|
|
|
698
|
-
###
|
|
251
|
+
### Auth
|
|
699
252
|
|
|
700
|
-
|
|
253
|
+
| Export | Import |
|
|
254
|
+
|--------|--------|
|
|
255
|
+
| AuthOverlay | `@stevederico/skateboard-ui/AuthOverlay` |
|
|
256
|
+
| useAuthGate | `@stevederico/skateboard-ui/useAuthGate` |
|
|
257
|
+
| ProtectedRoute | `@stevederico/skateboard-ui/ProtectedRoute` |
|
|
701
258
|
|
|
702
|
-
|
|
703
|
-
import Sidebar from '@stevederico/skateboard-ui/Sidebar';
|
|
704
|
-
|
|
705
|
-
// Used internally by Layout. Renders automatically based on constants.
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
**Reads from constants:**
|
|
709
|
-
- `pages` — Navigation items rendered as sidebar menu buttons
|
|
710
|
-
- `appName` — Displayed in sidebar header
|
|
711
|
-
- `appIcon` — Icon in sidebar header
|
|
712
|
-
- `hideSidebarHeader` — Hides the header when `true`
|
|
713
|
-
|
|
714
|
-
**Features:**
|
|
715
|
-
- Collapsible to icon-only mode via rail
|
|
716
|
-
- Active page highlighting based on current route
|
|
717
|
-
- Tooltip labels when collapsed
|
|
718
|
-
- Footer with Settings button and user dropdown (account, billing, notifications, sign out)
|
|
719
|
-
|
|
720
|
-
### Header
|
|
721
|
-
|
|
722
|
-
```javascript
|
|
723
|
-
import Header from '@stevederico/skateboard-ui/Header';
|
|
724
|
-
|
|
725
|
-
<Header
|
|
726
|
-
title="Dashboard"
|
|
727
|
-
buttonTitle="Add"
|
|
728
|
-
onButtonTitleClick={() => console.log('clicked')}
|
|
729
|
-
buttonClass="bg-app text-white"
|
|
730
|
-
className="sticky top-0"
|
|
731
|
-
>
|
|
732
|
-
{/* Optional: custom right-side content */}
|
|
733
|
-
</Header>
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
**Props:**
|
|
737
|
-
|
|
738
|
-
| Prop | Type | Default | Description |
|
|
739
|
-
|------|------|---------|-------------|
|
|
740
|
-
| title | string | required | Header title |
|
|
741
|
-
| buttonTitle | string | — | Action button text (omit to hide) |
|
|
742
|
-
| onButtonTitleClick | function | — | Button click handler |
|
|
743
|
-
| buttonClass | string | — | Additional button CSS classes |
|
|
744
|
-
| className | string | — | Additional header CSS classes |
|
|
745
|
-
| children | ReactNode | — | Custom right-side content |
|
|
746
|
-
|
|
747
|
-
### DynamicIcon
|
|
259
|
+
### UI primitives
|
|
748
260
|
|
|
749
|
-
|
|
261
|
+
Import from `@stevederico/skateboard-ui/ui/*`.
|
|
750
262
|
|
|
751
263
|
```javascript
|
|
752
|
-
import
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
**Props:**
|
|
760
|
-
|
|
761
|
-
| Prop | Type | Default | Description |
|
|
762
|
-
|------|------|---------|-------------|
|
|
763
|
-
| name | string | required | Icon name (e.g. "home", "arrow-right", "Settings") |
|
|
764
|
-
| size | number | 24 | Icon size in pixels |
|
|
765
|
-
| color | string | 'currentColor' | Stroke color |
|
|
766
|
-
| strokeWidth | number | 2 | Stroke width |
|
|
767
|
-
| className | string | — | Additional CSS classes |
|
|
768
|
-
|
|
769
|
-
Icons vendored from [lucide](https://lucide.dev/icons/) at `scripts/vendor-icons.js`. Returns null if icon name not found.
|
|
770
|
-
|
|
771
|
-
### ThemeToggle
|
|
772
|
-
|
|
773
|
-
```javascript
|
|
774
|
-
import ThemeToggle from '@stevederico/skateboard-ui/ThemeToggle';
|
|
775
|
-
|
|
776
|
-
<ThemeToggle />
|
|
777
|
-
<ThemeToggle variant="landing" iconSize={18} />
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
**Props:**
|
|
781
|
-
|
|
782
|
-
| Prop | Type | Default | Description |
|
|
783
|
-
|------|------|---------|-------------|
|
|
784
|
-
| className | string | "" | Additional CSS classes |
|
|
785
|
-
| iconSize | number | 16 | Icon size in pixels |
|
|
786
|
-
| variant | string | "settings" | "settings" (ghost) or "landing" (outline) |
|
|
787
|
-
|
|
788
|
-
### TabBar
|
|
789
|
-
|
|
790
|
-
Mobile bottom navigation bar. Hidden on `md+` screens. Renders pages from `constants.pages` plus a Settings link.
|
|
791
|
-
|
|
792
|
-
```javascript
|
|
793
|
-
import TabBar from '@stevederico/skateboard-ui/TabBar';
|
|
794
|
-
|
|
795
|
-
// Used internally by Layout. Renders automatically.
|
|
264
|
+
import { Button } from '@stevederico/skateboard-ui/ui/button';
|
|
265
|
+
import { Card, CardHeader, CardTitle, CardContent } from '@stevederico/skateboard-ui/ui/card';
|
|
266
|
+
import { Input } from '@stevederico/skateboard-ui/ui/input';
|
|
267
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@stevederico/skateboard-ui/ui/dialog';
|
|
268
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@stevederico/skateboard-ui/ui/select';
|
|
269
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@stevederico/skateboard-ui/ui/dropdown-menu';
|
|
270
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@stevederico/skateboard-ui/ui/tabs';
|
|
796
271
|
```
|
|
797
272
|
|
|
798
|
-
**Features:**
|
|
799
|
-
- Fixed bottom position on mobile
|
|
800
|
-
- Active page highlighting with bold stroke
|
|
801
|
-
- Text labels under each icon
|
|
802
|
-
- Settings link appended automatically
|
|
803
|
-
|
|
804
|
-
### UpgradeSheet
|
|
805
|
-
|
|
806
|
-
Drawer component for premium upgrade prompts. Controlled via ref.
|
|
807
|
-
|
|
808
273
|
```javascript
|
|
809
|
-
import {
|
|
810
|
-
import
|
|
811
|
-
import { showUpgradeSheet } from '@stevederico/skateboard-ui/Utilities';
|
|
812
|
-
|
|
813
|
-
function MyComponent() {
|
|
814
|
-
const upgradeRef = useRef();
|
|
815
|
-
|
|
816
|
-
return (
|
|
817
|
-
<>
|
|
818
|
-
<button onClick={() => showUpgradeSheet(upgradeRef)}>
|
|
819
|
-
Upgrade
|
|
820
|
-
</button>
|
|
821
|
-
<UpgradeSheet ref={upgradeRef} userEmail={user.email} />
|
|
822
|
-
</>
|
|
823
|
-
);
|
|
824
|
-
}
|
|
274
|
+
import { cn } from '@stevederico/skateboard-ui/shadcn/lib/utils';
|
|
275
|
+
import { useIsMobile } from '@stevederico/skateboard-ui/shadcn/hooks/use-mobile';
|
|
825
276
|
```
|
|
826
277
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
**Props:**
|
|
830
|
-
|
|
831
|
-
| Prop | Type | Description |
|
|
832
|
-
|------|------|-------------|
|
|
833
|
-
| userEmail | string | User's email for Stripe checkout |
|
|
834
|
-
|
|
835
|
-
**Reads from constants:** `stripeProducts[0]` (title, price, features)
|
|
836
|
-
|
|
837
|
-
### TextView
|
|
838
|
-
|
|
839
|
-
Renders legal documents with placeholder replacement.
|
|
278
|
+
### Icons
|
|
840
279
|
|
|
841
280
|
```javascript
|
|
842
|
-
import
|
|
843
|
-
|
|
844
|
-
<TextView details={constants.termsOfService} />
|
|
281
|
+
import { ArrowUp, X } from '@stevederico/skateboard-ui/icons';
|
|
845
282
|
```
|
|
846
283
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
| Prop | Type | Description |
|
|
850
|
-
|------|------|-------------|
|
|
851
|
-
| details | string | Text content with optional placeholders |
|
|
852
|
-
| className | string | Additional CSS classes |
|
|
853
|
-
|
|
854
|
-
**Placeholders:** `_COMPANY_` → companyName, `_WEBSITE_` → companyWebsite, `_EMAIL_` → companyEmail
|
|
855
|
-
|
|
856
|
-
### ErrorBoundary
|
|
857
|
-
|
|
858
|
-
Catches render errors, unhandled promise rejections, and global errors. Shows an error card with retry options.
|
|
859
|
-
|
|
860
|
-
```javascript
|
|
861
|
-
import ErrorBoundary from '@stevederico/skateboard-ui/ErrorBoundary';
|
|
862
|
-
|
|
863
|
-
<ErrorBoundary>
|
|
864
|
-
<App />
|
|
865
|
-
</ErrorBoundary>
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
## Context (State Management)
|
|
284
|
+
## Context
|
|
869
285
|
|
|
870
286
|
```javascript
|
|
871
287
|
import { getState, useUser, useDispatch } from '@stevederico/skateboard-ui/Context';
|
|
872
288
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
// Access state
|
|
877
|
-
const user = state.user;
|
|
878
|
-
const constants = state.constants;
|
|
879
|
-
|
|
880
|
-
// Dispatch actions
|
|
881
|
-
dispatch({ type: 'SET_USER', payload: userData });
|
|
882
|
-
dispatch({ type: 'CLEAR_USER' });
|
|
883
|
-
}
|
|
289
|
+
const { state, dispatch } = getState();
|
|
290
|
+
dispatch({ type: 'SET_USER', payload: userData });
|
|
291
|
+
dispatch({ type: 'CLEAR_USER' });
|
|
884
292
|
```
|
|
885
293
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
import { useUser, useDispatch } from '@stevederico/skateboard-ui/Context';
|
|
892
|
-
|
|
893
|
-
// Only re-renders when user changes (not on sidebar/theme changes)
|
|
894
|
-
function ProfileCard() {
|
|
895
|
-
const user = useUser();
|
|
896
|
-
if (!user) return null;
|
|
897
|
-
return <div>{user.name}</div>;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Stable dispatch reference, never causes re-renders
|
|
901
|
-
function SignOutButton() {
|
|
902
|
-
const dispatch = useDispatch();
|
|
903
|
-
return <button onClick={() => dispatch({ type: 'CLEAR_USER' })}>Sign Out</button>;
|
|
904
|
-
}
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
| Hook | Returns | Re-renders on |
|
|
908
|
-
|------|---------|---------------|
|
|
909
|
-
| `getState()` | `{ state, dispatch }` | Any state change |
|
|
910
|
-
| `useUser()` | `user` or `null` | User changes only |
|
|
911
|
-
| `useDispatch()` | `dispatch` | Never (stable) |
|
|
912
|
-
|
|
913
|
-
### State Shape
|
|
914
|
-
|
|
915
|
-
```javascript
|
|
916
|
-
{
|
|
917
|
-
user: {
|
|
918
|
-
id: string,
|
|
919
|
-
email: string,
|
|
920
|
-
name: string,
|
|
921
|
-
subscription: {
|
|
922
|
-
status: 'active' | 'canceled' | null,
|
|
923
|
-
expires: number, // Unix timestamp (seconds)
|
|
924
|
-
stripeID: string
|
|
925
|
-
}
|
|
926
|
-
} | null,
|
|
927
|
-
|
|
928
|
-
ui: {
|
|
929
|
-
sidebarVisible: boolean,
|
|
930
|
-
tabBarVisible: boolean
|
|
931
|
-
},
|
|
932
|
-
|
|
933
|
-
authOverlay: {
|
|
934
|
-
visible: boolean,
|
|
935
|
-
pendingCallbacks: Function[] // queued 401 retries; run on sign-in, rejected on dismiss
|
|
936
|
-
},
|
|
937
|
-
|
|
938
|
-
constants: Object // App configuration
|
|
939
|
-
}
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
### Available Actions
|
|
294
|
+
| Hook | Re-renders on |
|
|
295
|
+
|------|---------------|
|
|
296
|
+
| `getState()` | Any state change |
|
|
297
|
+
| `useUser()` | User changes only |
|
|
298
|
+
| `useDispatch()` | Never (stable) |
|
|
943
299
|
|
|
944
300
|
| Action | Payload | Description |
|
|
945
301
|
|--------|---------|-------------|
|
|
946
302
|
| `SET_USER` | user object | Set authenticated user |
|
|
947
|
-
| `CLEAR_USER` | — | Clear user
|
|
303
|
+
| `CLEAR_USER` | — | Clear user |
|
|
948
304
|
| `SET_SIDEBAR_VISIBLE` | boolean | Show/hide sidebar |
|
|
949
305
|
| `SET_TABBAR_VISIBLE` | boolean | Show/hide tab bar |
|
|
950
|
-
| `
|
|
951
|
-
| `SHOW_AUTH_OVERLAY` | callback or null | Show auth dialog, optionally queue callback |
|
|
306
|
+
| `SHOW_AUTH_OVERLAY` | callback or null | Show auth dialog |
|
|
952
307
|
| `HIDE_AUTH_OVERLAY` | — | Hide auth dialog |
|
|
953
|
-
| `AUTH_OVERLAY_SUCCESS` | — |
|
|
954
|
-
|
|
955
|
-
### localStorage Keys
|
|
956
|
-
|
|
957
|
-
All keys are namespaced with `{appName}_`:
|
|
958
|
-
|
|
959
|
-
| Key | Description |
|
|
960
|
-
|-----|-------------|
|
|
961
|
-
| `{appName}_user` | Persisted user object |
|
|
962
|
-
| `{appName}_csrf` | CSRF token (fallback, primary is cookie) |
|
|
963
|
-
| `{appName}_beforeCheckoutURL` | Redirect URL after Stripe checkout |
|
|
964
|
-
| `{appName}_beforeManageURL` | Redirect URL after Stripe portal |
|
|
308
|
+
| `AUTH_OVERLAY_SUCCESS` | — | Run pending callback and close |
|
|
965
309
|
|
|
966
310
|
## Utilities
|
|
967
311
|
|
|
@@ -974,7 +318,6 @@ import {
|
|
|
974
318
|
isSubscriber,
|
|
975
319
|
getCSRFToken,
|
|
976
320
|
getBackendURL,
|
|
977
|
-
getAppKey,
|
|
978
321
|
getConstants,
|
|
979
322
|
getRemainingUsage,
|
|
980
323
|
trackUsage,
|
|
@@ -985,338 +328,38 @@ import {
|
|
|
985
328
|
hideSidebar,
|
|
986
329
|
showTabBar,
|
|
987
330
|
hideTabBar,
|
|
988
|
-
setSidebarVisible,
|
|
989
|
-
setTabBarVisible,
|
|
990
|
-
setUIVisibility,
|
|
991
331
|
timestampToString,
|
|
992
332
|
useListData,
|
|
993
333
|
useForm,
|
|
994
|
-
useAppSetup,
|
|
995
334
|
isAppMode,
|
|
996
|
-
validateConstants,
|
|
997
335
|
} from '@stevederico/skateboard-ui/Utilities';
|
|
998
336
|
```
|
|
999
337
|
|
|
1000
|
-
### API Requests
|
|
1001
|
-
|
|
1002
338
|
```javascript
|
|
1003
|
-
//
|
|
339
|
+
// API — auto-includes credentials, CSRF header, auth overlay on 401
|
|
1004
340
|
const data = await apiRequest('/deals');
|
|
341
|
+
await apiRequest('/deals', { method: 'POST', body: JSON.stringify({ name: 'New Deal' }) });
|
|
1005
342
|
|
|
1006
|
-
//
|
|
1007
|
-
const
|
|
1008
|
-
method: 'POST',
|
|
1009
|
-
body: JSON.stringify({ name: 'New Deal', amount: 5000 })
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
// GET with query params
|
|
1013
|
-
const filtered = await apiRequestWithParams('/deals', { status: 'active', limit: 10 });
|
|
1014
|
-
```
|
|
1015
|
-
|
|
1016
|
-
**Features:**
|
|
1017
|
-
- Auto-includes credentials (cookies)
|
|
1018
|
-
- Auto-adds `X-CSRF-Token` header for POST, PUT, DELETE, PATCH
|
|
1019
|
-
- On 401, shows the auth overlay and retries after sign-in (default); redirects to `/signout` only when `authOverlay: false`
|
|
1020
|
-
- Auto-retries once on CSRF 403 failure
|
|
1021
|
-
|
|
1022
|
-
### Auth Utilities
|
|
1023
|
-
|
|
1024
|
-
```javascript
|
|
1025
|
-
// Client-side check (fast, no network)
|
|
1026
|
-
if (isAuthenticated()) {
|
|
1027
|
-
const user = getCurrentUser();
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Server-side validation
|
|
1031
|
-
const user = await getCurrentUser(); // Calls GET /me
|
|
1032
|
-
|
|
1033
|
-
// Check subscription
|
|
1034
|
-
const subscribed = await isSubscriber(); // Calls GET /isSubscriber
|
|
1035
|
-
|
|
1036
|
-
// Get CSRF token (from cookie, falls back to localStorage)
|
|
1037
|
-
const token = getCSRFToken();
|
|
1038
|
-
|
|
1039
|
-
// Get backend URL (devBackendURL in dev, backendURL in production)
|
|
1040
|
-
const url = getBackendURL();
|
|
1041
|
-
|
|
1042
|
-
// Generate app-namespaced localStorage key
|
|
1043
|
-
const key = getAppKey('user'); // → "{appName}_user"
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
### Usage Tracking
|
|
1047
|
-
|
|
1048
|
-
```javascript
|
|
1049
|
-
// Check remaining usage for an action
|
|
1050
|
-
const usage = await getRemainingUsage('messages');
|
|
1051
|
-
// { remaining: 15, total: 20, isSubscriber: false }
|
|
1052
|
-
|
|
1053
|
-
// Track usage (decrements remaining)
|
|
1054
|
-
const updated = await trackUsage('messages');
|
|
1055
|
-
```
|
|
343
|
+
// Data fetching
|
|
344
|
+
const { data, loading, error, refetch } = useListData('/deals');
|
|
1056
345
|
|
|
1057
|
-
|
|
346
|
+
// Timestamps
|
|
347
|
+
timestampToString(1706000000, 'ago'); // "2 hours ago"
|
|
348
|
+
timestampToString(1706000000, 'DOB'); // "Jan 23, 2024"
|
|
1058
349
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
showCheckout('user@example.com', 0); // productIndex defaults to 0
|
|
1062
|
-
|
|
1063
|
-
// Open Stripe billing portal
|
|
350
|
+
// Stripe
|
|
351
|
+
showCheckout('user@example.com', 0);
|
|
1064
352
|
showManage('cus_abc123');
|
|
1065
|
-
|
|
1066
|
-
// Show upgrade sheet if not subscriber
|
|
1067
|
-
showUpgradeSheet(upgradeSheetRef);
|
|
1068
353
|
```
|
|
1069
354
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
```javascript
|
|
1073
|
-
import { useListData } from '@stevederico/skateboard-ui/Utilities';
|
|
1074
|
-
|
|
1075
|
-
function DealsList() {
|
|
1076
|
-
const { data, loading, error, refetch } = useListData('/deals');
|
|
1077
|
-
|
|
1078
|
-
if (loading) return <div>Loading...</div>;
|
|
1079
|
-
if (error) return <div>Error: {error}</div>;
|
|
1080
|
-
|
|
1081
|
-
return data.map(deal => <DealCard key={deal.id} {...deal} />);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// With custom sort
|
|
1085
|
-
const { data } = useListData('/deals', (a, b) => b.amount - a.amount);
|
|
1086
|
-
```
|
|
1087
|
-
|
|
1088
|
-
### Form Hook
|
|
1089
|
-
|
|
1090
|
-
```javascript
|
|
1091
|
-
import { useForm } from '@stevederico/skateboard-ui/Utilities';
|
|
1092
|
-
|
|
1093
|
-
function ContactForm() {
|
|
1094
|
-
const { values, handleChange, handleSubmit, reset, submitting, error } = useForm(
|
|
1095
|
-
{ name: '', email: '', message: '' },
|
|
1096
|
-
async (formValues) => {
|
|
1097
|
-
await apiRequest('/contact', {
|
|
1098
|
-
method: 'POST',
|
|
1099
|
-
body: JSON.stringify(formValues)
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
);
|
|
1103
|
-
|
|
1104
|
-
return (
|
|
1105
|
-
<form onSubmit={handleSubmit}>
|
|
1106
|
-
<input name="name" value={values.name} onChange={handleChange} />
|
|
1107
|
-
<input name="email" value={values.email} onChange={handleChange} />
|
|
1108
|
-
<textarea name="message" value={values.message} onChange={handleChange} />
|
|
1109
|
-
<button type="submit" disabled={submitting}>Send</button>
|
|
1110
|
-
{error && <p>{error}</p>}
|
|
1111
|
-
</form>
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
```
|
|
355
|
+
## Peer dependencies
|
|
1115
356
|
|
|
1116
|
-
### Timestamp Formatting
|
|
1117
|
-
|
|
1118
|
-
```javascript
|
|
1119
|
-
import { timestampToString } from '@stevederico/skateboard-ui/Utilities';
|
|
1120
|
-
|
|
1121
|
-
timestampToString(1706000000, 'ago'); // "2 hours ago"
|
|
1122
|
-
timestampToString(1706000000, 'DOB'); // "Jan 23, 2024"
|
|
1123
|
-
timestampToString(1706000000, 'DOBT'); // "Jan 23, 2024 3:00 PM"
|
|
1124
|
-
timestampToString(1706000000, 'ISO'); // "2024-01-23"
|
|
1125
|
-
timestampToString(1706000000, 'day-month-time');// "23 Jan 3:00 PM"
|
|
1126
|
-
timestampToString(1706000000, 'day'); // "Monday"
|
|
1127
|
-
timestampToString(1706000000, 'time'); // "3:00 PM"
|
|
1128
|
-
timestampToString(1706000000, 'full'); // "Monday, Jan 23, 2024 3:00 PM"
|
|
1129
|
-
```
|
|
1130
|
-
|
|
1131
|
-
### UI Visibility Control
|
|
1132
|
-
|
|
1133
|
-
```javascript
|
|
1134
|
-
// Programmatic control
|
|
1135
|
-
hideSidebar();
|
|
1136
|
-
showSidebar();
|
|
1137
|
-
hideTabBar();
|
|
1138
|
-
showTabBar();
|
|
1139
|
-
|
|
1140
|
-
// Set directly
|
|
1141
|
-
setSidebarVisible(false);
|
|
1142
|
-
setTabBarVisible(true);
|
|
1143
|
-
|
|
1144
|
-
// Batch control
|
|
1145
|
-
setUIVisibility({ sidebar: false, tabBar: false });
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
### Other Utilities
|
|
1149
|
-
|
|
1150
|
-
```javascript
|
|
1151
|
-
// Check if running inside native WebKit wrapper (iOS/macOS app)
|
|
1152
|
-
if (isAppMode()) { /* native context */ }
|
|
1153
|
-
|
|
1154
|
-
// Validate constants object (called internally by createSkateboardApp)
|
|
1155
|
-
validateConstants(constants);
|
|
1156
|
-
|
|
1157
|
-
// Get constants object
|
|
1158
|
-
const constants = getConstants();
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
## Lazy Authentication (Auth Overlay)
|
|
1162
|
-
|
|
1163
|
-
Let users explore `/app` without signing in — prompt them only when they perform a protected action. This is the **default** behavior.
|
|
1164
|
-
|
|
1165
|
-
### Setup
|
|
1166
|
-
|
|
1167
|
-
Lazy auth is on by default — no config needed. The `AuthOverlay` component is rendered automatically by `createSkateboardApp` and unauthenticated visitors can reach `/app` routes.
|
|
1168
|
-
|
|
1169
|
-
To opt out and require eager sign-in instead, set `authOverlay: false` in your constants. `ProtectedRoute` then validates the session up front and redirects unauthenticated users to `/signin`:
|
|
1170
|
-
|
|
1171
|
-
```json
|
|
1172
|
-
{
|
|
1173
|
-
"authOverlay": false
|
|
1174
|
-
}
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
|
-
### Usage
|
|
1178
|
-
|
|
1179
|
-
```javascript
|
|
1180
|
-
import { useAuthGate } from '@stevederico/skateboard-ui/useAuthGate';
|
|
1181
|
-
|
|
1182
|
-
function SaveButton() {
|
|
1183
|
-
const requireAuth = useAuthGate();
|
|
1184
|
-
|
|
1185
|
-
function handleSave() {
|
|
1186
|
-
requireAuth(() => {
|
|
1187
|
-
// Only runs if user is authenticated
|
|
1188
|
-
// If not signed in, auth overlay appears first
|
|
1189
|
-
saveThing();
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
return <button onClick={handleSave}>Save</button>;
|
|
1194
|
-
}
|
|
1195
|
-
```
|
|
1196
|
-
|
|
1197
|
-
### How It Works
|
|
1198
|
-
|
|
1199
|
-
1. User clicks a protected action (Save, Like, Post, etc.)
|
|
1200
|
-
2. `requireAuth()` checks if user is signed in
|
|
1201
|
-
3. If signed in — callback runs immediately
|
|
1202
|
-
4. If not — a modal dialog appears with sign-in/sign-up forms
|
|
1203
|
-
5. After successful auth, the original callback executes automatically
|
|
1204
|
-
6. User stays on the same page throughout — no navigation
|
|
1205
|
-
|
|
1206
|
-
The dialog supports toggling between sign-in and sign-up modes inline, and can be dismissed with the X button (cancels the action).
|
|
1207
|
-
|
|
1208
|
-
## Toast Notifications
|
|
1209
|
-
|
|
1210
|
-
Removed in v3.0. The Sonner dependency is gone — surface short-lived feedback with `Dialog` or `Alert` from `shadcn/ui` instead.
|
|
1211
|
-
|
|
1212
|
-
## Styling
|
|
1213
|
-
|
|
1214
|
-
Import base theme and override as needed:
|
|
1215
|
-
|
|
1216
|
-
```css
|
|
1217
|
-
/* styles.css */
|
|
1218
|
-
@import "@stevederico/skateboard-ui/styles.css";
|
|
1219
|
-
|
|
1220
|
-
@source '../../node_modules/@stevederico/skateboard-ui';
|
|
1221
|
-
|
|
1222
|
-
@theme {
|
|
1223
|
-
--color-app: var(--color-purple-500);
|
|
1224
|
-
}
|
|
1225
|
-
```
|
|
1226
|
-
|
|
1227
|
-
### Theme Variables
|
|
1228
|
-
|
|
1229
|
-
| Variable | Description |
|
|
1230
|
-
|----------|-------------|
|
|
1231
|
-
| `--color-app` | Primary brand color (used for app icon backgrounds, gradient buttons) |
|
|
1232
|
-
| `--background` | Page background |
|
|
1233
|
-
| `--foreground` | Text color |
|
|
1234
|
-
| `--accent` | Secondary backgrounds |
|
|
1235
|
-
| `--radius` | Border radius |
|
|
1236
|
-
|
|
1237
|
-
Dark mode is automatic via CSS custom properties and the in-house `ThemeProvider` (`components/core/ThemeProvider.tsx`).
|
|
1238
|
-
|
|
1239
|
-
## Components
|
|
1240
|
-
|
|
1241
|
-
47 self-contained components available at `@stevederico/skateboard-ui/ui/*`. The legacy `@stevederico/skateboard-ui/ui/*` paths still resolve to the same components (re-export shims), so existing imports keep working.
|
|
1242
|
-
|
|
1243
|
-
Components follow the shadcn API. `asChild` renders the styling onto a single child element; the old base-ui `render={<El/>}` / `nativeButton` props are still accepted for backward compatibility.
|
|
1244
|
-
|
|
1245
|
-
```javascript
|
|
1246
|
-
import { Button } from '@stevederico/skateboard-ui/ui/button';
|
|
1247
|
-
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardAction } from '@stevederico/skateboard-ui/ui/card';
|
|
1248
|
-
import { Input } from '@stevederico/skateboard-ui/ui/input';
|
|
1249
|
-
import { Label } from '@stevederico/skateboard-ui/ui/label';
|
|
1250
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@stevederico/skateboard-ui/ui/dialog';
|
|
1251
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@stevederico/skateboard-ui/ui/avatar';
|
|
1252
|
-
import { Badge } from '@stevederico/skateboard-ui/ui/badge';
|
|
1253
|
-
import { Separator } from '@stevederico/skateboard-ui/ui/separator';
|
|
1254
|
-
import { ScrollArea } from '@stevederico/skateboard-ui/ui/scroll-area';
|
|
1255
|
-
import { Skeleton } from '@stevederico/skateboard-ui/ui/skeleton';
|
|
1256
|
-
import { Alert, AlertDescription, AlertTitle } from '@stevederico/skateboard-ui/ui/alert';
|
|
1257
|
-
import { Progress } from '@stevederico/skateboard-ui/ui/progress';
|
|
1258
|
-
import { Switch } from '@stevederico/skateboard-ui/ui/switch';
|
|
1259
|
-
import { Checkbox } from '@stevederico/skateboard-ui/ui/checkbox';
|
|
1260
|
-
import { Textarea } from '@stevederico/skateboard-ui/ui/textarea';
|
|
1261
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from '@stevederico/skateboard-ui/ui/tooltip';
|
|
1262
|
-
```
|
|
1263
|
-
|
|
1264
|
-
```javascript
|
|
1265
|
-
import {
|
|
1266
|
-
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
1267
|
-
} from '@stevederico/skateboard-ui/ui/select';
|
|
1268
|
-
|
|
1269
|
-
import {
|
|
1270
|
-
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
|
1271
|
-
DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
|
1272
|
-
} from '@stevederico/skateboard-ui/ui/dropdown-menu';
|
|
1273
|
-
|
|
1274
|
-
import {
|
|
1275
|
-
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|
1276
|
-
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,
|
|
1277
|
-
AlertDialogTitle, AlertDialogTrigger,
|
|
1278
|
-
} from '@stevederico/skateboard-ui/ui/alert-dialog';
|
|
1279
|
-
|
|
1280
|
-
import {
|
|
1281
|
-
Accordion, AccordionContent, AccordionItem, AccordionTrigger,
|
|
1282
|
-
} from '@stevederico/skateboard-ui/ui/accordion';
|
|
1283
|
-
|
|
1284
|
-
import {
|
|
1285
|
-
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
1286
|
-
} from '@stevederico/skateboard-ui/ui/table';
|
|
1287
|
-
|
|
1288
|
-
import {
|
|
1289
|
-
Tabs, TabsContent, TabsList, TabsTrigger,
|
|
1290
|
-
} from '@stevederico/skateboard-ui/ui/tabs';
|
|
1291
|
-
```
|
|
1292
|
-
|
|
1293
|
-
### Utilities
|
|
1294
|
-
|
|
1295
|
-
```javascript
|
|
1296
|
-
// Tailwind className merger
|
|
1297
|
-
import { cn } from '@stevederico/skateboard-ui/shadcn/lib/utils';
|
|
1298
|
-
|
|
1299
|
-
cn('px-2 py-1', condition && 'bg-red-500', 'px-4'); // Merges without conflicts
|
|
1300
|
-
|
|
1301
|
-
// Mobile detection hook (< 768px)
|
|
1302
|
-
import { useIsMobile } from '@stevederico/skateboard-ui/shadcn/hooks/use-mobile';
|
|
1303
|
-
|
|
1304
|
-
const isMobile = useIsMobile();
|
|
1305
|
-
```
|
|
1306
|
-
|
|
1307
|
-
All components support dark mode automatically and accept a `className` prop for customization.
|
|
1308
|
-
|
|
1309
|
-
## Dependencies
|
|
1310
|
-
|
|
1311
|
-
### Required peer dependencies
|
|
1312
357
|
- React 19.1+
|
|
1313
358
|
- react-dom 19.1+
|
|
1314
359
|
- react-router 7.0+
|
|
1315
360
|
|
|
1316
|
-
### Hard dependencies
|
|
1317
|
-
|
|
1318
|
-
None. As of v4.0 the components are self-contained — `@base-ui/react` is no longer vendored or required. See the dep-count table near the top of this README for the full history.
|
|
1319
|
-
|
|
1320
361
|
## Repository
|
|
1321
362
|
|
|
1322
363
|
https://github.com/stevederico/skateboard-ui
|
|
364
|
+
|
|
365
|
+
Version history and migration notes: [CHANGELOG.md](./CHANGELOG.md)
|