diff --git a/app/[handle]/layout.tsx b/app/[handle]/layout.tsx
new file mode 100644
index 0000000..b71ff47
--- /dev/null
+++ b/app/[handle]/layout.tsx
@@ -0,0 +1,15 @@
+import { Masthead } from "@/components/masthead";
+import { Sidebar } from "@/components/sidebar";
+
+// A moldura do canal: Masthead + Body(Sidebar, Main), igual nas duas telas.
+export default function ChannelLayout({ children }: LayoutProps<"/[handle]">) {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/[handle]/videos/page.tsx b/app/[handle]/videos/page.tsx
new file mode 100644
index 0000000..700a991
--- /dev/null
+++ b/app/[handle]/videos/page.tsx
@@ -0,0 +1,38 @@
+import { CardVideo } from "@/components/card/video";
+import { ChannelHeader } from "@/components/channel-header";
+import { Chip } from "@/components/chip";
+import { VIDEOS } from "@/lib/fixtures";
+
+// Channel — Videos.
+//
+// O Header Region usa $space-14, que é 58px — NÃO os 56px de p-14 (§4).
+// O grid do design tem 3 linhas de 3 cards; em código isso vira auto-fill sobre
+// a largura medida do card (357px), com gap-x-4 dentro da linha e gap-y-8 entre
+// linhas, que são os dois gaps do .pen (§8).
+export default async function ChannelVideosPage({ params }: PageProps<"/[handle]/videos">) {
+ const { handle } = await params;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {VIDEOS.map((video) => (
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
index c887311..12d17af 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,69 +1,6 @@
-import Image from "next/image";
+import { redirect } from "next/navigation";
-export default function Home() {
- return (
-
-
-
-
-
- To get started, edit the{" "}
-
- page.tsx
- {" "}
- file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
- );
+// Só existe a tela do canal por enquanto. O feed de home não foi desenhado.
+export default function RootPage() {
+ redirect("/@FullCycle/videos");
}
diff --git a/components/avatar.tsx b/components/avatar.tsx
new file mode 100644
index 0000000..b9457c6
--- /dev/null
+++ b/components/avatar.tsx
@@ -0,0 +1,13 @@
+import Image from "next/image";
+
+// Avatar — 160x160 circular, recortado.
+export function Avatar({ src, alt }: { src: string; alt: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/components/badge/overlay.tsx b/components/badge/overlay.tsx
new file mode 100644
index 0000000..fc48da1
--- /dev/null
+++ b/components/badge/overlay.tsx
@@ -0,0 +1,11 @@
+// Badge / Overlay — duração sobre a thumbnail. $radius-xs é 4px = rounded-sm.
+export function BadgeOverlay({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/button/icon.tsx b/components/button/icon.tsx
new file mode 100644
index 0000000..397fcc5
--- /dev/null
+++ b/components/button/icon.tsx
@@ -0,0 +1,16 @@
+import type { LucideIcon } from "lucide-react";
+
+// Button / Icon — 40x40, circular. §9: hover e foco são acréscimo de
+// engenharia; o design não os define.
+export function ButtonIcon({ icon: Icon, label }: { icon: LucideIcon; label: string }) {
+ return (
+
+ );
+}
diff --git a/components/button/outline.tsx b/components/button/outline.tsx
new file mode 100644
index 0000000..315dd91
--- /dev/null
+++ b/components/button/outline.tsx
@@ -0,0 +1,22 @@
+import type { LucideIcon } from "lucide-react";
+
+// Button / Outline — pill com borda (Sign in). O padding horizontal é 15px no
+// design: fora da grade de 4, então valor arbitrário (§2.5).
+export function ButtonOutline({
+ icon: Icon,
+ children,
+}: {
+ icon: LucideIcon;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/components/button/primary.tsx b/components/button/primary.tsx
new file mode 100644
index 0000000..a47c4da
--- /dev/null
+++ b/components/button/primary.tsx
@@ -0,0 +1,12 @@
+// Button / Primary — pill preenchido com a superfície inversa (Subscribe).
+export function ButtonPrimary({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
diff --git a/components/card/video.tsx b/components/card/video.tsx
new file mode 100644
index 0000000..d57c00a
--- /dev/null
+++ b/components/card/video.tsx
@@ -0,0 +1,35 @@
+import Image from "next/image";
+import { BadgeOverlay } from "@/components/badge/overlay";
+import type { Video } from "@/lib/fixtures";
+
+// Card / Video — variante do grid.
+//
+// A altura de 201px da thumbnail no .pen é 16:9 da largura do card congelada
+// (357.33 x 9/16). O formato não tem primitiva de proporção; o código tem, e o
+// layout é fluido — então aspect-video, nunca h-[201px] (§8).
+//
+// Os insets de 26px e 24px no bloco de texto fazem o título quebrar antes da
+// metadata. São valores do design fora da grade de 4 (§2.5).
+export function CardVideo({ video }: { video: Video }) {
+ return (
+
+
+
+
+ {video.duration}
+
+
+
+
+
{video.title}
+
{video.meta}
+
+
+ );
+}
diff --git a/components/channel-header.tsx b/components/channel-header.tsx
new file mode 100644
index 0000000..d11cece
--- /dev/null
+++ b/components/channel-header.tsx
@@ -0,0 +1,74 @@
+import Image from "next/image";
+import { Link2, Search } from "lucide-react";
+import { Avatar } from "@/components/avatar";
+import { ButtonPrimary } from "@/components/button/primary";
+import { Tab } from "@/components/tab";
+import { CHANNEL } from "@/lib/fixtures";
+
+const TABS = ["Home", "Videos", "Shorts", "Live", "Playlists"] as const;
+export type ChannelTab = (typeof TABS)[number];
+
+function tabHref(handle: string, tab: ChannelTab) {
+ return tab === "Home" ? `/${handle}` : `/${handle}/${tab.toLowerCase()}`;
+}
+
+// Channel Header — banner, identidade e a barra de abas.
+// O banner tem 172px no design (43 x 4), logo h-43 na escala nativa.
+export function ChannelHeader({ handle, activeTab }: { handle: string; activeTab: ChannelTab }) {
+ return (
+
+ );
+}
diff --git a/components/chip.tsx b/components/chip.tsx
new file mode 100644
index 0000000..1868058
--- /dev/null
+++ b/components/chip.tsx
@@ -0,0 +1,19 @@
+// Chip — $radius-sm é 8px, logo rounded-lg e NÃO rounded-sm (§6).
+export function Chip({ label, active = false }: { label: string; active?: boolean }) {
+ return (
+
+ );
+}
diff --git a/components/logo/youtube.tsx b/components/logo/youtube.tsx
new file mode 100644
index 0000000..36fcee1
--- /dev/null
+++ b/components/logo/youtube.tsx
@@ -0,0 +1,19 @@
+// Wordmark do YouTube, transcrito de design/pendev/logo-youtube.json.
+// Não é ícone lucide: são paths próprios, com os três grupos tokenizados.
+// O triângulo é branco fixo nos dois temas — assim está no design.
+
+export function YouTubeWordmark() {
+ return (
+
+ );
+}
diff --git a/components/masthead.tsx b/components/masthead.tsx
new file mode 100644
index 0000000..a1c41ba
--- /dev/null
+++ b/components/masthead.tsx
@@ -0,0 +1,34 @@
+import Link from "next/link";
+import { CircleUserRound, EllipsisVertical, Menu } from "lucide-react";
+import { ButtonIcon } from "@/components/button/icon";
+import { ButtonOutline } from "@/components/button/outline";
+import { SearchBar } from "@/components/search-bar";
+import { YouTubeWordmark } from "@/components/logo/youtube";
+
+// Masthead — 56px de altura, três regiões distribuídas.
+// O "BR" é 10px literal: o design não o tokeniza, e não existe papel para ele.
+export function Masthead() {
+ return (
+
+
+
+
+
+ BR
+
+
+
+
+
+
+
+
+
+ Sign in
+
+
+ );
+}
diff --git a/components/search-bar.tsx b/components/search-bar.tsx
new file mode 100644
index 0000000..b66874c
--- /dev/null
+++ b/components/search-bar.tsx
@@ -0,0 +1,34 @@
+import { Mic, Search } from "lucide-react";
+
+// Search Bar — campo (fill) + submit de 64 + gap 16 + voz de 40, total 656.
+// O radius por canto do design vira rounded-l-full / rounded-r-full (§6).
+export function SearchBar() {
+ return (
+
+ );
+}
diff --git a/components/sidebar-item.tsx b/components/sidebar-item.tsx
new file mode 100644
index 0000000..290757a
--- /dev/null
+++ b/components/sidebar-item.tsx
@@ -0,0 +1,31 @@
+import Link from "next/link";
+import type { LucideIcon } from "lucide-react";
+
+// Sidebar Item — 40px, rounded-nav (10px, o único radius fora da escala nativa).
+export function SidebarItem({
+ icon: Icon,
+ label,
+ href,
+ active = false,
+}: {
+ icon: LucideIcon;
+ label: string;
+ href: string;
+ active?: boolean;
+}) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/components/sidebar.tsx b/components/sidebar.tsx
new file mode 100644
index 0000000..28c6726
--- /dev/null
+++ b/components/sidebar.tsx
@@ -0,0 +1,84 @@
+import {
+ ChevronDown, CircleUserRound, Clapperboard, Film, Flag, History,
+ House, Music, Radio, SquarePlay, Tv, type LucideIcon,
+} from "lucide-react";
+import { ButtonOutline } from "@/components/button/outline";
+import { SidebarItem } from "@/components/sidebar-item";
+
+// O design passa o glyph como dado. O mapa explícito mantém só os ícones usados
+// no bundle e torna um nome inválido erro de tipo (§11).
+type NavItem = { icon: LucideIcon; label: string; href: string };
+
+const PRIMARY: NavItem[] = [
+ { icon: House, label: "Home", href: "/" },
+ { icon: Clapperboard, label: "Shorts", href: "/shorts" },
+ { icon: Tv, label: "Subscriptions", href: "/feed/subscriptions" },
+ { icon: CircleUserRound, label: "You", href: "/feed/you" },
+ { icon: History, label: "History", href: "/feed/history" },
+];
+
+const EXPLORE: NavItem[] = [
+ { icon: Music, label: "Music", href: "/music" },
+ { icon: Film, label: "Movies", href: "/movies" },
+ { icon: Radio, label: "Live", href: "/live" },
+ { icon: ChevronDown, label: "Show more", href: "/explore" },
+];
+
+// DIVERGE DO DESIGN: o .pen pede o glyph `youtube` da lucide para Premium e
+// Kids, mas ícones de marca saíram da biblioteca (não existe em lucide-react
+// 1.34). SquarePlay é a forma mais próxima. Precisa de decisão de design.
+const MORE: NavItem[] = [
+ { icon: SquarePlay, label: "YouTube Premium", href: "/premium" },
+ { icon: Music, label: "YouTube Music", href: "/music" },
+ { icon: SquarePlay, label: "YouTube Kids", href: "/kids" },
+];
+
+const REPORT: NavItem[] = [{ icon: Flag, label: "Report history", href: "/report" }];
+
+function Group({ title, items }: { title?: string; items: NavItem[] }) {
+ return (
+
+ {title ? (
+
+
{title}
+
+ ) : null}
+ {items.map((item) => (
+
+ ))}
+
+ );
+}
+
+function Divider() {
+ return ;
+}
+
+export function Sidebar() {
+ return (
+
+ );
+}
diff --git a/components/tab.tsx b/components/tab.tsx
new file mode 100644
index 0000000..038aee5
--- /dev/null
+++ b/components/tab.tsx
@@ -0,0 +1,22 @@
+import Link from "next/link";
+
+// Tab — 48px de altura, indicador de 2px rente à base. O padding [13,0,1,0] do
+// design está fora da grade de 4 (§2.5).
+export function Tab({ label, href, active = false }: { label: string; href: string; active?: boolean }) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
diff --git a/lib/fixtures.ts b/lib/fixtures.ts
new file mode 100644
index 0000000..c5b90a3
--- /dev/null
+++ b/lib/fixtures.ts
@@ -0,0 +1,77 @@
+// Fixtures extraídas de design/pendev/youtube-channel.pen (tela Channel — Videos).
+// Conteúdo é DADO, não design: o componente recebe tudo por prop.
+
+export type Video = {
+ id: string;
+ title: string;
+ meta: string;
+ duration: string;
+};
+
+export const VIDEOS: Video[] = [
+ {
+ "id": "mG7ZC63xS-k",
+ "title": "Coding with AI this way is a waste of time",
+ "meta": "971 views • 3 days ago",
+ "duration": "23:42"
+ },
+ {
+ "id": "w2z4Fai2s9c",
+ "title": "Stop Creating AI Skills Without Doing This",
+ "meta": "1.8K views • 5 days ago",
+ "duration": "12:29"
+ },
+ {
+ "id": "RO5y-fCIBy8",
+ "title": "The Developer as You Know It Is Coming to an End!",
+ "meta": "3.8K views • 7 days ago",
+ "duration": "17:46"
+ },
+ {
+ "id": "V3Mtur9JuKY",
+ "title": "Google ADK in Practice: Build Your First AI Agent",
+ "meta": "1K views • 10 days ago",
+ "duration": "30:05"
+ },
+ {
+ "id": "HGvUc9nDJC0",
+ "title": "Agent Skills: Everything You Need to Know to Build Skills",
+ "meta": "705 views • 12 days ago",
+ "duration": "17:32"
+ },
+ {
+ "id": "FCwIlzanP4Q",
+ "title": "Harness Engineering: Agents, Sub-Agents, and Context Window",
+ "meta": "1.3K views • 2 weeks ago",
+ "duration": "16:06"
+ },
+ {
+ "id": "9_atL4yQEa0",
+ "title": "Developers: Understand What AI Agents Really Are",
+ "meta": "843 views • 2 weeks ago",
+ "duration": "14:21"
+ },
+ {
+ "id": "ZfpYVS7oG6A",
+ "title": "What Is the Developer's Role When AI Already Writes the Code?",
+ "meta": "5.5K views • 2 weeks ago",
+ "duration": "17:07"
+ },
+ {
+ "id": "aYEa5svlzC0",
+ "title": "RAG is Not an AI Agent: The Difference Most People Ignore",
+ "meta": "4.9K views • 3 weeks ago",
+ "duration": "15:35"
+ }
+];
+
+export const CHANNEL = {
+ name: "Full Cycle",
+ handle: "@FullCycle",
+ subscribers: "196K subscribers",
+ videoCount: "1K videos",
+ description:
+ "A Full Cycle ajuda desenvolvedores a desenvolverem aplicações de grande porte!",
+ link: "fullcycle.com.br",
+ moreLinks: "and 1 more link",
+} as const;
diff --git a/public/avatar.jpg b/public/avatar.jpg
new file mode 100644
index 0000000..56f2b1a
Binary files /dev/null and b/public/avatar.jpg differ
diff --git a/public/banner.jpg b/public/banner.jpg
new file mode 100644
index 0000000..a8cd9bf
Binary files /dev/null and b/public/banner.jpg differ
diff --git a/public/thumbs/9_atL4yQEa0.jpg b/public/thumbs/9_atL4yQEa0.jpg
new file mode 100644
index 0000000..53cac94
Binary files /dev/null and b/public/thumbs/9_atL4yQEa0.jpg differ
diff --git a/public/thumbs/FCwIlzanP4Q.jpg b/public/thumbs/FCwIlzanP4Q.jpg
new file mode 100644
index 0000000..e9eca8e
Binary files /dev/null and b/public/thumbs/FCwIlzanP4Q.jpg differ
diff --git a/public/thumbs/HGvUc9nDJC0.jpg b/public/thumbs/HGvUc9nDJC0.jpg
new file mode 100644
index 0000000..29628c0
Binary files /dev/null and b/public/thumbs/HGvUc9nDJC0.jpg differ
diff --git a/public/thumbs/RO5y-fCIBy8.jpg b/public/thumbs/RO5y-fCIBy8.jpg
new file mode 100644
index 0000000..f40fa03
Binary files /dev/null and b/public/thumbs/RO5y-fCIBy8.jpg differ
diff --git a/public/thumbs/V3Mtur9JuKY.jpg b/public/thumbs/V3Mtur9JuKY.jpg
new file mode 100644
index 0000000..65f2d5b
Binary files /dev/null and b/public/thumbs/V3Mtur9JuKY.jpg differ
diff --git a/public/thumbs/ZfpYVS7oG6A.jpg b/public/thumbs/ZfpYVS7oG6A.jpg
new file mode 100644
index 0000000..982a793
Binary files /dev/null and b/public/thumbs/ZfpYVS7oG6A.jpg differ
diff --git a/public/thumbs/aYEa5svlzC0.jpg b/public/thumbs/aYEa5svlzC0.jpg
new file mode 100644
index 0000000..040d25b
Binary files /dev/null and b/public/thumbs/aYEa5svlzC0.jpg differ
diff --git a/public/thumbs/mG7ZC63xS-k.jpg b/public/thumbs/mG7ZC63xS-k.jpg
new file mode 100644
index 0000000..8ff89e0
Binary files /dev/null and b/public/thumbs/mG7ZC63xS-k.jpg differ
diff --git a/public/thumbs/w2z4Fai2s9c.jpg b/public/thumbs/w2z4Fai2s9c.jpg
new file mode 100644
index 0000000..55466d6
Binary files /dev/null and b/public/thumbs/w2z4Fai2s9c.jpg differ