@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.
- package/README.md +191 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +887 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/templates/README.md +120 -0
- package/templates/api/.env.example +62 -0
- package/templates/api/Dockerfile +32 -0
- package/templates/api/README.md +132 -0
- package/templates/api/alembic/env.py +106 -0
- package/templates/api/alembic/script.py.mako +28 -0
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
- package/templates/api/alembic.ini +36 -0
- package/templates/api/config/generators.yaml +25 -0
- package/templates/api/config/storage_config.yaml +26 -0
- package/templates/api/docs/ADDING_GENERATORS.md +409 -0
- package/templates/api/docs/GENERATORS_API.md +502 -0
- package/templates/api/docs/MIGRATIONS.md +472 -0
- package/templates/api/docs/storage_providers.md +337 -0
- package/templates/api/pyproject.toml +165 -0
- package/templates/api/src/boards/__init__.py +10 -0
- package/templates/api/src/boards/api/app.py +171 -0
- package/templates/api/src/boards/api/auth.py +75 -0
- package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
- package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
- package/templates/api/src/boards/api/endpoints/setup.py +505 -0
- package/templates/api/src/boards/api/endpoints/sse.py +129 -0
- package/templates/api/src/boards/api/endpoints/storage.py +74 -0
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
- package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
- package/templates/api/src/boards/auth/__init__.py +15 -0
- package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
- package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
- package/templates/api/src/boards/auth/adapters/base.py +73 -0
- package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
- package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
- package/templates/api/src/boards/auth/adapters/none.py +102 -0
- package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
- package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
- package/templates/api/src/boards/auth/context.py +35 -0
- package/templates/api/src/boards/auth/factory.py +115 -0
- package/templates/api/src/boards/auth/middleware.py +221 -0
- package/templates/api/src/boards/auth/provisioning.py +129 -0
- package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
- package/templates/api/src/boards/cli.py +354 -0
- package/templates/api/src/boards/config.py +116 -0
- package/templates/api/src/boards/database/__init__.py +7 -0
- package/templates/api/src/boards/database/cli.py +110 -0
- package/templates/api/src/boards/database/connection.py +252 -0
- package/templates/api/src/boards/database/models.py +19 -0
- package/templates/api/src/boards/database/seed_data.py +182 -0
- package/templates/api/src/boards/dbmodels/__init__.py +455 -0
- package/templates/api/src/boards/generators/__init__.py +57 -0
- package/templates/api/src/boards/generators/artifacts.py +53 -0
- package/templates/api/src/boards/generators/base.py +140 -0
- package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
- package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
- package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
- package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
- package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
- package/templates/api/src/boards/generators/loader.py +253 -0
- package/templates/api/src/boards/generators/registry.py +114 -0
- package/templates/api/src/boards/generators/resolution.py +515 -0
- package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
- package/templates/api/src/boards/graphql/__init__.py +7 -0
- package/templates/api/src/boards/graphql/access_control.py +136 -0
- package/templates/api/src/boards/graphql/mutations/root.py +136 -0
- package/templates/api/src/boards/graphql/queries/root.py +116 -0
- package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
- package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
- package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
- package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
- package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
- package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
- package/templates/api/src/boards/graphql/schema.py +81 -0
- package/templates/api/src/boards/graphql/types/board.py +102 -0
- package/templates/api/src/boards/graphql/types/generation.py +130 -0
- package/templates/api/src/boards/graphql/types/generator.py +17 -0
- package/templates/api/src/boards/graphql/types/user.py +47 -0
- package/templates/api/src/boards/jobs/repository.py +104 -0
- package/templates/api/src/boards/logging.py +195 -0
- package/templates/api/src/boards/middleware.py +339 -0
- package/templates/api/src/boards/progress/__init__.py +4 -0
- package/templates/api/src/boards/progress/models.py +25 -0
- package/templates/api/src/boards/progress/publisher.py +64 -0
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +118 -0
- package/templates/api/src/boards/storage/__init__.py +52 -0
- package/templates/api/src/boards/storage/base.py +363 -0
- package/templates/api/src/boards/storage/config.py +187 -0
- package/templates/api/src/boards/storage/factory.py +278 -0
- package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
- package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
- package/templates/api/src/boards/storage/implementations/local.py +201 -0
- package/templates/api/src/boards/storage/implementations/s3.py +294 -0
- package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
- package/templates/api/src/boards/tenant_isolation.py +446 -0
- package/templates/api/src/boards/validation.py +262 -0
- package/templates/api/src/boards/workers/__init__.py +1 -0
- package/templates/api/src/boards/workers/actors.py +201 -0
- package/templates/api/src/boards/workers/cli.py +125 -0
- package/templates/api/src/boards/workers/context.py +188 -0
- package/templates/api/src/boards/workers/middleware.py +58 -0
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.dev.yaml +39 -0
- package/templates/compose.yaml +109 -0
- package/templates/docker/env.example +23 -0
- package/templates/web/.env.example +28 -0
- package/templates/web/Dockerfile +51 -0
- package/templates/web/components.json +22 -0
- package/templates/web/imageLoader.js +18 -0
- package/templates/web/next-env.d.ts +5 -0
- package/templates/web/next.config.js +36 -0
- package/templates/web/package.json +37 -0
- package/templates/web/postcss.config.mjs +7 -0
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
- package/templates/web/src/app/globals.css +120 -0
- package/templates/web/src/app/layout.tsx +21 -0
- package/templates/web/src/app/page.tsx +35 -0
- package/templates/web/src/app/providers.tsx +18 -0
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
- package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
- package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
- package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
- package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
- package/templates/web/src/components/header.tsx +30 -0
- package/templates/web/src/components/ui/button.tsx +58 -0
- package/templates/web/src/components/ui/card.tsx +92 -0
- package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
- package/templates/web/src/lib/utils.ts +6 -0
- 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
|
+
};
|