create-starbase 5.0.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/README.md +94 -0
- package/dist/index.d.ts +2 -0
- package/package.json +54 -0
- package/template/.claude/commands/audit.md +14 -0
- package/template/.claude/commands/review.md +23 -0
- package/template/.editorconfig +14 -0
- package/template/.env +1 -0
- package/template/.env.example +1 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +12 -0
- package/template/.prettierrc.json +7 -0
- package/template/.vscode/extensions.json +8 -0
- package/template/.vscode/settings.json +48 -0
- package/template/CLAUDE.md +109 -0
- package/template/README.md +46 -0
- package/template/eslint.config.js +27 -0
- package/template/index.html +47 -0
- package/template/package.json +51 -0
- package/template/src/lib/queries/github.ts +13 -0
- package/template/src/lib/queries/index.ts +1 -0
- package/template/src/lib/theme/app.css +3 -0
- package/template/src/lib/theme/base.css +37 -0
- package/template/src/lib/theme/tailwind.css +167 -0
- package/template/src/lib/utils/cn.ts +16 -0
- package/template/src/lib/utils/darkMode.ts +44 -0
- package/template/src/lib/utils/index.ts +2 -0
- package/template/src/main.tsx +38 -0
- package/template/src/routeTree.gen.ts +77 -0
- package/template/src/routes/__root.tsx +49 -0
- package/template/src/routes/index.tsx +29 -0
- package/template/src/routes/liftoff.tsx +22 -0
- package/template/src/ui/atoms/Button.tsx +106 -0
- package/template/src/ui/atoms/Code.tsx +52 -0
- package/template/src/ui/atoms/Link.tsx +78 -0
- package/template/src/ui/atoms/StarbaseLogo.tsx +19 -0
- package/template/src/ui/molecules/DarkModeToggle.tsx +29 -0
- package/template/src/ui/molecules/PageHeader.tsx +35 -0
- package/template/src/ui/molecules/Stargazers.tsx +24 -0
- package/template/src/ui/organisms/.gitkeep +0 -0
- package/template/src/ui/templates/.gitkeep +0 -0
- package/template/tsconfig.app.json +39 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +27 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Theme Prefix: "sb-" (starbase)
|
|
3
|
+
To customize for your project, find/replace "sb-" with your own prefix.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
@import 'tailwindcss';
|
|
7
|
+
|
|
8
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
9
|
+
@custom-variant is-active (&:not(:disabled):hover, &:not(:disabled):focus-visible);
|
|
10
|
+
|
|
11
|
+
@theme {
|
|
12
|
+
/* Fonts */
|
|
13
|
+
--font-sans: 'Nunito', sans-serif;
|
|
14
|
+
--font-display: 'Bricolage Grotesque', serif;
|
|
15
|
+
--font-mono: 'Roboto Mono', monospace;
|
|
16
|
+
|
|
17
|
+
/* Text Variant: h1 - Bold display headings */
|
|
18
|
+
/* 22px → 30px */
|
|
19
|
+
--text-h1: clamp(1.375rem, 1.125rem + 0.75vw, 1.875rem);
|
|
20
|
+
--text-h1--font-weight: 600;
|
|
21
|
+
--text-h1--line-height: 1.15;
|
|
22
|
+
--text-h1--letter-spacing: -0.01em;
|
|
23
|
+
|
|
24
|
+
/* Text Variant: h2 */
|
|
25
|
+
/* 20px → 24px */
|
|
26
|
+
--text-h2: clamp(1.25rem, 1.1rem + 0.45vw, 1.5rem);
|
|
27
|
+
--text-h2--font-weight: 600;
|
|
28
|
+
--text-h2--line-height: 1.2;
|
|
29
|
+
--text-h2--letter-spacing: -0.005em;
|
|
30
|
+
|
|
31
|
+
/* Text Variant: h3 */
|
|
32
|
+
/* 18px → 22px */
|
|
33
|
+
--text-h3: clamp(1.125rem, 1rem + 0.4vw, 1.375rem);
|
|
34
|
+
--text-h3--font-weight: 600;
|
|
35
|
+
--text-h3--line-height: 1.25;
|
|
36
|
+
--text-h3--letter-spacing: -0.005em;
|
|
37
|
+
|
|
38
|
+
/* Text Variant: h4 */
|
|
39
|
+
/* 17px → 20px */
|
|
40
|
+
--text-h4: clamp(1.0625rem, 0.975rem + 0.275vw, 1.25rem);
|
|
41
|
+
--text-h4--font-weight: 600;
|
|
42
|
+
--text-h4--line-height: 1.3;
|
|
43
|
+
--text-h4--letter-spacing: 0;
|
|
44
|
+
|
|
45
|
+
/* Text Variant: h5 */
|
|
46
|
+
/* 16px → 18px */
|
|
47
|
+
--text-h5: clamp(1rem, 0.95rem + 0.15vw, 1.125rem);
|
|
48
|
+
--text-h5--font-weight: 500;
|
|
49
|
+
--text-h5--line-height: 1.35;
|
|
50
|
+
--text-h5--letter-spacing: 0;
|
|
51
|
+
|
|
52
|
+
/* Text Variant: base - 16px to 17px */
|
|
53
|
+
--text-base: clamp(1rem, 0.974rem + 0.098vw, 1.063rem);
|
|
54
|
+
--text-base--font-weight: 400;
|
|
55
|
+
--text-base--line-height: 1.55;
|
|
56
|
+
--text-base--letter-spacing: 0.005em;
|
|
57
|
+
|
|
58
|
+
/* Text Variant: sm - 15px to 16px */
|
|
59
|
+
--text-sm: clamp(0.9375rem, 0.912rem + 0.098vw, 1rem);
|
|
60
|
+
--text-sm--font-weight: 400;
|
|
61
|
+
--text-sm--line-height: 1.5;
|
|
62
|
+
--text-sm--letter-spacing: 0.005em;
|
|
63
|
+
|
|
64
|
+
/* Text Variant: xs - 14px to 15px */
|
|
65
|
+
--text-xs: clamp(0.875rem, 0.849rem + 0.098vw, 0.9375rem);
|
|
66
|
+
--text-xs--font-weight: 400;
|
|
67
|
+
--text-xs--line-height: 1.45;
|
|
68
|
+
--text-xs--letter-spacing: 0.01em;
|
|
69
|
+
|
|
70
|
+
/* Spacing (padding, margins, height, width, etc) */
|
|
71
|
+
--spacing-wrapper-page-full-x: clamp(0.75rem, 0.25rem + 1.5vw, 1rem);
|
|
72
|
+
|
|
73
|
+
/* List Style Types */
|
|
74
|
+
--list-style-type-circle: circle;
|
|
75
|
+
--list-style-type-roman: lower-roman;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
Theme Color Semantics
|
|
80
|
+
Mapping primitives to functional roles.
|
|
81
|
+
*/
|
|
82
|
+
@layer base {
|
|
83
|
+
:root {
|
|
84
|
+
/* Light Mode Mappings */
|
|
85
|
+
|
|
86
|
+
/* Surfaces */
|
|
87
|
+
--sb-surface: var(--color-stone-100);
|
|
88
|
+
--sb-surface-raised: var(--color-stone-50);
|
|
89
|
+
--sb-canvas: var(--color-stone-200);
|
|
90
|
+
|
|
91
|
+
/* Text */
|
|
92
|
+
--sb-fg: var(--color-stone-700);
|
|
93
|
+
--sb-fg-subtle: var(--color-stone-600);
|
|
94
|
+
--sb-fg-title: var(--color-stone-800);
|
|
95
|
+
|
|
96
|
+
/* Borders */
|
|
97
|
+
--sb-divider: var(--color-stone-300);
|
|
98
|
+
|
|
99
|
+
/* Action */
|
|
100
|
+
--sb-action: var(--color-red-700);
|
|
101
|
+
--sb-action-active: var(--color-red-800);
|
|
102
|
+
|
|
103
|
+
/* Navigation */
|
|
104
|
+
--sb-anchor: var(--color-red-700);
|
|
105
|
+
--sb-anchor-active: var(--color-red-800);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
::selection {
|
|
109
|
+
background-color: var(--color-stone-300);
|
|
110
|
+
color: var(--color-stone-900);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.dark {
|
|
114
|
+
/* Dark Mode Mappings */
|
|
115
|
+
|
|
116
|
+
/* Surfaces */
|
|
117
|
+
--sb-surface: var(--color-zinc-900);
|
|
118
|
+
--sb-surface-raised: var(--color-zinc-800);
|
|
119
|
+
--sb-canvas: var(--color-zinc-950);
|
|
120
|
+
|
|
121
|
+
/* Text */
|
|
122
|
+
--sb-fg: var(--color-zinc-300);
|
|
123
|
+
--sb-fg-subtle: var(--color-zinc-400);
|
|
124
|
+
--sb-fg-title: var(--color-zinc-200);
|
|
125
|
+
|
|
126
|
+
/* Borders */
|
|
127
|
+
--sb-divider: var(--color-zinc-800);
|
|
128
|
+
|
|
129
|
+
/* Action */
|
|
130
|
+
--sb-action: var(--color-red-500);
|
|
131
|
+
--sb-action-active: var(--color-red-400);
|
|
132
|
+
|
|
133
|
+
/* Navigation */
|
|
134
|
+
--sb-anchor: var(--color-red-400);
|
|
135
|
+
--sb-anchor-active: var(--color-red-300);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.dark ::selection {
|
|
139
|
+
background-color: var(--color-zinc-700);
|
|
140
|
+
color: var(--color-zinc-100);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
Color Tokens
|
|
146
|
+
Exposing semantic variables as Tailwind utilities.
|
|
147
|
+
*/
|
|
148
|
+
@theme {
|
|
149
|
+
/* Backgrounds */
|
|
150
|
+
--color-sb-surface: var(--sb-surface);
|
|
151
|
+
--color-sb-surface-raised: var(--sb-surface-raised);
|
|
152
|
+
--color-sb-canvas: var(--sb-canvas);
|
|
153
|
+
|
|
154
|
+
/* Text / Foreground */
|
|
155
|
+
--color-sb-fg: var(--sb-fg);
|
|
156
|
+
--color-sb-fg-subtle: var(--sb-fg-subtle);
|
|
157
|
+
--color-sb-fg-title: var(--sb-fg-title);
|
|
158
|
+
|
|
159
|
+
/* Borders */
|
|
160
|
+
--color-sb-divider: var(--sb-divider);
|
|
161
|
+
|
|
162
|
+
/* UI Elements */
|
|
163
|
+
--color-sb-action: var(--sb-action);
|
|
164
|
+
--color-sb-action-active: var(--sb-action-active);
|
|
165
|
+
--color-sb-anchor: var(--sb-anchor);
|
|
166
|
+
--color-sb-anchor-active: var(--sb-anchor-active);
|
|
167
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
const twMerge = extendTailwindMerge({
|
|
5
|
+
extend: {
|
|
6
|
+
theme: {
|
|
7
|
+
spacing: ['wrapper-page-full-x'],
|
|
8
|
+
text: ['h1', 'h2', 'h3', 'h4', 'h5', 'base', 'sm', 'xs'],
|
|
9
|
+
},
|
|
10
|
+
classGroups: {
|
|
11
|
+
'list-style-type': [{ list: ['circle', 'roman'] }],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
|
|
3
|
+
const DARK_MODE_COOKIE = 'theme-preference';
|
|
4
|
+
const DARK_CLASS = 'dark';
|
|
5
|
+
|
|
6
|
+
export type ThemePreference = 'light' | 'dark' | 'system';
|
|
7
|
+
|
|
8
|
+
export function getThemePreference(): ThemePreference {
|
|
9
|
+
const value = Cookies.get(DARK_MODE_COOKIE);
|
|
10
|
+
|
|
11
|
+
if (value === 'light' || value === 'dark') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return 'system';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setThemePreference(preference: ThemePreference): void {
|
|
19
|
+
Cookies.set(DARK_MODE_COOKIE, preference, {
|
|
20
|
+
expires: 365,
|
|
21
|
+
sameSite: 'lax',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getEffectiveTheme(
|
|
26
|
+
preference: ThemePreference,
|
|
27
|
+
): 'light' | 'dark' {
|
|
28
|
+
if (preference === 'system') {
|
|
29
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
30
|
+
? 'dark'
|
|
31
|
+
: 'light';
|
|
32
|
+
}
|
|
33
|
+
return preference;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyTheme(theme: 'light' | 'dark'): void {
|
|
37
|
+
if (theme === 'dark') {
|
|
38
|
+
document.documentElement.classList.add(DARK_CLASS);
|
|
39
|
+
document.documentElement.style.backgroundColor = 'var(--sb-canvas)';
|
|
40
|
+
} else {
|
|
41
|
+
document.documentElement.classList.remove(DARK_CLASS);
|
|
42
|
+
document.documentElement.style.backgroundColor = 'var(--sb-surface)';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
|
5
|
+
|
|
6
|
+
import '@fontsource/bricolage-grotesque/400.css';
|
|
7
|
+
import '@fontsource/bricolage-grotesque/500.css';
|
|
8
|
+
import '@fontsource/bricolage-grotesque/600.css';
|
|
9
|
+
import '@fontsource/nunito/400.css';
|
|
10
|
+
import '@fontsource/nunito/500.css';
|
|
11
|
+
import '@fontsource/nunito/600.css';
|
|
12
|
+
import '@fontsource/roboto-mono/400.css';
|
|
13
|
+
|
|
14
|
+
import './lib/theme/app.css';
|
|
15
|
+
|
|
16
|
+
import { routeTree } from './routeTree.gen';
|
|
17
|
+
|
|
18
|
+
const queryClient = new QueryClient();
|
|
19
|
+
|
|
20
|
+
const router = createRouter({
|
|
21
|
+
routeTree,
|
|
22
|
+
defaultPreload: 'intent',
|
|
23
|
+
context: { queryClient },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
declare module '@tanstack/react-router' {
|
|
27
|
+
interface Register {
|
|
28
|
+
router: typeof router;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
createRoot(document.getElementById('root')!).render(
|
|
33
|
+
<StrictMode>
|
|
34
|
+
<QueryClientProvider client={queryClient}>
|
|
35
|
+
<RouterProvider router={router} />
|
|
36
|
+
</QueryClientProvider>
|
|
37
|
+
</StrictMode>,
|
|
38
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as LiftoffRouteImport } from './routes/liftoff'
|
|
13
|
+
import { Route as IndexRouteImport } from './routes/index'
|
|
14
|
+
|
|
15
|
+
const LiftoffRoute = LiftoffRouteImport.update({
|
|
16
|
+
id: '/liftoff',
|
|
17
|
+
path: '/liftoff',
|
|
18
|
+
getParentRoute: () => rootRouteImport,
|
|
19
|
+
} as any)
|
|
20
|
+
const IndexRoute = IndexRouteImport.update({
|
|
21
|
+
id: '/',
|
|
22
|
+
path: '/',
|
|
23
|
+
getParentRoute: () => rootRouteImport,
|
|
24
|
+
} as any)
|
|
25
|
+
|
|
26
|
+
export interface FileRoutesByFullPath {
|
|
27
|
+
'/': typeof IndexRoute
|
|
28
|
+
'/liftoff': typeof LiftoffRoute
|
|
29
|
+
}
|
|
30
|
+
export interface FileRoutesByTo {
|
|
31
|
+
'/': typeof IndexRoute
|
|
32
|
+
'/liftoff': typeof LiftoffRoute
|
|
33
|
+
}
|
|
34
|
+
export interface FileRoutesById {
|
|
35
|
+
__root__: typeof rootRouteImport
|
|
36
|
+
'/': typeof IndexRoute
|
|
37
|
+
'/liftoff': typeof LiftoffRoute
|
|
38
|
+
}
|
|
39
|
+
export interface FileRouteTypes {
|
|
40
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
41
|
+
fullPaths: '/' | '/liftoff'
|
|
42
|
+
fileRoutesByTo: FileRoutesByTo
|
|
43
|
+
to: '/' | '/liftoff'
|
|
44
|
+
id: '__root__' | '/' | '/liftoff'
|
|
45
|
+
fileRoutesById: FileRoutesById
|
|
46
|
+
}
|
|
47
|
+
export interface RootRouteChildren {
|
|
48
|
+
IndexRoute: typeof IndexRoute
|
|
49
|
+
LiftoffRoute: typeof LiftoffRoute
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare module '@tanstack/react-router' {
|
|
53
|
+
interface FileRoutesByPath {
|
|
54
|
+
'/liftoff': {
|
|
55
|
+
id: '/liftoff'
|
|
56
|
+
path: '/liftoff'
|
|
57
|
+
fullPath: '/liftoff'
|
|
58
|
+
preLoaderRoute: typeof LiftoffRouteImport
|
|
59
|
+
parentRoute: typeof rootRouteImport
|
|
60
|
+
}
|
|
61
|
+
'/': {
|
|
62
|
+
id: '/'
|
|
63
|
+
path: '/'
|
|
64
|
+
fullPath: '/'
|
|
65
|
+
preLoaderRoute: typeof IndexRouteImport
|
|
66
|
+
parentRoute: typeof rootRouteImport
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
72
|
+
IndexRoute: IndexRoute,
|
|
73
|
+
LiftoffRoute: LiftoffRoute,
|
|
74
|
+
}
|
|
75
|
+
export const routeTree = rootRouteImport
|
|
76
|
+
._addFileChildren(rootRouteChildren)
|
|
77
|
+
._addFileTypes<FileRouteTypes>()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
import {
|
|
4
|
+
HeadContent,
|
|
5
|
+
createRootRouteWithContext,
|
|
6
|
+
Outlet,
|
|
7
|
+
} from '@tanstack/react-router';
|
|
8
|
+
import { DarkModeToggle } from 'molecules/DarkModeToggle';
|
|
9
|
+
import { Stargazers } from 'molecules/Stargazers';
|
|
10
|
+
|
|
11
|
+
export interface RouterContext {
|
|
12
|
+
queryClient: QueryClient;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const devtools =
|
|
16
|
+
!import.meta.env.PROD && import.meta.env.VITE_DEVTOOLS === 'true';
|
|
17
|
+
|
|
18
|
+
const TanStackRouterDevtools = devtools
|
|
19
|
+
? React.lazy(() =>
|
|
20
|
+
import('@tanstack/react-router-devtools').then((res) => ({
|
|
21
|
+
default: res.TanStackRouterDevtools,
|
|
22
|
+
})),
|
|
23
|
+
)
|
|
24
|
+
: () => null;
|
|
25
|
+
|
|
26
|
+
const ReactQueryDevtools = devtools
|
|
27
|
+
? React.lazy(() =>
|
|
28
|
+
import('@tanstack/react-query-devtools').then((res) => ({
|
|
29
|
+
default: res.ReactQueryDevtools,
|
|
30
|
+
})),
|
|
31
|
+
)
|
|
32
|
+
: () => null;
|
|
33
|
+
|
|
34
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
35
|
+
component: () => (
|
|
36
|
+
<>
|
|
37
|
+
<HeadContent />
|
|
38
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-6 p-8">
|
|
39
|
+
<Outlet />
|
|
40
|
+
<footer className="flex items-center gap-3">
|
|
41
|
+
<Stargazers />
|
|
42
|
+
<DarkModeToggle />
|
|
43
|
+
</footer>
|
|
44
|
+
</main>
|
|
45
|
+
<TanStackRouterDevtools />
|
|
46
|
+
<ReactQueryDevtools />
|
|
47
|
+
</>
|
|
48
|
+
),
|
|
49
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
import { Code } from 'atoms/Code';
|
|
3
|
+
import { RouterLink } from 'atoms/Link';
|
|
4
|
+
import { PageHeader } from 'molecules/PageHeader';
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/')({
|
|
7
|
+
component: Index,
|
|
8
|
+
head: () => ({
|
|
9
|
+
meta: [{ title: 'Starbase' }],
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function Index() {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-col items-center gap-6">
|
|
16
|
+
<PageHeader title="Starbase" />
|
|
17
|
+
<p className="text-sb-fg-subtle max-w-md text-center text-balance">
|
|
18
|
+
A launchpad for modern React apps, built on Vite, TypeScript, Tailwind
|
|
19
|
+
CSS, TanStack Router, and TanStack Query. Start your mission today:
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<Code>npm create starbase@latest</Code>
|
|
23
|
+
|
|
24
|
+
<RouterLink to="/liftoff" className="text-sm">
|
|
25
|
+
Ready for liftoff?
|
|
26
|
+
</RouterLink>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
import { RouterLink } from 'atoms/Link';
|
|
3
|
+
import { PageHeader } from 'molecules/PageHeader';
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/liftoff')({
|
|
6
|
+
component: Liftoff,
|
|
7
|
+
head: () => ({
|
|
8
|
+
meta: [{ title: 'Liftoff — Starbase' }],
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function Liftoff() {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex flex-col items-center gap-6">
|
|
15
|
+
<PageHeader title="Liftoff" />
|
|
16
|
+
<p className="text-sb-fg-subtle">More details coming soon.</p>
|
|
17
|
+
<RouterLink to="/" className="text-sm">
|
|
18
|
+
Back to home
|
|
19
|
+
</RouterLink>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createLink, type LinkComponent } from '@tanstack/react-router';
|
|
3
|
+
import { cn } from 'utils';
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant = 'anchor' | 'outline' | 'ghost';
|
|
6
|
+
|
|
7
|
+
type ButtonSize = 'sm' | 'md' | 'lg';
|
|
8
|
+
|
|
9
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
10
|
+
variant?: ButtonVariant;
|
|
11
|
+
iconOnly?: boolean;
|
|
12
|
+
size?: ButtonSize;
|
|
13
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ButtonLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
17
|
+
variant?: ButtonVariant;
|
|
18
|
+
iconOnly?: boolean;
|
|
19
|
+
size?: ButtonSize;
|
|
20
|
+
ref?: React.Ref<HTMLAnchorElement>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const makeButtonClasses = (
|
|
24
|
+
variant?: ButtonVariant,
|
|
25
|
+
iconOnly?: boolean,
|
|
26
|
+
size: ButtonSize = 'md',
|
|
27
|
+
className?: string,
|
|
28
|
+
) =>
|
|
29
|
+
cn(
|
|
30
|
+
// Base styles
|
|
31
|
+
'inline-flex items-center justify-center',
|
|
32
|
+
'font-sans font-semibold rounded-md border border-transparent outline-none focus-visible:outline-sb-action cursor-pointer',
|
|
33
|
+
'transition-all duration-150 ease-out',
|
|
34
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none',
|
|
35
|
+
{
|
|
36
|
+
/* iconOnly sizes (square buttons) */
|
|
37
|
+
'shrink-0 p-0 size-8': iconOnly && size === 'sm',
|
|
38
|
+
'shrink-0 p-0 size-9': iconOnly && size === 'md',
|
|
39
|
+
'shrink-0 p-0 size-11 rounded-lg': iconOnly && size === 'lg',
|
|
40
|
+
|
|
41
|
+
/* text button sizes */
|
|
42
|
+
'text-sm px-3 py-1.5': !iconOnly && size === 'sm',
|
|
43
|
+
'text-base px-4 py-2': !iconOnly && size === 'md',
|
|
44
|
+
'text-lg px-5 py-2.5': !iconOnly && size === 'lg',
|
|
45
|
+
|
|
46
|
+
/* Variant: anchor */
|
|
47
|
+
'bg-sb-anchor border-sb-anchor text-sb-surface-raised shadow-sm is-active:bg-sb-anchor-active is-active:border-sb-anchor-active is-active:shadow-md':
|
|
48
|
+
variant === 'anchor',
|
|
49
|
+
|
|
50
|
+
/* Variant: outline */
|
|
51
|
+
'bg-transparent border-sb-divider text-sb-fg is-active:bg-sb-canvas is-active:border-sb-fg-subtle/30':
|
|
52
|
+
variant === 'outline',
|
|
53
|
+
|
|
54
|
+
/* Variant: ghost */
|
|
55
|
+
'bg-transparent text-sb-fg is-active:bg-sb-fg/5': variant === 'ghost',
|
|
56
|
+
},
|
|
57
|
+
className,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const ButtonLink = ({
|
|
61
|
+
children,
|
|
62
|
+
className,
|
|
63
|
+
variant = 'anchor',
|
|
64
|
+
iconOnly,
|
|
65
|
+
size,
|
|
66
|
+
ref,
|
|
67
|
+
...rest
|
|
68
|
+
}: ButtonLinkProps) => {
|
|
69
|
+
return (
|
|
70
|
+
<a
|
|
71
|
+
{...rest}
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={makeButtonClasses(variant, iconOnly, size, className)}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</a>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const Button = ({
|
|
81
|
+
children,
|
|
82
|
+
className,
|
|
83
|
+
variant = 'anchor',
|
|
84
|
+
iconOnly,
|
|
85
|
+
size,
|
|
86
|
+
type = 'button',
|
|
87
|
+
ref,
|
|
88
|
+
...rest
|
|
89
|
+
}: ButtonProps) => {
|
|
90
|
+
return (
|
|
91
|
+
<button
|
|
92
|
+
{...rest}
|
|
93
|
+
ref={ref}
|
|
94
|
+
type={type}
|
|
95
|
+
className={makeButtonClasses(variant, iconOnly, size, className)}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const CreatedLinkComponent = createLink(ButtonLink);
|
|
103
|
+
|
|
104
|
+
export const RouterButtonLink: LinkComponent<typeof ButtonLink> = (props) => {
|
|
105
|
+
return <CreatedLinkComponent preload={'intent'} {...props} />;
|
|
106
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type HTMLAttributes, type Ref, useEffect, useState } from 'react';
|
|
2
|
+
import { useCopyToClipboard } from 'usehooks-ts';
|
|
3
|
+
import { LuCopy, LuCheck } from 'react-icons/lu';
|
|
4
|
+
import { Button } from 'atoms/Button';
|
|
5
|
+
import { cn } from 'utils';
|
|
6
|
+
|
|
7
|
+
export interface CodeProps extends HTMLAttributes<HTMLElement> {
|
|
8
|
+
children: string;
|
|
9
|
+
ref?: Ref<HTMLElement>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Code = ({ children, className, ref, ...rest }: CodeProps) => {
|
|
13
|
+
const [, copy] = useCopyToClipboard();
|
|
14
|
+
const [copied, setCopied] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!copied) return;
|
|
18
|
+
const id = setTimeout(() => setCopied(false), 2000);
|
|
19
|
+
return () => clearTimeout(id);
|
|
20
|
+
}, [copied]);
|
|
21
|
+
|
|
22
|
+
const handleCopy = () => {
|
|
23
|
+
copy(children).then(() => setCopied(true));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<code
|
|
28
|
+
{...rest}
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
'inline-flex items-center gap-2 font-mono',
|
|
32
|
+
'bg-sb-canvas text-sb-fg px-4 py-2 rounded-lg',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
<Button
|
|
38
|
+
variant="ghost"
|
|
39
|
+
iconOnly
|
|
40
|
+
size="sm"
|
|
41
|
+
onClick={handleCopy}
|
|
42
|
+
aria-label={copied ? 'Copied' : 'Copy to clipboard'}
|
|
43
|
+
>
|
|
44
|
+
{copied ? (
|
|
45
|
+
<LuCheck className="size-4" />
|
|
46
|
+
) : (
|
|
47
|
+
<LuCopy className="size-4" />
|
|
48
|
+
)}
|
|
49
|
+
</Button>
|
|
50
|
+
</code>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnchorHTMLAttributes,
|
|
3
|
+
type ButtonHTMLAttributes,
|
|
4
|
+
type Ref,
|
|
5
|
+
} from 'react';
|
|
6
|
+
import { createLink, type LinkComponent } from '@tanstack/react-router';
|
|
7
|
+
import { cn } from 'utils';
|
|
8
|
+
|
|
9
|
+
type LinkVariant = 'anchor' | 'fg' | 'fg-subtle';
|
|
10
|
+
|
|
11
|
+
type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
12
|
+
variant?: LinkVariant;
|
|
13
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type LinkButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
17
|
+
variant?: LinkVariant;
|
|
18
|
+
ref?: Ref<HTMLButtonElement>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const makeLinkClasses = (variant?: LinkVariant, className?: string) => {
|
|
22
|
+
return cn(
|
|
23
|
+
'outline-none focus-visible:outline-sb-action transition-colors duration-100',
|
|
24
|
+
'underline decoration-current/30 underline-offset-4',
|
|
25
|
+
'hover:decoration-current focus-visible:decoration-current',
|
|
26
|
+
{
|
|
27
|
+
'text-sb-anchor hover:text-sb-anchor-active': variant === 'anchor',
|
|
28
|
+
'text-sb-fg hover:text-sb-anchor': variant === 'fg',
|
|
29
|
+
'text-sb-fg-subtle hover:text-sb-fg': variant === 'fg-subtle',
|
|
30
|
+
},
|
|
31
|
+
className,
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Link = ({
|
|
36
|
+
children,
|
|
37
|
+
className,
|
|
38
|
+
variant = 'anchor',
|
|
39
|
+
ref,
|
|
40
|
+
...rest
|
|
41
|
+
}: LinkProps) => {
|
|
42
|
+
return (
|
|
43
|
+
<a {...rest} ref={ref} className={makeLinkClasses(variant, className)}>
|
|
44
|
+
{children}
|
|
45
|
+
</a>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const CreatedLinkComponent = createLink(Link);
|
|
50
|
+
|
|
51
|
+
export const RouterLink: LinkComponent<typeof Link> = (props) => {
|
|
52
|
+
return <CreatedLinkComponent preload={'intent'} {...props} />;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const LinkButton = ({
|
|
56
|
+
children,
|
|
57
|
+
className,
|
|
58
|
+
type = 'button',
|
|
59
|
+
variant = 'anchor',
|
|
60
|
+
disabled,
|
|
61
|
+
ref,
|
|
62
|
+
...rest
|
|
63
|
+
}: LinkButtonProps) => {
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
{...rest}
|
|
67
|
+
ref={ref}
|
|
68
|
+
type={type}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
className={cn(
|
|
71
|
+
makeLinkClasses(variant, className),
|
|
72
|
+
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
};
|