create-reactivite 1.6.0 → 1.7.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/index.js CHANGED
@@ -20,18 +20,65 @@ const TEMPLATES = ["template", "template2", "template3"];
20
20
 
21
21
  // 🧩 argv parse — `name` positional + `--template/-t <val>` + `--help/--version`
22
22
  const parseArgs = (argv) => {
23
- const args = { _: [], template: undefined, help: false, version: false };
23
+ // storybook: undefined = soruş, false = --no-storybook ilə açıq imtina
24
+ const args = {
25
+ _: [],
26
+ template: undefined,
27
+ help: false,
28
+ version: false,
29
+ storybook: undefined,
30
+ };
24
31
  for (let i = 0; i < argv.length; i++) {
25
32
  const a = argv[i];
26
33
  if (a === "--help" || a === "-h") args.help = true;
27
34
  else if (a === "--version" || a === "-v") args.version = true;
28
35
  else if (a === "--template" || a === "-t") args.template = argv[++i];
29
36
  else if (a.startsWith("--template=")) args.template = a.split("=")[1];
37
+ else if (a === "--no-storybook") args.storybook = false;
38
+ else if (a === "--storybook") args.storybook = true;
30
39
  else if (!a.startsWith("-")) args._.push(a);
31
40
  }
32
41
  return args;
33
42
  };
34
43
 
44
+ // 🧩 Storybook-u target layihədən tamamilə çıxarır (user istəməyəndə).
45
+ // Yalnız `template`-də Storybook var. Strip mexaniki: config qovluğu,
46
+ // bütün *.stories.* faylları, package.json-dakı dep + skriptlər.
47
+ const stripStorybook = (targetPath) => {
48
+ // .storybook config qovluğu
49
+ fs.rmSync(path.join(targetPath, ".storybook"), {
50
+ recursive: true,
51
+ force: true,
52
+ });
53
+
54
+ // src altındakı bütün story fayllarını rekursiv sil
55
+ const walk = (dir) => {
56
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
57
+ const full = path.join(dir, entry.name);
58
+ if (entry.isDirectory()) walk(full);
59
+ else if (/\.stories\.(t|j)sx?$/.test(entry.name)) fs.rmSync(full);
60
+ }
61
+ };
62
+ const srcDir = path.join(targetPath, "src");
63
+ if (fs.existsSync(srcDir)) walk(srcDir);
64
+
65
+ // package.json-dan storybook dep + skriptlərini təmizlə
66
+ const pkgPath = path.join(targetPath, "package.json");
67
+ try {
68
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
69
+ delete pkg.scripts?.storybook;
70
+ delete pkg.scripts?.["build-storybook"];
71
+ for (const dep of Object.keys(pkg.devDependencies || {})) {
72
+ if (dep === "storybook" || dep.startsWith("@storybook/")) {
73
+ delete pkg.devDependencies[dep];
74
+ }
75
+ }
76
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
77
+ } catch {
78
+ // package.json oxunmadı — keç
79
+ }
80
+ };
81
+
35
82
  const printHelp = () => {
36
83
  console.log(`
37
84
  ${cyan("create-reactivite")} — scaffold React / Next.js / Rspack apps
@@ -41,6 +88,7 @@ const printHelp = () => {
41
88
 
42
89
  ${dim("Options:")}
43
90
  -t, --template <name> ${TEMPLATES.join(" | ")}
91
+ --no-storybook Skip Storybook (template only)
44
92
  -h, --help Show this help
45
93
  -v, --version Show version
46
94
 
@@ -162,6 +210,27 @@ const printBanner = () => {
162
210
  }
163
211
  }
164
212
 
213
+ // 🧩 0a. Storybook — yalnız `template`-də mövcuddur. argv ilə qərar
214
+ // verilməyibsə soruş (default: bəli). Digər template-lərdə bu addım yoxdur.
215
+ let wantStorybook = true;
216
+ if (template === "template") {
217
+ if (argv.storybook !== undefined) {
218
+ wantStorybook = argv.storybook;
219
+ } else {
220
+ const res = await prompts({
221
+ type: "confirm",
222
+ name: "sb",
223
+ message: "Include Storybook (UI library workbench + stories)?",
224
+ initial: true,
225
+ });
226
+ if (res.sb === undefined) {
227
+ console.log("❌ Operation cancelled.");
228
+ process.exit(1);
229
+ }
230
+ wantStorybook = res.sb;
231
+ }
232
+ }
233
+
165
234
  // 🧩 1. Target folderı müəyyən edirik
166
235
  const isCurrentDir = projectName === "." || projectName === "./";
167
236
  const targetPath = isCurrentDir
@@ -205,6 +274,12 @@ const printBanner = () => {
205
274
  fs.renameSync(gitignoreSrc, path.join(targetPath, ".gitignore"));
206
275
  }
207
276
 
277
+ // Storybook istənilmirsə — config, story faylları və dep-ləri çıxar
278
+ if (template === "template" && !wantStorybook) {
279
+ console.log("🧹 Removing Storybook...");
280
+ stripStorybook(targetPath);
281
+ }
282
+
208
283
  // 🧩 3a. Layihənin adını target package.json-a yazırıq
209
284
  // (template-dən gələn ad `template`/`template2`/`template3` olur)
210
285
  const pkgPath = path.join(targetPath, "package.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-reactivite",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "⚡ Scaffold modern frontend projects in seconds — pick a template: React + Vite, Next.js 16 (App Router), or Rspack. All ship Tailwind v4, shadcn/ui, TypeScript and a clean, production-ready structure. The Next.js template adds i18n, TanStack Query, axios/orval, Zustand, husky and Vitest. Zero setup hassle.",
5
5
  "bin": {
6
6
  "create-reactivite": "./index.js"
@@ -0,0 +1,20 @@
1
+ import type { StorybookConfig } from "@storybook/react-vite";
2
+
3
+ /**
4
+ * Storybook is OPTIONAL. The generator (create-reactivite) asks whether
5
+ * to keep it; if you said no, this folder, the *.stories.tsx files and
6
+ * the storybook devDependencies/scripts were stripped out.
7
+ *
8
+ * Stories live next to the components in src/. The @ alias and Tailwind
9
+ * are inherited from vite.config.ts automatically.
10
+ */
11
+ const config: StorybookConfig = {
12
+ stories: ["../src/**/*.stories.@(ts|tsx)"],
13
+ addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
14
+ framework: {
15
+ name: "@storybook/react-vite",
16
+ options: {},
17
+ },
18
+ };
19
+
20
+ export default config;
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import type { Decorator, Preview } from "@storybook/react-vite";
3
+
4
+ import "../src/global.css";
5
+
6
+ /** The three UI-library themes, exposed in the Storybook toolbar. */
7
+ const THEMES = [
8
+ { value: "minimal", title: "Minimal", left: "⬜" },
9
+ { value: "brutalist", title: "Brutalist", left: "🟨" },
10
+ { value: "aurora", title: "Aurora", left: "🟪" },
11
+ ];
12
+
13
+ /**
14
+ * Applies the selected theme by setting `data-theme` on both the
15
+ * <html> element (so the iframe body background matches) and the story
16
+ * wrapper (so the rendered subtree picks up the CSS vars).
17
+ */
18
+ const withTheme: Decorator = (Story, context) => {
19
+ const theme = (context.globals.theme as string) ?? "minimal";
20
+
21
+ React.useEffect(() => {
22
+ document.documentElement.setAttribute("data-theme", theme);
23
+ }, [theme]);
24
+
25
+ return (
26
+ <div
27
+ data-theme={theme}
28
+ className="bg-background text-foreground flex min-h-screen items-center justify-center p-8"
29
+ >
30
+ <Story />
31
+ </div>
32
+ );
33
+ };
34
+
35
+ const preview: Preview = {
36
+ parameters: {
37
+ layout: "fullscreen",
38
+ controls: { expanded: true },
39
+ },
40
+ globalTypes: {
41
+ theme: {
42
+ description: "UI library theme",
43
+ defaultValue: "minimal",
44
+ toolbar: {
45
+ title: "Theme",
46
+ icon: "paintbrush",
47
+ items: THEMES,
48
+ dynamicTitle: true,
49
+ },
50
+ },
51
+ },
52
+ decorators: [withTheme],
53
+ };
54
+
55
+ export default preview;
@@ -12,6 +12,9 @@ dist
12
12
  dist-ssr
13
13
  *.local
14
14
 
15
+ # Storybook build output
16
+ storybook-static
17
+
15
18
  # Editor directories and files
16
19
  .vscode/*
17
20
  !.vscode/extensions.json
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="en" data-theme="minimal">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
@@ -7,7 +7,9 @@
7
7
  "dev": "vite",
8
8
  "build": "tsc -b && vite build",
9
9
  "lint": "eslint .",
10
- "preview": "vite preview"
10
+ "preview": "vite preview",
11
+ "storybook": "storybook dev -p 6006",
12
+ "build-storybook": "storybook build"
11
13
  },
12
14
  "dependencies": {
13
15
  "@radix-ui/react-accordion": "^1.2.14",
@@ -37,6 +39,10 @@
37
39
  },
38
40
  "devDependencies": {
39
41
  "@eslint/js": "^10.0.1",
42
+ "@storybook/addon-a11y": "^10.4.6",
43
+ "@storybook/addon-docs": "^10.4.6",
44
+ "@storybook/react-vite": "^10.4.6",
45
+ "storybook": "^10.4.6",
40
46
  "@types/node": "^26.0.0",
41
47
  "@types/react": "^19.2.17",
42
48
  "@types/react-dom": "^19.2.3",
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+
4
+ import { AuthForm } from "./auth-form";
5
+
6
+ const meta = {
7
+ title: "UI Library/AuthForm",
8
+ component: AuthForm,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ onSubmit: fn(),
12
+ },
13
+ } satisfies Meta<typeof AuthForm>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const SignIn: Story = {};
19
+
20
+ export const Custom: Story = {
21
+ args: {
22
+ title: "Log in to Acme",
23
+ description: "Enter your credentials below",
24
+ submitLabel: "Continue",
25
+ },
26
+ };
@@ -0,0 +1,116 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Checkbox } from "@/components/ui/checkbox"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Separator } from "@/components/ui/separator"
8
+ import {
9
+ Card,
10
+ CardContent,
11
+ CardDescription,
12
+ CardFooter,
13
+ CardHeader,
14
+ CardTitle,
15
+ } from "@/components/ui/card"
16
+
17
+ export interface AuthFormProps
18
+ extends Omit<React.ComponentProps<typeof Card>, "onSubmit"> {
19
+ title?: string
20
+ description?: string
21
+ submitLabel?: string
22
+ /** Receives the entered email + password. */
23
+ onSubmit?: (data: { email: string; password: string }) => void
24
+ }
25
+
26
+ /**
27
+ * AuthForm — sign-in card composed from Card + Input + Checkbox +
28
+ * Button + Separator. Self-contained controlled inputs.
29
+ */
30
+ export function AuthForm({
31
+ title = "Welcome back",
32
+ description = "Sign in to your account to continue",
33
+ submitLabel = "Sign in",
34
+ onSubmit,
35
+ className,
36
+ ...props
37
+ }: AuthFormProps) {
38
+ const [email, setEmail] = React.useState("")
39
+ const [password, setPassword] = React.useState("")
40
+
41
+ return (
42
+ <Card className={cn("w-full max-w-sm", className)} {...props}>
43
+ <CardHeader className="text-center">
44
+ <CardTitle className="text-xl">{title}</CardTitle>
45
+ <CardDescription>{description}</CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <form
49
+ className="grid gap-4"
50
+ onSubmit={(e) => {
51
+ e.preventDefault()
52
+ onSubmit?.({ email, password })
53
+ }}
54
+ >
55
+ <div className="grid gap-2">
56
+ <label htmlFor="auth-email" className="text-sm font-medium">
57
+ Email
58
+ </label>
59
+ <Input
60
+ id="auth-email"
61
+ type="email"
62
+ placeholder="you@example.com"
63
+ value={email}
64
+ onChange={(e) => setEmail(e.target.value)}
65
+ autoComplete="email"
66
+ />
67
+ </div>
68
+ <div className="grid gap-2">
69
+ <div className="flex items-center justify-between">
70
+ <label htmlFor="auth-pass" className="text-sm font-medium">
71
+ Password
72
+ </label>
73
+ <a
74
+ href="#"
75
+ className="text-muted-foreground text-xs underline-offset-4 hover:underline"
76
+ >
77
+ Forgot?
78
+ </a>
79
+ </div>
80
+ <Input
81
+ id="auth-pass"
82
+ type="password"
83
+ placeholder="••••••••"
84
+ value={password}
85
+ onChange={(e) => setPassword(e.target.value)}
86
+ autoComplete="current-password"
87
+ />
88
+ </div>
89
+ <label className="flex items-center gap-2 text-sm">
90
+ <Checkbox id="auth-remember" defaultChecked />
91
+ Remember me for 30 days
92
+ </label>
93
+ <Button type="submit" className="w-full">
94
+ {submitLabel}
95
+ </Button>
96
+ </form>
97
+ <div className="my-4 flex items-center gap-3">
98
+ <Separator className="flex-1" />
99
+ <span className="text-muted-foreground text-xs">OR</span>
100
+ <Separator className="flex-1" />
101
+ </div>
102
+ <Button variant="outline" className="w-full">
103
+ Continue with GitHub
104
+ </Button>
105
+ </CardContent>
106
+ <CardFooter className="justify-center">
107
+ <span className="text-muted-foreground text-sm">
108
+ No account?{" "}
109
+ <a href="#" className="text-foreground font-medium underline-offset-4 hover:underline">
110
+ Sign up
111
+ </a>
112
+ </span>
113
+ </CardFooter>
114
+ </Card>
115
+ )
116
+ }
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+ import { SearchX, FolderPlus } from "lucide-react";
4
+
5
+ import { EmptyState } from "./empty-state";
6
+
7
+ const meta = {
8
+ title: "UI Library/EmptyState",
9
+ component: EmptyState,
10
+ tags: ["autodocs"],
11
+ args: { onAction: fn() },
12
+ } satisfies Meta<typeof EmptyState>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ title: "No projects yet",
20
+ description: "Create your first project to get started.",
21
+ actionLabel: "New project",
22
+ icon: FolderPlus,
23
+ },
24
+ };
25
+
26
+ export const NoResults: Story = {
27
+ args: {
28
+ title: "No results found",
29
+ description: "Try adjusting your search or filters.",
30
+ icon: SearchX,
31
+ },
32
+ };
@@ -0,0 +1,54 @@
1
+ import * as React from "react"
2
+ import { type LucideIcon, Inbox } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { Button } from "@/components/ui/button"
6
+
7
+ export interface EmptyStateProps extends React.ComponentProps<"div"> {
8
+ icon?: LucideIcon
9
+ title: string
10
+ description?: string
11
+ actionLabel?: string
12
+ onAction?: () => void
13
+ }
14
+
15
+ /**
16
+ * EmptyState — zero-data placeholder composed from a dashed frame +
17
+ * icon chip + Button. Used for empty lists, tables, search results.
18
+ */
19
+ export function EmptyState({
20
+ icon: Icon = Inbox,
21
+ title,
22
+ description,
23
+ actionLabel,
24
+ onAction,
25
+ className,
26
+ ...props
27
+ }: EmptyStateProps) {
28
+ return (
29
+ <div
30
+ className={cn(
31
+ "border-border bg-card flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed px-6 py-14 text-center",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ <span className="bg-muted text-muted-foreground flex size-12 items-center justify-center rounded-full">
37
+ <Icon className="size-6" />
38
+ </span>
39
+ <div className="space-y-1">
40
+ <h3 className="text-base font-semibold">{title}</h3>
41
+ {description && (
42
+ <p className="text-muted-foreground mx-auto max-w-xs text-sm">
43
+ {description}
44
+ </p>
45
+ )}
46
+ </div>
47
+ {actionLabel && (
48
+ <Button onClick={onAction} className="mt-1">
49
+ {actionLabel}
50
+ </Button>
51
+ )}
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Zap, ShieldCheck, Sparkles } from "lucide-react";
3
+
4
+ import { FeatureCard } from "./feature-card";
5
+
6
+ const meta = {
7
+ title: "UI Library/FeatureCard",
8
+ component: FeatureCard,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof FeatureCard>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ icon: Zap,
18
+ title: "Blazing fast",
19
+ description:
20
+ "Vite + SWC give you instant HMR and sub-second cold starts out of the box.",
21
+ },
22
+ };
23
+
24
+ export const Grid: Story = {
25
+ args: { icon: Zap, title: "Blazing fast", description: "..." },
26
+ render: () => (
27
+ <div className="grid w-full max-w-4xl grid-cols-1 gap-4 sm:grid-cols-3">
28
+ <FeatureCard
29
+ icon={Zap}
30
+ title="Blazing fast"
31
+ description="Instant HMR and sub-second cold starts out of the box."
32
+ />
33
+ <FeatureCard
34
+ icon={ShieldCheck}
35
+ title="Type safe"
36
+ description="End-to-end TypeScript with strict mode enabled by default."
37
+ />
38
+ <FeatureCard
39
+ icon={Sparkles}
40
+ title="Themeable"
41
+ description="Swap the whole look with a single data-theme attribute."
42
+ />
43
+ </div>
44
+ ),
45
+ };
@@ -0,0 +1,52 @@
1
+ import * as React from "react"
2
+ import { type LucideIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card"
12
+
13
+ export interface FeatureCardProps extends React.ComponentProps<typeof Card> {
14
+ icon: LucideIcon
15
+ title: string
16
+ description: string
17
+ }
18
+
19
+ /**
20
+ * FeatureCard — marketing/feature tile composed from Card.
21
+ * The icon chip uses primary/accent vars so it re-skins per theme.
22
+ */
23
+ export function FeatureCard({
24
+ icon: Icon,
25
+ title,
26
+ description,
27
+ className,
28
+ ...props
29
+ }: FeatureCardProps) {
30
+ return (
31
+ <Card
32
+ className={cn(
33
+ "gap-4 transition-shadow hover:shadow-md",
34
+ className
35
+ )}
36
+ {...props}
37
+ >
38
+ <CardHeader>
39
+ <span className="bg-primary text-primary-foreground mb-2 flex size-11 items-center justify-center rounded-lg">
40
+ <Icon className="size-5" />
41
+ </span>
42
+ <CardTitle>{title}</CardTitle>
43
+ <CardDescription>{description}</CardDescription>
44
+ </CardHeader>
45
+ <CardContent className="text-muted-foreground text-sm">
46
+ <span className="text-foreground font-medium underline-offset-4 hover:underline cursor-pointer">
47
+ Learn more →
48
+ </span>
49
+ </CardContent>
50
+ </Card>
51
+ )
52
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ui-lib — composed components built on top of shadcn/ui primitives
3
+ * (src/components/ui). These are the building blocks meant for use
4
+ * inside the template's pages. Every one is var-driven, so it adapts
5
+ * to the active `data-theme` (minimal | brutalist | aurora) with no
6
+ * per-theme code. See each component's Storybook story for variants.
7
+ */
8
+ export { StatCard, type StatCardProps } from "./stat-card"
9
+ export { FeatureCard, type FeatureCardProps } from "./feature-card"
10
+ export { PricingCard, type PricingCardProps } from "./pricing-card"
11
+ export { AuthForm, type AuthFormProps } from "./auth-form"
12
+ export { EmptyState, type EmptyStateProps } from "./empty-state"
13
+ export { PageHeader, type PageHeaderProps } from "./page-header"
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Plus, Download } from "lucide-react";
3
+
4
+ import { PageHeader } from "./page-header";
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ const meta = {
8
+ title: "UI Library/PageHeader",
9
+ component: PageHeader,
10
+ tags: ["autodocs"],
11
+ parameters: { layout: "padded" },
12
+ } satisfies Meta<typeof PageHeader>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ eyebrow: "Workspace",
20
+ title: "Projects",
21
+ description: "Manage and organize all of your team's projects.",
22
+ },
23
+ render: (args) => (
24
+ <div className="w-full max-w-3xl">
25
+ <PageHeader
26
+ {...args}
27
+ actions={
28
+ <>
29
+ <Button variant="outline">
30
+ <Download /> Export
31
+ </Button>
32
+ <Button>
33
+ <Plus /> New
34
+ </Button>
35
+ </>
36
+ }
37
+ />
38
+ </div>
39
+ ),
40
+ };
41
+
42
+ export const TitleOnly: Story = {
43
+ args: { title: "Settings" },
44
+ render: (args) => (
45
+ <div className="w-full max-w-3xl">
46
+ <PageHeader {...args} />
47
+ </div>
48
+ ),
49
+ };
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+ import { Separator } from "@/components/ui/separator"
5
+
6
+ export interface PageHeaderProps extends React.ComponentProps<"div"> {
7
+ title: string
8
+ description?: string
9
+ /** Right-aligned action slot, e.g. buttons. */
10
+ actions?: React.ReactNode
11
+ /** Optional eyebrow / breadcrumb line above the title. */
12
+ eyebrow?: string
13
+ }
14
+
15
+ /**
16
+ * PageHeader — the title block at the top of a page/section.
17
+ * title + description on the left, actions on the right, then a rule.
18
+ */
19
+ export function PageHeader({
20
+ title,
21
+ description,
22
+ actions,
23
+ eyebrow,
24
+ className,
25
+ ...props
26
+ }: PageHeaderProps) {
27
+ return (
28
+ <div className={cn("space-y-4", className)} {...props}>
29
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
30
+ <div className="space-y-1">
31
+ {eyebrow && (
32
+ <p className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
33
+ {eyebrow}
34
+ </p>
35
+ )}
36
+ <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
37
+ {description && (
38
+ <p className="text-muted-foreground text-sm">{description}</p>
39
+ )}
40
+ </div>
41
+ {actions && <div className="flex items-center gap-2">{actions}</div>}
42
+ </div>
43
+ <Separator />
44
+ </div>
45
+ )
46
+ }