Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions desktop/src/apps/AppStudioApp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import React from "react";
import { AppStudioApp } from "./AppStudioApp";

describe("AppStudioApp", () => {
it("renders the titlebar with App Studio", () => {
render(<AppStudioApp windowId="test" />);
expect(screen.getByText("App Studio")).toBeInTheDocument();
});

it("renders all rail items", () => {
const { container } = render(<AppStudioApp windowId="test" />);
const nav = container.querySelector("nav[aria-label='App Studio views']");
expect(nav).toBeInTheDocument();
// Rail buttons are inside the nav
const railBtns = nav!.querySelectorAll("button");
const labels = Array.from(railBtns).map((b) => b.getAttribute("aria-label"));
expect(labels).toContain("Build");
expect(labels).toContain("Templates");
expect(labels).toContain("Publish");
expect(labels).toContain("SDK");
});

it("shows Build view by default", () => {
render(<AppStudioApp windowId="test" />);
expect(screen.getByRole("heading", { name: /^build$/i })).toBeInTheDocument();
// checkerboard sandbox area has the live preview header text
expect(screen.getByText("Build log")).toBeInTheDocument();
});

it("switches to Templates view and shows template cards", () => {
const { container } = render(<AppStudioApp windowId="test" />);
const nav = container.querySelector("nav[aria-label='App Studio views']")!;
const templatesBtn = Array.from(nav.querySelectorAll("button")).find(
(b) => b.getAttribute("aria-label") === "Templates"
)!;
fireEvent.click(templatesBtn);
// hero heading
expect(screen.getByText(/build a taOS app in plain words/i)).toBeInTheDocument();
// template card labels
expect(screen.getByText("Dashboard")).toBeInTheDocument();
expect(screen.getByText("Tracker")).toBeInTheDocument();
expect(screen.getByText("Kanban")).toBeInTheDocument();
expect(screen.getByText("Blank")).toBeInTheDocument();
});

it("switches to Publish view and shows capability rows", () => {
const { container } = render(<AppStudioApp windowId="test" />);
const nav = container.querySelector("nav[aria-label='App Studio views']")!;
const publishBtn = Array.from(nav.querySelectorAll("button")).find(
(b) => b.getAttribute("aria-label") === "Publish"
)!;
fireEvent.click(publishBtn);
// app identity
expect(screen.getAllByText("Chore Quest").length).toBeGreaterThan(0);
// capability row labels
expect(screen.getByTestId("perm-row-workspace")).toBeInTheDocument();
expect(screen.getByTestId("perm-row-notifications")).toBeInTheDocument();
expect(screen.getByTestId("perm-row-household")).toBeInTheDocument();
// publish button
expect(screen.getByRole("button", { name: /publish to my store/i })).toBeInTheDocument();
});
});
90 changes: 90 additions & 0 deletions desktop/src/apps/AppStudioApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useState } from "react";
import { Sparkles, LayoutGrid, Share2, CircleDot } from "lucide-react";
import { BuildView } from "./appstudio/BuildView";
import { TemplatesView } from "./appstudio/TemplatesView";
import { PublishView } from "./appstudio/PublishView";

/* ------------------------------------------------------------------ */
/* App Studio -- shell */
/* */
/* Build taOS apps from plain words. An agent generates them against */
/* the taOS SDK, sandboxed and safe. Publish to your Store or share */
/* with family. */
/* */
/* Shell follows the canonical studio pattern from GameStudioApp: */
/* 46px centered titlebar, 68px icon rail, per-view subfolder. */
/* ------------------------------------------------------------------ */

type AppStudioView = "build" | "templates" | "publish" | "sdk";

const RAIL: { id: AppStudioView; label: string; icon: typeof Sparkles }[] = [
{ id: "build", label: "Build", icon: Sparkles },
{ id: "templates", label: "Templates", icon: LayoutGrid },
{ id: "publish", label: "Publish", icon: Share2 },
];

const RAIL_BOTTOM: { id: AppStudioView; label: string; icon: typeof Sparkles }[] = [
{ id: "sdk", label: "SDK", icon: CircleDot },
];

export function AppStudioApp({ windowId: _windowId }: { windowId: string }) {
const [view, setView] = useState<AppStudioView>("build");

function RailButton({ id, label, icon: Icon }: { id: AppStudioView; label: string; icon: typeof Sparkles }) {
const on = view === id;
return (
<button
key={id}
type="button"
aria-label={label}
aria-current={on ? "page" : undefined}
onClick={() => setView(id)}
className={`flex h-[46px] w-[46px] flex-col items-center justify-center gap-0.5 rounded-xl text-[9px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
on
? "bg-gradient-to-b from-accent/25 to-transparent text-accent"
: "text-shell-text-tertiary hover:bg-white/10 hover:text-shell-text-secondary"
}`}
>
<Icon size={21} />
{label}
</button>
);
}

return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-shell-bg text-shell-text select-none">
{/* titlebar */}
<div className="flex h-[46px] flex-none items-center justify-center border-b border-shell-border">
<span className="text-[13px] font-semibold tracking-[-0.01em]">App Studio</span>
</div>

<div className="flex min-h-0 flex-1">
{/* left rail */}
<nav
aria-label="App Studio views"
className="flex w-[68px] flex-none flex-col items-center gap-1.5 border-r border-shell-border bg-shell-bg-deep py-3.5"
>
{RAIL.map((r) => (
<RailButton key={r.id} {...r} />
))}
<div className="flex-1" />
{RAIL_BOTTOM.map((r) => (
<RailButton key={r.id} {...r} />
))}
</nav>

{/* active surface */}
<div className="flex min-w-0 flex-1 flex-col">
{view === "build" && <BuildView />}
{view === "templates" && <TemplatesView />}
{view === "publish" && <PublishView />}
{view === "sdk" && (
<div className="flex flex-1 items-center justify-center text-[13px] text-shell-text-tertiary">
SDK docs coming soon
</div>
)}
</div>
</div>
</div>
);
}
71 changes: 71 additions & 0 deletions desktop/src/apps/CodingStudioApp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { CodingStudioApp } from "./CodingStudioApp";

function renderApp() {
return render(<CodingStudioApp windowId="test-window" />);
}

describe("CodingStudioApp", () => {
it("renders the app titlebar", () => {
renderApp();
expect(screen.getByText("Coding Studio")).toBeDefined();
});

it("renders all rail items", () => {
renderApp();
// Rail buttons use aria-label for exact matching via the nav element
const nav = screen.getByRole("navigation", { name: "Coding Studio views" });
expect(nav).toBeDefined();
expect(screen.getByRole("button", { name: "Code" })).toBeDefined();
expect(screen.getByRole("button", { name: "Preview" })).toBeDefined();
expect(screen.getByRole("button", { name: "Templates" })).toBeDefined();
expect(screen.getByRole("button", { name: "Models" })).toBeDefined();
});

it("shows Build view by default with Build rail item active", () => {
renderApp();
// The rail Build button (inside nav) should be aria-current="page"
const nav = screen.getByRole("navigation", { name: "Coding Studio views" });
const railBuildBtn = nav.querySelector('[aria-label="Build"]') as HTMLElement;
expect(railBuildBtn).toBeTruthy();
expect(railBuildBtn.getAttribute("aria-current")).toBe("page");
});

it("switches to Templates view on rail click", () => {
renderApp();
fireEvent.click(screen.getByRole("button", { name: "Templates" }));
expect(screen.getByRole("button", { name: "Templates" }).getAttribute("aria-current")).toBe(
"page",
);
expect(screen.getByText("Describe what you want to build.")).toBeDefined();
});

it("Templates view shows all 8 template cards", () => {
renderApp();
fireEvent.click(screen.getByRole("button", { name: "Templates" }));
const expectedNames = [
"Web App",
"REST API",
"CLI Tool",
"Discord Bot",
"Static Site",
"Data Pipeline",
"Python Script",
"Browser Extension",
];
for (const name of expectedNames) {
expect(screen.getByText(name)).toBeDefined();
}
});

it("switches to Preview view on rail click and shows preview header", () => {
renderApp();
fireEvent.click(screen.getByRole("button", { name: "Preview" }));
expect(screen.getByRole("button", { name: "Preview" }).getAttribute("aria-current")).toBe(
"page",
);
// Preview header h2
expect(screen.getByRole("heading", { name: "Preview" })).toBeDefined();
});
});
74 changes: 74 additions & 0 deletions desktop/src/apps/CodingStudioApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState } from "react";
import { Sparkles, Code2, Play, LayoutGrid, Settings2 } from "lucide-react";
import { BuildView } from "./codingstudio/BuildView";
import { TemplatesView } from "./codingstudio/TemplatesView";
import { PreviewView } from "./codingstudio/PreviewView";

type CodingView = "build" | "code" | "preview" | "templates";

const RAIL: { id: CodingView; label: string; icon: typeof Sparkles }[] = [
{ id: "build", label: "Build", icon: Sparkles },
{ id: "code", label: "Code", icon: Code2 },
{ id: "preview", label: "Preview", icon: Play },
{ id: "templates", label: "Templates", icon: LayoutGrid },
];

export function CodingStudioApp({ windowId: _windowId }: { windowId: string }) {
const [view, setView] = useState<CodingView>("build");

return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-shell-bg text-shell-text select-none">
{/* title strip */}
<div className="flex h-[46px] flex-none items-center justify-center border-b border-shell-border">
<span className="text-[13px] font-semibold tracking-[-0.01em]">Coding Studio</span>
</div>

<div className="flex min-h-0 flex-1">
{/* left rail */}
<nav
aria-label="Coding Studio views"
className="flex w-[68px] flex-none flex-col items-center gap-1.5 border-r border-shell-border bg-shell-bg-deep py-3.5"
>
{RAIL.map((r) => {
const Icon = r.icon;
const on = view === r.id;
return (
<button
key={r.id}
type="button"
aria-label={r.label}
aria-current={on ? "page" : undefined}
onClick={() => setView(r.id)}
className={`flex h-[46px] w-[46px] flex-col items-center justify-center gap-0.5 rounded-xl text-[9px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
on
? "bg-gradient-to-b from-accent/25 to-transparent text-accent"
: "text-shell-text-tertiary hover:bg-white/10 hover:text-shell-text-secondary"
}`}
>
<Icon size={21} />
{r.label}
</button>
);
})}
<div className="flex-1" />
<button
type="button"
aria-label="Models"
className="flex h-[46px] w-[46px] flex-col items-center justify-center gap-0.5 rounded-xl text-[9px] font-semibold text-shell-text-tertiary transition-colors hover:bg-white/10 hover:text-shell-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
<Settings2 size={21} />
Models
</button>
</nav>

{/* active surface */}
<div className="flex min-w-0 flex-1 flex-col">
{view === "build" && <BuildView />}
{view === "code" && <BuildView />}
{view === "preview" && <PreviewView />}
{view === "templates" && <TemplatesView />}
</div>
</div>
</div>
);
}
69 changes: 69 additions & 0 deletions desktop/src/apps/DesignStudioApp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { DesignStudioApp } from "./DesignStudioApp";

function renderApp() {
return render(<DesignStudioApp windowId="test-window" />);
}

describe("DesignStudioApp", () => {
it("renders the app titlebar", () => {
renderApp();
expect(screen.getByText("Design Studio")).toBeDefined();
});

it("renders all rail items", () => {
renderApp();
const nav = screen.getByRole("navigation", { name: "Design Studio views" });
expect(nav).toBeDefined();
expect(screen.getByRole("button", { name: "Design" })).toBeDefined();
expect(screen.getByRole("button", { name: "Templates" })).toBeDefined();
expect(screen.getByRole("button", { name: "Elements" })).toBeDefined();
expect(screen.getByRole("button", { name: "Magic" })).toBeDefined();
expect(screen.getByRole("button", { name: "Brand" })).toBeDefined();
});

it("shows Design view by default with Design rail item active", () => {
renderApp();
const nav = screen.getByRole("navigation", { name: "Design Studio views" });
const designBtn = nav.querySelector('[aria-label="Design"]') as HTMLElement;
expect(designBtn).toBeTruthy();
expect(designBtn.getAttribute("aria-current")).toBe("page");
});

it("default Design view renders the canvas artboard", () => {
renderApp();
expect(screen.getByText("Untitled poster")).toBeDefined();
});

it("switches to Templates view and shows template cards", () => {
renderApp();
fireEvent.click(screen.getByRole("button", { name: "Templates" }));
expect(screen.getByRole("button", { name: "Templates" }).getAttribute("aria-current")).toBe(
"page",
);
const expectedCards = [
"Instagram Post",
"Story",
"Poster",
"Presentation",
"Logo",
"Flyer",
"Banner",
"Business Card",
];
for (const name of expectedCards) {
expect(screen.getByText(name)).toBeDefined();
}
});

it("switches to Magic view and shows prompt bar and result tiles", () => {
renderApp();
fireEvent.click(screen.getByRole("button", { name: "Magic" }));
expect(screen.getByRole("button", { name: "Magic" }).getAttribute("aria-current")).toBe("page");
expect(screen.getByText("Describe the design you need.")).toBeDefined();
expect(screen.getByText("Bold, centered")).toBeDefined();
expect(screen.getByText("Split layout")).toBeDefined();
expect(screen.getAllByText("Editorial").length).toBeGreaterThan(0);
});
});
Loading
Loading