@hznrkv/sidebar 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.
Files changed (2) hide show
  1. package/index.js +476 -0
  2. package/package.json +16 -0
package/index.js ADDED
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs"
3
+ import path from "path"
4
+ import { execaCommand } from "execa"
5
+ import prompts from "prompts"
6
+
7
+ const cwd = process.cwd()
8
+
9
+ function exists(file) {
10
+ return fs.existsSync(path.join(cwd, file))
11
+ }
12
+
13
+ function readJson(file) {
14
+ try {
15
+ return JSON.parse(fs.readFileSync(path.join(cwd, file), "utf8"))
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ function write(filePath, content) {
22
+ const full = path.join(cwd, filePath)
23
+ fs.mkdirSync(path.dirname(full), { recursive: true })
24
+ fs.writeFileSync(full, content, "utf8")
25
+ }
26
+
27
+ async function run(cmd) {
28
+ console.log(`\n→ ${cmd}\n`)
29
+ await execaCommand(cmd, { cwd, stdio: "inherit" })
30
+ }
31
+
32
+ // ─── Arquivos da sidebar ──────────────────────────────────────────────────────
33
+
34
+ const FILES = {
35
+ "src/components/app-sidebar.tsx": `"use client"
36
+
37
+ import * as React from "react"
38
+ import { NavMain } from "@/components/nav-main"
39
+ import { NavUser } from "@/components/nav-user"
40
+ import { TeamSwitcher } from "@/components/team-switcher"
41
+ import {
42
+ Sidebar,
43
+ SidebarContent,
44
+ SidebarFooter,
45
+ SidebarHeader,
46
+ } from "@/components/ui/sidebar"
47
+ import { LayoutDashboardIcon, SettingsIcon, UserIcon } from "lucide-react"
48
+
49
+ const data = {
50
+ user: {
51
+ name: "Usuário",
52
+ email: "usuario@exemplo.com",
53
+ avatar: "",
54
+ },
55
+ teams: [
56
+ {
57
+ name: "Meu Projeto",
58
+ logo: <LayoutDashboardIcon />,
59
+ plan: "Free",
60
+ },
61
+ ],
62
+ navMain: [
63
+ {
64
+ title: "Dashboard",
65
+ url: "/",
66
+ icon: <LayoutDashboardIcon />,
67
+ isActive: true,
68
+ },
69
+ {
70
+ title: "Perfil",
71
+ url: "/perfil",
72
+ icon: <UserIcon />,
73
+ },
74
+ {
75
+ title: "Configurações",
76
+ url: "/configuracoes",
77
+ icon: <SettingsIcon />,
78
+ },
79
+ ],
80
+ }
81
+
82
+ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
83
+ return (
84
+ <Sidebar variant="floating" collapsible="icon" {...props}>
85
+ <SidebarHeader>
86
+ <TeamSwitcher teams={data.teams} />
87
+ </SidebarHeader>
88
+ <SidebarContent>
89
+ <NavMain items={data.navMain} />
90
+ </SidebarContent>
91
+ <SidebarFooter>
92
+ <NavUser user={data.user} />
93
+ </SidebarFooter>
94
+ </Sidebar>
95
+ )
96
+ }
97
+ `,
98
+
99
+ "src/components/nav-main.tsx": `"use client"
100
+
101
+ import { ChevronRightIcon } from "lucide-react"
102
+ import {
103
+ Collapsible,
104
+ CollapsibleContent,
105
+ CollapsibleTrigger,
106
+ } from "@/components/ui/collapsible"
107
+ import {
108
+ SidebarGroup,
109
+ SidebarGroupLabel,
110
+ SidebarMenu,
111
+ SidebarMenuButton,
112
+ SidebarMenuItem,
113
+ SidebarMenuSub,
114
+ SidebarMenuSubButton,
115
+ SidebarMenuSubItem,
116
+ } from "@/components/ui/sidebar"
117
+
118
+ type NavItem = {
119
+ title: string
120
+ url: string
121
+ icon?: React.ReactNode
122
+ isActive?: boolean
123
+ items?: { title: string; url: string }[]
124
+ }
125
+
126
+ export function NavMain({ items }: { items: NavItem[] }) {
127
+ return (
128
+ <SidebarGroup>
129
+ <SidebarGroupLabel>Menu</SidebarGroupLabel>
130
+ <SidebarMenu>
131
+ {items.map((item) =>
132
+ item.items?.length ? (
133
+ <Collapsible
134
+ key={item.title}
135
+ asChild
136
+ defaultOpen={item.isActive}
137
+ className="group/collapsible"
138
+ >
139
+ <SidebarMenuItem>
140
+ <CollapsibleTrigger asChild>
141
+ <SidebarMenuButton tooltip={item.title}>
142
+ {item.icon}
143
+ <span>{item.title}</span>
144
+ <ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
145
+ </SidebarMenuButton>
146
+ </CollapsibleTrigger>
147
+ <CollapsibleContent>
148
+ <SidebarMenuSub>
149
+ {item.items.map((sub) => (
150
+ <SidebarMenuSubItem key={sub.title}>
151
+ <SidebarMenuSubButton asChild>
152
+ <a href={sub.url}>{sub.title}</a>
153
+ </SidebarMenuSubButton>
154
+ </SidebarMenuSubItem>
155
+ ))}
156
+ </SidebarMenuSub>
157
+ </CollapsibleContent>
158
+ </SidebarMenuItem>
159
+ </Collapsible>
160
+ ) : (
161
+ <SidebarMenuItem key={item.title}>
162
+ <SidebarMenuButton asChild tooltip={item.title} isActive={item.isActive}>
163
+ <a href={item.url}>
164
+ {item.icon}
165
+ <span>{item.title}</span>
166
+ </a>
167
+ </SidebarMenuButton>
168
+ </SidebarMenuItem>
169
+ )
170
+ )}
171
+ </SidebarMenu>
172
+ </SidebarGroup>
173
+ )
174
+ }
175
+ `,
176
+
177
+ "src/components/nav-user.tsx": `"use client"
178
+
179
+ import {
180
+ BadgeCheckIcon,
181
+ BellIcon,
182
+ ChevronsUpDownIcon,
183
+ CreditCardIcon,
184
+ LogOutIcon,
185
+ SparklesIcon,
186
+ } from "lucide-react"
187
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
188
+ import {
189
+ DropdownMenu,
190
+ DropdownMenuContent,
191
+ DropdownMenuGroup,
192
+ DropdownMenuItem,
193
+ DropdownMenuLabel,
194
+ DropdownMenuSeparator,
195
+ DropdownMenuTrigger,
196
+ } from "@/components/ui/dropdown-menu"
197
+ import {
198
+ SidebarMenu,
199
+ SidebarMenuButton,
200
+ SidebarMenuItem,
201
+ useSidebar,
202
+ } from "@/components/ui/sidebar"
203
+
204
+ type User = { name: string; email: string; avatar: string }
205
+
206
+ export function NavUser({ user }: { user: User }) {
207
+ const { isMobile } = useSidebar()
208
+
209
+ return (
210
+ <SidebarMenu>
211
+ <SidebarMenuItem>
212
+ <DropdownMenu>
213
+ <DropdownMenuTrigger asChild>
214
+ <SidebarMenuButton
215
+ size="lg"
216
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
217
+ >
218
+ <Avatar className="h-8 w-8 rounded-lg">
219
+ <AvatarImage src={user.avatar} alt={user.name} />
220
+ <AvatarFallback className="rounded-lg">
221
+ {user.name.slice(0, 2).toUpperCase()}
222
+ </AvatarFallback>
223
+ </Avatar>
224
+ <div className="grid flex-1 text-left text-sm leading-tight">
225
+ <span className="truncate font-semibold">{user.name}</span>
226
+ <span className="truncate text-xs">{user.email}</span>
227
+ </div>
228
+ <ChevronsUpDownIcon className="ml-auto size-4" />
229
+ </SidebarMenuButton>
230
+ </DropdownMenuTrigger>
231
+ <DropdownMenuContent
232
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
233
+ side={isMobile ? "bottom" : "right"}
234
+ align="end"
235
+ sideOffset={4}
236
+ >
237
+ <DropdownMenuLabel className="p-0 font-normal">
238
+ <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
239
+ <Avatar className="h-8 w-8 rounded-lg">
240
+ <AvatarImage src={user.avatar} alt={user.name} />
241
+ <AvatarFallback className="rounded-lg">
242
+ {user.name.slice(0, 2).toUpperCase()}
243
+ </AvatarFallback>
244
+ </Avatar>
245
+ <div className="grid flex-1 text-left text-sm leading-tight">
246
+ <span className="truncate font-semibold">{user.name}</span>
247
+ <span className="truncate text-xs">{user.email}</span>
248
+ </div>
249
+ </div>
250
+ </DropdownMenuLabel>
251
+ <DropdownMenuSeparator />
252
+ <DropdownMenuGroup>
253
+ <DropdownMenuItem>
254
+ <SparklesIcon />
255
+ Upgrade para Pro
256
+ </DropdownMenuItem>
257
+ </DropdownMenuGroup>
258
+ <DropdownMenuSeparator />
259
+ <DropdownMenuGroup>
260
+ <DropdownMenuItem>
261
+ <BadgeCheckIcon />
262
+ Conta
263
+ </DropdownMenuItem>
264
+ <DropdownMenuItem>
265
+ <CreditCardIcon />
266
+ Faturamento
267
+ </DropdownMenuItem>
268
+ <DropdownMenuItem>
269
+ <BellIcon />
270
+ Notificações
271
+ </DropdownMenuItem>
272
+ </DropdownMenuGroup>
273
+ <DropdownMenuSeparator />
274
+ <DropdownMenuItem>
275
+ <LogOutIcon />
276
+ Sair
277
+ </DropdownMenuItem>
278
+ </DropdownMenuContent>
279
+ </DropdownMenu>
280
+ </SidebarMenuItem>
281
+ </SidebarMenu>
282
+ )
283
+ }
284
+ `,
285
+
286
+ "src/components/team-switcher.tsx": `"use client"
287
+
288
+ import * as React from "react"
289
+ import { ChevronsUpDownIcon, PlusIcon } from "lucide-react"
290
+ import {
291
+ DropdownMenu,
292
+ DropdownMenuContent,
293
+ DropdownMenuItem,
294
+ DropdownMenuLabel,
295
+ DropdownMenuSeparator,
296
+ DropdownMenuTrigger,
297
+ } from "@/components/ui/dropdown-menu"
298
+ import {
299
+ SidebarMenu,
300
+ SidebarMenuButton,
301
+ SidebarMenuItem,
302
+ useSidebar,
303
+ } from "@/components/ui/sidebar"
304
+
305
+ type Team = { name: string; logo: React.ReactNode; plan: string }
306
+
307
+ export function TeamSwitcher({ teams }: { teams: Team[] }) {
308
+ const { isMobile } = useSidebar()
309
+ const [active, setActive] = React.useState(teams[0])
310
+
311
+ return (
312
+ <SidebarMenu>
313
+ <SidebarMenuItem>
314
+ <DropdownMenu>
315
+ <DropdownMenuTrigger asChild>
316
+ <SidebarMenuButton
317
+ size="lg"
318
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
319
+ >
320
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
321
+ {active.logo}
322
+ </div>
323
+ <div className="grid flex-1 text-left text-sm leading-tight">
324
+ <span className="truncate font-semibold">{active.name}</span>
325
+ <span className="truncate text-xs">{active.plan}</span>
326
+ </div>
327
+ <ChevronsUpDownIcon className="ml-auto" />
328
+ </SidebarMenuButton>
329
+ </DropdownMenuTrigger>
330
+ <DropdownMenuContent
331
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
332
+ align="start"
333
+ side={isMobile ? "bottom" : "right"}
334
+ sideOffset={4}
335
+ >
336
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
337
+ Times
338
+ </DropdownMenuLabel>
339
+ {teams.map((team) => (
340
+ <DropdownMenuItem key={team.name} onClick={() => setActive(team)} className="gap-2 p-2">
341
+ <div className="flex size-6 items-center justify-center rounded-sm border">
342
+ {team.logo}
343
+ </div>
344
+ {team.name}
345
+ </DropdownMenuItem>
346
+ ))}
347
+ <DropdownMenuSeparator />
348
+ <DropdownMenuItem className="gap-2 p-2">
349
+ <div className="flex size-6 items-center justify-center rounded-md border bg-background">
350
+ <PlusIcon className="size-4" />
351
+ </div>
352
+ <div className="font-medium text-muted-foreground">Adicionar time</div>
353
+ </DropdownMenuItem>
354
+ </DropdownMenuContent>
355
+ </DropdownMenu>
356
+ </SidebarMenuItem>
357
+ </SidebarMenu>
358
+ )
359
+ }
360
+ `,
361
+
362
+ "src/app/(app)/layout.tsx": `import { AppSidebar } from "@/components/app-sidebar"
363
+ import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"
364
+
365
+ export default function AppLayout({ children }: { children: React.ReactNode }) {
366
+ return (
367
+ <SidebarProvider className="p-2 gap-2">
368
+ <AppSidebar />
369
+ <SidebarInset className="rounded-xl overflow-hidden">
370
+ {children}
371
+ </SidebarInset>
372
+ </SidebarProvider>
373
+ )
374
+ }
375
+ `,
376
+
377
+ "src/app/(app)/page.tsx": `export default function Home() {
378
+ return (
379
+ <div className="p-6">
380
+ <h1 className="text-2xl font-semibold">Dashboard</h1>
381
+ <p className="text-muted-foreground mt-1">Bem-vindo ao seu projeto.</p>
382
+ </div>
383
+ )
384
+ }
385
+ `,
386
+
387
+ "src/app/page.tsx": `import { redirect } from "next/navigation"
388
+
389
+ export default function Root() {
390
+ redirect("/")
391
+ }
392
+ `,
393
+ }
394
+
395
+ // ─── Main ─────────────────────────────────────────────────────────────────────
396
+
397
+ async function main() {
398
+ console.log("\n@hznrkv/sidebar — instalador\n")
399
+
400
+ // 1. Next.js
401
+ const pkg = readJson("package.json")
402
+ const hasNext = pkg?.dependencies?.next || pkg?.devDependencies?.next
403
+
404
+ if (!hasNext) {
405
+ const { confirm } = await prompts({
406
+ type: "confirm",
407
+ name: "confirm",
408
+ message: "Next.js não encontrado. Criar projeto Next.js aqui?",
409
+ initial: true,
410
+ })
411
+ if (!confirm) {
412
+ console.log("Abortado. Inicialize um projeto Next.js antes de continuar.")
413
+ process.exit(1)
414
+ }
415
+ await run("pnpm create next-app@latest .")
416
+ } else {
417
+ console.log("✔ Next.js encontrado")
418
+ }
419
+
420
+ // 2. Shadcn
421
+ const hasShadcn = exists("components.json")
422
+
423
+ if (!hasShadcn) {
424
+ const { mode } = await prompts({
425
+ type: "select",
426
+ name: "mode",
427
+ message: "Shadcn não encontrado. Como quer inicializar?",
428
+ choices: [
429
+ { title: "Instalação padrão", value: "default" },
430
+ { title: "Tenho um preset pronto", value: "preset" },
431
+ ],
432
+ })
433
+
434
+ if (mode === "preset") {
435
+ const { cmd } = await prompts({
436
+ type: "text",
437
+ name: "cmd",
438
+ message: "Cole o comando completo do preset:",
439
+ hint: "ex: pnpm dlx shadcn@latest init --preset b0 --template next",
440
+ })
441
+ await run(cmd.trim())
442
+ } else {
443
+ await run("pnpm dlx shadcn@latest init")
444
+ }
445
+ } else {
446
+ console.log("✔ Shadcn encontrado")
447
+ }
448
+
449
+ // 3. Componentes Shadcn necessários
450
+ console.log("\n→ Instalando componentes Shadcn necessários...")
451
+ await run("pnpm dlx shadcn@latest add sidebar avatar dropdown-menu collapsible tooltip")
452
+
453
+ // 4. lucide-react
454
+ const updatedPkg = readJson("package.json")
455
+ const hasLucide = updatedPkg?.dependencies?.["lucide-react"] || updatedPkg?.devDependencies?.["lucide-react"]
456
+ if (!hasLucide) {
457
+ await run("pnpm add lucide-react")
458
+ } else {
459
+ console.log("✔ lucide-react encontrado")
460
+ }
461
+
462
+ // 5. Escrever arquivos da sidebar
463
+ console.log("\n→ Gerando arquivos da sidebar...")
464
+ for (const [filePath, content] of Object.entries(FILES)) {
465
+ write(filePath, content)
466
+ console.log(` ✔ ${filePath}`)
467
+ }
468
+
469
+ console.log("\n✔ Sidebar instalada com sucesso!")
470
+ console.log("\nPróximo passo:\n pnpm dev\n")
471
+ }
472
+
473
+ main().catch((err) => {
474
+ console.error(err)
475
+ process.exit(1)
476
+ })
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@hznrkv/sidebar",
3
+ "version": "1.0.0",
4
+ "description": "CLI para instalar a sidebar @hznrkv em projetos Next.js + Shadcn",
5
+ "bin": {
6
+ "sidebar": "./index.js"
7
+ },
8
+ "type": "module",
9
+ "dependencies": {
10
+ "execa": "^9.5.2",
11
+ "prompts": "^2.4.2"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ }
16
+ }