@weirdfingers/baseboards 0.2.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 (139) hide show
  1. package/README.md +191 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +887 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +64 -0
  6. package/templates/README.md +120 -0
  7. package/templates/api/.env.example +62 -0
  8. package/templates/api/Dockerfile +32 -0
  9. package/templates/api/README.md +132 -0
  10. package/templates/api/alembic/env.py +106 -0
  11. package/templates/api/alembic/script.py.mako +28 -0
  12. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
  13. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
  14. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
  16. package/templates/api/alembic.ini +36 -0
  17. package/templates/api/config/generators.yaml +25 -0
  18. package/templates/api/config/storage_config.yaml +26 -0
  19. package/templates/api/docs/ADDING_GENERATORS.md +409 -0
  20. package/templates/api/docs/GENERATORS_API.md +502 -0
  21. package/templates/api/docs/MIGRATIONS.md +472 -0
  22. package/templates/api/docs/storage_providers.md +337 -0
  23. package/templates/api/pyproject.toml +165 -0
  24. package/templates/api/src/boards/__init__.py +10 -0
  25. package/templates/api/src/boards/api/app.py +171 -0
  26. package/templates/api/src/boards/api/auth.py +75 -0
  27. package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
  28. package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
  29. package/templates/api/src/boards/api/endpoints/setup.py +505 -0
  30. package/templates/api/src/boards/api/endpoints/sse.py +129 -0
  31. package/templates/api/src/boards/api/endpoints/storage.py +74 -0
  32. package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
  33. package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
  34. package/templates/api/src/boards/auth/__init__.py +15 -0
  35. package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
  36. package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
  37. package/templates/api/src/boards/auth/adapters/base.py +73 -0
  38. package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
  39. package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
  40. package/templates/api/src/boards/auth/adapters/none.py +102 -0
  41. package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
  42. package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
  43. package/templates/api/src/boards/auth/context.py +35 -0
  44. package/templates/api/src/boards/auth/factory.py +115 -0
  45. package/templates/api/src/boards/auth/middleware.py +221 -0
  46. package/templates/api/src/boards/auth/provisioning.py +129 -0
  47. package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
  48. package/templates/api/src/boards/cli.py +354 -0
  49. package/templates/api/src/boards/config.py +116 -0
  50. package/templates/api/src/boards/database/__init__.py +7 -0
  51. package/templates/api/src/boards/database/cli.py +110 -0
  52. package/templates/api/src/boards/database/connection.py +252 -0
  53. package/templates/api/src/boards/database/models.py +19 -0
  54. package/templates/api/src/boards/database/seed_data.py +182 -0
  55. package/templates/api/src/boards/dbmodels/__init__.py +455 -0
  56. package/templates/api/src/boards/generators/__init__.py +57 -0
  57. package/templates/api/src/boards/generators/artifacts.py +53 -0
  58. package/templates/api/src/boards/generators/base.py +140 -0
  59. package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
  60. package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
  61. package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
  62. package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
  63. package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
  64. package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
  65. package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
  66. package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
  67. package/templates/api/src/boards/generators/loader.py +253 -0
  68. package/templates/api/src/boards/generators/registry.py +114 -0
  69. package/templates/api/src/boards/generators/resolution.py +515 -0
  70. package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
  71. package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
  72. package/templates/api/src/boards/graphql/__init__.py +7 -0
  73. package/templates/api/src/boards/graphql/access_control.py +136 -0
  74. package/templates/api/src/boards/graphql/mutations/root.py +136 -0
  75. package/templates/api/src/boards/graphql/queries/root.py +116 -0
  76. package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
  77. package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
  78. package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
  79. package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
  80. package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
  81. package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
  82. package/templates/api/src/boards/graphql/schema.py +81 -0
  83. package/templates/api/src/boards/graphql/types/board.py +102 -0
  84. package/templates/api/src/boards/graphql/types/generation.py +130 -0
  85. package/templates/api/src/boards/graphql/types/generator.py +17 -0
  86. package/templates/api/src/boards/graphql/types/user.py +47 -0
  87. package/templates/api/src/boards/jobs/repository.py +104 -0
  88. package/templates/api/src/boards/logging.py +195 -0
  89. package/templates/api/src/boards/middleware.py +339 -0
  90. package/templates/api/src/boards/progress/__init__.py +4 -0
  91. package/templates/api/src/boards/progress/models.py +25 -0
  92. package/templates/api/src/boards/progress/publisher.py +64 -0
  93. package/templates/api/src/boards/py.typed +0 -0
  94. package/templates/api/src/boards/redis_pool.py +118 -0
  95. package/templates/api/src/boards/storage/__init__.py +52 -0
  96. package/templates/api/src/boards/storage/base.py +363 -0
  97. package/templates/api/src/boards/storage/config.py +187 -0
  98. package/templates/api/src/boards/storage/factory.py +278 -0
  99. package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
  100. package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
  101. package/templates/api/src/boards/storage/implementations/local.py +201 -0
  102. package/templates/api/src/boards/storage/implementations/s3.py +294 -0
  103. package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
  104. package/templates/api/src/boards/tenant_isolation.py +446 -0
  105. package/templates/api/src/boards/validation.py +262 -0
  106. package/templates/api/src/boards/workers/__init__.py +1 -0
  107. package/templates/api/src/boards/workers/actors.py +201 -0
  108. package/templates/api/src/boards/workers/cli.py +125 -0
  109. package/templates/api/src/boards/workers/context.py +188 -0
  110. package/templates/api/src/boards/workers/middleware.py +58 -0
  111. package/templates/api/src/py.typed +0 -0
  112. package/templates/compose.dev.yaml +39 -0
  113. package/templates/compose.yaml +109 -0
  114. package/templates/docker/env.example +23 -0
  115. package/templates/web/.env.example +28 -0
  116. package/templates/web/Dockerfile +51 -0
  117. package/templates/web/components.json +22 -0
  118. package/templates/web/imageLoader.js +18 -0
  119. package/templates/web/next-env.d.ts +5 -0
  120. package/templates/web/next.config.js +36 -0
  121. package/templates/web/package.json +37 -0
  122. package/templates/web/postcss.config.mjs +7 -0
  123. package/templates/web/public/favicon.ico +0 -0
  124. package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
  125. package/templates/web/src/app/globals.css +120 -0
  126. package/templates/web/src/app/layout.tsx +21 -0
  127. package/templates/web/src/app/page.tsx +35 -0
  128. package/templates/web/src/app/providers.tsx +18 -0
  129. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
  130. package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
  131. package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
  132. package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
  133. package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
  134. package/templates/web/src/components/header.tsx +30 -0
  135. package/templates/web/src/components/ui/button.tsx +58 -0
  136. package/templates/web/src/components/ui/card.tsx +92 -0
  137. package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
  138. package/templates/web/src/lib/utils.ts +6 -0
  139. package/templates/web/tsconfig.json +47 -0
@@ -0,0 +1,251 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Settings, ArrowUp, X } from "lucide-react";
5
+ import Image from "next/image";
6
+ import { GeneratorSelector, GeneratorInfo } from "./GeneratorSelector";
7
+ import { ArtifactInputSlots } from "./ArtifactInputSlots";
8
+
9
+ interface Generation {
10
+ id: string;
11
+ artifactType: string;
12
+ storageUrl?: string | null;
13
+ thumbnailUrl?: string | null;
14
+ }
15
+
16
+ interface ArtifactSlot {
17
+ name: string;
18
+ type: string;
19
+ required: boolean;
20
+ }
21
+
22
+ interface GenerationInputProps {
23
+ generators: GeneratorInfo[];
24
+ availableArtifacts: Generation[];
25
+ onSubmit: (params: {
26
+ generatorName: string;
27
+ prompt: string;
28
+ artifacts: Map<string, Generation>;
29
+ settings: Record<string, unknown>;
30
+ }) => void;
31
+ isGenerating?: boolean;
32
+ }
33
+
34
+ export function GenerationInput({
35
+ generators,
36
+ availableArtifacts,
37
+ onSubmit,
38
+ isGenerating = false,
39
+ }: GenerationInputProps) {
40
+ const [selectedGenerator, setSelectedGenerator] =
41
+ useState<GeneratorInfo | null>(generators[0] || null);
42
+ const [prompt, setPrompt] = useState("");
43
+ const [selectedArtifacts, setSelectedArtifacts] = useState<
44
+ Map<string, Generation>
45
+ >(new Map());
46
+ const [attachedImage, setAttachedImage] = useState<Generation | null>(null);
47
+ const [showSettings, setShowSettings] = useState(false);
48
+
49
+ // Parse input schema to determine if this generator needs artifact inputs
50
+ const getArtifactSlots = (): ArtifactSlot[] => {
51
+ if (!selectedGenerator) return [];
52
+
53
+ const schema = selectedGenerator.inputSchema as {
54
+ properties?: Record<
55
+ string,
56
+ {
57
+ type?: string;
58
+ anyOf?: Array<{ $ref?: string }>;
59
+ description?: string;
60
+ }
61
+ >;
62
+ required?: string[];
63
+ };
64
+
65
+ if (!schema.properties) return [];
66
+
67
+ const slots: ArtifactSlot[] = [];
68
+
69
+ for (const [key, value] of Object.entries(schema.properties)) {
70
+ // Check if this property references an artifact type
71
+ const isArtifact =
72
+ value.anyOf?.some((ref) => ref.$ref?.includes("Artifact")) || false;
73
+
74
+ if (isArtifact) {
75
+ // Determine artifact type from the key or description
76
+ let type = "video";
77
+ if (key.toLowerCase().includes("audio")) type = "audio";
78
+ if (key.toLowerCase().includes("video")) type = "video";
79
+ if (key.toLowerCase().includes("image")) type = "image";
80
+
81
+ slots.push({
82
+ name: key,
83
+ type,
84
+ required: schema.required?.includes(key) || false,
85
+ });
86
+ }
87
+ }
88
+
89
+ return slots;
90
+ };
91
+
92
+ const artifactSlots = getArtifactSlots();
93
+ const needsArtifactInputs = artifactSlots.length > 0;
94
+
95
+ const handleSubmit = () => {
96
+ if (!selectedGenerator || !prompt.trim()) return;
97
+
98
+ onSubmit({
99
+ generatorName: selectedGenerator.name,
100
+ prompt: prompt.trim(),
101
+ artifacts: selectedArtifacts,
102
+ settings: {},
103
+ });
104
+
105
+ // Reset form
106
+ setPrompt("");
107
+ setSelectedArtifacts(new Map());
108
+ setAttachedImage(null);
109
+ };
110
+
111
+ const handleSelectArtifact = (
112
+ slotName: string,
113
+ artifact: Generation | null
114
+ ) => {
115
+ const newSelected = new Map(selectedArtifacts);
116
+ if (artifact) {
117
+ newSelected.set(slotName, artifact);
118
+ } else {
119
+ newSelected.delete(slotName);
120
+ }
121
+ setSelectedArtifacts(newSelected);
122
+ };
123
+
124
+ const canSubmit =
125
+ selectedGenerator &&
126
+ prompt.trim() &&
127
+ !isGenerating &&
128
+ (!needsArtifactInputs ||
129
+ artifactSlots
130
+ .filter((s) => s.required)
131
+ .every((s) => selectedArtifacts.has(s.name)));
132
+
133
+ return (
134
+ <div className="border border-gray-300 rounded-lg bg-white shadow-sm">
135
+ {/* Generator selector header */}
136
+ <div className="px-4 py-3 border-b border-gray-200 flex items-center justify-center">
137
+ <GeneratorSelector
138
+ generators={generators}
139
+ selectedGenerator={selectedGenerator}
140
+ onSelect={setSelectedGenerator}
141
+ />
142
+ </div>
143
+
144
+ {/* Artifact input slots (for generators like lipsync) */}
145
+ {needsArtifactInputs && (
146
+ <div className="px-4 py-4 border-b border-gray-200">
147
+ <ArtifactInputSlots
148
+ slots={artifactSlots}
149
+ selectedArtifacts={selectedArtifacts}
150
+ availableArtifacts={availableArtifacts}
151
+ onSelectArtifact={handleSelectArtifact}
152
+ />
153
+ {artifactSlots.every((s) => selectedArtifacts.has(s.name)) && (
154
+ <div className="mt-3 flex items-center gap-2 text-sm text-green-600">
155
+ <svg
156
+ className="w-4 h-4"
157
+ fill="none"
158
+ strokeLinecap="round"
159
+ strokeLinejoin="round"
160
+ strokeWidth="2"
161
+ viewBox="0 0 24 24"
162
+ stroke="currentColor"
163
+ >
164
+ <path d="M5 13l4 4L19 7"></path>
165
+ </svg>
166
+ <span>
167
+ Both files ready for {selectedGenerator?.name} generation
168
+ </span>
169
+ </div>
170
+ )}
171
+ </div>
172
+ )}
173
+
174
+ {/* Attached image preview (if any) */}
175
+ {attachedImage && (
176
+ <div className="px-4 py-3 border-b border-gray-200">
177
+ <div className="flex items-center gap-3">
178
+ <Image
179
+ src={attachedImage.thumbnailUrl || attachedImage.storageUrl || ""}
180
+ alt="Attached"
181
+ className="w-16 h-16 object-cover rounded"
182
+ width={64}
183
+ height={64}
184
+ />
185
+ <div className="flex-1">
186
+ <p className="text-sm font-medium">Image attached</p>
187
+ <p className="text-xs text-gray-500">
188
+ ID: {attachedImage.id.substring(0, 8)}
189
+ </p>
190
+ </div>
191
+ <button
192
+ onClick={() => setAttachedImage(null)}
193
+ className="p-1 hover:bg-gray-100 rounded"
194
+ >
195
+ <X className="w-4 h-4" />
196
+ </button>
197
+ </div>
198
+ </div>
199
+ )}
200
+
201
+ {/* Prompt input area */}
202
+ <div className="px-4 py-4 flex items-end gap-3">
203
+ <textarea
204
+ value={prompt}
205
+ onChange={(e) => setPrompt(e.target.value)}
206
+ placeholder={
207
+ needsArtifactInputs
208
+ ? "Add optional prompt or instructions..."
209
+ : "Describe what you want to generate..."
210
+ }
211
+ className="flex-1 resize-none border-none outline-none text-base min-h-[60px] max-h-[200px]"
212
+ onKeyDown={(e) => {
213
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && canSubmit) {
214
+ handleSubmit();
215
+ }
216
+ }}
217
+ />
218
+ <div className="flex items-center gap-2">
219
+ <button
220
+ onClick={() => setShowSettings(!showSettings)}
221
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
222
+ title="Settings"
223
+ >
224
+ <Settings className="w-5 h-5 text-gray-600" />
225
+ </button>
226
+ <button
227
+ onClick={handleSubmit}
228
+ disabled={!canSubmit}
229
+ className={`p-3 rounded-full transition-all ${
230
+ canSubmit
231
+ ? "bg-orange-500 hover:bg-orange-600 text-white shadow-lg"
232
+ : "bg-gray-200 text-gray-400 cursor-not-allowed"
233
+ }`}
234
+ title="Generate (⌘+Enter)"
235
+ >
236
+ <ArrowUp className="w-5 h-5" />
237
+ </button>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Settings panel (collapsed by default) */}
242
+ {showSettings && (
243
+ <div className="px-4 py-4 border-t border-gray-200 bg-gray-50">
244
+ <p className="text-sm text-gray-600">
245
+ Generator-specific settings will appear here
246
+ </p>
247
+ </div>
248
+ )}
249
+ </div>
250
+ );
251
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Zap, Check } from "lucide-react";
5
+
6
+ export interface GeneratorInfo {
7
+ name: string;
8
+ description: string;
9
+ artifactType: string;
10
+ inputSchema: Record<string, unknown>;
11
+ }
12
+
13
+ interface GeneratorSelectorProps {
14
+ generators: GeneratorInfo[];
15
+ selectedGenerator: GeneratorInfo | null;
16
+ onSelect: (generator: GeneratorInfo) => void;
17
+ }
18
+
19
+ export function GeneratorSelector({
20
+ generators,
21
+ selectedGenerator,
22
+ onSelect,
23
+ }: GeneratorSelectorProps) {
24
+ const [isOpen, setIsOpen] = useState(false);
25
+
26
+ const getGeneratorIcon = (name: string) => {
27
+ // You can customize icons per generator here
28
+ return <Zap className="w-4 h-4" />;
29
+ };
30
+
31
+ return (
32
+ <div className="relative">
33
+ <button
34
+ onClick={() => setIsOpen(!isOpen)}
35
+ className="px-4 py-2 bg-white border border-gray-300 rounded-lg shadow-sm hover:bg-gray-50 flex items-center gap-2"
36
+ >
37
+ {selectedGenerator ? (
38
+ <>
39
+ {getGeneratorIcon(selectedGenerator.name)}
40
+ <span className="font-medium">{selectedGenerator.name}</span>
41
+ </>
42
+ ) : (
43
+ <span className="text-gray-500">Select Generator</span>
44
+ )}
45
+ </button>
46
+
47
+ {isOpen && (
48
+ <>
49
+ <div
50
+ className="fixed inset-0 z-10"
51
+ onClick={() => setIsOpen(false)}
52
+ />
53
+ <div className="absolute top-full mt-2 left-0 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[250px] max-h-[400px] overflow-y-auto">
54
+ {generators.map((generator) => (
55
+ <button
56
+ key={generator.name}
57
+ onClick={() => {
58
+ onSelect(generator);
59
+ setIsOpen(false);
60
+ }}
61
+ className="w-full px-4 py-3 hover:bg-gray-50 flex items-start gap-3 text-left border-b border-gray-100 last:border-b-0"
62
+ >
63
+ <div className="flex-shrink-0 mt-0.5">
64
+ {getGeneratorIcon(generator.name)}
65
+ </div>
66
+ <div className="flex-1 min-w-0">
67
+ <div className="flex items-center gap-2">
68
+ <span className="font-medium text-sm">
69
+ {generator.name}
70
+ </span>
71
+ <span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded">
72
+ {generator.artifactType}
73
+ </span>
74
+ </div>
75
+ <p className="text-xs text-gray-600 mt-1">
76
+ {generator.description}
77
+ </p>
78
+ </div>
79
+ {selectedGenerator?.name === generator.name && (
80
+ <Check className="w-4 h-4 text-green-600 flex-shrink-0" />
81
+ )}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ </>
86
+ )}
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import {
5
+ NavigationMenu,
6
+ NavigationMenuItem,
7
+ NavigationMenuLink,
8
+ NavigationMenuList,
9
+ navigationMenuTriggerStyle,
10
+ } from "@/components/ui/navigation-menu";
11
+
12
+ export function Header() {
13
+ return (
14
+ <header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
15
+ <div className="container flex h-14 max-w-screen-2xl items-center">
16
+ <NavigationMenu>
17
+ <NavigationMenuList>
18
+ <NavigationMenuItem>
19
+ <Link href="/" legacyBehavior passHref>
20
+ <NavigationMenuLink className={navigationMenuTriggerStyle()}>
21
+ Boards
22
+ </NavigationMenuLink>
23
+ </Link>
24
+ </NavigationMenuItem>
25
+ </NavigationMenuList>
26
+ </NavigationMenu>
27
+ </div>
28
+ </header>
29
+ );
30
+ }
@@ -0,0 +1,58 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ );
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"button"> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "button";
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ export { Button, buttonVariants };
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
@@ -0,0 +1,168 @@
1
+ import * as React from "react";
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3
+ import { cva } from "class-variance-authority";
4
+ import { ChevronDownIcon } from "lucide-react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function NavigationMenu({
9
+ className,
10
+ children,
11
+ viewport = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
14
+ viewport?: boolean;
15
+ }) {
16
+ return (
17
+ <NavigationMenuPrimitive.Root
18
+ data-slot="navigation-menu"
19
+ data-viewport={viewport}
20
+ className={cn(
21
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
22
+ className
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ {viewport && <NavigationMenuViewport />}
28
+ </NavigationMenuPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ function NavigationMenuList({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
36
+ return (
37
+ <NavigationMenuPrimitive.List
38
+ data-slot="navigation-menu-list"
39
+ className={cn(
40
+ "group flex flex-1 list-none items-center justify-center gap-1",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function NavigationMenuItem({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
52
+ return (
53
+ <NavigationMenuPrimitive.Item
54
+ data-slot="navigation-menu-item"
55
+ className={cn("relative", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ const navigationMenuTriggerStyle = cva(
62
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
63
+ );
64
+
65
+ function NavigationMenuTrigger({
66
+ className,
67
+ children,
68
+ ...props
69
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
70
+ return (
71
+ <NavigationMenuPrimitive.Trigger
72
+ data-slot="navigation-menu-trigger"
73
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
74
+ {...props}
75
+ >
76
+ {children}{" "}
77
+ <ChevronDownIcon
78
+ className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
79
+ aria-hidden="true"
80
+ />
81
+ </NavigationMenuPrimitive.Trigger>
82
+ );
83
+ }
84
+
85
+ function NavigationMenuContent({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
89
+ return (
90
+ <NavigationMenuPrimitive.Content
91
+ data-slot="navigation-menu-content"
92
+ className={cn(
93
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
94
+ "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
95
+ className
96
+ )}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ function NavigationMenuViewport({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
106
+ return (
107
+ <div
108
+ className={cn(
109
+ "absolute top-full left-0 isolate z-50 flex justify-center"
110
+ )}
111
+ >
112
+ <NavigationMenuPrimitive.Viewport
113
+ data-slot="navigation-menu-viewport"
114
+ className={cn(
115
+ "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
116
+ className
117
+ )}
118
+ {...props}
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ const NavigationMenuLink = React.forwardRef<
125
+ React.ElementRef<typeof NavigationMenuPrimitive.Link>,
126
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Link>
127
+ >(({ className, ...props }, ref) => (
128
+ <NavigationMenuPrimitive.Link
129
+ ref={ref}
130
+ data-slot="navigation-menu-link"
131
+ className={cn(
132
+ "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
133
+ className
134
+ )}
135
+ {...props}
136
+ />
137
+ ));
138
+ NavigationMenuLink.displayName = "NavigationMenuLink";
139
+
140
+ function NavigationMenuIndicator({
141
+ className,
142
+ ...props
143
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
144
+ return (
145
+ <NavigationMenuPrimitive.Indicator
146
+ data-slot="navigation-menu-indicator"
147
+ className={cn(
148
+ "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
149
+ className
150
+ )}
151
+ {...props}
152
+ >
153
+ <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
154
+ </NavigationMenuPrimitive.Indicator>
155
+ );
156
+ }
157
+
158
+ export {
159
+ NavigationMenu,
160
+ NavigationMenuList,
161
+ NavigationMenuItem,
162
+ NavigationMenuContent,
163
+ NavigationMenuTrigger,
164
+ NavigationMenuLink,
165
+ NavigationMenuIndicator,
166
+ NavigationMenuViewport,
167
+ navigationMenuTriggerStyle,
168
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }