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:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
118
apps/web/src/components/categories/view-toggle-icons.tsx
Normal file
118
apps/web/src/components/categories/view-toggle-icons.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user