feat(web): görünüm toggle için animasyonlu CSS ikonları (grid/tree/columns)

unlumen cards-view/list-view motion gerektirdiği için saf CSS eşdeğerleri:
- CardsViewIcon (grid): 2×2 kareler yer değiştirir
- TreeViewIcon (tree): bespoke dallanan ağaç, kademeli çizilir (tree-branch-in)
- ListViewIcon (columns): 3 çizgi birer slot kayar
isActive ile sürülür, motion-reduce saygılı. CategoryViewToggle + 4 inline
katalog toggle'ı bu ikonlara çevrildi; kullanılmayan lucide import'ları temizlendi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 00:44:03 +03:00
parent 6499c98ae9
commit 1db9a632cb
7 changed files with 175 additions and 32 deletions

View File

@@ -1,16 +1,16 @@
import { cn } from "@sase/ui";
import { Columns2, LayoutGrid, List } from "lucide-react";
import { CardsViewIcon, ListViewIcon, TreeViewIcon, type ViewIconProps } from "./view-toggle-icons";
export type CategoryViewMode = "grid" | "tree" | "columns";
const OPTIONS: Array<{
value: CategoryViewMode;
label: string;
Icon: React.ComponentType<{ className?: string }>;
Icon: React.ComponentType<ViewIconProps>;
}> = [
{ value: "grid", label: "Izgara görünümü", Icon: LayoutGrid },
{ value: "tree", label: "Ağaç görünümü", Icon: List },
{ value: "columns", label: "Sütun görünümü", Icon: Columns2 },
{ value: "grid", label: "Izgara görünümü", Icon: CardsViewIcon },
{ value: "tree", label: "Ağaç görünümü", Icon: TreeViewIcon },
{ value: "columns", label: "Sütun görünümü", Icon: ListViewIcon },
];
export function CategoryViewToggle({
@@ -50,7 +50,7 @@ export function CategoryViewToggle({
: "text-muted-foreground hover:bg-background/60 hover:text-foreground",
)}
>
<Icon className="h-4 w-4" />
<Icon isActive={active} />
</button>
);
})}

View File

@@ -0,0 +1,118 @@
import { cn } from "@sase/ui";
/**
* Görünüm-toggle için animasyonlu ikonlar (saf CSS, motion bağımlılığı yok).
* unlumen cards-view / list-view ikonlarından uyarlanmıştır; tree ikonu bespoke.
* Her ikon `isActive` ile sürülür ve dinlenme (rest) görünümü her iki durumda aynıdır;
* yalnızca geçiş/animasyon farklıdır.
*/
export interface ViewIconProps {
isActive: boolean;
className?: string;
}
// ── Grid → 2×2 kareler, aktifken yer değiştirir (cards-view-icon) ──
// 16px kutuda 4px kareler, 6.5px adımla; kareler komşu hücreye kayar (hepsi aynı görünür).
const CARD_POS = [
{ left: 2.75, top: 2.75 }, // 0 TL
{ left: 9.25, top: 2.75 }, // 1 TR
{ left: 2.75, top: 9.25 }, // 2 BL
{ left: 9.25, top: 9.25 }, // 3 BR
];
const CARD_TRAVEL = [
{ x: 0, y: 6.5 }, // 0 → BL
{ x: -6.5, y: 0 }, // 1 → TL
{ x: 6.5, y: 0 }, // 2 → BR
{ x: 0, y: -6.5 }, // 3 → TR
];
export function CardsViewIcon({ isActive, className }: ViewIconProps) {
return (
<span className={cn("relative inline-block h-4 w-4", className)} aria-hidden="true">
{CARD_POS.map((p, i) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: sabit uzunlukta statik ikon parçaları
key={i}
className="absolute size-[4px] rounded-[1px] bg-current transition-transform duration-[420ms] ease-out motion-reduce:transition-none"
style={{
left: p.left,
top: p.top,
transform: isActive
? `translate(${CARD_TRAVEL[i].x}px, ${CARD_TRAVEL[i].y}px)`
: "none",
transitionDelay: `${i * 67}ms`,
}}
/>
))}
</span>
);
}
// ── List (columns butonu) → 3 yatay çizgi, aktifken birer slot kayar (list-view-icon) ──
const LINE_TRAVEL = [5, 5, -10];
export function ListViewIcon({ isActive, className }: ViewIconProps) {
return (
<span
className={cn(
"inline-flex h-4 w-4 flex-col items-center justify-center gap-[3px]",
className,
)}
aria-hidden="true"
>
{LINE_TRAVEL.map((dy, i) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: sabit uzunlukta statik ikon parçaları
key={i}
className="h-[2px] w-3 rounded-full bg-current transition-transform duration-[360ms] ease-out motion-reduce:transition-none"
style={{
transform: isActive ? `translateY(${dy}px)` : "none",
transitionDelay: `${i * 20}ms`,
}}
/>
))}
</span>
);
}
// ── Tree → kök + iki dal; aktifken dallar kademeli "çizilir" (bespoke) ──
export function TreeViewIcon({ isActive, className }: ViewIconProps) {
return (
<span
// key: aktif olunca yeniden mount edip dal giriş animasyonunu tetikler
key={isActive ? "on" : "off"}
className={cn("relative inline-block h-4 w-4", className)}
aria-hidden="true"
>
{/* kök düğüm */}
<span
className="absolute rounded-[1.5px] bg-current"
style={{ left: 2, top: 2.5, width: 4, height: 2.5 }}
/>
{/* dikey bağlayıcı */}
<span
className="absolute rounded bg-current/50"
style={{ left: 3, top: 5, width: 1.5, height: 7.5 }}
/>
{/* iki çocuk dal: dirsek + düğüm */}
{[0, 1].map((i) => (
<span
key={i}
className={cn("absolute inline-flex items-center", isActive && "animate-tree-branch-in")}
style={{
left: 3,
top: i === 0 ? 6.25 : 10.25,
animationDelay: `${i * 90}ms`,
animationFillMode: "backwards",
}}
>
<span className="rounded bg-current/50" style={{ width: 3.5, height: 1.5 }} />
<span
className="ml-[1.5px] rounded-[1.5px] bg-current"
style={{ width: 4, height: 2.5 }}
/>
</span>
))}
</span>
);
}

View File

@@ -168,6 +168,17 @@
}
}
@keyframes tree-branch-in {
0% {
opacity: 0;
transform: translateX(-3px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes icon-pop {
0% {
opacity: 0;
@@ -221,6 +232,9 @@
.animate-icon-pop {
animation: icon-pop 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-tree-branch-in {
animation: tree-branch-in 0.34s cubic-bezier(0.16, 1, 0.3, 1);
}
.carousel-track:hover .animate-scroll-left {
animation-play-state: paused;
}

View File

@@ -1,3 +1,8 @@
import {
CardsViewIcon,
ListViewIcon,
TreeViewIcon,
} from "@/components/categories/view-toggle-icons";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
@@ -6,7 +11,7 @@ import { Skeleton, cn } from "@sase/ui";
import { Button } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ChevronRight, Columns2, LayoutGrid, Library, List, Lock } from "lucide-react";
import { ChevronRight, Library, Lock } from "lucide-react";
import { useState } from "react";
import { KEYS_10 } from "@/lib/keys";
@@ -52,9 +57,9 @@ function CatalogBrandsPage() {
className="inline-flex items-center gap-0.5 rounded-lg border border-border bg-muted/40 p-0.5"
>
{[
{ mode: "grid" as const, Icon: LayoutGrid, label: "Izgara" },
{ mode: "tree" as const, Icon: List, label: "Liste" },
{ mode: "columns" as const, Icon: Columns2, label: "Sütun" },
{ mode: "grid" as const, Icon: CardsViewIcon, label: "Izgara" },
{ mode: "tree" as const, Icon: TreeViewIcon, label: "Liste" },
{ mode: "columns" as const, Icon: ListViewIcon, label: "Sütun" },
].map(({ mode, Icon, label }) => (
<button
key={mode}
@@ -70,7 +75,7 @@ function CatalogBrandsPage() {
)}
title={label}
>
<Icon className="size-3.5" />
<Icon isActive={viewMode === mode} className="size-3.5" />
</button>
))}
</div>

View File

@@ -1,5 +1,10 @@
import { ModelListColumns } from "@/components/catalog/model-list-columns";
import { ModelListTree } from "@/components/catalog/model-list-tree";
import {
CardsViewIcon,
ListViewIcon,
TreeViewIcon,
} from "@/components/categories/view-toggle-icons";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { KEYS_4, KEYS_9 } from "@/lib/keys";
@@ -7,16 +12,7 @@ import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Skeleton, cn } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import {
ArrowLeft,
BookOpen,
Car,
ChevronRight,
Columns2,
LayoutGrid,
List,
Loader2,
} from "lucide-react";
import { ArrowLeft, BookOpen, Car, ChevronRight, Loader2 } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/dashboard/catalog_/$brandName/")({
@@ -167,7 +163,7 @@ function CatalogModelsPage() {
)}
title="Izgara"
>
<LayoutGrid className="size-4" />
<CardsViewIcon isActive={viewMode === "grid"} />
</button>
<button
type="button"
@@ -178,7 +174,7 @@ function CatalogModelsPage() {
)}
title="Liste"
>
<List className="size-4" />
<TreeViewIcon isActive={viewMode === "tree"} />
</button>
<button
type="button"
@@ -191,7 +187,7 @@ function CatalogModelsPage() {
)}
title="Sutun"
>
<Columns2 className="size-4" />
<ListViewIcon isActive={viewMode === "columns"} />
</button>
</div>

View File

@@ -1,13 +1,18 @@
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import {
CardsViewIcon,
ListViewIcon,
TreeViewIcon,
} from "@/components/categories/view-toggle-icons";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Skeleton, cn } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { ArrowLeft } from "lucide-react";
import { Suspense, lazy, useState } from "react";
import { KEYS_8 } from "@/lib/keys";
@@ -162,7 +167,7 @@ function CatalogCategoryPage() {
)}
title="Izgara"
>
<LayoutGrid className="size-4" />
<CardsViewIcon isActive={viewMode === "grid"} />
</button>
<button
type="button"
@@ -173,7 +178,7 @@ function CatalogCategoryPage() {
)}
title="Agac"
>
<List className="size-4" />
<TreeViewIcon isActive={viewMode === "tree"} />
</button>
<button
type="button"
@@ -186,7 +191,7 @@ function CatalogCategoryPage() {
)}
title="Sutun"
>
<Columns2 className="size-4" />
<ListViewIcon isActive={viewMode === "columns"} />
</button>
</div>
)}

View File

@@ -4,13 +4,18 @@ import { PsaVariantSelector } from "@/components/catalog/psa-variant-selector";
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import {
CardsViewIcon,
ListViewIcon,
TreeViewIcon,
} from "@/components/categories/view-toggle-icons";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { ArrowLeft } from "lucide-react";
import { useState } from "react";
import { KEYS_8 } from "@/lib/keys";
@@ -203,7 +208,7 @@ function CatalogVehiclePage() {
className={`rounded p-1.5 ${viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground"}`}
title="Izgara"
>
<LayoutGrid className="size-4" />
<CardsViewIcon isActive={viewMode === "grid"} />
</button>
<button
type="button"
@@ -211,7 +216,7 @@ function CatalogVehiclePage() {
className={`rounded p-1.5 ${viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground"}`}
title="Agac"
>
<List className="size-4" />
<TreeViewIcon isActive={viewMode === "tree"} />
</button>
<button
type="button"
@@ -219,7 +224,7 @@ function CatalogVehiclePage() {
className={`rounded p-1.5 ${viewMode === "columns" ? "bg-accent" : "text-muted-foreground hover:text-foreground"}`}
title="Sutun"
>
<Columns2 className="size-4" />
<ListViewIcon isActive={viewMode === "columns"} />
</button>
</div>
</div>