diff --git a/app/(app)/avatar/page.tsx b/app/(app)/avatar/page.tsx index 878014c..1fb05b6 100644 --- a/app/(app)/avatar/page.tsx +++ b/app/(app)/avatar/page.tsx @@ -7,7 +7,7 @@ export default function AvatarPage() { return ( Loading...

}> diff --git a/app/(app)/customize/page.tsx b/app/(app)/customize/page.tsx new file mode 100644 index 0000000..4a7efa8 --- /dev/null +++ b/app/(app)/customize/page.tsx @@ -0,0 +1,18 @@ +import { Suspense } from "react"; + +import { CustomizePageContent } from "@/components/avatar/customize-page-content"; +import { AppShell } from "@/components/layout/app-shell"; + +export default function CustomizePage() { + return ( + + Loading...

}> + +
+
+ ); +} diff --git a/app/(app)/habits/page.tsx b/app/(app)/habits/page.tsx index b5a34e2..33dd2ae 100644 --- a/app/(app)/habits/page.tsx +++ b/app/(app)/habits/page.tsx @@ -5,9 +5,9 @@ export default function HabitsPage() { return ( - + ); } diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index a17f949..a98544d 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -10,11 +10,11 @@ export default function AppLayout({ children: React.ReactNode; }) { return ( - }> + }> - {children} +
{children}
); diff --git a/app/character-creator/page.tsx b/app/character-creator/page.tsx index 9d6407e..6e12be5 100644 --- a/app/character-creator/page.tsx +++ b/app/character-creator/page.tsx @@ -3,5 +3,5 @@ import { redirect } from "next/navigation"; import { routes } from "@/lib/routes"; export default function CharacterCreatorPage() { - redirect(`${routes.avatar}?tab=customize`); + redirect(routes.customize); } diff --git a/app/globals.css b/app/globals.css index 9e332aa..da5233a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,57 +4,58 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; + --bottom-nav-height: 6rem; + --background: 252 38% 10%; + --foreground: 52 100% 88%; + --card: 252 32% 14%; + --card-foreground: 52 100% 88%; + --popover: 252 32% 14%; + --popover-foreground: 52 100% 88%; + --primary: 318 100% 62%; + --primary-foreground: 252 38% 10%; + --secondary: 198 100% 45%; + --secondary-foreground: 252 38% 10%; + --muted: 252 24% 20%; + --muted-foreground: 52 40% 68%; + --accent: 142 70% 45%; + --accent-foreground: 252 38% 10%; + --destructive: 0 84% 58%; + --destructive-foreground: 52 100% 96%; + --border: 280 45% 38%; + --input: 280 35% 28%; + --ring: 318 100% 62%; + --chart-1: 318 100% 62%; + --chart-2: 198 100% 45%; + --chart-3: 142 70% 45%; + --chart-4: 48 100% 55%; + --chart-5: 280 70% 55%; + --radius: 0px; + --retro-shadow: 4px 4px 0 0 hsl(280 45% 28%); + --retro-shadow-sm: 2px 2px 0 0 hsl(280 45% 28%); + --lcd-bg: 142 35% 22%; + --lcd-border: 142 45% 35%; } + .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + --background: 252 38% 10%; + --foreground: 52 100% 88%; + --card: 252 32% 14%; + --card-foreground: 52 100% 88%; + --popover: 252 32% 14%; + --popover-foreground: 52 100% 88%; + --primary: 318 100% 62%; + --primary-foreground: 252 38% 10%; + --secondary: 198 100% 45%; + --secondary-foreground: 252 38% 10%; + --muted: 252 24% 20%; + --muted-foreground: 52 40% 68%; + --accent: 142 70% 45%; + --accent-foreground: 252 38% 10%; + --destructive: 0 84% 58%; + --destructive-foreground: 52 100% 96%; + --border: 280 45% 38%; + --input: 280 35% 28%; + --ring: 318 100% 62%; } } @@ -62,7 +63,135 @@ * { @apply border-border; } + + html { + overflow-x: clip; + } + body { - @apply bg-background text-foreground; + @apply overflow-x-hidden bg-background text-foreground text-xs leading-relaxed; + font-family: var(--font-pixel), monospace; + image-rendering: pixelated; } } + +@layer components { + .pixel-border { + border: 2px solid hsl(var(--border)); + box-shadow: var(--retro-shadow); + } + + .pixel-border-sm { + border: 2px solid hsl(var(--border)); + box-shadow: var(--retro-shadow-sm); + } + + .pixel-inset { + box-shadow: inset 0 0 0 2px hsl(var(--border) / 0.5), + inset 3px 3px 0 0 hsl(var(--background)); + } + + .tamagotchi-shell { + @apply pixel-border bg-card; + background-image: linear-gradient( + 180deg, + hsl(var(--card)) 0%, + hsl(var(--muted)) 100% + ); + } + + .tamagotchi-lcd { + @apply pixel-inset relative overflow-hidden; + background: hsl(var(--lcd-bg)); + border: 3px solid hsl(var(--lcd-border)); + } + + /* Matches pet-tab LCD: max-w-lg main minus px-5 and shell p-4 */ + .tamagotchi-lcd-pet-match { + @apply aspect-[4/3] w-full shrink-0; + max-width: calc(32rem - 2.5rem - 2rem); + } + + .customize-body { + min-height: 26rem; + } + + @media (min-width: 1024px) { + .customize-body { + min-height: 30rem; + } + } + + .retro-btn { + @apply inline-flex items-center justify-center border-2 border-border bg-primary text-primary-foreground; + box-shadow: var(--retro-shadow-sm); + transition: transform 0.08s steps(2), box-shadow 0.08s steps(2); + } + + .retro-btn:hover:not(:disabled) { + transform: translate(-1px, -1px); + box-shadow: 3px 3px 0 0 hsl(280 45% 28%); + } + + .retro-btn:active:not(:disabled) { + transform: translate(2px, 2px); + box-shadow: none; + } + + .retro-card { + @apply border-2 border-border bg-card text-card-foreground; + box-shadow: var(--retro-shadow); + } +} + +@layer utilities { + .pb-nav { + padding-bottom: var(--bottom-nav-height); + } + + .image-pixelated { + image-rendering: pixelated; + } +} + +@keyframes pet-breathe { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +@keyframes parallax-drift { + 0%, + 100% { + transform: translate3d(0, 0, 0); + } + 50% { + transform: translate3d(6px, -4px, 0); + } +} + +@keyframes star-twinkle { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} + +.animate-pet-breathe { + animation: pet-breathe 2.4s ease-in-out infinite; +} + +.animate-parallax-drift { + animation: parallax-drift 8s ease-in-out infinite; +} + +.animate-star-twinkle { + animation: star-twinkle 3s steps(2, end) infinite; +} diff --git a/app/layout.tsx b/app/layout.tsx index 0326579..0cd06d7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Geist } from "next/font/google"; +import { Press_Start_2P } from "next/font/google"; import { ThemeProvider } from "next-themes"; import { GlobalBottomNav } from "@/components/layout/global-bottom-nav"; @@ -16,10 +16,11 @@ export const metadata: Metadata = { description: "Build habits, care for your pixel pet, and track wellness every day.", }; -const geistSans = Geist({ - variable: "--font-geist-sans", - display: "swap", +const pixelFont = Press_Start_2P({ + weight: "400", subsets: ["latin"], + variable: "--font-pixel", + display: "swap", }); export default function RootLayout({ @@ -29,7 +30,7 @@ export default function RootLayout({ }>) { return ( - + -
{children}
+ {children}
diff --git a/components/avatar/avatar-customize-layout.tsx b/components/avatar/avatar-customize-layout.tsx new file mode 100644 index 0000000..1c067cc --- /dev/null +++ b/components/avatar/avatar-customize-layout.tsx @@ -0,0 +1,27 @@ +import { AppPageHeader } from "@/components/layout/app-page-header"; + +type AvatarCustomizeLayoutProps = { + eyebrow?: string; + title: string; + description: string; + children: React.ReactNode; +}; + +export function AvatarCustomizeLayout({ + eyebrow, + title, + description, + children, +}: AvatarCustomizeLayoutProps) { + return ( +
+ + +
{children}
+
+ ); +} diff --git a/components/avatar/avatar-page-content.tsx b/components/avatar/avatar-page-content.tsx index 6e9b21a..162ba44 100644 --- a/components/avatar/avatar-page-content.tsx +++ b/components/avatar/avatar-page-content.tsx @@ -1,58 +1,24 @@ "use client"; -import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; -import { CharacterCreator } from "@/components/character/character-creator"; -import { AvatarStatusCard } from "@/components/avatar/avatar-status-card"; -import { DailyQuizLinkButton } from "@/components/daily-quiz/daily-quiz-link-button"; -import { HabitTracker } from "@/components/habits/habit-tracker"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useToast } from "@/components/ui/toast-provider"; +import { PetHabitat } from "@/components/pet/pet-habitat"; import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events"; import { getAvatarCustomization, - saveAvatarCustomization, - saveAvatarName, type AvatarCustomization, } from "@/lib/avatar-customization-storage"; -import { getOwnedItemIds } from "@/lib/shop-storage"; -import { cn } from "@/lib/utils"; - -type AvatarTab = "home" | "customize"; export function AvatarPageContent() { - const { toast } = useToast(); - const searchParams = useSearchParams(); - const [activeTab, setActiveTab] = useState("home"); const [customization, setCustomization] = useState( null, ); - const [avatarName, setAvatarName] = useState(""); - const [isSavingName, setIsSavingName] = useState(false); - const [isSavingAvatar, setIsSavingAvatar] = useState(false); - const [ownedVariantIds, setOwnedVariantIds] = useState([]); - - useEffect(() => { - if (searchParams.get("tab") === "customize") { - setActiveTab("customize"); - } - }, [searchParams]); - const [loadError, setLoadError] = useState(null); const loadCustomization = useCallback(async () => { try { setLoadError(null); - const [next, owned] = await Promise.all([ - getAvatarCustomization(), - getOwnedItemIds(), - ]); - setCustomization(next); - setAvatarName(next.name); - setOwnedVariantIds(owned); + setCustomization(await getAvatarCustomization()); } catch (error) { setLoadError( error instanceof Error ? error.message : "Could not load your avatar.", @@ -69,123 +35,15 @@ export function AvatarPageContent() { }; }, [loadCustomization]); - const handleSaveName = async () => { - setIsSavingName(true); - - try { - const savedName = await saveAvatarName(avatarName); - setAvatarName(savedName); - toast("Pet name saved.", "success"); - } catch (error) { - toast( - error instanceof Error ? error.message : "Could not save name.", - "error", - ); - } finally { - setIsSavingName(false); - } - }; - - const handleSaveAvatar = async (nextCustomization: AvatarCustomization) => { - setIsSavingAvatar(true); - - try { - await saveAvatarCustomization({ - ...nextCustomization, - name: avatarName.trim() || nextCustomization.name, - equippedItems: customization?.equippedItems ?? [], - }); - toast("Avatar updated.", "success"); - setActiveTab("home"); - await loadCustomization(); - } catch (error) { - toast( - error instanceof Error ? error.message : "Could not save avatar.", - "error", - ); - throw error; - } finally { - setIsSavingAvatar(false); - } - }; - if (loadError) { - return

{loadError}

; + return

{loadError}

; } if (!customization) { return ( -

Loading your pet...

+

Loading your pet...

); } - return ( -
-
- {( - [ - { id: "home", label: "Overview" }, - { id: "customize", label: "Customize" }, - ] as const - ).map((tab) => ( - - ))} -
- -
- -
- setAvatarName(event.target.value)} - /> - -
-
- - {activeTab === "home" ? ( - <> - - - - - ) : ( - - )} -
- ); + return ; } diff --git a/components/avatar/customize-page-content.tsx b/components/avatar/customize-page-content.tsx new file mode 100644 index 0000000..fc521e6 --- /dev/null +++ b/components/avatar/customize-page-content.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { CharacterCreator } from "@/components/character/character-creator"; +import { useToast } from "@/components/ui/toast-provider"; +import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events"; +import { + getAvatarCustomization, + saveAvatarCustomization, + type AvatarCustomization, +} from "@/lib/avatar-customization-storage"; +import { getOwnedItemIds } from "@/lib/shop-storage"; + +export function CustomizePageContent() { + const { toast } = useToast(); + const [customization, setCustomization] = useState( + null, + ); + const [avatarName, setAvatarName] = useState(""); + const [isSavingAvatar, setIsSavingAvatar] = useState(false); + const [ownedVariantIds, setOwnedVariantIds] = useState([]); + const [loadError, setLoadError] = useState(null); + + const loadCustomization = useCallback(async () => { + try { + setLoadError(null); + const [next, owned] = await Promise.all([ + getAvatarCustomization(), + getOwnedItemIds(), + ]); + setCustomization(next); + setAvatarName(next.name); + setOwnedVariantIds(owned); + } catch (error) { + setLoadError( + error instanceof Error ? error.message : "Could not load your avatar.", + ); + } + }, []); + + useEffect(() => { + void loadCustomization(); + + window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, loadCustomization); + return () => { + window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, loadCustomization); + }; + }, [loadCustomization]); + + const handleSaveAvatar = async (nextCustomization: AvatarCustomization) => { + setIsSavingAvatar(true); + + try { + await saveAvatarCustomization({ + ...nextCustomization, + name: avatarName.trim() || nextCustomization.name, + equippedItems: customization?.equippedItems ?? [], + }); + toast("Avatar updated.", "success"); + await loadCustomization(); + } catch (error) { + toast( + error instanceof Error ? error.message : "Could not save avatar.", + "error", + ); + throw error; + } finally { + setIsSavingAvatar(false); + } + }; + + if (loadError) { + return

{loadError}

; + } + + if (!customization) { + return

Loading customization...

; + } + + return ( + + ); +} diff --git a/components/character/character-creator.tsx b/components/character/character-creator.tsx index 9e64cea..b2f0df0 100644 --- a/components/character/character-creator.tsx +++ b/components/character/character-creator.tsx @@ -7,24 +7,30 @@ import { CharacterColorSliders } from "@/components/character/character-color-sl import { CharacterLayerPreview } from "@/components/character/character-layer-preview"; import { CharacterLayerTabs } from "@/components/character/character-layer-tabs"; import { CharacterPieceSelector } from "@/components/character/character-piece-selector"; +import { ParallaxRoomBackground } from "@/components/pet/parallax-room-background"; +import { RoomStyleSelector } from "@/components/pet/room-style-selector"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import type { AvatarCustomization } from "@/lib/avatar-customization-storage"; import { clampHsl } from "@/lib/character/color-utils"; import { - CHARACTER_LAYERS, COLOR_PRESETS, DEFAULT_GRAY_COLOR, LAYER_DEFAULT_COLORS, NONE_VARIANT_ID, buildDefaultVariants, getLayerById, + type CharacterCreatorTabId, type ColorPreset, } from "@/lib/character/presets"; import type { LayerColorState, LayerVariantState } from "@/lib/character/types"; import { isVariantUnlocked } from "@/lib/character/variant-access"; +import { + isRoomBackgroundUnlocked, + normalizeRoomBackgroundId, + type RoomBackgroundId, +} from "@/lib/room-backgrounds"; function SectionLabel({ children }: { children: React.ReactNode }) { return ( @@ -74,14 +80,20 @@ export function CharacterCreator({ const [variants, setVariants] = useState( initialCustomization?.variants ?? buildDefaultVariants(), ); + const [roomBackground, setRoomBackground] = useState( + normalizeRoomBackgroundId(initialCustomization?.roomBackground), + ); const [activePresetId, setActivePresetId] = useState( DEFAULT_GRAY_COLOR.id, ); - const [activeLayerId, setActiveLayerId] = useState< - (typeof CHARACTER_LAYERS)[number]["id"] - >("skin"); + const [activeTabId, setActiveTabId] = useState("skin"); const [saveError, setSaveError] = useState(null); + const isRoomTab = activeTabId === "room"; + const isSkinTab = activeTabId === "skin"; + const showStyleSection = !isSkinTab; + const showColorAdjust = !isRoomTab; + const activeLayerId = isRoomTab ? "skin" : activeTabId; const activeLayer = getLayerById(activeLayerId); const activeColor = colors[activeLayerId]; const ownedSet = new Set(ownedVariantIds); @@ -112,9 +124,14 @@ export function CharacterCreator({ })); }; - const handleLayerChange = (layerId: (typeof CHARACTER_LAYERS)[number]["id"]) => { - setActiveLayerId(layerId); - const layerColor = colors[layerId]; + const handleTabChange = (tabId: CharacterCreatorTabId) => { + setActiveTabId(tabId); + + if (tabId === "room") { + return; + } + + const layerColor = colors[tabId]; const matchingPreset = COLOR_PRESETS.find( (preset) => preset.hsl.h === layerColor.h && @@ -135,12 +152,21 @@ export function CharacterCreator({ })); }; + const handleRoomChange = (roomId: RoomBackgroundId) => { + if (!isRoomBackgroundUnlocked(roomId, ownedVariantIds)) { + return; + } + + setRoomBackground(roomId); + }; + const buildCustomization = (): AvatarCustomization => ({ name, colors, variants, customized: true, equippedItems: initialCustomization?.equippedItems ?? [], + roomBackground, }); const handleSave = async () => { @@ -160,9 +186,9 @@ export function CharacterCreator({ }; return ( -
+
{showNameField ? ( -
+
) : null} - + -
- - - - - +
+
+
+ +
+ +
+
+
+
- - -
+
+ {showStyleSection ? ( +
Style
- + ) : ( + + )} +
+
+ ) : null} + + {showColorAdjust ? ( + <> +
+ Color +
+ +
+
+ +
+ Adjust + -
-
+ + + ) : null} -
- Color -
- -
-
- -
- Adjust - -
- - {onSave ? ( -
- - {saveError ? ( -

{saveError}

- ) : null} -
- ) : null} -
-
+ {onSave ? ( +
+ + {saveError ? ( +

{saveError}

+ ) : null} +
+ ) : null} +
); diff --git a/components/character/character-layer-tabs.tsx b/components/character/character-layer-tabs.tsx index 9333a27..f6fb788 100644 --- a/components/character/character-layer-tabs.tsx +++ b/components/character/character-layer-tabs.tsx @@ -1,38 +1,41 @@ "use client"; -import { CHARACTER_LAYERS, type CharacterLayerId } from "@/lib/character/presets"; +import { + CHARACTER_CUSTOMIZATION_TABS, + type CharacterCreatorTabId, +} from "@/lib/character/presets"; import { cn } from "@/lib/utils"; type CharacterLayerTabsProps = { - activeLayerId: CharacterLayerId; - onLayerChange: (layerId: CharacterLayerId) => void; + activeTabId: CharacterCreatorTabId; + onTabChange: (tabId: CharacterCreatorTabId) => void; }; export function CharacterLayerTabs({ - activeLayerId, - onLayerChange, + activeTabId, + onTabChange, }: CharacterLayerTabsProps) { return (
- {CHARACTER_LAYERS.map((layer) => ( + {CHARACTER_CUSTOMIZATION_TABS.map((tab) => ( ))}
diff --git a/components/habits/habit-tracker.tsx b/components/habits/habit-tracker.tsx index 6e5df1c..4553fa0 100644 --- a/components/habits/habit-tracker.tsx +++ b/components/habits/habit-tracker.tsx @@ -35,7 +35,7 @@ import { getProfilePreferences } from "@/lib/profile-preferences-storage"; import { hasCompletedDailyQuizToday } from "@/lib/daily-quiz-storage"; type HabitTrackerProps = { - mode?: "daily" | "manage"; + mode?: "daily" | "manage" | "all"; }; const reasonLabels = { @@ -409,6 +409,90 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { ); } + if (mode === "all") { + return ( +
+ + + Today's tasks + + Personalized from your focus topics + {quizCompletedToday ? ", today's quiz," : ""} and custom habits. + + + + {error ?

{error}

: null} + {coinMessage ? ( +

{coinMessage}

+ ) : null} + +
+
+ + + + Custom habits + + Add your own habits. Custom habits always appear in your daily + task list. + + + + +
+ setNewHabitLabel(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void handleAddHabit(); + } + }} + /> + +
+
+
+ + + + How daily tasks are picked + + Your tasks combine focus preferences, custom habits, and today's + quiz. + + + +

Focus topics: {focusTopic ?? "None selected"}

+

+ Daily quiz today:{" "} + {quizCompletedToday ? "Completed" : "Not completed yet"} +

+

+ Suggested habits can appear when your quiz shows low energy, high + stress, or poor sleep. +

+
+
+
+ ); + } + if (mode === "manage") { return (
diff --git a/components/layout/app-nav.tsx b/components/layout/app-nav.tsx index d03db2c..a004c13 100644 --- a/components/layout/app-nav.tsx +++ b/components/layout/app-nav.tsx @@ -5,6 +5,7 @@ import { usePathname } from "next/navigation"; import { Home, ListPlus, + Palette, ShoppingBag, Sparkles, UserRound, @@ -19,10 +20,12 @@ const gridColsClass = { 3: "grid-cols-3", 4: "grid-cols-4", 5: "grid-cols-5", + 6: "grid-cols-6", } as const; const navIcons = { [routes.avatar]: Home, + [routes.customize]: Palette, [routes.habits]: ListPlus, [routes.dailyQuiz]: Sparkles, [routes.shop]: ShoppingBag, @@ -37,14 +40,21 @@ export function AppNav({ items = appNavItems }: AppNavProps) { const pathname = usePathname(); const columnCount = Math.min( items.length, - 5, + 6, ) as keyof typeof gridColsClass; return ( -