Stepper de ejemplo integrado
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -12,6 +12,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@stepperize/react": "^5.1.9",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-devtools": "^0.7.0",
|
"@tanstack/react-devtools": "^0.7.0",
|
||||||
"@tanstack/react-query": "^5.66.5",
|
"@tanstack/react-query": "^5.66.5",
|
||||||
@@ -362,6 +363,10 @@
|
|||||||
|
|
||||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
|
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
|
||||||
|
|
||||||
|
"@stepperize/core": ["@stepperize/core@1.2.7", "", { "peerDependencies": { "typescript": ">=5.0.2" } }, "sha512-XiUwLZ0XRAfaDK6AzWVgqvI/BcrylyplhUXKO8vzgRw0FTmyMKHAAbQLDvU//ZJAqnmG2cSLZDSkcwLxU5zSYA=="],
|
||||||
|
|
||||||
|
"@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="],
|
||||||
|
|
||||||
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
|
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@stepperize/react": "^5.1.9",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-devtools": "^0.7.0",
|
"@tanstack/react-devtools": "^0.7.0",
|
||||||
"@tanstack/react-query": "^5.66.5",
|
"@tanstack/react-query": "^5.66.5",
|
||||||
|
|||||||
42
src/components/planes/App.css
Normal file
42
src/components/planes/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
163
src/components/planes/stepper.tsx
Normal file
163
src/components/planes/stepper.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { defineStepper } from '@stepperize/react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
// import './App.css'
|
||||||
|
|
||||||
|
const { useStepper, steps, utils } = defineStepper(
|
||||||
|
{
|
||||||
|
id: 'shipping',
|
||||||
|
title: 'Shipping',
|
||||||
|
description: 'Enter your shipping details',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'payment',
|
||||||
|
title: 'Payment',
|
||||||
|
description: 'Enter your payment details',
|
||||||
|
},
|
||||||
|
{ id: 'complete', title: 'Complete', description: 'Checkout complete' },
|
||||||
|
)
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const stepper = useStepper()
|
||||||
|
|
||||||
|
const currentIndex = utils.getIndex(stepper.current.id)
|
||||||
|
return (
|
||||||
|
<div className="w-[450px] space-y-6 rounded-lg border p-6">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="text-lg font-medium">Checkout</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
Step {currentIndex + 1} of {steps.length}
|
||||||
|
</span>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Checkout Steps" className="group my-4">
|
||||||
|
<ol
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
>
|
||||||
|
{stepper.all.map((step, index, array) => (
|
||||||
|
<React.Fragment key={step.id}>
|
||||||
|
<li className="flex flex-shrink-0 items-center gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
variant={index <= currentIndex ? 'default' : 'secondary'}
|
||||||
|
aria-current={
|
||||||
|
stepper.current.id === step.id ? 'step' : undefined
|
||||||
|
}
|
||||||
|
aria-posinset={index + 1}
|
||||||
|
aria-setsize={steps.length}
|
||||||
|
aria-selected={stepper.current.id === step.id}
|
||||||
|
className="flex size-10 items-center justify-center rounded-full"
|
||||||
|
onClick={() => stepper.goTo(step.id)}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium">{step.title}</span>
|
||||||
|
</li>
|
||||||
|
{index < array.length - 1 && (
|
||||||
|
<Separator
|
||||||
|
className={`flex-1 ${
|
||||||
|
index < currentIndex ? 'bg-primary' : 'bg-muted'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{stepper.switch({
|
||||||
|
shipping: () => <ShippingComponent />,
|
||||||
|
payment: () => <PaymentComponent />,
|
||||||
|
complete: () => <CompleteComponent />,
|
||||||
|
})}
|
||||||
|
{!stepper.isLast ? (
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={stepper.prev}
|
||||||
|
disabled={stepper.isFirst}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button onClick={stepper.next}>
|
||||||
|
{stepper.isLast ? 'Complete' : 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button onClick={stepper.reset}>Reset</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShippingComponent = () => {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label htmlFor="name" className="text-start text-sm font-medium">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<Input id="name" placeholder="John Doe" className="w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label htmlFor="address" className="text-start text-sm font-medium">
|
||||||
|
Address
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
id="address"
|
||||||
|
placeholder="123 Main St, Anytown USA"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentComponent = () => {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label htmlFor="card-number" className="text-start text-sm font-medium">
|
||||||
|
Card Number
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="card-number"
|
||||||
|
placeholder="4111 1111 1111 1111"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="expiry-date"
|
||||||
|
className="text-start text-sm font-medium"
|
||||||
|
>
|
||||||
|
Expiry Date
|
||||||
|
</label>
|
||||||
|
<Input id="expiry-date" placeholder="MM/YY" className="w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label htmlFor="cvc" className="text-start text-sm font-medium">
|
||||||
|
CVC
|
||||||
|
</label>
|
||||||
|
<Input id="cvc" placeholder="123" className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CompleteComponent = () => {
|
||||||
|
return <h3 className="py-4 text-lg font-medium">Stepper complete 🔥</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
534
src/components/stepper.tsx
Normal file
534
src/components/stepper.tsx
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import * as Stepperize from '@stepperize/react'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const StepperContext = React.createContext<Stepper.ConfigProps | null>(null)
|
||||||
|
|
||||||
|
const useStepperProvider = (): Stepper.ConfigProps => {
|
||||||
|
const context = React.useContext(StepperContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useStepper must be used within a StepperProvider.')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineStepper = <const Steps extends Array<Stepperize.Step>>(
|
||||||
|
...steps: Steps
|
||||||
|
): Stepper.DefineProps<Steps> => {
|
||||||
|
const { Scoped, useStepper, ...rest } = Stepperize.defineStepper(...steps)
|
||||||
|
|
||||||
|
const StepperContainer = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<'div'>, 'children'> & {
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((props: { methods: Stepperize.Stepper<Steps> }) => React.ReactNode)
|
||||||
|
}) => {
|
||||||
|
const methods = useStepper()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
date-component="stepper"
|
||||||
|
className={cn('w-full', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{typeof children === 'function' ? children({ methods }) : children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
useStepper,
|
||||||
|
Stepper: {
|
||||||
|
Provider: ({
|
||||||
|
variant = 'horizontal',
|
||||||
|
labelOrientation = 'horizontal',
|
||||||
|
tracking = false,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StepperContext.Provider
|
||||||
|
value={{ variant, labelOrientation, tracking }}
|
||||||
|
>
|
||||||
|
<Scoped
|
||||||
|
initialStep={props.initialStep}
|
||||||
|
initialMetadata={props.initialMetadata}
|
||||||
|
>
|
||||||
|
<StepperContainer className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</StepperContainer>
|
||||||
|
</Scoped>
|
||||||
|
</StepperContext.Provider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Navigation: ({
|
||||||
|
children,
|
||||||
|
'aria-label': ariaLabel = 'Stepper Navigation',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { variant } = useStepperProvider()
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
date-component="stepper-navigation"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
role="tablist"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
date-component="stepper-navigation-list"
|
||||||
|
className={classForNavigationList({ variant: variant })}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Step: ({ children, className, icon, ...props }) => {
|
||||||
|
const { variant, labelOrientation } = useStepperProvider()
|
||||||
|
const { current } = useStepper()
|
||||||
|
|
||||||
|
const utils = rest.utils
|
||||||
|
const steps = rest.steps
|
||||||
|
|
||||||
|
const stepIndex = utils.getIndex(props.of)
|
||||||
|
const step = steps[stepIndex]
|
||||||
|
const currentIndex = utils.getIndex(current.id)
|
||||||
|
|
||||||
|
const isLast = utils.getLast().id === props.of
|
||||||
|
const isActive = current.id === props.of
|
||||||
|
|
||||||
|
const dataState = getStepState(currentIndex, stepIndex)
|
||||||
|
const childMap = useStepChildren(children)
|
||||||
|
|
||||||
|
const title = childMap.get('title')
|
||||||
|
const description = childMap.get('description')
|
||||||
|
const panel = childMap.get('panel')
|
||||||
|
|
||||||
|
if (variant === 'circle') {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
date-component="stepper-step"
|
||||||
|
className={cn(
|
||||||
|
'flex shrink-0 items-center gap-4 rounded-md transition-colors',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CircleStepIndicator
|
||||||
|
currentStep={stepIndex + 1}
|
||||||
|
totalSteps={steps.length}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
date-component="stepper-step-content"
|
||||||
|
className="flex flex-col items-start gap-1"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<li
|
||||||
|
date-component="stepper-step"
|
||||||
|
className={cn([
|
||||||
|
'group peer relative flex items-center gap-2',
|
||||||
|
'data-[variant=vertical]:flex-row',
|
||||||
|
'data-[label-orientation=vertical]:w-full',
|
||||||
|
'data-[label-orientation=vertical]:flex-col',
|
||||||
|
'data-[label-orientation=vertical]:justify-center',
|
||||||
|
])}
|
||||||
|
data-variant={variant}
|
||||||
|
data-label-orientation={labelOrientation}
|
||||||
|
data-state={dataState}
|
||||||
|
data-disabled={props.disabled}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id={`step-${step.id}`}
|
||||||
|
date-component="stepper-step-indicator"
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
tabIndex={dataState !== 'inactive' ? 0 : -1}
|
||||||
|
className="rounded-full"
|
||||||
|
variant={dataState !== 'inactive' ? 'default' : 'secondary'}
|
||||||
|
size="icon"
|
||||||
|
aria-controls={`step-panel-${props.of}`}
|
||||||
|
aria-current={isActive ? 'step' : undefined}
|
||||||
|
aria-posinset={stepIndex + 1}
|
||||||
|
aria-setsize={steps.length}
|
||||||
|
aria-selected={isActive}
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
onStepKeyDown(
|
||||||
|
e,
|
||||||
|
utils.getNext(props.of),
|
||||||
|
utils.getPrev(props.of),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ?? stepIndex + 1}
|
||||||
|
</Button>
|
||||||
|
{variant === 'horizontal' && labelOrientation === 'vertical' && (
|
||||||
|
<StepperSeparator
|
||||||
|
orientation="horizontal"
|
||||||
|
labelOrientation={labelOrientation}
|
||||||
|
isLast={isLast}
|
||||||
|
state={dataState}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
date-component="stepper-step-content"
|
||||||
|
className="flex flex-col items-start"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{variant === 'horizontal' && labelOrientation === 'horizontal' && (
|
||||||
|
<StepperSeparator
|
||||||
|
orientation="horizontal"
|
||||||
|
isLast={isLast}
|
||||||
|
state={dataState}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === 'vertical' && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{!isLast && (
|
||||||
|
<div className="flex justify-center ps-[calc(var(--spacing)_*_4.5_-_1px)]">
|
||||||
|
<StepperSeparator
|
||||||
|
orientation="vertical"
|
||||||
|
isLast={isLast}
|
||||||
|
state={dataState}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="my-3 flex-1 ps-4">{panel}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
Panel: ({ children, asChild, ...props }) => {
|
||||||
|
const Comp = asChild ? Slot : 'div'
|
||||||
|
const { tracking } = useStepperProvider()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-step-panel"
|
||||||
|
ref={(node) => scrollIntoStepperPanel(node, tracking)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Controls: ({ children, className, asChild, ...props }) => {
|
||||||
|
const Comp = asChild ? Slot : 'div'
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-controls"
|
||||||
|
className={cn('flex justify-end gap-4', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Title = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
asChild,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'h4'> & { asChild?: boolean }) => {
|
||||||
|
const Comp = asChild ? Slot : 'h4'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-step-title"
|
||||||
|
className={cn('text-base font-medium', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Description = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
asChild,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'p'> & { asChild?: boolean }) => {
|
||||||
|
const Comp = asChild ? Slot : 'p'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-step-description"
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepperSeparator = ({
|
||||||
|
orientation,
|
||||||
|
isLast,
|
||||||
|
labelOrientation,
|
||||||
|
state,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
isLast: boolean
|
||||||
|
state: string
|
||||||
|
disabled?: boolean
|
||||||
|
} & VariantProps<typeof classForSeparator>) => {
|
||||||
|
if (isLast) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
date-component="stepper-separator"
|
||||||
|
data-orientation={orientation}
|
||||||
|
data-state={state}
|
||||||
|
data-disabled={disabled}
|
||||||
|
role="separator"
|
||||||
|
tabIndex={-1}
|
||||||
|
className={classForSeparator({ orientation, labelOrientation })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CircleStepIndicator = ({
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
|
size = 80,
|
||||||
|
strokeWidth = 6,
|
||||||
|
}: Stepper.CircleStepIndicatorProps) => {
|
||||||
|
const radius = (size - strokeWidth) / 2
|
||||||
|
const circumference = radius * 2 * Math.PI
|
||||||
|
const fillPercentage = (currentStep / totalSteps) * 100
|
||||||
|
const dashOffset = circumference - (circumference * fillPercentage) / 100
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
date-component="stepper-step-indicator"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={currentStep}
|
||||||
|
aria-valuemin={1}
|
||||||
|
aria-valuemax={totalSteps}
|
||||||
|
tabIndex={-1}
|
||||||
|
className="relative inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg width={size} height={size}>
|
||||||
|
<title>Step Indicator</title>
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={dashOffset}
|
||||||
|
className="text-primary transition-all duration-300 ease-in-out"
|
||||||
|
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-medium" aria-live="polite">
|
||||||
|
{currentStep} of {totalSteps}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const classForNavigationList = cva('flex gap-2', {
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
horizontal: 'flex-row items-center justify-between',
|
||||||
|
vertical: 'flex-col',
|
||||||
|
circle: 'flex-row items-center justify-between',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const classForSeparator = cva(
|
||||||
|
[
|
||||||
|
'bg-muted',
|
||||||
|
'data-[state=completed]:bg-primary data-[disabled]:opacity-50',
|
||||||
|
'transition-all duration-300 ease-in-out',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
horizontal: 'h-0.5 flex-1',
|
||||||
|
vertical: 'h-full w-0.5',
|
||||||
|
},
|
||||||
|
labelOrientation: {
|
||||||
|
vertical:
|
||||||
|
'absolute top-5 right-[calc(-50%+20px)] left-[calc(50%+30px)] block shrink-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function scrollIntoStepperPanel(
|
||||||
|
node: HTMLDivElement | null,
|
||||||
|
tracking?: boolean,
|
||||||
|
) {
|
||||||
|
if (tracking) {
|
||||||
|
node?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStepChildren = (children: React.ReactNode) => {
|
||||||
|
return React.useMemo(() => extractChildren(children), [children])
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractChildren = (children: React.ReactNode) => {
|
||||||
|
const childrenArray = React.Children.toArray(children)
|
||||||
|
const map = new Map<string, React.ReactNode>()
|
||||||
|
|
||||||
|
for (const child of childrenArray) {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
if (child.type === Title) {
|
||||||
|
map.set('title', child)
|
||||||
|
} else if (child.type === Description) {
|
||||||
|
map.set('description', child)
|
||||||
|
} else {
|
||||||
|
map.set('panel', child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStepKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLButtonElement>,
|
||||||
|
nextStep: Stepperize.Step,
|
||||||
|
prevStep: Stepperize.Step,
|
||||||
|
) => {
|
||||||
|
const { key } = e
|
||||||
|
const directions = {
|
||||||
|
next: ['ArrowRight', 'ArrowDown'],
|
||||||
|
prev: ['ArrowLeft', 'ArrowUp'],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directions.next.includes(key) || directions.prev.includes(key)) {
|
||||||
|
const direction = directions.next.includes(key) ? 'next' : 'prev'
|
||||||
|
const step = direction === 'next' ? nextStep : prevStep
|
||||||
|
|
||||||
|
if (!step) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepElement = document.getElementById(`step-${step.id}`)
|
||||||
|
if (!stepElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive =
|
||||||
|
stepElement.parentElement?.getAttribute('data-state') !== 'inactive'
|
||||||
|
if (isActive || direction === 'prev') {
|
||||||
|
stepElement.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStepState = (currentIndex: number, stepIndex: number) => {
|
||||||
|
if (currentIndex === stepIndex) {
|
||||||
|
return 'active'
|
||||||
|
}
|
||||||
|
if (currentIndex > stepIndex) {
|
||||||
|
return 'completed'
|
||||||
|
}
|
||||||
|
return 'inactive'
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Stepper {
|
||||||
|
export type StepperVariant = 'horizontal' | 'vertical' | 'circle'
|
||||||
|
export type StepperLabelOrientation = 'horizontal' | 'vertical'
|
||||||
|
|
||||||
|
export type ConfigProps = {
|
||||||
|
variant?: StepperVariant
|
||||||
|
labelOrientation?: StepperLabelOrientation
|
||||||
|
tracking?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefineProps<Steps extends Array<Stepperize.Step>> = Omit<
|
||||||
|
Stepperize.StepperReturn<Steps>,
|
||||||
|
'Scoped'
|
||||||
|
> & {
|
||||||
|
Stepper: {
|
||||||
|
Provider: (
|
||||||
|
props: Omit<Stepperize.ScopedProps<Steps>, 'children'> &
|
||||||
|
Omit<React.ComponentProps<'div'>, 'children'> &
|
||||||
|
Stepper.ConfigProps & {
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((props: {
|
||||||
|
methods: Stepperize.Stepper<Steps>
|
||||||
|
}) => React.ReactNode)
|
||||||
|
},
|
||||||
|
) => React.ReactElement
|
||||||
|
Navigation: (props: React.ComponentProps<'nav'>) => React.ReactElement
|
||||||
|
Step: (
|
||||||
|
props: React.ComponentProps<'button'> & {
|
||||||
|
of: Stepperize.Get.Id<Steps>
|
||||||
|
icon?: React.ReactNode
|
||||||
|
},
|
||||||
|
) => React.ReactElement
|
||||||
|
Title: (props: AsChildProps<'h4'>) => React.ReactElement
|
||||||
|
Description: (props: AsChildProps<'p'>) => React.ReactElement
|
||||||
|
Panel: (props: AsChildProps<'div'>) => React.ReactElement
|
||||||
|
Controls: (props: AsChildProps<'div'>) => React.ReactElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CircleStepIndicatorProps = {
|
||||||
|
currentStep: number
|
||||||
|
totalSteps: number
|
||||||
|
size?: number
|
||||||
|
strokeWidth?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsChildProps<T extends React.ElementType> = React.ComponentProps<T> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export { defineStepper }
|
||||||
26
src/components/ui/separator.tsx
Normal file
26
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -9,18 +9,14 @@
|
|||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// 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 rootRouteImport } from './routes/__root'
|
||||||
import { Route as PlanesRouteImport } from './routes/planes'
|
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as DashboardRouteImport } from './routes/dashboard'
|
import { Route as DashboardRouteImport } from './routes/dashboard'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as PlanesIndexRouteImport } from './routes/planes/index'
|
|
||||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||||
|
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
|
||||||
|
import { Route as PlanesPlanIdRouteRouteImport } from './routes/planes/$planId/route'
|
||||||
|
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
||||||
|
|
||||||
const PlanesRoute = PlanesRouteImport.update({
|
|
||||||
id: '/planes',
|
|
||||||
path: '/planes',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@@ -36,40 +32,54 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const PlanesIndexRoute = PlanesIndexRouteImport.update({
|
|
||||||
id: '/',
|
|
||||||
path: '/',
|
|
||||||
getParentRoute: () => PlanesRoute,
|
|
||||||
} as any)
|
|
||||||
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
|
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
|
||||||
id: '/demo/tanstack-query',
|
id: '/demo/tanstack-query',
|
||||||
path: '/demo/tanstack-query',
|
path: '/demo/tanstack-query',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const PlanesListaRouteRoute = PlanesListaRouteRouteImport.update({
|
||||||
|
id: '/planes/_lista',
|
||||||
|
path: '/planes',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdRouteRoute = PlanesPlanIdRouteRouteImport.update({
|
||||||
|
id: '/planes/$planId',
|
||||||
|
path: '/planes/$planId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
|
||||||
|
id: '/nuevo',
|
||||||
|
path: '/nuevo',
|
||||||
|
getParentRoute: () => PlanesListaRouteRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/planes': typeof PlanesRouteWithChildren
|
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
||||||
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes/': typeof PlanesIndexRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
||||||
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes': typeof PlanesIndexRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/planes': typeof PlanesRouteWithChildren
|
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
||||||
|
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes/': typeof PlanesIndexRoute
|
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -77,38 +87,41 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
|
| '/planes/$planId'
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/planes/'
|
| '/planes/nuevo'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/dashboard' | '/login' | '/demo/tanstack-query' | '/planes'
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/login'
|
||||||
|
| '/planes/$planId'
|
||||||
|
| '/planes'
|
||||||
|
| '/demo/tanstack-query'
|
||||||
|
| '/planes/nuevo'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/planes'
|
| '/planes/$planId'
|
||||||
|
| '/planes/_lista'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/planes/'
|
| '/planes/_lista/nuevo'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
DashboardRoute: typeof DashboardRoute
|
DashboardRoute: typeof DashboardRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
PlanesRoute: typeof PlanesRouteWithChildren
|
PlanesPlanIdRouteRoute: typeof PlanesPlanIdRouteRoute
|
||||||
|
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
|
||||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
'/planes': {
|
|
||||||
id: '/planes'
|
|
||||||
path: '/planes'
|
|
||||||
fullPath: '/planes'
|
|
||||||
preLoaderRoute: typeof PlanesRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/login': {
|
'/login': {
|
||||||
id: '/login'
|
id: '/login'
|
||||||
path: '/login'
|
path: '/login'
|
||||||
@@ -130,13 +143,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/planes/': {
|
|
||||||
id: '/planes/'
|
|
||||||
path: '/'
|
|
||||||
fullPath: '/planes/'
|
|
||||||
preLoaderRoute: typeof PlanesIndexRouteImport
|
|
||||||
parentRoute: typeof PlanesRoute
|
|
||||||
}
|
|
||||||
'/demo/tanstack-query': {
|
'/demo/tanstack-query': {
|
||||||
id: '/demo/tanstack-query'
|
id: '/demo/tanstack-query'
|
||||||
path: '/demo/tanstack-query'
|
path: '/demo/tanstack-query'
|
||||||
@@ -144,25 +150,47 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DemoTanstackQueryRouteImport
|
preLoaderRoute: typeof DemoTanstackQueryRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/planes/_lista': {
|
||||||
|
id: '/planes/_lista'
|
||||||
|
path: '/planes'
|
||||||
|
fullPath: '/planes'
|
||||||
|
preLoaderRoute: typeof PlanesListaRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId': {
|
||||||
|
id: '/planes/$planId'
|
||||||
|
path: '/planes/$planId'
|
||||||
|
fullPath: '/planes/$planId'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/_lista/nuevo': {
|
||||||
|
id: '/planes/_lista/nuevo'
|
||||||
|
path: '/nuevo'
|
||||||
|
fullPath: '/planes/nuevo'
|
||||||
|
preLoaderRoute: typeof PlanesListaNuevoRouteImport
|
||||||
|
parentRoute: typeof PlanesListaRouteRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlanesRouteChildren {
|
interface PlanesListaRouteRouteChildren {
|
||||||
PlanesIndexRoute: typeof PlanesIndexRoute
|
PlanesListaNuevoRoute: typeof PlanesListaNuevoRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlanesRouteChildren: PlanesRouteChildren = {
|
const PlanesListaRouteRouteChildren: PlanesListaRouteRouteChildren = {
|
||||||
PlanesIndexRoute: PlanesIndexRoute,
|
PlanesListaNuevoRoute: PlanesListaNuevoRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlanesRouteWithChildren =
|
const PlanesListaRouteRouteWithChildren =
|
||||||
PlanesRoute._addFileChildren(PlanesRouteChildren)
|
PlanesListaRouteRoute._addFileChildren(PlanesListaRouteRouteChildren)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
DashboardRoute: DashboardRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
PlanesRoute: PlanesRouteWithChildren,
|
PlanesPlanIdRouteRoute: PlanesPlanIdRouteRoute,
|
||||||
|
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
|
||||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|||||||
10
src/routes/planes/$planId/route.tsx
Normal file
10
src/routes/planes/$planId/route.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/$planId')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { planId } = Route.useParams()
|
||||||
|
return <div>Hello "/planes/{planId}"!</div>
|
||||||
|
}
|
||||||
29
src/routes/planes/_lista/nuevo.tsx
Normal file
29
src/routes/planes/_lista/nuevo.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import CheckoutStepper from '@/components/planes/stepper'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/_lista/nuevo')({
|
||||||
|
component: NuevoPlanModal,
|
||||||
|
})
|
||||||
|
|
||||||
|
function NuevoPlanModal() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// Navegamos de regreso a la lista manteniendo el scroll donde estaba
|
||||||
|
navigate({ to: '/planes', resetScroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
{/* DialogContent es la "caja" blanca del modal.
|
||||||
|
Le damos un ancho máximo un poco mayor a tu stepper (que mide 450px)
|
||||||
|
para que quepa cómodamente.
|
||||||
|
*/}
|
||||||
|
<DialogContent className="p-6 sm:max-w-[500px]">
|
||||||
|
<CheckoutStepper />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, Link } from '@tanstack/react-router'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import BarraBusqueda from '@/components/planes/BarraBusqueda'
|
|||||||
import Filtro from '@/components/planes/Filtro'
|
import Filtro from '@/components/planes/Filtro'
|
||||||
import PlanEstudiosCard from '@/components/planes/PlanEstudiosCard'
|
import PlanEstudiosCard from '@/components/planes/PlanEstudiosCard'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes')({
|
export const Route = createFileRoute('/planes/_lista')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -313,6 +313,10 @@ function RouteComponent() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Link to="/planes/nuevo" className="text-blue-500" resetScroll={false}>
|
||||||
|
Nuevo plan de estudios
|
||||||
|
</Link>
|
||||||
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
import { AppLayout } from '@/components/layout/AppLayout'
|
|
||||||
import { PlanGrid } from '@/components/plans/PlanGrid'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/')({
|
|
||||||
component: PlanesPage,
|
|
||||||
})
|
|
||||||
|
|
||||||
function PlanesPage() {
|
|
||||||
return (
|
|
||||||
<AppLayout>
|
|
||||||
<PlanGrid />
|
|
||||||
</AppLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user