@stampui/blocks 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai-chat-shell.d.ts +1 -0
- package/dist/components/ai-chat-shell.js +23 -0
- package/dist/components/prompt-input.d.ts +5 -0
- package/dist/components/prompt-input.js +47 -0
- package/dist/components/registry-card.d.ts +6 -0
- package/dist/components/registry-card.js +15 -0
- package/dist/components/registry-explorer.d.ts +8 -0
- package/dist/components/registry-explorer.js +38 -0
- package/dist/components/token-stream.d.ts +7 -0
- package/dist/components/token-stream.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/manifests.d.ts +3 -0
- package/dist/manifests.js +1666 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +2 -0
- package/package.json +28 -0
- package/src/components/blocks/ai-chat-shell.tsx +97 -0
- package/src/components/blocks/auth-panel.tsx +203 -0
- package/src/components/blocks/feature-grid.tsx +122 -0
- package/src/components/blocks/hero-section.tsx +73 -0
- package/src/components/blocks/notification-center.tsx +185 -0
- package/src/components/blocks/onboarding-flow.tsx +230 -0
- package/src/components/blocks/pricing-section.tsx +135 -0
- package/src/components/blocks/project-command-center.tsx +188 -0
- package/src/components/blocks/prompt-input.tsx +81 -0
- package/src/components/blocks/registry-card.tsx +104 -0
- package/src/components/blocks/registry-explorer.tsx +78 -0
- package/src/components/blocks/settings-layout.tsx +178 -0
- package/src/components/blocks/stats-strip.tsx +100 -0
- package/src/components/blocks/token-stream.tsx +42 -0
- package/src/components/blocks/usage-card.tsx +116 -0
- package/src/components/core/accordion.tsx +58 -0
- package/src/components/core/alert-dialog.tsx +113 -0
- package/src/components/core/alert.tsx +48 -0
- package/src/components/core/animated-number.tsx +77 -0
- package/src/components/core/aspect-ratio.tsx +20 -0
- package/src/components/core/avatar-stack.tsx +61 -0
- package/src/components/core/avatar.tsx +90 -0
- package/src/components/core/badge.tsx +39 -0
- package/src/components/core/breadcrumb.tsx +63 -0
- package/src/components/core/button-group.tsx +37 -0
- package/src/components/core/button.tsx +110 -0
- package/src/components/core/calendar.tsx +143 -0
- package/src/components/core/card.tsx +60 -0
- package/src/components/core/carousel.tsx +170 -0
- package/src/components/core/chart.tsx +377 -0
- package/src/components/core/checkbox.tsx +64 -0
- package/src/components/core/collapsible.tsx +30 -0
- package/src/components/core/combobox.tsx +114 -0
- package/src/components/core/command-box.tsx +22 -0
- package/src/components/core/command.tsx +165 -0
- package/src/components/core/confirm-action.tsx +94 -0
- package/src/components/core/context-menu.tsx +139 -0
- package/src/components/core/copy-button.tsx +41 -0
- package/src/components/core/data-table.tsx +173 -0
- package/src/components/core/date-picker.tsx +73 -0
- package/src/components/core/dialog.tsx +83 -0
- package/src/components/core/drawer.tsx +87 -0
- package/src/components/core/dropdown-menu.tsx +147 -0
- package/src/components/core/empty.tsx +34 -0
- package/src/components/core/field.tsx +39 -0
- package/src/components/core/file-upload.tsx +143 -0
- package/src/components/core/hover-card.tsx +31 -0
- package/src/components/core/inline-edit.tsx +104 -0
- package/src/components/core/input-group.tsx +47 -0
- package/src/components/core/input-otp.tsx +108 -0
- package/src/components/core/input.tsx +37 -0
- package/src/components/core/kbd.tsx +47 -0
- package/src/components/core/label.tsx +28 -0
- package/src/components/core/marquee.tsx +61 -0
- package/src/components/core/menubar.tsx +120 -0
- package/src/components/core/multi-select.tsx +145 -0
- package/src/components/core/native-select.tsx +27 -0
- package/src/components/core/navigation-menu.tsx +130 -0
- package/src/components/core/number-stepper.tsx +80 -0
- package/src/components/core/pagination.tsx +80 -0
- package/src/components/core/password-input.tsx +90 -0
- package/src/components/core/popover.tsx +34 -0
- package/src/components/core/progress.tsx +63 -0
- package/src/components/core/radio-group.tsx +77 -0
- package/src/components/core/resizable.tsx +250 -0
- package/src/components/core/scroll-area.tsx +38 -0
- package/src/components/core/select.tsx +128 -0
- package/src/components/core/separator.tsx +47 -0
- package/src/components/core/sheet.tsx +118 -0
- package/src/components/core/sidebar.tsx +129 -0
- package/src/components/core/skeleton.tsx +32 -0
- package/src/components/core/slider.tsx +97 -0
- package/src/components/core/sonner.tsx +29 -0
- package/src/components/core/spinner.tsx +60 -0
- package/src/components/core/status-pulse.tsx +67 -0
- package/src/components/core/stepper.tsx +111 -0
- package/src/components/core/switch.tsx +72 -0
- package/src/components/core/table.tsx +104 -0
- package/src/components/core/tabs.tsx +55 -0
- package/src/components/core/tag-input.tsx +93 -0
- package/src/components/core/textarea.tsx +44 -0
- package/src/components/core/timeline.tsx +81 -0
- package/src/components/core/toggle-group.tsx +56 -0
- package/src/components/core/toggle.tsx +66 -0
- package/src/components/core/tooltip.tsx +31 -0
- package/src/components/core/typing-indicator.tsx +51 -0
- package/src/index.ts +8 -0
- package/src/manifests.ts +1682 -0
- package/src/types.ts +58 -0
- package/src/ui.ts +13 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
|
+
import { cx } from "@/lib/cx"
|
|
7
|
+
|
|
8
|
+
const avatarStyles = cva(
|
|
9
|
+
"relative flex shrink-0 overflow-hidden rounded-full",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
xs: "h-6 w-6 text-[10px]",
|
|
14
|
+
sm: "h-8 w-8 text-xs",
|
|
15
|
+
md: "h-10 w-10 text-sm",
|
|
16
|
+
lg: "h-12 w-12 text-base",
|
|
17
|
+
xl: "h-16 w-16 text-lg",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { size: "md" },
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export interface AvatarProps
|
|
25
|
+
extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
|
|
26
|
+
VariantProps<typeof avatarStyles> {}
|
|
27
|
+
|
|
28
|
+
const Avatar = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
30
|
+
AvatarProps
|
|
31
|
+
>(({ className, size, ...props }, ref) => (
|
|
32
|
+
<AvatarPrimitive.Root
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cx(avatarStyles({ size }), className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
))
|
|
38
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
39
|
+
|
|
40
|
+
const AvatarImage = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<AvatarPrimitive.Image
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cx("aspect-square h-full w-full object-cover", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
))
|
|
50
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
51
|
+
|
|
52
|
+
const AvatarFallback = React.forwardRef<
|
|
53
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
54
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
55
|
+
>(({ className, ...props }, ref) => (
|
|
56
|
+
<AvatarPrimitive.Fallback
|
|
57
|
+
ref={ref}
|
|
58
|
+
className={cx(
|
|
59
|
+
"flex h-full w-full items-center justify-center rounded-full",
|
|
60
|
+
"bg-surface-3 text-muted-foreground font-medium",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
))
|
|
66
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
67
|
+
|
|
68
|
+
interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
69
|
+
max?: number
|
|
70
|
+
children: React.ReactNode
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function AvatarGroup({ children, max, className, ...props }: AvatarGroupProps) {
|
|
74
|
+
const childArray = React.Children.toArray(children)
|
|
75
|
+
const shown = max ? childArray.slice(0, max) : childArray
|
|
76
|
+
const overflow = max ? childArray.length - max : 0
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={cx("flex -space-x-2", className)} {...props}>
|
|
80
|
+
{shown}
|
|
81
|
+
{overflow > 0 && (
|
|
82
|
+
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-3 border-2 border-background text-xs font-medium text-muted-foreground">
|
|
83
|
+
+{overflow}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cx } from "@/lib/cx"
|
|
4
|
+
|
|
5
|
+
const badgeStyles = cva(
|
|
6
|
+
"inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium tracking-wide transition-colors",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-surface-2 text-foreground border border-border",
|
|
11
|
+
secondary: "bg-surface-2 text-muted-foreground border border-border-soft",
|
|
12
|
+
success: "bg-green-950/60 text-green-400 border border-green-900/50",
|
|
13
|
+
warning: "bg-yellow-950/60 text-yellow-400 border border-yellow-900/50",
|
|
14
|
+
danger: "bg-red-950/60 text-red-400 border border-red-900/50",
|
|
15
|
+
info: "bg-blue-950/60 text-blue-400 border border-blue-900/50",
|
|
16
|
+
outline: "bg-transparent text-muted-foreground border border-border",
|
|
17
|
+
neutral: "bg-surface-2 text-muted-foreground border border-border-soft",
|
|
18
|
+
free: "bg-surface-2 text-muted-foreground border border-border-soft",
|
|
19
|
+
pro: "bg-surface-3 text-foreground border border-border-strong",
|
|
20
|
+
new: "bg-surface-raised text-foreground border border-border",
|
|
21
|
+
signal: "bg-surface-3 text-foreground border border-border-strong",
|
|
22
|
+
locked: "bg-surface-3 text-muted-foreground border border-border",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
variant: "default",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export interface BadgeProps
|
|
32
|
+
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
33
|
+
VariantProps<typeof badgeStyles> {}
|
|
34
|
+
|
|
35
|
+
export function Badge({ className, variant, ...props }: BadgeProps) {
|
|
36
|
+
return (
|
|
37
|
+
<span className={cx(badgeStyles({ variant }), className)} {...props} />
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
4
|
+
import { cx } from "@/lib/cx"
|
|
5
|
+
|
|
6
|
+
export function Breadcrumb({ className, ...props }: React.ComponentPropsWithoutRef<"nav">) {
|
|
7
|
+
return <nav aria-label="breadcrumb" className={className} {...props} />
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function BreadcrumbList({ className, ...props }: React.ComponentPropsWithoutRef<"ol">) {
|
|
11
|
+
return (
|
|
12
|
+
<ol className={cx("flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground", className)} {...props} />
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function BreadcrumbItem({ className, ...props }: React.ComponentPropsWithoutRef<"li">) {
|
|
17
|
+
return <li className={cx("inline-flex items-center gap-1.5", className)} {...props} />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function BreadcrumbLink({
|
|
21
|
+
className,
|
|
22
|
+
asChild,
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }) {
|
|
25
|
+
const Comp = asChild ? Slot : "a"
|
|
26
|
+
return (
|
|
27
|
+
<Comp className={cx("transition-colors hover:text-foreground", className)} {...props} />
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
role="link"
|
|
35
|
+
aria-current="page"
|
|
36
|
+
aria-disabled="true"
|
|
37
|
+
className={cx("font-medium text-foreground", className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function BreadcrumbSeparator({ children, className }: { children?: React.ReactNode; className?: string }) {
|
|
44
|
+
return (
|
|
45
|
+
<li role="presentation" aria-hidden="true" className={cx("text-muted-foreground", className)}>
|
|
46
|
+
{children ?? <ChevronRight className="h-3.5 w-3.5" />}
|
|
47
|
+
</li>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
|
|
52
|
+
return (
|
|
53
|
+
<span
|
|
54
|
+
role="presentation"
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
className={cx("flex h-9 w-9 items-center justify-center", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
60
|
+
<span className="sr-only">More</span>
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cx } from "@/lib/cx"
|
|
3
|
+
|
|
4
|
+
interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
orientation?: "horizontal" | "vertical"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ButtonGroup({
|
|
9
|
+
className,
|
|
10
|
+
orientation = "horizontal",
|
|
11
|
+
...props
|
|
12
|
+
}: ButtonGroupProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
role="group"
|
|
16
|
+
data-orientation={orientation}
|
|
17
|
+
className={cx(
|
|
18
|
+
"inline-flex",
|
|
19
|
+
orientation === "horizontal"
|
|
20
|
+
? [
|
|
21
|
+
"flex-row",
|
|
22
|
+
"[&>*:not(:first-child)]:border-l-0",
|
|
23
|
+
"[&>*:not(:first-child)]:rounded-l-none",
|
|
24
|
+
"[&>*:not(:last-child)]:rounded-r-none",
|
|
25
|
+
]
|
|
26
|
+
: [
|
|
27
|
+
"flex-col",
|
|
28
|
+
"[&>*:not(:first-child)]:border-t-0",
|
|
29
|
+
"[&>*:not(:first-child)]:rounded-t-none",
|
|
30
|
+
"[&>*:not(:last-child)]:rounded-b-none",
|
|
31
|
+
],
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cx } from "@/lib/cx"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Floyka Button Component
|
|
8
|
+
*
|
|
9
|
+
* Design tokens used (defined in globals.css / tailwind.config):
|
|
10
|
+
* --foreground #FAFAFA (primary text)
|
|
11
|
+
* --background #070708 (main bg)
|
|
12
|
+
* --surface-2 #09090B (raised surface)
|
|
13
|
+
* --border #23252A (default border)
|
|
14
|
+
* --border-strong slightly brighter graphite
|
|
15
|
+
* --muted-foreground dimmed text
|
|
16
|
+
*
|
|
17
|
+
* Variants follow Floyka design rules:
|
|
18
|
+
* - primary → white bg / dark text (the only "colored" action)
|
|
19
|
+
* - outline → transparent bg / border / foreground text
|
|
20
|
+
* - ghost → transparent bg / muted text, subtle hover
|
|
21
|
+
* - danger → danger-tinted surface, used for destructive actions
|
|
22
|
+
*
|
|
23
|
+
* All variants:
|
|
24
|
+
* - rounded-lg (12px radius per design rules)
|
|
25
|
+
* - no glow, no thick borders, no scale animations
|
|
26
|
+
* - 150-200ms ease-out transitions
|
|
27
|
+
* - focus-visible ring uses ring-border-strong (NOT blue/neon)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const buttonVariants = cva(
|
|
31
|
+
[
|
|
32
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap",
|
|
33
|
+
"rounded-lg text-sm font-medium",
|
|
34
|
+
"transition-all duration-[150ms] ease-out",
|
|
35
|
+
"outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
36
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
37
|
+
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
38
|
+
],
|
|
39
|
+
{
|
|
40
|
+
variants: {
|
|
41
|
+
size: {
|
|
42
|
+
sm: "h-8 px-3 text-xs rounded-md gap-1.5",
|
|
43
|
+
md: "h-9 px-4",
|
|
44
|
+
lg: "h-10 px-5 text-base",
|
|
45
|
+
icon: "h-9 w-9 rounded-lg",
|
|
46
|
+
"icon-sm": "h-8 w-8 rounded-md",
|
|
47
|
+
},
|
|
48
|
+
variant: {
|
|
49
|
+
/** Primary — white bg, dark text. The strongest CTA. */
|
|
50
|
+
primary:
|
|
51
|
+
"bg-foreground text-background hover:bg-foreground/90",
|
|
52
|
+
|
|
53
|
+
/** Outline — bordered, transparent. Secondary actions. */
|
|
54
|
+
outline:
|
|
55
|
+
"border border-border bg-transparent text-foreground hover:bg-surface-2 hover:border-border-strong",
|
|
56
|
+
|
|
57
|
+
/** Ghost — no border, no bg. Nav actions, icon rows. */
|
|
58
|
+
ghost:
|
|
59
|
+
"bg-transparent text-muted-foreground hover:bg-surface-2 hover:text-foreground",
|
|
60
|
+
|
|
61
|
+
/** Danger — destructive action. Subtle tinted surface. */
|
|
62
|
+
danger:
|
|
63
|
+
"bg-transparent border border-border text-foreground hover:bg-red-950/40 hover:border-red-900/50",
|
|
64
|
+
|
|
65
|
+
/** Rounded — pill-shaped outline. Decorative / marketing CTAs. */
|
|
66
|
+
rounded:
|
|
67
|
+
"border border-border bg-transparent text-foreground hover:bg-surface-2 hover:border-border-strong rounded-full",
|
|
68
|
+
|
|
69
|
+
/** Dashed — dashed border. Draft states, placeholder actions. */
|
|
70
|
+
dashed:
|
|
71
|
+
"border border-dashed border-border bg-transparent text-muted-foreground hover:bg-surface-2 hover:text-foreground hover:border-border-strong",
|
|
72
|
+
|
|
73
|
+
/** Dashed Rounded — pill shape with dashed border. */
|
|
74
|
+
"dashed-rounded":
|
|
75
|
+
"border border-dashed border-border bg-transparent text-muted-foreground hover:bg-surface-2 hover:text-foreground hover:border-border-strong rounded-full",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultVariants: {
|
|
79
|
+
variant: "primary",
|
|
80
|
+
size: "md",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
export interface ButtonProps
|
|
86
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
87
|
+
VariantProps<typeof buttonVariants> {
|
|
88
|
+
/**
|
|
89
|
+
* When true, the button renders as a `Slot` — allowing you to pass any
|
|
90
|
+
* child element (e.g. `<a>` or Next.js `<Link>`) while keeping all
|
|
91
|
+
* button styles applied.
|
|
92
|
+
*/
|
|
93
|
+
asChild?: boolean
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
97
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
98
|
+
const Comp = asChild ? Slot : "button"
|
|
99
|
+
return (
|
|
100
|
+
<Comp
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cx(buttonVariants({ variant, size, className }))}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
Button.displayName = "Button"
|
|
109
|
+
|
|
110
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
5
|
+
import { cx } from "@/lib/cx"
|
|
6
|
+
|
|
7
|
+
const MONTHS = ["January", "February", "March", "April", "May", "June",
|
|
8
|
+
"July", "August", "September", "October", "November", "December"]
|
|
9
|
+
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
|
|
10
|
+
|
|
11
|
+
function getDaysInMonth(year: number, month: number) {
|
|
12
|
+
return new Date(year, month + 1, 0).getDate()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getFirstDayOfMonth(year: number, month: number) {
|
|
16
|
+
return new Date(year, month, 1).getDay()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isSameDay(a: Date, b: Date) {
|
|
20
|
+
return a.getFullYear() === b.getFullYear() &&
|
|
21
|
+
a.getMonth() === b.getMonth() &&
|
|
22
|
+
a.getDate() === b.getDate()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isToday(date: Date) {
|
|
26
|
+
return isSameDay(date, new Date())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CalendarProps {
|
|
30
|
+
selected?: Date
|
|
31
|
+
onSelect?: (date: Date) => void
|
|
32
|
+
disabled?: (date: Date) => boolean
|
|
33
|
+
fromDate?: Date
|
|
34
|
+
toDate?: Date
|
|
35
|
+
className?: string
|
|
36
|
+
initialMonth?: Date
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Calendar({
|
|
40
|
+
selected,
|
|
41
|
+
onSelect,
|
|
42
|
+
disabled,
|
|
43
|
+
fromDate,
|
|
44
|
+
toDate,
|
|
45
|
+
className,
|
|
46
|
+
initialMonth,
|
|
47
|
+
}: CalendarProps) {
|
|
48
|
+
const today = new Date()
|
|
49
|
+
const [viewYear, setViewYear] = React.useState(
|
|
50
|
+
initialMonth?.getFullYear() ?? selected?.getFullYear() ?? today.getFullYear()
|
|
51
|
+
)
|
|
52
|
+
const [viewMonth, setViewMonth] = React.useState(
|
|
53
|
+
initialMonth?.getMonth() ?? selected?.getMonth() ?? today.getMonth()
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
function prevMonth() {
|
|
57
|
+
if (viewMonth === 0) { setViewMonth(11); setViewYear(viewYear - 1) }
|
|
58
|
+
else setViewMonth(viewMonth - 1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nextMonth() {
|
|
62
|
+
if (viewMonth === 11) { setViewMonth(0); setViewYear(viewYear + 1) }
|
|
63
|
+
else setViewMonth(viewMonth + 1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const daysInMonth = getDaysInMonth(viewYear, viewMonth)
|
|
67
|
+
const firstDay = getFirstDayOfMonth(viewYear, viewMonth)
|
|
68
|
+
const cells: (Date | null)[] = [
|
|
69
|
+
...Array(firstDay).fill(null),
|
|
70
|
+
...Array.from({ length: daysInMonth }, (_, i) => new Date(viewYear, viewMonth, i + 1)),
|
|
71
|
+
]
|
|
72
|
+
while (cells.length % 7 !== 0) cells.push(null)
|
|
73
|
+
|
|
74
|
+
function isDisabled(date: Date) {
|
|
75
|
+
if (disabled?.(date)) return true
|
|
76
|
+
if (fromDate && date < fromDate) return true
|
|
77
|
+
if (toDate && date > toDate) return true
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={cx("w-full select-none p-3", className)}>
|
|
83
|
+
{/* Header */}
|
|
84
|
+
<div className="flex items-center justify-between mb-3">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={prevMonth}
|
|
88
|
+
className="flex h-7 w-7 items-center justify-center rounded-md border border-border text-muted-foreground hover:bg-surface-2 hover:text-foreground transition-colors"
|
|
89
|
+
>
|
|
90
|
+
<ChevronLeft className="h-4 w-4" />
|
|
91
|
+
</button>
|
|
92
|
+
<span className="text-sm font-medium">
|
|
93
|
+
{MONTHS[viewMonth]} {viewYear}
|
|
94
|
+
</span>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={nextMonth}
|
|
98
|
+
className="flex h-7 w-7 items-center justify-center rounded-md border border-border text-muted-foreground hover:bg-surface-2 hover:text-foreground transition-colors"
|
|
99
|
+
>
|
|
100
|
+
<ChevronRight className="h-4 w-4" />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Day headers */}
|
|
105
|
+
<div className="grid grid-cols-7 mb-1">
|
|
106
|
+
{DAYS.map((d) => (
|
|
107
|
+
<div key={d} className="flex h-8 items-center justify-center text-xs font-medium text-muted-foreground">
|
|
108
|
+
{d}
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Day cells */}
|
|
114
|
+
<div className="grid grid-cols-7 gap-y-0.5">
|
|
115
|
+
{cells.map((date, i) => {
|
|
116
|
+
if (!date) return <div key={`empty-${i}`} />
|
|
117
|
+
const sel = selected && isSameDay(date, selected)
|
|
118
|
+
const tod = isToday(date)
|
|
119
|
+
const dis = isDisabled(date)
|
|
120
|
+
return (
|
|
121
|
+
<button
|
|
122
|
+
key={date.toISOString()}
|
|
123
|
+
type="button"
|
|
124
|
+
disabled={dis}
|
|
125
|
+
onClick={() => !dis && onSelect?.(date)}
|
|
126
|
+
className={cx(
|
|
127
|
+
"flex h-8 w-full items-center justify-center rounded-md text-sm transition-colors",
|
|
128
|
+
"disabled:pointer-events-none disabled:opacity-30",
|
|
129
|
+
sel
|
|
130
|
+
? "bg-foreground text-background font-medium"
|
|
131
|
+
: tod
|
|
132
|
+
? "border border-border font-medium text-foreground hover:bg-surface-2"
|
|
133
|
+
: "text-foreground hover:bg-surface-2"
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
{date.getDate()}
|
|
137
|
+
</button>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cx } from "@/lib/cx"
|
|
4
|
+
|
|
5
|
+
const cardStyles = cva(
|
|
6
|
+
"flex flex-col rounded-xl overflow-hidden text-card-foreground",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
surface: "bg-card border border-border",
|
|
11
|
+
elevated: "bg-surface-2 border border-border-strong",
|
|
12
|
+
command: "bg-surface-3 border border-border-strong",
|
|
13
|
+
preview: "bg-surface-2 border border-dashed border-border-soft",
|
|
14
|
+
muted: "bg-muted border border-transparent",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: "surface",
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export interface CardProps
|
|
24
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
25
|
+
VariantProps<typeof cardStyles> {}
|
|
26
|
+
|
|
27
|
+
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
28
|
+
({ className, variant, ...props }, ref) => (
|
|
29
|
+
<div ref={ref} className={cx(cardStyles({ variant }), className)} {...props} />
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
Card.displayName = "Card"
|
|
33
|
+
|
|
34
|
+
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
35
|
+
({ className, ...props }, ref) => (
|
|
36
|
+
<div ref={ref} className={cx("px-6 py-5 flex flex-col gap-1.5", className)} {...props} />
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
CardHeader.displayName = "CardHeader"
|
|
40
|
+
|
|
41
|
+
export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
42
|
+
({ className, ...props }, ref) => (
|
|
43
|
+
<h3 ref={ref} className={cx("text-base font-semibold leading-tight text-foreground", className)} {...props} />
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
CardTitle.displayName = "CardTitle"
|
|
47
|
+
|
|
48
|
+
export const CardBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
49
|
+
({ className, ...props }, ref) => (
|
|
50
|
+
<div ref={ref} className={cx("px-6 pb-6 flex-1", className)} {...props} />
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
CardBody.displayName = "CardBody"
|
|
54
|
+
|
|
55
|
+
export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
56
|
+
({ className, ...props }, ref) => (
|
|
57
|
+
<div ref={ref} className={cx("px-6 py-4 bg-muted/10 border-t border-border/50 flex items-center", className)} {...props} />
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
CardFooter.displayName = "CardFooter"
|