Skip to content
221 changes: 100 additions & 121 deletions fastapps/cli/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,129 +121,108 @@ def create_widget(name: str, auth_type: str = None, scopes: list = None, templat
dest_dir = widget_dir / item.name
shutil.copytree(item, dest_dir, dirs_exist_ok=True)

# Install additional dependencies for templates
if template in ["list", "carousel", "albums"]:
console.print(f"\n[green][OK] Widget '{name}' created from '{template}' template![/green]")

dep_name = {"list": "Tailwind CSS", "carousel": "Carousel", "albums": "Albums"}.get(template, "Template")
console.print(f"\n[cyan]Installing {dep_name} dependencies...[/cyan]")

# Check if package.json exists
package_json_path = Path("package.json")
if package_json_path.exists():
try:
# Read current package.json
with open(package_json_path, 'r') as f:
package_data = json.load(f)

# Add Tailwind dependencies to devDependencies
if 'devDependencies' not in package_data:
package_data['devDependencies'] = {}
if 'dependencies' not in package_data:
package_data['dependencies'] = {}

# Template-specific dependencies
if template == "list":
# Dev dependencies for list
template_dev_deps = {
"@tailwindcss/vite": "^4.1.11",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11"
}
# Runtime dependencies for list
template_deps = {
"lucide-react": "^0.552.0"
}
elif template == "carousel":
# Dev dependencies for carousel
template_dev_deps = {
"@tailwindcss/vite": "^4.1.11",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11"
}
# Runtime dependencies for carousel
template_deps = {
"lucide-react": "^0.552.0",
"embla-carousel-react": "^8.6.0"
}
elif template == "albums":
# Dev dependencies for albums
template_dev_deps = {
"@tailwindcss/vite": "^4.1.11",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11"
}
# Runtime dependencies for albums
template_deps = {
"lucide-react": "^0.552.0",
"embla-carousel-react": "^8.6.0"
}
else:
template_dev_deps = {}
template_deps = {}

# Check if dependencies already exist
deps_to_install = []
for dep, version in template_dev_deps.items():
if dep not in package_data['devDependencies']:
package_data['devDependencies'][dep] = version
deps_to_install.append(dep)

for dep, version in template_deps.items():
if dep not in package_data['dependencies']:
package_data['dependencies'][dep] = version
deps_to_install.append(dep)

# Write updated package.json
if deps_to_install:
with open(package_json_path, 'w') as f:
json.dump(package_data, f, indent=2)

dep_type = "template" if template else "Tailwind"
console.print(f"[cyan]Added {len(deps_to_install)} {dep_type} dependencies to package.json[/cyan]")

# Run npm install
console.print("[cyan]Running npm install...[/cyan]")
try:
result = subprocess.run(
["npm", "install"],
capture_output=True,
text=True,
check=True
)
success_msg = f"{dep_name} dependencies" if template in ["list", "carousel", "albums"] else "Dependencies"
console.print(f"[green]✓ {success_msg} installed[/green]")
except subprocess.CalledProcessError as e:
console.print("[yellow]⚠ npm install failed. Run 'npm install' manually[/yellow]")
except FileNotFoundError:
console.print("[yellow]⚠ npm not found. Run 'npm install' manually[/yellow]")
else:
success_msg = f"{dep_name} dependencies" if template in ["list", "carousel", "albums"] else "Dependencies"
console.print(f"[green]✓ {success_msg} already installed[/green]")

except Exception as e:
console.print(f"[yellow]⚠ Could not update package.json: {e}[/yellow]")
if template in ["carousel", "albums"]:
console.print(f"[yellow]Please install {template} dependencies manually:[/yellow]")
console.print("[dim] npm install embla-carousel-react lucide-react[/dim]")
console.print("[dim] npm install -D @tailwindcss/vite tailwindcss autoprefixer postcss[/dim]")
else:
console.print("[yellow]Please install Tailwind CSS manually:[/yellow]")
console.print("[dim] npm install -D @tailwindcss/vite tailwindcss autoprefixer postcss[/dim]")
else:
console.print("[yellow]⚠ package.json not found[/yellow]")
if template in ["carousel", "albums"]:
console.print(f"[yellow]Please install {template} dependencies manually:[/yellow]")
console.print("[dim] npm install embla-carousel-react lucide-react[/dim]")
console.print("[dim] npm install -D @tailwindcss/vite tailwindcss autoprefixer postcss[/dim]")
# Install dependencies for templates (Apps SDK UI + Tailwind + template extras)
console.print(f"\n[green][OK] Widget '{name}' created from '{template_name}' template![/green]")

dep_name = {"list": "List", "carousel": "Carousel", "albums": "Albums"}.get(template_name, "Widget")
console.print(f"\n[cyan]Installing {dep_name} dependencies...[/cyan]")

# Check if package.json exists
package_json_path = Path("package.json")
if package_json_path.exists():
try:
# Read current package.json
with open(package_json_path, 'r') as f:
package_data = json.load(f)

if 'devDependencies' not in package_data:
package_data['devDependencies'] = {}
if 'dependencies' not in package_data:
package_data['dependencies'] = {}

# Base deps shared across templates (Apps SDK UI + Tailwind 4)
template_dev_deps = {
"@tailwindcss/vite": "^4.1.11",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11"
}
template_deps = {
"@openai/apps-sdk-ui": "^0.1.0"
}

# Template-specific runtime deps
if template_name == "list":
template_deps.update({
"lucide-react": "^0.552.0"
})
elif template_name == "carousel":
template_deps.update({
"lucide-react": "^0.552.0",
"embla-carousel-react": "^8.6.0"
})
elif template_name == "albums":
template_deps.update({
"lucide-react": "^0.552.0",
"embla-carousel-react": "^8.6.0"
})

# Check if dependencies already exist
deps_to_install = []
for dep, version in template_dev_deps.items():
if dep not in package_data['devDependencies']:
package_data['devDependencies'][dep] = version
deps_to_install.append(dep)

for dep, version in template_deps.items():
if dep not in package_data['dependencies']:
package_data['dependencies'][dep] = version
deps_to_install.append(dep)

# Write updated package.json
if deps_to_install:
with open(package_json_path, 'w') as f:
json.dump(package_data, f, indent=2)

console.print(f"[cyan]Added {len(deps_to_install)} dependencies to package.json[/cyan]")

# Run npm install
console.print("[cyan]Running npm install...[/cyan]")
try:
result = subprocess.run(
["npm", "install"],
capture_output=True,
text=True,
check=True
)
console.print(f"[green]✓ {dep_name} dependencies installed[/green]")
except subprocess.CalledProcessError as e:
console.print("[yellow]⚠ npm install failed. Run 'npm install' manually[/yellow]")
except FileNotFoundError:
console.print("[yellow]⚠ npm not found. Run 'npm install' manually[/yellow]")
else:
console.print("[yellow]Please install Tailwind CSS manually:[/yellow]")
console.print("[dim] npm install -D @tailwindcss/vite tailwindcss autoprefixer postcss[/dim]")
console.print(f"[green]✓ {dep_name} dependencies already installed[/green]")

except Exception as e:
console.print(f"[yellow]⚠ Could not update package.json: {e}[/yellow]")
console.print("[yellow]Please install dependencies manually:[/yellow]")
manual_deps = ["@openai/apps-sdk-ui"]
if template_name in ["carousel", "albums"]:
manual_deps.extend(["embla-carousel-react", "lucide-react"])
elif template_name == "list":
manual_deps.append("lucide-react")
console.print(f"[dim] npm install {' '.join(manual_deps)}[/dim]")
console.print("[dim] npm install -D @tailwindcss/vite tailwindcss autoprefixer postcss[/dim]")
else:
console.print(f"\n[green][OK] Widget '{name}' created![/green]")
console.print("[yellow]⚠ package.json not found[/yellow]")
manual_deps = ["@openai/apps-sdk-ui"]
if template_name in ["carousel", "albums"]:
manual_deps.extend(["embla-carousel-react", "lucide-react"])
elif template_name == "list":
manual_deps.append("lucide-react")
console.print("[yellow]Please install dependencies manually:[/yellow]")
console.print(f"[dim] npm install {' '.join(manual_deps)}[/dim]")
console.print("[dim] npm install -D @tailwindcss/vite tailwindcss autoprefixer postcss[/dim]")

console.print("\n[green][OK] Widget created successfully![/green]")
console.print("\n[cyan]Created files:[/cyan]")
Expand Down
36 changes: 23 additions & 13 deletions fastapps/templates/albums/widget/AlbumCard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from "react";
import { Button } from "@openai/apps-sdk-ui/components/Button";
import { Image } from "@openai/apps-sdk-ui/components/Image";

function AlbumCard({ album, onSelect }) {
if (!album) return null;
Expand All @@ -8,25 +10,33 @@ function AlbumCard({ album, onSelect }) {
const title = album.title || "Album";

return (
<button
<Button
type="button"
className="group relative cursor-pointer flex-shrink-0 w-[272px] bg-transparent text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/40 dark:focus-visible:ring-white/40 rounded-3xl"
variant="ghost"
color="secondary"
className="group relative cursor-pointer flex-shrink-0 w-[272px] bg-transparent text-left rounded-3xl p-0 focus-visible:ring-2 focus-visible:ring-default flex-col items-start"
style={{ height: "auto" }}
onClick={() => onSelect?.(album)}
aria-label={`Open ${title}`}
block
>
<div className="aspect-[4/3] w-full overflow-hidden rounded-2xl shadow-lg">
<img
src={album.cover}
alt={title}
className="h-full w-full object-cover"
loading="lazy"
/>
<div className="w-full overflow-hidden rounded-2xl shadow-card bg-surface">
<div className="aspect-[4/3] w-full overflow-hidden">
<Image
src={album.cover}
alt={title}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
</div>
<div className="pt-3 px-1.5">
<div className="text-base font-medium truncate text-black dark:text-white">{title}</div>
<div className="text-sm text-black/80 dark:text-white/80 mt-0.5">{photoSummary}</div>
<div className="pt-3 px-1.5 w-full">
<div className="text-base font-medium truncate text-foreground">
{title}
</div>
<div className="text-sm text-secondary mt-0.5">{photoSummary}</div>
</div>
</button>
</Button>
);
}

Expand Down
9 changes: 5 additions & 4 deletions fastapps/templates/albums/widget/FilmStrip.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { Image } from "@openai/apps-sdk-ui/components/Image";

export default function FilmStrip({ album, selectedIndex, onSelect }) {
if (!album?.photos?.length) {
Expand All @@ -13,16 +14,16 @@ export default function FilmStrip({ album, selectedIndex, onSelect }) {
type="button"
onClick={() => onSelect?.(idx)}
className={
"block w-full p-[1px] pointer-events-auto rounded-xl cursor-pointer border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/40 dark:focus-visible:ring-white/40 " +
"block w-full p-[1px] pointer-events-auto rounded-xl cursor-pointer border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-default " +
(idx === selectedIndex
? "border-black bg-black/5 dark:border-white dark:bg-white/10"
: "border-black/10 hover:border-black/50 dark:border-white/20 dark:hover:border-white/60")
? "border-strong bg-primary-soft-alpha"
: "border-default hover:border-strong")
}
aria-pressed={idx === selectedIndex}
aria-label={`View ${photo.title || `photo ${idx + 1}`}`}
>
<div className="aspect-[5/3] rounded-lg overflow-hidden w-full">
<img
<Image
src={photo.url}
alt={photo.title || `Photo ${idx + 1}`}
className="h-full w-full object-cover"
Expand Down
62 changes: 28 additions & 34 deletions fastapps/templates/albums/widget/FullscreenViewer.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react";
import { Button } from "@openai/apps-sdk-ui/components/Button";
import { Image } from "@openai/apps-sdk-ui/components/Image";
import { ArrowLeft } from "lucide-react";
import { useMaxHeight } from "fastapps";
import FilmStrip from "./FilmStrip";

export default function FullscreenViewer({ album, onBack }) {
const maxHeight = useMaxHeight() ?? undefined;
const [index, setIndex] = React.useState(0);
const photos = Array.isArray(album?.photos) ? album.photos : [];

Expand All @@ -25,45 +25,39 @@ export default function FullscreenViewer({ album, onBack }) {
const photo = photos[index];

return (
<div
className="relative w-full h-full bg-white"
style={{
maxHeight,
height: maxHeight,
}}
>
{/* Back button */}
<div className="relative w-full bg-surface">
{onBack && (
<button
<Button
aria-label="Back to albums"
className="absolute left-4 top-4 z-20 inline-flex items-center justify-center h-10 w-10 rounded-full bg-white text-black shadow-lg ring ring-black/5 hover:bg-gray-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/40"
className="absolute left-4 top-4 z-20 shadow-lg"
variant="outline"
color="secondary"
size="md"
uniform
pill
onClick={onBack}
type="button"
>
<ArrowLeft
strokeWidth={1.5}
className="h-5 w-5"
aria-hidden="true"
/>
</button>
<ArrowLeft strokeWidth={1.5} className="h-4 w-4" aria-hidden="true" />
</Button>
)}

<div className="absolute inset-0 flex flex-row overflow-hidden">
{/* Film strip */}
<div className="hidden md:block absolute pointer-events-none z-10 left-0 top-0 bottom-0 w-40">
<FilmStrip album={{ ...album, photos }} selectedIndex={index} onSelect={setIndex} />
<div className="flex flex-col md:flex-row items-stretch gap-6 md:gap-10 px-4 sm:px-6 md:px-10 py-8 md:py-12">
<div className="hidden md:flex w-40 shrink-0">
<FilmStrip
album={{ ...album, photos }}
selectedIndex={index}
onSelect={setIndex}
/>
</div>
{/* Main photo */}
<div className="flex-1 min-w-0 px-40 py-10 relative flex items-center justify-center">
<div className="relative w-full h-full">
{photo ? (
<img
src={photo.url}
alt={photo.title || album.title || "Photo"}
className="absolute inset-0 m-auto rounded-3xl shadow-sm border border-black/10 max-w-full max-h-full object-contain"
/>
) : null}
</div>

<div className="flex-1 min-w-0 flex items-center justify-center">
{photo ? (
<Image
src={photo.url}
alt={photo.title || album.title || "Photo"}
className="w-full max-h-[70vh] rounded-3xl shadow-sm border border-default object-contain"
/>
) : null}
</div>
</div>
</div>
Expand Down
Loading
Loading