@smicolon/fasla-ui 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -0
- package/bin/fasla-ui.mjs +54 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +241 -0
- package/dist/lib/utils.d.ts +9 -0
- package/dist/lib/utils.js +9 -0
- package/dist/motion/index.d.ts +33 -0
- package/dist/motion/index.js +59 -0
- package/dist/registry/index.d.ts +17 -0
- package/dist/registry/index.js +145 -0
- package/dist/tokens/index.d.ts +55 -0
- package/dist/tokens/index.js +34 -0
- package/package.json +97 -0
- package/registry/blocks/app-shell/AppShell.tsx +66 -0
- package/registry/blocks/app-shell/index.ts +1 -0
- package/registry/blocks/data-table/DataTable.tsx +292 -0
- package/registry/blocks/data-table/index.ts +7 -0
- package/registry/blocks/empty-state/EmptyState.test.tsx +120 -0
- package/registry/blocks/empty-state/EmptyState.tsx +164 -0
- package/registry/blocks/empty-state/index.ts +6 -0
- package/registry/blocks/form-section/FormSection.tsx +140 -0
- package/registry/blocks/form-section/index.ts +8 -0
- package/registry/blocks/navbar/Navbar.tsx +190 -0
- package/registry/blocks/navbar/index.ts +16 -0
- package/registry/blocks/page-header/PageHeader.tsx +64 -0
- package/registry/blocks/page-header/index.ts +1 -0
- package/registry/blocks/sidebar/Sidebar.tsx +202 -0
- package/registry/blocks/sidebar/index.ts +16 -0
- package/registry/blocks/stats-card/StatsCard.tsx +134 -0
- package/registry/blocks/stats-card/index.ts +6 -0
- package/registry/effects/animated-gradient/animated-gradient.tsx +136 -0
- package/registry/effects/animated-gradient/index.ts +6 -0
- package/registry/effects/border-beam/border-beam.tsx +132 -0
- package/registry/effects/border-beam/index.ts +6 -0
- package/registry/effects/glow-card/glow-card.tsx +145 -0
- package/registry/effects/glow-card/index.ts +6 -0
- package/registry/effects/shimmer-button/index.ts +1 -0
- package/registry/effects/shimmer-button/shimmer-button.tsx +91 -0
- package/registry/effects/spotlight/index.ts +6 -0
- package/registry/effects/spotlight/spotlight.tsx +162 -0
- package/registry/effects/text-reveal/index.ts +6 -0
- package/registry/effects/text-reveal/text-reveal.tsx +139 -0
- package/registry/effects/typewriter-text/index.ts +6 -0
- package/registry/effects/typewriter-text/typewriter-text.tsx +220 -0
- package/registry/ui/avatar/avatar.tsx +135 -0
- package/registry/ui/avatar/index.ts +1 -0
- package/registry/ui/badge/badge.test.tsx +51 -0
- package/registry/ui/badge/badge.tsx +39 -0
- package/registry/ui/badge/index.ts +1 -0
- package/registry/ui/button/button.test.tsx +87 -0
- package/registry/ui/button/button.tsx +88 -0
- package/registry/ui/button/index.ts +1 -0
- package/registry/ui/card/card.test.tsx +119 -0
- package/registry/ui/card/card.tsx +75 -0
- package/registry/ui/card/index.ts +8 -0
- package/registry/ui/checkbox/checkbox.tsx +113 -0
- package/registry/ui/checkbox/index.ts +1 -0
- package/registry/ui/combobox/combobox.tsx +410 -0
- package/registry/ui/combobox/index.ts +1 -0
- package/registry/ui/input/index.ts +1 -0
- package/registry/ui/input/input.test.tsx +78 -0
- package/registry/ui/input/input.tsx +76 -0
- package/registry/ui/select/index.ts +1 -0
- package/registry/ui/select/select.tsx +94 -0
- package/registry/ui/skeleton/index.ts +7 -0
- package/registry/ui/skeleton/skeleton.test.tsx +104 -0
- package/registry/ui/skeleton/skeleton.tsx +88 -0
- package/registry/ui/switch/index.ts +1 -0
- package/registry/ui/switch/switch.tsx +89 -0
- package/registry/ui/tabs/index.ts +10 -0
- package/registry/ui/tabs/tabs.tsx +150 -0
- package/registry/ui/textarea/index.ts +1 -0
- package/registry/ui/textarea/textarea.tsx +73 -0
- package/registry.json +247 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
3
|
+
import { EmptyState, EmptySearchResults, EmptyData } from "./EmptyState"
|
|
4
|
+
|
|
5
|
+
describe("EmptyState", () => {
|
|
6
|
+
it("renders with required title prop", () => {
|
|
7
|
+
render(<EmptyState title="No items found" />)
|
|
8
|
+
expect(screen.getByText("No items found")).toBeInTheDocument()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("renders with description", () => {
|
|
12
|
+
render(
|
|
13
|
+
<EmptyState
|
|
14
|
+
title="No items"
|
|
15
|
+
description="Start by adding your first item"
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
expect(screen.getByText("Start by adding your first item")).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("renders with icon", () => {
|
|
22
|
+
render(
|
|
23
|
+
<EmptyState
|
|
24
|
+
title="Empty"
|
|
25
|
+
icon={<span data-testid="icon">📭</span>}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
expect(screen.getByTestId("icon")).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("renders with action", () => {
|
|
32
|
+
render(
|
|
33
|
+
<EmptyState
|
|
34
|
+
title="No items"
|
|
35
|
+
action={<button>Add Item</button>}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
expect(screen.getByRole("button", { name: "Add Item" })).toBeInTheDocument()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("renders with secondary action", () => {
|
|
42
|
+
render(
|
|
43
|
+
<EmptyState
|
|
44
|
+
title="No items"
|
|
45
|
+
action={<button>Primary</button>}
|
|
46
|
+
secondaryAction={<button>Secondary</button>}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
expect(screen.getByRole("button", { name: "Primary" })).toBeInTheDocument()
|
|
50
|
+
expect(screen.getByRole("button", { name: "Secondary" })).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("renders with small size", () => {
|
|
54
|
+
render(<EmptyState title="Empty" size="sm" data-testid="empty" />)
|
|
55
|
+
expect(screen.getByTestId("empty")).toHaveClass("py-8")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("renders with default size", () => {
|
|
59
|
+
render(<EmptyState title="Empty" size="default" data-testid="empty" />)
|
|
60
|
+
expect(screen.getByTestId("empty")).toHaveClass("py-12")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("renders with large size", () => {
|
|
64
|
+
render(<EmptyState title="Empty" size="lg" data-testid="empty" />)
|
|
65
|
+
expect(screen.getByTestId("empty")).toHaveClass("py-16")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("applies custom className", () => {
|
|
69
|
+
render(<EmptyState title="Empty" className="custom-class" data-testid="empty" />)
|
|
70
|
+
expect(screen.getByTestId("empty")).toHaveClass("custom-class")
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe("EmptySearchResults", () => {
|
|
75
|
+
it("renders with default no results message", () => {
|
|
76
|
+
render(<EmptySearchResults />)
|
|
77
|
+
expect(screen.getByText("No results found")).toBeInTheDocument()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("renders with query in description", () => {
|
|
81
|
+
render(<EmptySearchResults query="test search" />)
|
|
82
|
+
expect(screen.getByText('No results for "test search". Try a different search term.')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("renders without query showing generic message", () => {
|
|
86
|
+
render(<EmptySearchResults />)
|
|
87
|
+
expect(screen.getByText("Try adjusting your search or filters.")).toBeInTheDocument()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("renders search icon", () => {
|
|
91
|
+
const { container } = render(<EmptySearchResults />)
|
|
92
|
+
const svg = container.querySelector("svg")
|
|
93
|
+
expect(svg).toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("EmptyData", () => {
|
|
98
|
+
it("renders with default resource name", () => {
|
|
99
|
+
render(<EmptyData />)
|
|
100
|
+
expect(screen.getByText("No items yet")).toBeInTheDocument()
|
|
101
|
+
expect(screen.getByText("Get started by creating your first item.")).toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("renders with custom resource name", () => {
|
|
105
|
+
render(<EmptyData resourceName="projects" />)
|
|
106
|
+
expect(screen.getByText("No projects yet")).toBeInTheDocument()
|
|
107
|
+
expect(screen.getByText("Get started by creating your first project.")).toBeInTheDocument()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("renders with action passed through", () => {
|
|
111
|
+
render(<EmptyData action={<button>Create</button>} />)
|
|
112
|
+
expect(screen.getByRole("button", { name: "Create" })).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("renders document icon", () => {
|
|
116
|
+
const { container } = render(<EmptyData />)
|
|
117
|
+
const svg = container.querySelector("svg")
|
|
118
|
+
expect(svg).toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../../src/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Icon to display (should be a React element) */
|
|
6
|
+
icon?: React.ReactNode
|
|
7
|
+
/** Main title */
|
|
8
|
+
title: string
|
|
9
|
+
/** Description text */
|
|
10
|
+
description?: string
|
|
11
|
+
/** Primary action button/link */
|
|
12
|
+
action?: React.ReactNode
|
|
13
|
+
/** Secondary action */
|
|
14
|
+
secondaryAction?: React.ReactNode
|
|
15
|
+
/** Size variant */
|
|
16
|
+
size?: "sm" | "default" | "lg"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sizeClasses = {
|
|
20
|
+
sm: {
|
|
21
|
+
container: "py-8",
|
|
22
|
+
icon: "[&_svg]:h-8 [&_svg]:w-8",
|
|
23
|
+
title: "text-base",
|
|
24
|
+
description: "text-sm",
|
|
25
|
+
},
|
|
26
|
+
default: {
|
|
27
|
+
container: "py-12",
|
|
28
|
+
icon: "[&_svg]:h-12 [&_svg]:w-12",
|
|
29
|
+
title: "text-lg",
|
|
30
|
+
description: "text-sm",
|
|
31
|
+
},
|
|
32
|
+
lg: {
|
|
33
|
+
container: "py-16",
|
|
34
|
+
icon: "[&_svg]:h-16 [&_svg]:w-16",
|
|
35
|
+
title: "text-xl",
|
|
36
|
+
description: "text-base",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Empty state component for displaying when there's no data.
|
|
42
|
+
* Provides contextual messaging and primary actions to guide users.
|
|
43
|
+
*/
|
|
44
|
+
export function EmptyState({
|
|
45
|
+
icon,
|
|
46
|
+
title,
|
|
47
|
+
description,
|
|
48
|
+
action,
|
|
49
|
+
secondaryAction,
|
|
50
|
+
size = "default",
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: EmptyStateProps) {
|
|
54
|
+
const sizes = sizeClasses[size]
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex flex-col items-center justify-center text-center",
|
|
60
|
+
sizes.container,
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{icon && (
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
"mb-4 text-muted-foreground/50",
|
|
69
|
+
sizes.icon
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{icon}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
<h3 className={cn("font-semibold text-foreground", sizes.title)}>
|
|
76
|
+
{title}
|
|
77
|
+
</h3>
|
|
78
|
+
{description && (
|
|
79
|
+
<p
|
|
80
|
+
className={cn(
|
|
81
|
+
"mt-2 max-w-sm text-muted-foreground",
|
|
82
|
+
sizes.description
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{description}
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
{(action || secondaryAction) && (
|
|
89
|
+
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
|
|
90
|
+
{action}
|
|
91
|
+
{secondaryAction}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Pre-built empty state for no search results */
|
|
99
|
+
export function EmptySearchResults({
|
|
100
|
+
query,
|
|
101
|
+
onClear,
|
|
102
|
+
...props
|
|
103
|
+
}: Omit<EmptyStateProps, "title" | "description"> & {
|
|
104
|
+
query?: string
|
|
105
|
+
onClear?: () => void
|
|
106
|
+
}) {
|
|
107
|
+
return (
|
|
108
|
+
<EmptyState
|
|
109
|
+
title="No results found"
|
|
110
|
+
description={
|
|
111
|
+
query
|
|
112
|
+
? `No results for "${query}". Try a different search term.`
|
|
113
|
+
: "Try adjusting your search or filters."
|
|
114
|
+
}
|
|
115
|
+
icon={
|
|
116
|
+
<svg
|
|
117
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
118
|
+
fill="none"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
strokeWidth={1.5}
|
|
121
|
+
stroke="currentColor"
|
|
122
|
+
>
|
|
123
|
+
<path
|
|
124
|
+
strokeLinecap="round"
|
|
125
|
+
strokeLinejoin="round"
|
|
126
|
+
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
|
127
|
+
/>
|
|
128
|
+
</svg>
|
|
129
|
+
}
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Pre-built empty state for no data */
|
|
136
|
+
export function EmptyData({
|
|
137
|
+
resourceName = "items",
|
|
138
|
+
...props
|
|
139
|
+
}: Omit<EmptyStateProps, "title" | "description"> & {
|
|
140
|
+
resourceName?: string
|
|
141
|
+
}) {
|
|
142
|
+
return (
|
|
143
|
+
<EmptyState
|
|
144
|
+
title={`No ${resourceName} yet`}
|
|
145
|
+
description={`Get started by creating your first ${resourceName.replace(/s$/, "")}.`}
|
|
146
|
+
icon={
|
|
147
|
+
<svg
|
|
148
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
149
|
+
fill="none"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
strokeWidth={1.5}
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
>
|
|
154
|
+
<path
|
|
155
|
+
strokeLinecap="round"
|
|
156
|
+
strokeLinejoin="round"
|
|
157
|
+
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../../src/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface FormSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Section title */
|
|
6
|
+
title: string
|
|
7
|
+
/** Section description */
|
|
8
|
+
description?: string
|
|
9
|
+
/** Form fields content */
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
/** Whether to show a divider at the bottom */
|
|
12
|
+
divider?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Form section for grouping related form fields.
|
|
17
|
+
* Provides consistent spacing and typography for form organization.
|
|
18
|
+
*/
|
|
19
|
+
export function FormSection({
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
children,
|
|
23
|
+
divider = true,
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: FormSectionProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cn(
|
|
30
|
+
"grid gap-6 pb-6",
|
|
31
|
+
divider && "border-b",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
<div className="space-y-1">
|
|
37
|
+
<h3 className="text-base font-medium">{title}</h3>
|
|
38
|
+
{description && (
|
|
39
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="grid gap-4">{children}</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
48
|
+
/** Field label */
|
|
49
|
+
label: string
|
|
50
|
+
/** Field description/help text */
|
|
51
|
+
description?: string
|
|
52
|
+
/** Error message */
|
|
53
|
+
error?: string
|
|
54
|
+
/** Whether the field is required */
|
|
55
|
+
required?: boolean
|
|
56
|
+
/** The form input element */
|
|
57
|
+
children: React.ReactNode
|
|
58
|
+
/** HTML for attribute to connect label with input */
|
|
59
|
+
htmlFor?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Form field wrapper with label, description, and error handling.
|
|
64
|
+
*/
|
|
65
|
+
export function FormField({
|
|
66
|
+
label,
|
|
67
|
+
description,
|
|
68
|
+
error,
|
|
69
|
+
required,
|
|
70
|
+
children,
|
|
71
|
+
htmlFor,
|
|
72
|
+
className,
|
|
73
|
+
...props
|
|
74
|
+
}: FormFieldProps) {
|
|
75
|
+
return (
|
|
76
|
+
<div className={cn("space-y-2", className)} {...props}>
|
|
77
|
+
<label
|
|
78
|
+
htmlFor={htmlFor}
|
|
79
|
+
className={cn(
|
|
80
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
81
|
+
error && "text-destructive"
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{label}
|
|
85
|
+
{required && <span className="ml-1 text-destructive">*</span>}
|
|
86
|
+
</label>
|
|
87
|
+
{children}
|
|
88
|
+
{description && !error && (
|
|
89
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
90
|
+
)}
|
|
91
|
+
{error && (
|
|
92
|
+
<p className="text-xs text-destructive" role="alert">
|
|
93
|
+
{error}
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface FormActionsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
101
|
+
/** Action buttons */
|
|
102
|
+
children: React.ReactNode
|
|
103
|
+
/** Alignment of actions */
|
|
104
|
+
align?: "left" | "right" | "center" | "between"
|
|
105
|
+
/** Whether to make it sticky at the bottom */
|
|
106
|
+
sticky?: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const alignClasses = {
|
|
110
|
+
left: "justify-start",
|
|
111
|
+
right: "justify-end",
|
|
112
|
+
center: "justify-center",
|
|
113
|
+
between: "justify-between",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Form actions bar for submit/cancel buttons.
|
|
118
|
+
* Can be sticky at the bottom of the form.
|
|
119
|
+
*/
|
|
120
|
+
export function FormActions({
|
|
121
|
+
children,
|
|
122
|
+
align = "right",
|
|
123
|
+
sticky = false,
|
|
124
|
+
className,
|
|
125
|
+
...props
|
|
126
|
+
}: FormActionsProps) {
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={cn(
|
|
130
|
+
"flex items-center gap-3 pt-6",
|
|
131
|
+
alignClasses[align],
|
|
132
|
+
sticky && "sticky bottom-0 border-t bg-background py-4",
|
|
133
|
+
className
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../../src/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface NavbarProps extends React.HTMLAttributes<HTMLElement> {
|
|
5
|
+
/** Whether the navbar is sticky */
|
|
6
|
+
sticky?: boolean
|
|
7
|
+
/** Whether to show border at bottom */
|
|
8
|
+
bordered?: boolean
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Navbar({
|
|
13
|
+
sticky = true,
|
|
14
|
+
bordered = true,
|
|
15
|
+
className,
|
|
16
|
+
children,
|
|
17
|
+
...props
|
|
18
|
+
}: NavbarProps) {
|
|
19
|
+
return (
|
|
20
|
+
<nav
|
|
21
|
+
className={cn(
|
|
22
|
+
"z-50 flex h-14 w-full items-center bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
|
|
23
|
+
sticky && "sticky top-0",
|
|
24
|
+
bordered && "border-b",
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
<div className="container flex h-full items-center px-4">
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NavbarBrandProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
children: React.ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function NavbarBrand({ className, children, ...props }: NavbarBrandProps) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={cn("flex items-center gap-2 font-semibold", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface NavbarContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
52
|
+
/** Alignment of content */
|
|
53
|
+
align?: "start" | "center" | "end"
|
|
54
|
+
children: React.ReactNode
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function NavbarContent({
|
|
58
|
+
align = "start",
|
|
59
|
+
className,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: NavbarContentProps) {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex flex-1 items-center gap-4",
|
|
67
|
+
align === "start" && "justify-start",
|
|
68
|
+
align === "center" && "justify-center",
|
|
69
|
+
align === "end" && "justify-end",
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface NavbarItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
80
|
+
children: React.ReactNode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function NavbarItem({ className, children, ...props }: NavbarItemProps) {
|
|
84
|
+
return (
|
|
85
|
+
<div className={cn("flex items-center", className)} {...props}>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface NavbarLinkProps
|
|
92
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
93
|
+
/** Whether link is active */
|
|
94
|
+
active?: boolean
|
|
95
|
+
children: React.ReactNode
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function NavbarLink({
|
|
99
|
+
active,
|
|
100
|
+
className,
|
|
101
|
+
children,
|
|
102
|
+
...props
|
|
103
|
+
}: NavbarLinkProps) {
|
|
104
|
+
return (
|
|
105
|
+
<a
|
|
106
|
+
className={cn(
|
|
107
|
+
"text-sm font-medium transition-colors hover:text-foreground",
|
|
108
|
+
active ? "text-foreground" : "text-muted-foreground",
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
{...props}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</a>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface NavbarMenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
119
|
+
/** Whether to show on mobile */
|
|
120
|
+
mobileMenu?: boolean
|
|
121
|
+
children: React.ReactNode
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function NavbarMenu({
|
|
125
|
+
mobileMenu,
|
|
126
|
+
className,
|
|
127
|
+
children,
|
|
128
|
+
...props
|
|
129
|
+
}: NavbarMenuProps) {
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
className={cn(
|
|
133
|
+
"flex items-center gap-6",
|
|
134
|
+
mobileMenu && "hidden md:flex",
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface NavbarToggleProps
|
|
145
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
146
|
+
/** Whether menu is open */
|
|
147
|
+
open?: boolean
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function NavbarToggle({
|
|
151
|
+
open,
|
|
152
|
+
className,
|
|
153
|
+
...props
|
|
154
|
+
}: NavbarToggleProps) {
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
className={cn(
|
|
158
|
+
"flex h-9 w-9 items-center justify-center rounded-md md:hidden",
|
|
159
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
aria-label={open ? "Close menu" : "Open menu"}
|
|
163
|
+
{...props}
|
|
164
|
+
>
|
|
165
|
+
<svg
|
|
166
|
+
width="20"
|
|
167
|
+
height="20"
|
|
168
|
+
viewBox="0 0 24 24"
|
|
169
|
+
fill="none"
|
|
170
|
+
stroke="currentColor"
|
|
171
|
+
strokeWidth="2"
|
|
172
|
+
strokeLinecap="round"
|
|
173
|
+
strokeLinejoin="round"
|
|
174
|
+
>
|
|
175
|
+
{open ? (
|
|
176
|
+
<>
|
|
177
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
178
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
179
|
+
</>
|
|
180
|
+
) : (
|
|
181
|
+
<>
|
|
182
|
+
<line x1="4" y1="12" x2="20" y2="12" />
|
|
183
|
+
<line x1="4" y1="6" x2="20" y2="6" />
|
|
184
|
+
<line x1="4" y1="18" x2="20" y2="18" />
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</svg>
|
|
188
|
+
</button>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Navbar,
|
|
3
|
+
NavbarBrand,
|
|
4
|
+
NavbarContent,
|
|
5
|
+
NavbarItem,
|
|
6
|
+
NavbarLink,
|
|
7
|
+
NavbarMenu,
|
|
8
|
+
NavbarToggle,
|
|
9
|
+
type NavbarProps,
|
|
10
|
+
type NavbarBrandProps,
|
|
11
|
+
type NavbarContentProps,
|
|
12
|
+
type NavbarItemProps,
|
|
13
|
+
type NavbarLinkProps,
|
|
14
|
+
type NavbarMenuProps,
|
|
15
|
+
type NavbarToggleProps,
|
|
16
|
+
} from "./Navbar"
|