-
Notifications
You must be signed in to change notification settings - Fork 1
Channel — Videos screen #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e16c5db
e779d1e
51aa38e
4c13e4e
0edc0df
9470a51
5e6d841
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="flex min-h-full flex-col bg-surface-page"> | ||
| <Masthead /> | ||
| <div className="flex w-full flex-1"> | ||
| <Sidebar /> | ||
| <main className="flex w-full min-w-0 flex-1 flex-col">{children}</main> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <> | ||
| <div className="w-full px-[58px]"> | ||
| <ChannelHeader handle={handle} activeTab="Videos" /> | ||
| </div> | ||
|
|
||
| <hr className="w-full border-0 border-t border-border-divider" /> | ||
|
|
||
| <div className="flex w-full flex-col px-12 pb-12"> | ||
| <div className="flex w-full gap-2 pt-4"> | ||
| <Chip label="Latest" active /> | ||
| <Chip label="Popular" /> | ||
| <Chip label="Oldest" /> | ||
| </div> | ||
|
|
||
| <div className="grid w-full grid-cols-[repeat(auto-fill,minmax(357px,1fr))] gap-x-4 gap-y-8 pt-6"> | ||
| {VIDEOS.map((video) => ( | ||
| <CardVideo key={video.id} video={video} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,69 +1,6 @@ | ||
| import Image from "next/image"; | ||
| import { redirect } from "next/navigation"; | ||
|
|
||
| export default function Home() { | ||
| return ( | ||
| <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> | ||
| <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> | ||
| <Image | ||
| className="dark:invert h-5 w-[100px]" | ||
| src="/next.svg" | ||
| alt="Next.js logo" | ||
| width={100} | ||
| height={20} | ||
| priority | ||
| /> | ||
| <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> | ||
| <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> | ||
| To get started, edit the{" "} | ||
| <code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]"> | ||
| page.tsx | ||
| </code>{" "} | ||
| file. | ||
| </h1> | ||
| <p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> | ||
| Looking for a starting point or more instructions? Head over to{" "} | ||
| <a | ||
| href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
| className="font-medium text-zinc-950 dark:text-zinc-50" | ||
| > | ||
| Templates | ||
| </a>{" "} | ||
| or the{" "} | ||
| <a | ||
| href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
| className="font-medium text-zinc-950 dark:text-zinc-50" | ||
| > | ||
| Learning | ||
| </a>{" "} | ||
| center. | ||
| </p> | ||
| </div> | ||
| <div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> | ||
| <a | ||
| className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]" | ||
| href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| > | ||
| <Image | ||
| className="dark:invert h-[14px] w-4" | ||
| src="/vercel.svg" | ||
| alt="Vercel logomark" | ||
| width={16} | ||
| height={14} | ||
| /> | ||
| Deploy Now | ||
| </a> | ||
| <a | ||
| className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" | ||
| href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| > | ||
| Documentation | ||
| </a> | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| // Só existe a tela do canal por enquanto. O feed de home não foi desenhado. | ||
| export default function RootPage() { | ||
| redirect("/@FullCycle/videos"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import Image from "next/image"; | ||
|
|
||
| // Avatar — 160x160 circular, recortado. | ||
| export function Avatar({ src, alt }: { src: string; alt: string }) { | ||
| return ( | ||
| <div | ||
| data-component="Avatar" | ||
| className="relative size-40 shrink-0 overflow-hidden rounded-full" | ||
| > | ||
| <Image src={src} alt={alt} fill sizes="160px" className="object-cover" priority /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Badge / Overlay — duração sobre a thumbnail. $radius-xs é 4px = rounded-sm. | ||
| export function BadgeOverlay({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <span | ||
| data-component="Badge / Overlay" | ||
| className="flex items-center justify-center rounded-sm bg-surface-overlay px-1 py-px text-caption font-medium text-text-on-overlay" | ||
| > | ||
| {children} | ||
| </span> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <button | ||
| type="button" | ||
| aria-label={label} | ||
| data-component="Button / Icon" | ||
| className="flex size-10 items-center justify-center rounded-full text-text-primary hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-focus focus-visible:outline-none" | ||
| > | ||
| <Icon className="size-6" aria-hidden /> | ||
| </button> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <button | ||
| type="button" | ||
| data-component="Button / Outline" | ||
| className="flex h-10 items-center gap-2 rounded-full border border-border-control px-[15px] text-body font-medium text-text-link hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-focus focus-visible:outline-none" | ||
| > | ||
| <Icon className="size-6" aria-hidden /> | ||
| {children} | ||
| </button> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Button / Primary — pill preenchido com a superfície inversa (Subscribe). | ||
| export function ButtonPrimary({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| data-component="Button / Primary" | ||
| className="flex h-10 items-center justify-center rounded-full bg-surface-inverse px-4 text-body font-medium text-text-inverse focus-visible:ring-2 focus-visible:ring-focus focus-visible:outline-none" | ||
| > | ||
| {children} | ||
| </button> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <article data-component="Card / Video" className="flex w-full flex-col gap-3"> | ||
| <div className="relative aspect-video w-full overflow-hidden rounded-xl"> | ||
| <Image | ||
| src={`/thumbs/${video.id}.jpg`} | ||
| alt="" | ||
| fill | ||
| sizes="(max-width: 1100px) 50vw, 33vw" | ||
| className="object-cover" | ||
| /> | ||
| <div className="absolute right-0 bottom-0 p-2"> | ||
| <BadgeOverlay>{video.duration}</BadgeOverlay> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-0.5 pr-[26px]"> | ||
| <h3 className="pr-6 text-title font-medium text-text-primary">{video.title}</h3> | ||
| <p className="text-body text-text-secondary">{video.meta}</p> | ||
| </div> | ||
| </article> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div data-component="Channel Header" className="flex w-full flex-col"> | ||
| <div className="relative h-43 w-full overflow-hidden rounded-2xl"> | ||
| <Image src="/banner.jpg" alt="" fill sizes="100vw" className="object-cover" priority /> | ||
| </div> | ||
|
|
||
| <div className="flex w-full items-center gap-4 pt-4 pb-1"> | ||
| <Avatar src="/avatar.jpg" alt={CHANNEL.name} /> | ||
|
|
||
| <div className="flex w-full flex-col gap-3"> | ||
| <div className="flex flex-col gap-1.5"> | ||
| <h1 className="text-display font-bold text-text-primary">{CHANNEL.name}</h1> | ||
| <div className="flex items-center gap-1 text-body"> | ||
| <span className="font-medium text-text-primary">{CHANNEL.handle}</span> | ||
| <span className="text-text-secondary">•</span> | ||
| <span className="text-text-secondary">{CHANNEL.subscribers}</span> | ||
| <span className="text-text-secondary">•</span> | ||
| <span className="text-text-secondary">{CHANNEL.videoCount}</span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <p className="flex items-end gap-1 text-body"> | ||
| <span className="text-text-secondary">{CHANNEL.description}</span> | ||
| <span className="font-medium text-text-primary">...more</span> | ||
| </p> | ||
|
|
||
| <div className="flex items-center gap-1 text-body"> | ||
| <span className="flex items-center gap-2"> | ||
| <Link2 className="size-4 text-text-secondary" aria-hidden /> | ||
| <a href={`https://${CHANNEL.link}`} className="text-text-link"> | ||
| {CHANNEL.link} | ||
| </a> | ||
| </span> | ||
| <span className="font-medium text-text-primary">{CHANNEL.moreLinks}</span> | ||
| </div> | ||
|
|
||
| <div className="flex gap-3 pt-0.5"> | ||
| <ButtonPrimary>Subscribe</ButtonPrimary> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <nav aria-label="Channel sections" className="flex h-12 w-full gap-6"> | ||
| {TABS.map((tab) => ( | ||
| <Tab key={tab} label={tab} href={tabHref(handle, tab)} active={tab === activeTab} /> | ||
| ))} | ||
| <button | ||
| type="button" | ||
| aria-label="Search this channel" | ||
| className="flex h-12 items-start px-2 pt-[13px] text-text-secondary focus-visible:ring-2 focus-visible:ring-focus focus-visible:outline-none" | ||
| > | ||
| <Search className="size-6" aria-hidden /> | ||
| </button> | ||
| </nav> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 ( | ||||||||||||||
| <button | ||||||||||||||
| type="button" | ||||||||||||||
| data-component="Chip" | ||||||||||||||
| aria-pressed={active} | ||||||||||||||
| className={[ | ||||||||||||||
| "flex h-8 items-center justify-center rounded-lg px-3 text-body font-medium", | ||||||||||||||
| "focus-visible:ring-2 focus-visible:ring-focus focus-visible:outline-none", | ||||||||||||||
| active | ||||||||||||||
| ? "bg-surface-inverse text-text-inverse" | ||||||||||||||
| : "bg-surface-chip text-text-primary hover:bg-surface-hover", | ||||||||||||||
|
Comment on lines
+11
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regra 7 — variante de componente que não existe no O { "cornerRadius": "$radius-sm", "fill": "$surface-chip", "height": 32,
"children": [{ "name": "Label", "fill": "$text-primary", ... }] }E em Os tokens usados existem, mas §2.4 fala de componente, não de token: "Nenhum componente que não exista no O ramo inativo já bate com o design ( Correção: ou o
Suggested change
|
||||||||||||||
| ].join(" ")} | ||||||||||||||
| > | ||||||||||||||
| {label} | ||||||||||||||
| </button> | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <svg viewBox="0 0 93 20" className="h-5 w-[93px]" aria-label="YouTube" role="img"> | ||
| <path className="fill-brand-primary" d="M14.4848 20c0 0 9.0847 0 11.3381-0.6 1.2688-0.34 2.223-1.32 2.5579-2.53 0.6192-2.22 0.6192-6.89 0.6192-6.89 0 0 0-4.64-0.6192-6.84-0.3349-1.24-1.2891-2.2-2.5579-2.53-2.2534-0.61-11.3381-0.61-11.3381-0.61 0 0-9.06443 0-11.30769 0.61-1.24851 0.33-2.22296 1.29-2.57823 2.53-0.59888 2.2-0.59888 6.84-0.59888 6.84 0 0 0 4.67 0.59888 6.89 0.35527 1.21 1.32972 2.19 2.57823 2.53 2.24326 0.6 11.30769 0.6 11.30769 0.6z" /> | ||
| <path className="fill-white" d="M19 10l-7.5-4.25v8.5l7.5-4.25z" /> | ||
| <path className="fill-text-primary" d="M37.1384 18.8999v-5.46l3.47-11.33996h-2.59l-1.32 5.15c-0.3 1.18-0.57 2.41-0.77 3.54996h-0.16c-0.11-0.99996-0.43-2.30996-0.75-3.56996l-1.28-5.13h-2.59l3.42 11.33996v5.46h2.57z" /> | ||
| <path className="fill-text-primary" d="M44.1003 6.29994c-3.03 0-4.07 1.75-4.07 5.51996v1.79c0 3.38 0.65 5.5 4.01 5.5 3.31 0 4.02-2.02 4.02-5.5v-1.79c0-3.36996-0.68-5.51996-3.96-5.51996z m1.29 8.41996c0 1.64-0.29 2.67-1.34 2.67-1.03 0-1.32-1.04-1.32-2.67v-4.04c0-1.39996 0.2-2.64996 1.32-2.64996 1.18 0 1.34 1.32 1.34 2.64996v4.04z" /> | ||
| <path className="fill-text-primary" d="M52.2713 19.0899c1.46 0 2.37-0.61 3.12-1.71h0.11l0.11 1.52h1.9899v-12.35996h-2.6399v9.92996c-0.2801 0.49-0.9301 0.85-1.5401 0.85-0.77 0-1.0099-0.61-1.0099-1.63v-9.14996h-2.6301v9.26996c0 2.01 0.5801 3.28 2.4901 3.28z" /> | ||
| <path className="fill-text-primary" d="M62.8261 18.8999v-14.74996h3.04v-2.05h-8.69v2.05h3.04v14.74996h2.61z" /> | ||
| <path className="fill-text-primary" d="M67.8728 19.0899c1.46 0 2.37-0.61 3.12-1.71h0.11l0.11 1.52h1.99v-12.35996h-2.64v9.92996c-0.28 0.49-0.93 0.85-1.54 0.85-0.77 0-1.01-0.61-1.01-1.63v-9.14996h-2.63v9.26996c0 2.01 0.58 3.28 2.49 3.28z" /> | ||
| <path className="fill-text-primary" d="M80.6744 6.26994c-1.28 0-2.2 0.56-2.81 1.47h-0.13c0.08-1.2 0.14-2.22 0.14-3.03v-3.27h-2.55l-0.01 10.73996 0.01 6.72h2.22l0.19-1.2h0.07c0.59 0.81 1.5 1.32 2.71 1.32 2.01 0 2.87-1.73 2.87-5.41v-1.91c0-3.43996-0.39-5.42996-2.71-5.42996z m0.09 7.33996c0 2.3-0.34 3.67-1.41 3.67-0.5 0-1.19-0.24-1.5-0.69v-7.34996c0.27-0.7 0.87-1.21 1.54-1.21 1.08 0 1.37 1.31 1.37 3.69996v1.88z" /> | ||
| <path className="fill-text-primary" d="M92.6517 11.4999c0-2.97996-0.3-5.18996-3.73-5.18996-3.23 0-3.95 2.15-3.95 5.30996v2.17c0 3.08 0.66 5.32 3.87 5.32 2.54 0 3.85-1.27 3.7-3.73l-2.25-0.12c-0.03 1.52-0.38 2.14-1.39 2.14-1.27 0-1.33-1.21-1.33-3.01v-0.84h5.08v-2.05z m-3.79-3.52996c1.22 0 1.31 1.15 1.31 3.09996v1.01h-2.6v-1.01c0-1.92996 0.08-3.09996 1.29-3.09996z" /> | ||
| </svg> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <header | ||
| data-component="Masthead" | ||
| className="flex h-14 w-full items-center justify-between bg-surface-page px-4" | ||
| > | ||
| <div className="flex items-center gap-4"> | ||
| <ButtonIcon icon={Menu} label="Guide" /> | ||
| <Link href="/" className="flex items-start gap-[3px]"> | ||
| <YouTubeWordmark /> | ||
| <span className="text-[10px]/[1.15] text-text-secondary">BR</span> | ||
| </Link> | ||
| </div> | ||
|
|
||
| <div className="flex w-[656px] items-center"> | ||
| <SearchBar /> | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-2"> | ||
| <ButtonIcon icon={EllipsisVertical} label="Settings" /> | ||
| <ButtonOutline icon={CircleUserRound}>Sign in</ButtonOutline> | ||
| </div> | ||
| </header> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regra 10 (drift de valor) — glyph do ícone diverge do design (§11)
O
.penespecificalinkpara oLink IcondoChannel Links:{ "fill": "$text-secondary", "height": 16, "icon": "link", "library": "lucide", "name": "Link Icon", "width": 16 }O código usa
Link2, que é outro glyph da lucide (corrente inclinada em vez da corrente partida). §11 é normativo e a tradução é mecânica — o kebab do.penvira PascalCase:"link"→Link. Diferente do casoyoutube→SquarePlayemcomponents/sidebar.tsx:27-33, aqui não há motivo de biblioteca:Linkexiste emlucide-react.O resto do nó bate (
size-4= 16 ✓,text-text-secondary=$text-secondary✓).Correção — importar o glyph certo (na linha 2,
import { Link2, Search }→import { Link as LinkIcon, Search }; não há colisão comnext/linkneste arquivo):