mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat(node): support TanStack Start builds (#672)
* feat: add generation script for latest tanstack example Generated-by: aiautocommit * test(examples): add tanstack-latest example project setup Generated-by: aiautocommit * feat(node): add helper methods to check production and dev dependencies Generated-by: aiautocommit * feat(logger): support absolute URLs in DocsURL and suggestions Generated-by: aiautocommit * feat(node): exclude TanStack Start packages from Vite static build Generated-by: aiautocommit * feat(node): add support for TanStack Start srvx fallback Generated-by: aiautocommit * test(core): update test snapshots for TanStack build plan Generated-by: aiautocommit * docs(node): document TanStack Start detection and build behavior Generated-by: aiautocommit * feat(node): add TanStack Start framework support and fallback server Generated-by: aiautocommit
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
"directory": "/app/node_modules/.cache",
|
||||
"type": "shared"
|
||||
},
|
||||
"vite": {
|
||||
"tanstack-start": {
|
||||
"directory": "/app/node_modules/.vite",
|
||||
"type": "shared"
|
||||
}
|
||||
@@ -139,7 +139,7 @@
|
||||
{
|
||||
"caches": [
|
||||
"node-modules",
|
||||
"vite"
|
||||
"tanstack-start"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"caches": {
|
||||
"apt": {
|
||||
"directory": "/var/cache/apt",
|
||||
"type": "locked"
|
||||
},
|
||||
"apt-lists": {
|
||||
"directory": "/var/lib/apt/lists",
|
||||
"type": "locked"
|
||||
},
|
||||
"node-modules": {
|
||||
"directory": "/app/node_modules/.cache",
|
||||
"type": "shared"
|
||||
},
|
||||
"npm-install": {
|
||||
"directory": "/root/.npm",
|
||||
"type": "shared"
|
||||
},
|
||||
"tanstack-start": {
|
||||
"directory": "/app/node_modules/.vite",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"step": "packages:apt:runtime"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/node_modules"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".yarn"
|
||||
],
|
||||
"include": [
|
||||
"/root/.cache",
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "npx srvx --prod -s ../client dist/server/server.js",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FETCH_RETRIES": "5",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"RAILPACK_VERSION": "dev"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"generated-mise-toml": "[generated-mise-toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "generated-mise-toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "mise install",
|
||||
"customName": "install mise packages: node"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.7.18"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"npm-install"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"path": "/app/node_modules/.bin"
|
||||
},
|
||||
{
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"dest": "package-lock.json",
|
||||
"src": "package-lock.json"
|
||||
},
|
||||
{
|
||||
"dest": "package.json",
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FETCH_RETRIES": "5",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"node-modules",
|
||||
"tanstack-start"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "npm run build"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"apt",
|
||||
"apt-lists"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libatomic1'",
|
||||
"customName": "install apt packages: libatomic1"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.7.18"
|
||||
}
|
||||
],
|
||||
"name": "packages:apt:runtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
+11
-4
@@ -2,6 +2,7 @@ package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -20,7 +21,7 @@ const (
|
||||
type Msg struct {
|
||||
Level Level
|
||||
Msg string
|
||||
DocsPath string // optional railpack.com-relative path, e.g. "/guides/installing-packages"
|
||||
DocsPath string // optional Railpack-relative path or absolute URL
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
@@ -45,8 +46,8 @@ func (l *Logger) LogDeprecation(format string, args ...any) {
|
||||
l.log(Deprecation, format, args...)
|
||||
}
|
||||
|
||||
// LogSuggestion records a helpful config suggestion. docsPath is an optional
|
||||
// railpack.com-relative path shown as a styled docs link when pretty-printed.
|
||||
// LogSuggestion records a helpful config suggestion with an optional docs link.
|
||||
// Relative paths resolve against railpack.com; absolute URLs are used unchanged.
|
||||
func (l *Logger) LogSuggestion(msg string, docsPath ...string) {
|
||||
path := ""
|
||||
if len(docsPath) > 0 {
|
||||
@@ -74,11 +75,17 @@ func (l *Logger) log(level Level, format string, args ...any) {
|
||||
})
|
||||
}
|
||||
|
||||
// DocsURL builds an absolute docs URL from a domain-relative path.
|
||||
// DocsURL resolves Railpack-relative docs paths while preserving absolute URLs.
|
||||
func DocsURL(docsPath string) string {
|
||||
if docsPath == "" {
|
||||
return DocsBaseURL
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(docsPath)
|
||||
if err == nil && parsedURL.IsAbs() {
|
||||
return docsPath
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(docsPath, "/") {
|
||||
docsPath = "/" + docsPath
|
||||
}
|
||||
|
||||
@@ -32,4 +32,9 @@ func TestDocsURL(t *testing.T) {
|
||||
require.Equal(t, "https://railpack.com", DocsURL(""))
|
||||
require.Equal(t, "https://railpack.com/guides/installing-packages", DocsURL("/guides/installing-packages"))
|
||||
require.Equal(t, "https://railpack.com/guides/installing-packages", DocsURL("guides/installing-packages"))
|
||||
require.Equal(
|
||||
t,
|
||||
"https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro",
|
||||
DocsURL("https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,6 +150,15 @@ func (p *NodeProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
build.AddInput(plan.NewStepLayer(install.Name()))
|
||||
p.Build(ctx, build)
|
||||
|
||||
// note the best place for it, but it avoids having to worry about side effects in the framework helper functions
|
||||
if p.usesTanstackSrvxFallback() {
|
||||
ctx.Logger.LogInfo("No start script found; using srvx as production server")
|
||||
ctx.Logger.LogSuggestion(
|
||||
"Set up Nitro for production Node deploys",
|
||||
"https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro",
|
||||
)
|
||||
}
|
||||
|
||||
// Deploy
|
||||
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
|
||||
maps.Copy(ctx.Deploy.Variables, p.GetNodeEnvVars(ctx))
|
||||
@@ -252,6 +261,8 @@ func (p *NodeProvider) GetStartCommand(ctx *generate.GenerateContext) string {
|
||||
} else if p.isNuxt() {
|
||||
// Default Nuxt start command
|
||||
return "node .output/server/index.mjs"
|
||||
} else if start := p.getTanstackStartCommand(); start != "" {
|
||||
return start
|
||||
} else if start := p.getSvelteKitStartCommand(); start != "" {
|
||||
return start
|
||||
} else if pkg, _, ok := p.resolveNxDeployPackage(ctx); ok {
|
||||
@@ -330,6 +341,10 @@ func (p *NodeProvider) addCachesToBuildStep(ctx *generate.GenerateContext, build
|
||||
return p.isVitePackage(pkg, ctx)
|
||||
}, "node_modules/.vite")
|
||||
|
||||
p.addFrameworkCaches(ctx, build, "tanstack-start", func(pkg *WorkspacePackage, ctx *generate.GenerateContext) bool {
|
||||
return p.isTanstackStartPackage(pkg)
|
||||
}, "node_modules/.vite")
|
||||
|
||||
p.addFrameworkCaches(ctx, build, "astro", func(pkg *WorkspacePackage, ctx *generate.GenerateContext) bool {
|
||||
return p.isAstroPackage(pkg, ctx)
|
||||
}, "node_modules/.astro")
|
||||
@@ -713,7 +728,3 @@ func (p *NodeProvider) isNuxt() bool {
|
||||
func (p *NodeProvider) isRemix() bool {
|
||||
return p.hasDependency("@remix-run/node")
|
||||
}
|
||||
|
||||
func (p *NodeProvider) isTanstackStart() bool {
|
||||
return p.hasDependency("@tanstack/react-start")
|
||||
}
|
||||
|
||||
@@ -47,28 +47,10 @@ func (p *PackageJson) BuildScriptContains(value string) bool {
|
||||
}
|
||||
|
||||
func (p *PackageJson) hasDependency(dependency string) bool {
|
||||
if p.hasProductionDependency(dependency) {
|
||||
return true
|
||||
}
|
||||
|
||||
if p.DevDependencies != nil {
|
||||
if _, ok := p.DevDependencies[dependency]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PackageJson) hasProductionDependency(dependency string) bool {
|
||||
if p.Dependencies == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := p.Dependencies[dependency]
|
||||
return ok
|
||||
return p.hasProductionDependency(dependency) || p.hasDevDependency(dependency)
|
||||
}
|
||||
|
||||
// is there a dependency that requires the entire app to be loaded into the context
|
||||
func (p *PackageJson) hasLocalDependency() bool {
|
||||
allDeps := make(map[string]string)
|
||||
maps.Copy(allDeps, p.Dependencies)
|
||||
|
||||
@@ -65,6 +65,12 @@ func TestPackageJson(t *testing.T) {
|
||||
assert.True(t, packageJson.hasDependency("typescript"))
|
||||
assert.True(t, packageJson.hasDependency("@types/react"))
|
||||
assert.False(t, packageJson.hasDependency("nonexistent"))
|
||||
assert.True(t, packageJson.hasProductionDependency("next"))
|
||||
assert.False(t, packageJson.hasProductionDependency("typescript"))
|
||||
assert.False(t, packageJson.hasProductionDependency("nonexistent"))
|
||||
assert.True(t, packageJson.hasDevDependency("typescript"))
|
||||
assert.False(t, packageJson.hasDevDependency("next"))
|
||||
assert.False(t, packageJson.hasDevDependency("nonexistent"))
|
||||
|
||||
// Test engines
|
||||
assert.Equal(t, ">=20 <21", packageJson.Engines["node"])
|
||||
|
||||
@@ -24,6 +24,24 @@ const (
|
||||
PNPM_STORE_DIR = PNPM_HOME + "/store"
|
||||
)
|
||||
|
||||
func (p *PackageJson) hasProductionDependency(dependency string) bool {
|
||||
if p.Dependencies == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := p.Dependencies[dependency]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *PackageJson) hasDevDependency(dependency string) bool {
|
||||
if p.DevDependencies == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := p.DevDependencies[dependency]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p PackageManager) Name() string {
|
||||
switch p {
|
||||
case PackageManagerNpm:
|
||||
|
||||
@@ -41,6 +41,18 @@ func TestVite(t *testing.T) {
|
||||
isSPA: false,
|
||||
isVite: false,
|
||||
},
|
||||
{
|
||||
name: "tanstack-latest",
|
||||
path: "../../../examples/tanstack-latest",
|
||||
isSPA: false,
|
||||
isVite: false,
|
||||
},
|
||||
{
|
||||
name: "tanstack-start-with-start-script",
|
||||
path: "../../../examples/node-tanstack-start",
|
||||
isSPA: false,
|
||||
isVite: false,
|
||||
},
|
||||
{
|
||||
name: "cra",
|
||||
path: "../../../examples/node-cra",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package node
|
||||
|
||||
const (
|
||||
// Static dir is resolved relative to the server entry (dist/server/), matching TanStack docs.
|
||||
DefaultTanstackSrvxStartCommand = "srvx --prod -s ../client dist/server/server.js"
|
||||
)
|
||||
|
||||
func (p *NodeProvider) isTanstackStart() bool {
|
||||
return p.isTanstackStartPackage(p.workspace.Root)
|
||||
}
|
||||
|
||||
func (p *NodeProvider) isTanstackStartPackage(pkg *WorkspacePackage) bool {
|
||||
if pkg == nil || pkg.PackageJson == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return pkg.PackageJson.hasProductionDependency("@tanstack/react-start")
|
||||
}
|
||||
|
||||
// True when TanStack Start has no start script and we fall back to srvx.
|
||||
func (p *NodeProvider) usesTanstackSrvxFallback() bool {
|
||||
if !p.isTanstackStart() {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.getScripts(p.packageJson, "start") == ""
|
||||
}
|
||||
|
||||
func (p *NodeProvider) getTanstackStartCommand() string {
|
||||
if !p.usesTanstackSrvxFallback() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return p.packageManager.ExecCommand(DefaultTanstackSrvxStartCommand)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
testingUtils "github.com/railwayapp/railpack/core/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTanstackStartSrvxFallback(t *testing.T) {
|
||||
t.Run("uses package manager executor", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/tanstack-latest")
|
||||
provider := NodeProvider{}
|
||||
|
||||
err := provider.Initialize(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := map[PackageManager]string{
|
||||
PackageManagerNpm: "npx ",
|
||||
PackageManagerPnpm: "pnpm exec ",
|
||||
PackageManagerBun: "bunx ",
|
||||
}
|
||||
for packageManager, commandPrefix := range tests {
|
||||
provider.packageManager = packageManager
|
||||
require.Equal(t, commandPrefix+DefaultTanstackSrvxStartCommand, provider.getTanstackStartCommand())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oob template uses srvx fallback", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/tanstack-latest")
|
||||
provider := NodeProvider{}
|
||||
|
||||
err := provider.Initialize(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, provider.isTanstackStart())
|
||||
require.True(t, provider.usesTanstackSrvxFallback())
|
||||
require.False(t, provider.isSPA(ctx))
|
||||
require.False(t, provider.isVite(ctx))
|
||||
expectedStartCommand := "npx " + DefaultTanstackSrvxStartCommand
|
||||
require.Equal(t, expectedStartCommand, provider.GetStartCommand(ctx))
|
||||
|
||||
err = provider.Plan(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedStartCommand, ctx.Deploy.StartCmd)
|
||||
})
|
||||
|
||||
t.Run("explicit start skips srvx fallback", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/node-tanstack-start")
|
||||
provider := NodeProvider{}
|
||||
|
||||
err := provider.Initialize(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, provider.isTanstackStart())
|
||||
require.False(t, provider.usesTanstackSrvxFallback())
|
||||
require.False(t, provider.isSPA(ctx))
|
||||
require.Equal(t, "bun run start", provider.GetStartCommand(ctx))
|
||||
|
||||
err = provider.Plan(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -25,8 +25,8 @@ func (p *NodeProvider) isVitePackage(pkg *WorkspacePackage, ctx *generate.Genera
|
||||
|
||||
hasViteConfig := ctx.App.HasFile(viteConfigJS) || ctx.App.HasFile(viteConfigTS)
|
||||
|
||||
// SvelteKit does not build as a static site by default
|
||||
if p.isSvelteKitPackage(pkg) {
|
||||
// SvelteKit and TanStack Start do not build as static sites by default
|
||||
if p.isSvelteKitPackage(pkg) || p.isTanstackStartPackage(pkg) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,15 @@ With a single Next.js app, selection is automatic. With multiple apps, set
|
||||
`RAILPACK_NX_APP` to the project name, package name, or package path (e.g.
|
||||
`web`, `@org/web`, or `apps/web`).
|
||||
|
||||
#### TanStack Start
|
||||
|
||||
TanStack Start is detected via `@tanstack/react-start` and is not treated as a
|
||||
Vite SPA. If there is no `start` script, Railpack installs `srvx` globally and
|
||||
starts with `srvx --prod -s ../client dist/server/server.js`. For production
|
||||
Node deploys, set up Nitro per the
|
||||
[TanStack hosting docs](https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro).
|
||||
Railpack caches `node_modules/.vite`.
|
||||
|
||||
### Install
|
||||
|
||||
Railpack will only include the necessary files to install dependencies in order
|
||||
@@ -233,7 +242,6 @@ Including:
|
||||
- Next.js: Caches `.next/cache` for each Next.js app in the workspace
|
||||
- Remix: Caches `.cache`
|
||||
- Vite: Caches `node_modules/.vite`
|
||||
- Tanstack Start: Caches `node_modules/.vite`
|
||||
- Astro: Caches `node_modules/.astro`
|
||||
- React Router: Caches `.react-router`
|
||||
- Nuxt:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"projectName": "tanstack-latest",
|
||||
"mode": "file-router",
|
||||
"typescript": true,
|
||||
"tailwind": true,
|
||||
"packageManager": "npm",
|
||||
"git": false,
|
||||
"install": true,
|
||||
"intent": true,
|
||||
"addOnOptions": {},
|
||||
"includeExamples": true,
|
||||
"envVarValues": {},
|
||||
"routerOnly": false,
|
||||
"version": 1,
|
||||
"framework": "react",
|
||||
"chosenAddOns": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
.output
|
||||
.vinxi
|
||||
__unconfig*
|
||||
todos.json
|
||||
@@ -0,0 +1,97 @@
|
||||
<!-- intent-skills:start -->
|
||||
# TanStack Intent - before editing files, run the matching guidance command.
|
||||
tanstackIntent:
|
||||
- id: "@tanstack/devtools#devtools-app-setup"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools#devtools-app-setup"
|
||||
for: "Install TanStack Devtools, pick framework adapter (React/Vue/Solid/Preact), register plugins via plugins prop, configure shell (position, hotkeys, theme, hideUntilHover, requireUrlFlag, eventBusConfig). TanStackDevtools component, defaultOpen, localStorage persistence."
|
||||
- id: "@tanstack/devtools#devtools-marketplace"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools#devtools-marketplace"
|
||||
for: "Publish plugin to npm and submit to TanStack Devtools Marketplace. PluginMetadata registry format, plugin-registry.ts, pluginImport (importName, type), requires (packageName, minVersion), framework tagging, multi-framework submissions, featured plugins."
|
||||
- id: "@tanstack/devtools#devtools-plugin-panel"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools#devtools-plugin-panel"
|
||||
for: "Build devtools panel components that display emitted event data. Listen via EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui components. Plugin registration (name, render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core with devtools-ui for multi-framework support, or framework-specific panels."
|
||||
- id: "@tanstack/devtools#devtools-production"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools#devtools-production"
|
||||
for: "Handle devtools in production vs development. removeDevtoolsOnBuild, devDependency vs regular dependency, conditional imports, NoOp plugin variants for tree-shaking, non-Vite production exclusion patterns."
|
||||
- id: "@tanstack/devtools-event-client#devtools-bidirectional"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools-event-client#devtools-bidirectional"
|
||||
for: "Two-way event patterns between devtools panel and application. App-to-devtools observation, devtools-to-app commands, time-travel debugging with snapshots and revert. structuredClone for snapshot safety, distinct event suffixes for observation vs commands, serializable payloads only."
|
||||
- id: "@tanstack/devtools-event-client#devtools-event-client"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools-event-client#devtools-event-client"
|
||||
for: "Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions."
|
||||
- id: "@tanstack/devtools-event-client#devtools-instrumentation"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools-event-client#devtools-instrumentation"
|
||||
for: "Analyze library codebase for critical architecture and debugging points, add strategic event emissions. Identify middleware boundaries, state transitions, lifecycle hooks. Consolidate events (1 not 15), debounce high-frequency updates, DRY shared payload fields, guard emit() for production. Transparent server/client event bridging."
|
||||
- id: "@tanstack/devtools-vite#devtools-vite-plugin"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/devtools-vite#devtools-vite-plugin"
|
||||
for: "Configure @tanstack/devtools-vite for source inspection (data-tsd-source, inspectHotkey, ignore patterns), console piping (client-to-server, server-to-client, levels), enhanced logging, server event bus (port, host, HTTPS), production stripping (removeDevtoolsOnBuild), editor integration (launch-editor, custom editor.open). Must be FIRST plugin in Vite config. Vite ^6 || ^7 only."
|
||||
- id: "@tanstack/react-start#lifecycle/migrate-from-nextjs"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/react-start#lifecycle/migrate-from-nextjs"
|
||||
for: "Step-by-step migration from Next.js App Router to TanStack Start: route definition conversion, API mapping, server function conversion from Server Actions, middleware conversion, data fetching pattern changes."
|
||||
- id: "@tanstack/react-start#react-start"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/react-start#react-start"
|
||||
for: "React bindings for TanStack Start: createStart, StartClient, StartServer, React-specific imports, re-exports from @tanstack/react-router, full project setup with React, useServerFn hook."
|
||||
- id: "@tanstack/react-start#react-start/server-components"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/react-start#react-start/server-components"
|
||||
for: "Implement, review, debug, and refactor TanStack Start React Server Components in React 19 apps. Use when tasks mention @tanstack/react-start/rsc, renderServerComponent, createCompositeComponent, CompositeComponent, renderToReadableStream, createFromReadableStream, createFromFetch, Composite Components, React Flight streams, loader or query owned RSC caching, router.invalidate, structuralSharing: false, selective SSR, stale names like renderRsc or .validator, or migration from Next App Router RSC patterns. Do not use for generic SSR or non-TanStack RSC frameworks except brief comparison."
|
||||
- id: "@tanstack/router-core#router-core"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core"
|
||||
for: "Framework-agnostic core concepts for TanStack Router: route trees, createRouter, createRoute, createRootRoute, createRootRouteWithContext, addChildren, Register type declaration, route matching, route sorting, file naming conventions. Entry point for all router skills."
|
||||
- id: "@tanstack/router-core#router-core/auth-and-guards"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/auth-and-guards"
|
||||
for: "Route protection with beforeLoad, redirect()/throw redirect(), isRedirect helper, authenticated layout routes (_authenticated), non-redirect auth (inline login), RBAC with roles and permissions, auth provider integration (Auth0, Clerk, Supabase), router context for auth state."
|
||||
- id: "@tanstack/router-core#router-core/code-splitting"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/code-splitting"
|
||||
for: "Automatic code splitting (autoCodeSplitting), .lazy.tsx convention, createLazyFileRoute, createLazyRoute, lazyRouteComponent, getRouteApi for typed hooks in split files, codeSplitGroupings per-route override, splitBehavior programmatic config, critical vs non-critical properties."
|
||||
- id: "@tanstack/router-core#router-core/data-loading"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/data-loading"
|
||||
for: "Route loader option, loaderDeps for cache keys, staleTime/gcTime/ defaultPreloadStaleTime SWR caching, pendingComponent/pendingMs/ pendingMinMs, errorComponent/onError/onCatch, beforeLoad, router context and createRootRouteWithContext DI pattern, router.invalidate, Await component, deferred data loading with unawaited promises."
|
||||
- id: "@tanstack/router-core#router-core/navigation"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/navigation"
|
||||
for: "Link component, useNavigate, Navigate component, router.navigate, ToOptions/NavigateOptions/LinkOptions, from/to relative navigation, activeOptions/activeProps, preloading (intent/viewport/render), preloadDelay, navigation blocking (useBlocker, Block), createLink, linkOptions helper, scroll restoration, MatchRoute."
|
||||
- id: "@tanstack/router-core#router-core/not-found-and-errors"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/not-found-and-errors"
|
||||
for: "notFound() function, notFoundComponent, defaultNotFoundComponent, notFoundMode (fuzzy/root), errorComponent, CatchBoundary, CatchNotFound, isNotFound, NotFoundRoute (deprecated), route masking (mask option, createRouteMask, unmaskOnReload)."
|
||||
- id: "@tanstack/router-core#router-core/path-params"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/path-params"
|
||||
for: "Dynamic path segments ($paramName), splat routes ($ / _splat), optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext), useParams, params.parse/stringify, pathParamsAllowedCharacters, i18n locale patterns."
|
||||
- id: "@tanstack/router-core#router-core/search-params"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/search-params"
|
||||
for: "validateSearch, search param validation with Zod/Valibot/ArkType adapters, fallback(), search middlewares (retainSearchParams, stripSearchParams), custom serialization (parseSearch, stringifySearch), search param inheritance, loaderDeps for cache keys, reading and writing search params."
|
||||
- id: "@tanstack/router-core#router-core/ssr"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/ssr"
|
||||
for: "Non-streaming and streaming SSR, RouterClient/RouterServer, renderRouterToString/renderRouterToStream, createRequestHandler, defaultRenderHandler/defaultStreamHandler, HeadContent/Scripts components, head route option (meta/links/styles/scripts), ScriptOnce, automatic loader dehydration/hydration, memory history on server, data serialization, document head management."
|
||||
- id: "@tanstack/router-core#router-core/type-safety"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-core#router-core/type-safety"
|
||||
for: "Full type inference philosophy (never cast, never annotate inferred values), Register module declaration, from narrowing on hooks and Link, strict:false for shared components, getRouteApi for code-split typed access, addChildren with object syntax for TS perf, LinkProps and ValidateLinkOptions type utilities, as const satisfies pattern."
|
||||
- id: "@tanstack/router-plugin#router-plugin"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/router-plugin#router-plugin"
|
||||
for: "TanStack Router bundler plugin for route generation and automatic code splitting. Supports Vite, Webpack, Rspack, and esbuild. Configures autoCodeSplitting, routesDirectory, target framework, and code split groupings."
|
||||
- id: "@tanstack/start-client-core#start-core"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core"
|
||||
for: "Core overview for TanStack Start: tanstackStart() Vite plugin, getRouter() factory, root route document shell (HeadContent, Scripts, Outlet), client/server entry points, routeTree.gen.ts, tsconfig configuration. Entry point for all Start skills."
|
||||
- id: "@tanstack/start-client-core#start-core/auth-server-primitives"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core/auth-server-primitives"
|
||||
for: "Server-side authentication primitives for TanStack Start: session cookies (HttpOnly, Secure, SameSite, __Host- prefix), session read/issue/destroy via createServerFn and middleware, OAuth authorization-code flow with state and PKCE, password-reset enumeration defense, CSRF for non-GET RPCs, rate limiting auth endpoints, session rotation on privilege change. Pairs with router-core/auth-and-guards for the routing side."
|
||||
- id: "@tanstack/start-client-core#start-core/deployment"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core/deployment"
|
||||
for: "Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, Bun, Railway. Selective SSR (ssr option per route), SPA mode, static prerendering, ISR with Cache-Control headers, SEO and head management."
|
||||
- id: "@tanstack/start-client-core#start-core/execution-model"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core/execution-model"
|
||||
for: "Isomorphic-by-default principle, environment boundary functions (createServerFn, createServerOnlyFn, createClientOnlyFn, createIsomorphicFn), ClientOnly component, useHydrated hook, import protection, dead code elimination, environment variable safety (VITE_ prefix, process.env)."
|
||||
- id: "@tanstack/start-client-core#start-core/middleware"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core/middleware"
|
||||
for: "createMiddleware, request middleware (.server only), server function middleware (.client + .server), context passing via next({ context }), sendContext for client-server transfer, global middleware via createStart in src/start.ts, middleware factories, method order enforcement, fetch override precedence."
|
||||
- id: "@tanstack/start-client-core#start-core/server-functions"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core/server-functions"
|
||||
for: "createServerFn (GET/POST), validator (Zod or function), useServerFn hook, server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus), error handling (throw errors, redirect, notFound), streaming, FormData handling, file organization (.functions.ts, .server.ts)."
|
||||
- id: "@tanstack/start-client-core#start-core/server-routes"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-client-core#start-core/server-routes"
|
||||
for: "Server-side API endpoints using the server property on createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), createHandlers for per-handler middleware, handler context (request, params, context), request body parsing, response helpers, file naming for API routes."
|
||||
- id: "@tanstack/start-server-core#start-server-core"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/start-server-core#start-server-core"
|
||||
for: "Server-side runtime for TanStack Start: createStartHandler, request/response utilities (getRequest, setResponseHeader, setCookie, getCookie, useSession), three-phase request handling, AsyncLocalStorage context."
|
||||
- id: "@tanstack/virtual-file-routes#virtual-file-routes"
|
||||
run: "npx @tanstack/intent@latest load @tanstack/virtual-file-routes#virtual-file-routes"
|
||||
for: "Programmatic route tree building as an alternative to filesystem conventions: rootRoute, index, route, layout, physical, defineVirtualSubtreeConfig. Use with TanStack Router plugin's virtualRouteConfig option."
|
||||
<!-- intent-skills:end -->
|
||||
@@ -0,0 +1,193 @@
|
||||
Welcome to your new TanStack Start app!
|
||||
|
||||
# Getting Started
|
||||
|
||||
To run this application:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
# Building For Production
|
||||
|
||||
To build this application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
|
||||
|
||||
### Removing Tailwind CSS
|
||||
|
||||
If you prefer not to use Tailwind CSS:
|
||||
|
||||
1. Remove the demo pages in `src/routes/demo/`
|
||||
2. Replace the Tailwind import in `src/styles.css` with your own styles
|
||||
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
|
||||
4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D`
|
||||
|
||||
|
||||
|
||||
## Routing
|
||||
|
||||
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
|
||||
|
||||
### Adding A Route
|
||||
|
||||
To add a new route to your application just add a new file in the `./src/routes` directory.
|
||||
|
||||
TanStack will automatically generate the content of the route file for you.
|
||||
|
||||
Now that you have two routes you can use a `Link` component to navigate between them.
|
||||
|
||||
### Adding Links
|
||||
|
||||
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
|
||||
|
||||
```tsx
|
||||
import { Link } from "@tanstack/react-router";
|
||||
```
|
||||
|
||||
Then anywhere in your JSX you can use it like so:
|
||||
|
||||
```tsx
|
||||
<Link to="/about">About</Link>
|
||||
```
|
||||
|
||||
This will create a link that will navigate to the `/about` route.
|
||||
|
||||
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
|
||||
|
||||
### Using A Layout
|
||||
|
||||
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
|
||||
|
||||
Here is an example layout that includes a header:
|
||||
|
||||
```tsx
|
||||
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ title: 'My App' },
|
||||
],
|
||||
}),
|
||||
shellComponent: ({ children }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</nav>
|
||||
</header>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
||||
|
||||
## Server Functions
|
||||
|
||||
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
|
||||
|
||||
```tsx
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
|
||||
const getServerTime = createServerFn({
|
||||
method: 'GET',
|
||||
}).handler(async () => {
|
||||
return new Date().toISOString()
|
||||
})
|
||||
|
||||
// Use in a component
|
||||
function MyComponent() {
|
||||
const [time, setTime] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
getServerTime().then(setTime)
|
||||
}, [])
|
||||
|
||||
return <div>Server time: {time}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## API Routes
|
||||
|
||||
You can create API routes by using the `server` property in your route definitions:
|
||||
|
||||
```tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { json } from '@tanstack/react-start'
|
||||
|
||||
export const Route = createFileRoute('/api/hello')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: () => json({ message: 'Hello, World!' }),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Data Fetching
|
||||
|
||||
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/people')({
|
||||
loader: async () => {
|
||||
const response = await fetch('https://swapi.dev/api/people')
|
||||
return response.json()
|
||||
},
|
||||
component: PeopleComponent,
|
||||
})
|
||||
|
||||
function PeopleComponent() {
|
||||
const data = Route.useLoaderData()
|
||||
return (
|
||||
<ul>
|
||||
{data.results.map((person) => (
|
||||
<li key={person.name}>{person.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
|
||||
|
||||
# Demo files
|
||||
|
||||
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
|
||||
|
||||
# Learn More
|
||||
|
||||
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
|
||||
|
||||
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
cd "${0:A:h}"
|
||||
npx @tanstack/cli@latest create . --yes --force --no-git
|
||||
+4778
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "tanstack-latest",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3000",
|
||||
"generate-routes": "tsr generate",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "latest",
|
||||
"@tanstack/react-router": "latest",
|
||||
"@tanstack/react-router-devtools": "latest",
|
||||
"@tanstack/react-router-ssr-query": "latest",
|
||||
"@tanstack/react-start": "latest",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/devtools-vite": "latest",
|
||||
"@tanstack/router-cli": "^1.132.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"jsdom": "^28.1.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"lightningcss"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "TanStack App",
|
||||
"name": "Create TanStack App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,44 @@
|
||||
export default function Footer() {
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<footer className="mt-20 border-t border-[var(--line)] px-4 pb-14 pt-10 text-[var(--sea-ink-soft)]">
|
||||
<div className="page-wrap flex flex-col items-center justify-between gap-4 text-center sm:flex-row sm:text-left">
|
||||
<p className="m-0 text-sm">
|
||||
© {year} Your name here. All rights reserved.
|
||||
</p>
|
||||
<p className="island-kicker m-0">Built with TanStack Start</p>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-center gap-4">
|
||||
<a
|
||||
href="https://x.com/tan_stack"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
|
||||
>
|
||||
<span className="sr-only">Follow TanStack on X</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/TanStack"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
|
||||
>
|
||||
<span className="sr-only">Go to TanStack GitHub</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-[var(--line)] bg-[var(--header-bg)] px-4 backdrop-blur-lg">
|
||||
<nav className="page-wrap flex flex-wrap items-center gap-x-3 gap-y-2 py-3 sm:py-4">
|
||||
<h2 className="m-0 flex-shrink-0 text-base font-semibold tracking-tight">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm text-[var(--sea-ink)] no-underline shadow-[0_8px_24px_rgba(30,90,72,0.08)] sm:px-4 sm:py-2"
|
||||
>
|
||||
<span className="h-2 w-2 rounded-full bg-[linear-gradient(90deg,#56c6be,#7ed3bf)]" />
|
||||
TanStack Start
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="order-3 flex w-full flex-wrap items-center gap-x-4 gap-y-1 pb-1 text-sm font-semibold sm:order-none sm:w-auto sm:flex-nowrap sm:pb-0">
|
||||
<Link
|
||||
to="/"
|
||||
className="nav-link"
|
||||
activeProps={{ className: 'nav-link is-active' }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/about"
|
||||
className="nav-link"
|
||||
activeProps={{ className: 'nav-link is-active' }}
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<a
|
||||
href="https://tanstack.com/start/latest/docs/framework/react/overview"
|
||||
className="nav-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-1.5 sm:gap-2">
|
||||
<a
|
||||
href="https://x.com/tan_stack"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block"
|
||||
>
|
||||
<span className="sr-only">Follow TanStack on X</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/TanStack"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block"
|
||||
>
|
||||
<span className="sr-only">Go to TanStack GitHub</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'auto'
|
||||
|
||||
function getInitialMode(): ThemeMode {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem('theme')
|
||||
if (stored === 'light' || stored === 'dark' || stored === 'auto') {
|
||||
return stored
|
||||
}
|
||||
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
function applyThemeMode(mode: ThemeMode) {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode
|
||||
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(resolved)
|
||||
|
||||
if (mode === 'auto') {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', mode)
|
||||
}
|
||||
|
||||
document.documentElement.style.colorScheme = resolved
|
||||
}
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [mode, setMode] = useState<ThemeMode>('auto')
|
||||
|
||||
useEffect(() => {
|
||||
const initialMode = getInitialMode()
|
||||
setMode(initialMode)
|
||||
applyThemeMode(initialMode)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'auto') {
|
||||
return
|
||||
}
|
||||
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const onChange = () => applyThemeMode('auto')
|
||||
|
||||
media.addEventListener('change', onChange)
|
||||
return () => {
|
||||
media.removeEventListener('change', onChange)
|
||||
}
|
||||
}, [mode])
|
||||
|
||||
function toggleMode() {
|
||||
const nextMode: ThemeMode =
|
||||
mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light'
|
||||
setMode(nextMode)
|
||||
applyThemeMode(nextMode)
|
||||
window.localStorage.setItem('theme', nextMode)
|
||||
}
|
||||
|
||||
const label =
|
||||
mode === 'auto'
|
||||
? 'Theme mode: auto (system). Click to switch to light mode.'
|
||||
: `Theme mode: ${mode}. Click to switch mode.`
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMode}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
className="rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm font-semibold text-[var(--sea-ink)] shadow-[0_8px_22px_rgba(30,90,72,0.08)] transition hover:-translate-y-0.5"
|
||||
>
|
||||
{mode === 'auto' ? 'Auto' : mode === 'dark' ? 'Dark' : 'Light'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/about'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/about'
|
||||
id: '__root__' | '/' | '/about'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/about': {
|
||||
id: '/about'
|
||||
path: '/about'
|
||||
fullPath: '/about'
|
||||
preLoaderRoute: typeof AboutRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
export function getRouter() {
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
||||
import { TanStackDevtools } from '@tanstack/react-devtools'
|
||||
import Footer from '../components/Footer'
|
||||
import Header from '../components/Header'
|
||||
|
||||
import appCss from '../styles.css?url'
|
||||
|
||||
const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();`
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: 'utf-8',
|
||||
},
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
{
|
||||
title: 'TanStack Start Starter',
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
shellComponent: RootDocument,
|
||||
})
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="font-sans antialiased [overflow-wrap:anywhere] selection:bg-[rgba(79,184,178,0.24)]">
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: 'bottom-right',
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: 'Tanstack Router',
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/about')({
|
||||
component: About,
|
||||
})
|
||||
|
||||
function About() {
|
||||
return (
|
||||
<main className="page-wrap px-4 py-12">
|
||||
<section className="island-shell rounded-2xl p-6 sm:p-8">
|
||||
<p className="island-kicker mb-2">About</p>
|
||||
<h1 className="display-title mb-3 text-4xl font-bold text-[var(--sea-ink)] sm:text-5xl">
|
||||
A small starter with room to grow.
|
||||
</h1>
|
||||
<p className="m-0 max-w-3xl text-base leading-8 text-[var(--sea-ink-soft)]">
|
||||
TanStack Start gives you type-safe routing, server functions, and
|
||||
modern SSR defaults. Use this as a clean foundation, then layer in
|
||||
your own routes, styling, and add-ons.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/')({ component: App })
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main className="page-wrap px-4 pb-8 pt-14">
|
||||
<section className="island-shell rise-in relative overflow-hidden rounded-[2rem] px-6 py-10 sm:px-10 sm:py-14">
|
||||
<div className="pointer-events-none absolute -left-20 -top-24 h-56 w-56 rounded-full bg-[radial-gradient(circle,rgba(79,184,178,0.32),transparent_66%)]" />
|
||||
<div className="pointer-events-none absolute -bottom-20 -right-20 h-56 w-56 rounded-full bg-[radial-gradient(circle,rgba(47,106,74,0.18),transparent_66%)]" />
|
||||
<p className="island-kicker mb-3">TanStack Start Base Template</p>
|
||||
<h1 className="display-title mb-5 max-w-3xl text-4xl leading-[1.02] font-bold tracking-tight text-[var(--sea-ink)] sm:text-6xl">
|
||||
Start simple, ship quickly.
|
||||
</h1>
|
||||
<p className="mb-8 max-w-2xl text-base text-[var(--sea-ink-soft)] sm:text-lg">
|
||||
This base starter intentionally keeps things light: two routes, clean
|
||||
structure, and the essentials you need to build from scratch.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href="/about"
|
||||
className="rounded-full border border-[rgba(50,143,151,0.3)] bg-[rgba(79,184,178,0.14)] px-5 py-2.5 text-sm font-semibold text-[var(--lagoon-deep)] no-underline transition hover:-translate-y-0.5 hover:bg-[rgba(79,184,178,0.24)]"
|
||||
>
|
||||
About This Starter
|
||||
</a>
|
||||
<a
|
||||
href="https://tanstack.com/router"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-full border border-[rgba(23,58,64,0.2)] bg-white/50 px-5 py-2.5 text-sm font-semibold text-[var(--sea-ink)] no-underline transition hover:-translate-y-0.5 hover:border-[rgba(23,58,64,0.35)]"
|
||||
>
|
||||
Router Guide
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[
|
||||
[
|
||||
'Type-Safe Routing',
|
||||
'Routes and links stay in sync across every page.',
|
||||
],
|
||||
[
|
||||
'Server Functions',
|
||||
'Call server code from your UI without creating API boilerplate.',
|
||||
],
|
||||
[
|
||||
'Streaming by Default',
|
||||
'Ship progressively rendered responses for faster experiences.',
|
||||
],
|
||||
[
|
||||
'Tailwind Native',
|
||||
'Design quickly with utility-first styling and reusable tokens.',
|
||||
],
|
||||
].map(([title, desc], index) => (
|
||||
<article
|
||||
key={title}
|
||||
className="island-shell feature-card rise-in rounded-2xl p-5"
|
||||
style={{ animationDelay: `${index * 90 + 80}ms` }}
|
||||
>
|
||||
<h2 className="mb-2 text-base font-semibold text-[var(--sea-ink)]">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="m-0 text-sm text-[var(--sea-ink-soft)]">{desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="island-shell mt-8 rounded-2xl p-6">
|
||||
<p className="island-kicker mb-2">Quick Start</p>
|
||||
<ul className="m-0 list-disc space-y-2 pl-5 text-sm text-[var(--sea-ink-soft)]">
|
||||
<li>
|
||||
Edit <code>src/routes/index.tsx</code> to customize the home page.
|
||||
</li>
|
||||
<li>
|
||||
Update <code>src/components/Header.tsx</code> and{' '}
|
||||
<code>src/components/Footer.tsx</code> for brand links.
|
||||
</li>
|
||||
<li>
|
||||
Add routes in <code>src/routes</code> and tweak visual tokens in{' '}
|
||||
<code>src/styles.css</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],500;9..144,700&family=Manrope:wght@400;500;600;700;800&display=swap");
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--sea-ink: #173a40;
|
||||
--sea-ink-soft: #416166;
|
||||
--lagoon: #4fb8b2;
|
||||
--lagoon-deep: #328f97;
|
||||
--palm: #2f6a4a;
|
||||
--sand: #e7f0e8;
|
||||
--foam: #f3faf5;
|
||||
--surface: rgba(255, 255, 255, 0.74);
|
||||
--surface-strong: rgba(255, 255, 255, 0.9);
|
||||
--line: rgba(23, 58, 64, 0.14);
|
||||
--inset-glint: rgba(255, 255, 255, 0.82);
|
||||
--kicker: rgba(47, 106, 74, 0.9);
|
||||
--bg-base: #e7f3ec;
|
||||
--header-bg: rgba(251, 255, 248, 0.84);
|
||||
--chip-bg: rgba(255, 255, 255, 0.8);
|
||||
--chip-line: rgba(47, 106, 74, 0.18);
|
||||
--link-bg-hover: rgba(255, 255, 255, 0.9);
|
||||
--hero-a: rgba(79, 184, 178, 0.36);
|
||||
--hero-b: rgba(47, 106, 74, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--sea-ink: #d7ece8;
|
||||
--sea-ink-soft: #afcdc8;
|
||||
--lagoon: #60d7cf;
|
||||
--lagoon-deep: #8de5db;
|
||||
--palm: #6ec89a;
|
||||
--sand: #0f1a1e;
|
||||
--foam: #101d22;
|
||||
--surface: rgba(16, 30, 34, 0.8);
|
||||
--surface-strong: rgba(15, 27, 31, 0.92);
|
||||
--line: rgba(141, 229, 219, 0.18);
|
||||
--inset-glint: rgba(194, 247, 238, 0.14);
|
||||
--kicker: #b8efe5;
|
||||
--bg-base: #0a1418;
|
||||
--header-bg: rgba(10, 20, 24, 0.8);
|
||||
--chip-bg: rgba(13, 28, 32, 0.9);
|
||||
--chip-line: rgba(141, 229, 219, 0.24);
|
||||
--link-bg-hover: rgba(24, 44, 49, 0.8);
|
||||
--hero-a: rgba(96, 215, 207, 0.18);
|
||||
--hero-b: rgba(110, 200, 154, 0.12);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--sea-ink: #d7ece8;
|
||||
--sea-ink-soft: #afcdc8;
|
||||
--lagoon: #60d7cf;
|
||||
--lagoon-deep: #8de5db;
|
||||
--palm: #6ec89a;
|
||||
--sand: #0f1a1e;
|
||||
--foam: #101d22;
|
||||
--surface: rgba(16, 30, 34, 0.8);
|
||||
--surface-strong: rgba(15, 27, 31, 0.92);
|
||||
--line: rgba(141, 229, 219, 0.18);
|
||||
--inset-glint: rgba(194, 247, 238, 0.14);
|
||||
--kicker: #b8efe5;
|
||||
--bg-base: #0a1418;
|
||||
--header-bg: rgba(10, 20, 24, 0.8);
|
||||
--chip-bg: rgba(13, 28, 32, 0.9);
|
||||
--chip-line: rgba(141, 229, 219, 0.24);
|
||||
--link-bg-hover: rgba(24, 44, 49, 0.8);
|
||||
--hero-a: rgba(96, 215, 207, 0.18);
|
||||
--hero-b: rgba(110, 200, 154, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--sea-ink);
|
||||
font-family: var(--font-sans);
|
||||
background-color: var(--bg-base);
|
||||
background:
|
||||
radial-gradient(1100px 620px at -8% -10%, var(--hero-a), transparent 58%),
|
||||
radial-gradient(1050px 620px at 112% -12%, var(--hero-b), transparent 62%),
|
||||
radial-gradient(720px 380px at 50% 115%, rgba(79, 184, 178, 0.1), transparent 68%),
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--sand) 68%, white) 0%, var(--foam) 44%, var(--bg-base) 100%);
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
opacity: 0.28;
|
||||
background:
|
||||
radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.8), transparent 34%),
|
||||
radial-gradient(circle at 78% 26%, rgba(79, 184, 178, 0.2), transparent 42%),
|
||||
radial-gradient(circle at 42% 82%, rgba(47, 106, 74, 0.14), transparent 36%);
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
opacity: 0.14;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
mask-image: radial-gradient(circle at 50% 30%, black, transparent 78%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--lagoon-deep);
|
||||
text-decoration-color: rgba(50, 143, 151, 0.4);
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #246f76;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.9em;
|
||||
border: 1px solid var(--line);
|
||||
background: color-mix(in oklab, var(--surface-strong) 82%, white 18%);
|
||||
border-radius: 7px;
|
||||
padding: 2px 7px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.page-wrap {
|
||||
width: min(1080px, calc(100% - 2rem));
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.display-title {
|
||||
font-family: "Fraunces", Georgia, serif;
|
||||
}
|
||||
|
||||
.island-shell {
|
||||
border: 1px solid var(--line);
|
||||
background: linear-gradient(165deg, var(--surface-strong), var(--surface));
|
||||
box-shadow:
|
||||
0 1px 0 var(--inset-glint) inset,
|
||||
0 22px 44px rgba(30, 90, 72, 0.1),
|
||||
0 6px 18px rgba(23, 58, 64, 0.08);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: linear-gradient(165deg, color-mix(in oklab, var(--surface-strong) 93%, white 7%), var(--surface));
|
||||
box-shadow:
|
||||
0 1px 0 var(--inset-glint) inset,
|
||||
0 18px 34px rgba(30, 90, 72, 0.1),
|
||||
0 4px 14px rgba(23, 58, 64, 0.06);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in oklab, var(--lagoon-deep) 35%, var(--line));
|
||||
}
|
||||
|
||||
.demo-page {
|
||||
width: min(1080px, calc(100% - 2rem));
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(2.25rem, 6vw, 4.5rem);
|
||||
color: var(--sea-ink);
|
||||
}
|
||||
|
||||
.demo-page-wide {
|
||||
width: min(1280px, calc(100% - 2rem));
|
||||
}
|
||||
|
||||
.demo-center {
|
||||
min-height: calc(100vh - 13rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demo-panel,
|
||||
.demo-card {
|
||||
border: 1px solid var(--line);
|
||||
background: linear-gradient(165deg, var(--surface-strong), var(--surface));
|
||||
box-shadow:
|
||||
0 1px 0 var(--inset-glint) inset,
|
||||
0 18px 34px rgba(30, 90, 72, 0.1),
|
||||
0 4px 14px rgba(23, 58, 64, 0.06);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.demo-panel {
|
||||
border-radius: 1.25rem;
|
||||
padding: clamp(1.25rem, 4vw, 2rem);
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
margin: 0;
|
||||
color: var(--sea-ink);
|
||||
font-size: clamp(1.75rem, 4vw, 2.75rem);
|
||||
line-height: 1.05;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
margin: 0;
|
||||
color: var(--sea-ink);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.demo-muted {
|
||||
color: var(--sea-ink-soft);
|
||||
}
|
||||
|
||||
.demo-input,
|
||||
.demo-select,
|
||||
.demo-textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.75rem;
|
||||
background: color-mix(in oklab, var(--surface-strong) 88%, white 12%);
|
||||
color: var(--sea-ink);
|
||||
outline: none;
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
|
||||
.demo-input,
|
||||
.demo-select {
|
||||
padding: 0.7rem 0.9rem;
|
||||
}
|
||||
|
||||
.demo-input-fit {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.demo-textarea {
|
||||
min-height: 7rem;
|
||||
padding: 0.8rem 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.demo-input:focus,
|
||||
.demo-select:focus,
|
||||
.demo-textarea:focus {
|
||||
border-color: color-mix(in oklab, var(--lagoon-deep) 58%, var(--line));
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--lagoon) 24%, transparent);
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid color-mix(in oklab, var(--lagoon-deep) 34%, var(--line));
|
||||
border-radius: 0.85rem;
|
||||
background: color-mix(in oklab, var(--lagoon) 22%, var(--surface-strong));
|
||||
color: var(--sea-ink);
|
||||
padding: 0.72rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-button:hover {
|
||||
transform: translateY(-1px);
|
||||
background: color-mix(in oklab, var(--lagoon) 30%, var(--surface-strong));
|
||||
}
|
||||
|
||||
.demo-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.demo-button-secondary {
|
||||
border-color: var(--line);
|
||||
background: color-mix(in oklab, var(--surface-strong) 74%, transparent);
|
||||
color: var(--sea-ink-soft);
|
||||
}
|
||||
|
||||
.demo-button-danger {
|
||||
border-color: rgba(196, 71, 71, 0.28);
|
||||
background: rgba(196, 71, 71, 0.1);
|
||||
color: #9f3030;
|
||||
}
|
||||
|
||||
.demo-list-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.9rem;
|
||||
background: color-mix(in oklab, var(--chip-bg) 82%, transparent);
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
|
||||
.demo-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
border: 1px solid var(--chip-line);
|
||||
border-radius: 999px;
|
||||
background: var(--chip-bg);
|
||||
color: var(--sea-ink-soft);
|
||||
padding: 0.35rem 0.65rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.demo-alert {
|
||||
border: 1px solid rgba(193, 126, 42, 0.3);
|
||||
border-radius: 1rem;
|
||||
background: rgba(193, 126, 42, 0.1);
|
||||
color: var(--sea-ink);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-alert-danger {
|
||||
border-color: rgba(196, 71, 71, 0.3);
|
||||
background: rgba(196, 71, 71, 0.1);
|
||||
}
|
||||
|
||||
.demo-code-block {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1rem;
|
||||
background: color-mix(in oklab, var(--chip-bg) 88%, transparent);
|
||||
color: var(--sea-ink);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-table-shell {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1rem;
|
||||
background: color-mix(in oklab, var(--surface-strong) 78%, transparent);
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
color: var(--sea-ink);
|
||||
}
|
||||
|
||||
.demo-table th,
|
||||
.demo-table td {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.demo-table th {
|
||||
background: color-mix(in oklab, var(--chip-bg) 88%, transparent);
|
||||
color: var(--sea-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.demo-table tr:hover td {
|
||||
background: color-mix(in oklab, var(--lagoon) 8%, transparent);
|
||||
}
|
||||
|
||||
button,
|
||||
.island-shell,
|
||||
a {
|
||||
transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.island-kicker {
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-size: 0.69rem;
|
||||
color: var(--kicker);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--sea-ink-soft);
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -6px;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
background: linear-gradient(90deg, var(--lagoon), #7ed3bf);
|
||||
transition: transform 170ms ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.is-active {
|
||||
color: var(--sea-ink);
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.nav-link.is-active::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nav-link::after {
|
||||
bottom: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
border-top: 1px solid var(--line);
|
||||
background: color-mix(in oklab, var(--header-bg) 84%, transparent 16%);
|
||||
}
|
||||
|
||||
.rise-in {
|
||||
animation: rise-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes rise-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 3000,
|
||||
"expectedOutput": "Start simple, ship quickly."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"paths": {
|
||||
"#/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"target": "react"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { devtools } from '@tanstack/devtools-vite'
|
||||
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: { tsconfigPaths: true },
|
||||
plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()],
|
||||
})
|
||||
|
||||
export default config
|
||||
Reference in New Issue
Block a user