@greatapps/common 1.1.92 → 1.1.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/layouts/AppMobileNavBar.mjs +15 -5
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
- package/dist/components/layouts/MdSideBarNavigation.mjs +18 -3
- package/dist/components/layouts/MdSideBarNavigation.mjs.map +1 -1
- package/dist/components/navigation/AppNavigation.mjs +8 -4
- package/dist/components/navigation/AppNavigation.mjs.map +1 -1
- package/dist/components/navigation/NavItemsContext.mjs +16 -0
- package/dist/components/navigation/NavItemsContext.mjs.map +1 -0
- package/dist/components/navigation/shells/TabsMobileShell.mjs +58 -0
- package/dist/components/navigation/shells/TabsMobileShell.mjs.map +1 -0
- package/dist/components/navigation/subcomponents/NavItems.mjs +6 -0
- package/dist/components/navigation/subcomponents/NavItems.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/layouts/AppMobileNavBar.tsx +12 -2
- package/src/components/layouts/MdSideBarNavigation.tsx +16 -1
- package/src/components/navigation/AppNavigation.tsx +15 -5
- package/src/components/navigation/NavItemsContext.ts +20 -0
- package/src/components/navigation/shells/TabsMobileShell.tsx +71 -0
- package/src/components/navigation/subcomponents/NavItems.tsx +8 -1
|
@@ -7,6 +7,8 @@ function AppMobileNavBar({
|
|
|
7
7
|
appIconActive = false,
|
|
8
8
|
appIconIndicator = false,
|
|
9
9
|
logoIndicator = false,
|
|
10
|
+
onLogoClick,
|
|
11
|
+
onAppIconClick,
|
|
10
12
|
extraLeftSlot,
|
|
11
13
|
rightActionSlot,
|
|
12
14
|
menuItems = [],
|
|
@@ -15,15 +17,23 @@ function AppMobileNavBar({
|
|
|
15
17
|
const { openConfigurations } = useAccountModals();
|
|
16
18
|
return /* @__PURE__ */ jsxs("div", { className: "fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200", children: [
|
|
17
19
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
18
|
-
/* @__PURE__ */ jsxs(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/* @__PURE__ */ jsxs(
|
|
21
|
+
"div",
|
|
22
|
+
{
|
|
23
|
+
className: `relative size-10 flex items-center justify-center rounded-lg transition-colors ${onLogoClick ? "cursor-pointer hover:bg-gray-100" : ""}`,
|
|
24
|
+
onClick: onLogoClick,
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsx(Image, { src: "/icons/great-icon.svg", alt: "GreatApps", width: 28, height: 28 }),
|
|
27
|
+
logoIndicator && /* @__PURE__ */ jsx("div", { className: "absolute bottom-[-17px] left-1/2 -translate-x-1/2 w-5 h-[3px] bg-gray-950 rounded-t-xl" })
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
),
|
|
22
31
|
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
23
32
|
/* @__PURE__ */ jsx(
|
|
24
33
|
"div",
|
|
25
34
|
{
|
|
26
|
-
className: `size-10 flex items-center justify-center rounded-lg transition-colors duration-300 ${appIconActive ? "bg-pages-300" : "bg-pages-50"}`,
|
|
35
|
+
className: `size-10 flex items-center justify-center rounded-lg transition-colors duration-300 ${appIconActive ? "bg-pages-300" : "bg-pages-50"} ${onAppIconClick ? "cursor-pointer hover:opacity-80" : ""}`,
|
|
36
|
+
onClick: onAppIconClick,
|
|
27
37
|
children: /* @__PURE__ */ jsx(Image, { src: "/icons/icon-navbar.svg", alt: "GreatPages", width: 24, height: 24 })
|
|
28
38
|
}
|
|
29
39
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/layouts/AppMobileNavBar.tsx"],"sourcesContent":["'use client';\n\nimport { ReactNode } from 'react';\nimport Image from 'next/image';\nimport { ProfilePopover } from './ProfilePopover';\nimport { useAccountModals } from '../../store/useAccountModals';\nimport type { ProfileMenuItem } from './ProfilePopover';\n\nexport interface AppMobileNavBarProps {\n appIconActive?: boolean;\n /** Shows the colored indicator bar below the app icon */\n appIconIndicator?: boolean;\n /** Shows the gray indicator bar below the GreatApps logo */\n logoIndicator?: boolean;\n /** Extra slot rendered after the app icon (e.g. project selector) */\n extraLeftSlot?: ReactNode;\n /** Required: the right-side action element (toggle button, sheet button, etc.) */\n rightActionSlot: ReactNode;\n menuItems?: ProfileMenuItem[];\n onCreditsClick?: () => void;\n}\n\nexport function AppMobileNavBar({\n appIconActive = false,\n appIconIndicator = false,\n logoIndicator = false,\n extraLeftSlot,\n rightActionSlot,\n menuItems = [],\n onCreditsClick,\n}: AppMobileNavBarProps) {\n const { openConfigurations } = useAccountModals();\n\n return (\n <div className=\"fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200\">\n <div className=\"flex items-center gap-1.5\">\n <div
|
|
1
|
+
{"version":3,"sources":["../../../src/components/layouts/AppMobileNavBar.tsx"],"sourcesContent":["'use client';\n\nimport { ReactNode } from 'react';\nimport Image from 'next/image';\nimport { ProfilePopover } from './ProfilePopover';\nimport { useAccountModals } from '../../store/useAccountModals';\nimport type { ProfileMenuItem } from './ProfilePopover';\n\nexport interface AppMobileNavBarProps {\n appIconActive?: boolean;\n /** Shows the colored indicator bar below the app icon */\n appIconIndicator?: boolean;\n /** Shows the gray indicator bar below the GreatApps logo */\n logoIndicator?: boolean;\n /** Called when the GreatApps logo is clicked */\n onLogoClick?: () => void;\n /** Called when the app icon (e.g. GreatPages) is clicked */\n onAppIconClick?: () => void;\n /** Extra slot rendered after the app icon (e.g. project selector) */\n extraLeftSlot?: ReactNode;\n /** Required: the right-side action element (toggle button, sheet button, etc.) */\n rightActionSlot: ReactNode;\n menuItems?: ProfileMenuItem[];\n onCreditsClick?: () => void;\n}\n\nexport function AppMobileNavBar({\n appIconActive = false,\n appIconIndicator = false,\n logoIndicator = false,\n onLogoClick,\n onAppIconClick,\n extraLeftSlot,\n rightActionSlot,\n menuItems = [],\n onCreditsClick,\n}: AppMobileNavBarProps) {\n const { openConfigurations } = useAccountModals();\n\n return (\n <div className=\"fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200\">\n <div className=\"flex items-center gap-1.5\">\n <div\n className={`relative size-10 flex items-center justify-center rounded-lg transition-colors ${onLogoClick ? 'cursor-pointer hover:bg-gray-100' : ''}`}\n onClick={onLogoClick}\n >\n <Image src=\"/icons/great-icon.svg\" alt=\"GreatApps\" width={28} height={28} />\n {logoIndicator && (\n <div className=\"absolute bottom-[-17px] left-1/2 -translate-x-1/2 w-5 h-[3px] bg-gray-950 rounded-t-xl\" />\n )}\n </div>\n <div className=\"relative\">\n <div\n className={`size-10 flex items-center justify-center rounded-lg transition-colors duration-300 ${\n appIconActive ? 'bg-pages-300' : 'bg-pages-50'\n } ${onAppIconClick ? 'cursor-pointer hover:opacity-80' : ''}`}\n onClick={onAppIconClick}\n >\n <Image src=\"/icons/icon-navbar.svg\" alt=\"GreatPages\" width={24} height={24} />\n </div>\n {appIconIndicator && (\n <div className=\"absolute -bottom-[17px] left-1/2 -translate-x-1/2 w-5 h-[3px] bg-pages-300 rounded-t-xl\" />\n )}\n </div>\n {extraLeftSlot}\n </div>\n <div className=\"flex items-center gap-1.5\">\n <ProfilePopover\n side=\"bottom\"\n align=\"end\"\n contentClassName=\"\"\n onEditProfile={() => openConfigurations()}\n onCreditsClick={onCreditsClick}\n menuItems={menuItems}\n />\n {rightActionSlot}\n </div>\n </div>\n );\n}\n"],"mappings":";AA0CQ,SAIE,KAJF;AAvCR,OAAO,WAAW;AAClB,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AAqB1B,SAAS,gBAAgB;AAAA,EAC9B,gBAAgB;AAAA,EAChB,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY,CAAC;AAAA,EACb;AACF,GAAyB;AACvB,QAAM,EAAE,mBAAmB,IAAI,iBAAiB;AAEhD,SACE,qBAAC,SAAI,WAAU,qHACb;AAAA,yBAAC,SAAI,WAAU,6BACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,kFAAkF,cAAc,qCAAqC,EAAE;AAAA,UAClJ,SAAS;AAAA,UAET;AAAA,gCAAC,SAAM,KAAI,yBAAwB,KAAI,aAAY,OAAO,IAAI,QAAQ,IAAI;AAAA,YACzE,iBACC,oBAAC,SAAI,WAAU,0FAAyF;AAAA;AAAA;AAAA,MAE5G;AAAA,MACA,qBAAC,SAAI,WAAU,YACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,sFACT,gBAAgB,iBAAiB,aACnC,IAAI,iBAAiB,oCAAoC,EAAE;AAAA,YAC3D,SAAS;AAAA,YAET,8BAAC,SAAM,KAAI,0BAAyB,KAAI,cAAa,OAAO,IAAI,QAAQ,IAAI;AAAA;AAAA,QAC9E;AAAA,QACC,oBACC,oBAAC,SAAI,WAAU,2FAA0F;AAAA,SAE7G;AAAA,MACC;AAAA,OACH;AAAA,IACA,qBAAC,SAAI,WAAU,6BACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAM;AAAA,UACN,kBAAiB;AAAA,UACjB,eAAe,MAAM,mBAAmB;AAAA,UACxC;AAAA,UACA;AAAA;AAAA,MACF;AAAA,MACC;AAAA,OACH;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeft } from "lucide-react";
|
|
3
4
|
import { useMdSidebarStore } from "../../store/useMdSidebarStore";
|
|
4
5
|
import { cn } from "../../infra/utils/clsx";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
|
|
5
7
|
function MdSideBarNavigation({
|
|
6
8
|
className,
|
|
7
9
|
children,
|
|
@@ -20,13 +22,26 @@ function MdSideBarNavigation({
|
|
|
20
22
|
"div",
|
|
21
23
|
{
|
|
22
24
|
className: `hidden md:flex lg:hidden fixed top-0 bottom-0 left-[73px] z-30 transition-transform duration-300 ease-in-out overflow-visible ${isExpanded ? "translate-x-0" : "-translate-x-full"}`,
|
|
23
|
-
children: /* @__PURE__ */
|
|
25
|
+
children: /* @__PURE__ */ jsxs(
|
|
24
26
|
"div",
|
|
25
27
|
{
|
|
26
28
|
"data-slot": "md-sidebar-navigation",
|
|
27
|
-
className: cn("w-[273px] h-full flex flex-col p-4 bg-white border-r border-gray-200", className),
|
|
29
|
+
className: cn("relative w-[273px] h-full flex flex-col p-4 bg-white border-r border-gray-200", className),
|
|
28
30
|
...props,
|
|
29
|
-
children
|
|
31
|
+
children: [
|
|
32
|
+
isExpanded && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
33
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
34
|
+
"button",
|
|
35
|
+
{
|
|
36
|
+
onClick: collapse,
|
|
37
|
+
className: "absolute top-[68px] -right-4 size-8 flex items-center justify-center rounded-full border border-gray-200 bg-white hover:bg-gray-100 cursor-pointer transition-colors z-40",
|
|
38
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { size: 16, className: "text-gray-500" })
|
|
39
|
+
}
|
|
40
|
+
) }),
|
|
41
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Recolher menu" })
|
|
42
|
+
] }),
|
|
43
|
+
children
|
|
44
|
+
]
|
|
30
45
|
}
|
|
31
46
|
)
|
|
32
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/layouts/MdSideBarNavigation.tsx"],"sourcesContent":["\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { useMdSidebarStore } from \"../../store/useMdSidebarStore\"\r\nimport { cn } from \"../../infra/utils/clsx\"\r\n\r\nfunction MdSideBarNavigation({\r\n className,\r\n children,\r\n ...props\r\n}: React.ComponentProps<\"div\">) {\r\n const { isExpanded, collapse } = useMdSidebarStore()\r\n\r\n return (\r\n <>\r\n <div\r\n className={`hidden md:block lg:hidden fixed inset-0 bg-black/50 z-20 transition-opacity duration-300 ${\r\n isExpanded ? \"opacity-100\" : \"opacity-0 pointer-events-none\"\r\n }`}\r\n onClick={collapse}\r\n />\r\n <div\r\n className={`hidden md:flex lg:hidden fixed top-0 bottom-0 left-[73px] z-30 transition-transform duration-300 ease-in-out overflow-visible ${\r\n isExpanded ? \"translate-x-0\" : \"-translate-x-full\"\r\n }`}\r\n >\r\n <div\r\n data-slot=\"md-sidebar-navigation\"\r\n className={cn(\"w-[273px] h-full flex flex-col p-4 bg-white border-r border-gray-200\", className)}\r\n {...props}\r\n >\r\n {children}\r\n </div>\r\n </div>\r\n </>\r\n )\r\n}\r\n\r\nexport { MdSideBarNavigation };\r\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/components/layouts/MdSideBarNavigation.tsx"],"sourcesContent":["\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { ChevronLeft } from \"lucide-react\"\r\nimport { useMdSidebarStore } from \"../../store/useMdSidebarStore\"\r\nimport { cn } from \"../../infra/utils/clsx\"\r\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/overlay/Tooltip\"\r\n\r\nfunction MdSideBarNavigation({\r\n className,\r\n children,\r\n ...props\r\n}: React.ComponentProps<\"div\">) {\r\n const { isExpanded, collapse } = useMdSidebarStore()\r\n\r\n return (\r\n <>\r\n <div\r\n className={`hidden md:block lg:hidden fixed inset-0 bg-black/50 z-20 transition-opacity duration-300 ${\r\n isExpanded ? \"opacity-100\" : \"opacity-0 pointer-events-none\"\r\n }`}\r\n onClick={collapse}\r\n />\r\n <div\r\n className={`hidden md:flex lg:hidden fixed top-0 bottom-0 left-[73px] z-30 transition-transform duration-300 ease-in-out overflow-visible ${\r\n isExpanded ? \"translate-x-0\" : \"-translate-x-full\"\r\n }`}\r\n >\r\n <div\r\n data-slot=\"md-sidebar-navigation\"\r\n className={cn(\"relative w-[273px] h-full flex flex-col p-4 bg-white border-r border-gray-200\", className)}\r\n {...props}\r\n >\r\n {isExpanded && (\r\n <Tooltip>\r\n <TooltipTrigger asChild>\r\n <button\r\n onClick={collapse}\r\n className=\"absolute top-[68px] -right-4 size-8 flex items-center justify-center rounded-full border border-gray-200 bg-white hover:bg-gray-100 cursor-pointer transition-colors z-40\"\r\n >\r\n <ChevronLeft size={16} className=\"text-gray-500\" />\r\n </button>\r\n </TooltipTrigger>\r\n <TooltipContent side=\"right\">Recolher menu</TooltipContent>\r\n </Tooltip>\r\n )}\r\n {children}\r\n </div>\r\n </div>\r\n </>\r\n )\r\n}\r\n\r\nexport { MdSideBarNavigation };\r\n"],"mappings":";AAgBQ,mBACI,KAiBY,YAlBhB;AAbR,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,sBAAsB;AAExD,SAAS,oBAAoB;AAAA,EACzB;AAAA,EACA;AAAA,EACA,GAAG;AACP,GAAgC;AAC5B,QAAM,EAAE,YAAY,SAAS,IAAI,kBAAkB;AAEnD,SACI,iCACI;AAAA;AAAA,MAAC;AAAA;AAAA,QACG,WAAW,4FACP,aAAa,gBAAgB,+BACjC;AAAA,QACA,SAAS;AAAA;AAAA,IACb;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACG,WAAW,iIACP,aAAa,kBAAkB,mBACnC;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACG,aAAU;AAAA,YACV,WAAW,GAAG,iFAAiF,SAAS;AAAA,YACvG,GAAG;AAAA,YAEH;AAAA,4BACG,qBAAC,WACG;AAAA,oCAAC,kBAAe,SAAO,MACnB;AAAA,kBAAC;AAAA;AAAA,oBACG,SAAS;AAAA,oBACT,WAAU;AAAA,oBAEV,8BAAC,eAAY,MAAM,IAAI,WAAU,iBAAgB;AAAA;AAAA,gBACrD,GACJ;AAAA,gBACA,oBAAC,kBAAe,MAAK,SAAQ,2BAAa;AAAA,iBAC9C;AAAA,cAEH;AAAA;AAAA;AAAA,QACL;AAAA;AAAA,IACJ;AAAA,KACJ;AAER;","names":[]}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { DesktopShell } from "./shells/DesktopShell";
|
|
4
5
|
import { MdShell } from "./shells/MdShell";
|
|
5
6
|
import { MobileShell } from "./shells/MobileShell";
|
|
7
|
+
import { TabsMobileShell } from "./shells/TabsMobileShell";
|
|
6
8
|
import { NavItems } from "./subcomponents/NavItems";
|
|
7
9
|
import { ProjectSelector } from "./subcomponents/ProjectSelector";
|
|
8
10
|
import { PlanCard } from "./subcomponents/PlanCard";
|
|
9
11
|
import { CreditsCard } from "./subcomponents/CreditsCard";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
import { NavItemsContext } from "./NavItemsContext";
|
|
13
|
+
function AppNavigationRoot({ children, mobileVariant = "sheet" }) {
|
|
14
|
+
const [navItems, setNavItems] = useState([]);
|
|
15
|
+
return /* @__PURE__ */ jsxs(NavItemsContext.Provider, { value: { navItems, setNavItems, mobileVariant }, children: [
|
|
12
16
|
/* @__PURE__ */ jsx(DesktopShell, { children }),
|
|
13
17
|
/* @__PURE__ */ jsx(MdShell, { children }),
|
|
14
|
-
/* @__PURE__ */ jsx(MobileShell, { children })
|
|
18
|
+
mobileVariant === "sheet" ? /* @__PURE__ */ jsx(MobileShell, { children }) : /* @__PURE__ */ jsx(TabsMobileShell, { children })
|
|
15
19
|
] });
|
|
16
20
|
}
|
|
17
21
|
const AppNavigation = Object.assign(AppNavigationRoot, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/navigation/AppNavigation.tsx"],"sourcesContent":["'use client';\r\n\r\nimport { ReactNode } from 'react';\r\nimport { DesktopShell } from './shells/DesktopShell';\r\nimport { MdShell } from './shells/MdShell';\r\nimport { MobileShell } from './shells/MobileShell';\r\nimport { NavItems } from './subcomponents/NavItems';\r\nimport { ProjectSelector } from './subcomponents/ProjectSelector';\r\nimport { PlanCard } from './subcomponents/PlanCard';\r\nimport { CreditsCard } from './subcomponents/CreditsCard';\r\n\r\nexport type { NavItemConfig } from './subcomponents/NavItems';\r\nexport type { ProjectSelectorProps } from './subcomponents/ProjectSelector';\r\nexport type { PlanCardProps } from './subcomponents/PlanCard';\r\nexport type { CreditsCardProps } from './subcomponents/CreditsCard';\r\n\r\ninterface AppNavigationProps {\r\n children: ReactNode;\r\n}\r\n\r\nfunction AppNavigationRoot({ children }: AppNavigationProps) {\r\n return (\r\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/navigation/AppNavigation.tsx"],"sourcesContent":["'use client';\r\n\r\nimport { ReactNode, useState } from 'react';\r\nimport { DesktopShell } from './shells/DesktopShell';\r\nimport { MdShell } from './shells/MdShell';\r\nimport { MobileShell } from './shells/MobileShell';\r\nimport { TabsMobileShell } from './shells/TabsMobileShell';\r\nimport { NavItems } from './subcomponents/NavItems';\r\nimport { ProjectSelector } from './subcomponents/ProjectSelector';\r\nimport { PlanCard } from './subcomponents/PlanCard';\r\nimport { CreditsCard } from './subcomponents/CreditsCard';\r\nimport { NavItemsContext } from './NavItemsContext';\r\nimport type { NavItemConfig } from './subcomponents/NavItems';\r\n\r\nexport type { NavItemConfig } from './subcomponents/NavItems';\r\nexport type { ProjectSelectorProps } from './subcomponents/ProjectSelector';\r\nexport type { PlanCardProps } from './subcomponents/PlanCard';\r\nexport type { CreditsCardProps } from './subcomponents/CreditsCard';\r\n\r\ninterface AppNavigationProps {\r\n children: ReactNode;\r\n mobileVariant?: 'sheet' | 'tabs';\r\n}\r\n\r\nfunction AppNavigationRoot({ children, mobileVariant = 'sheet' }: AppNavigationProps) {\r\n const [navItems, setNavItems] = useState<NavItemConfig[]>([]);\r\n\r\n return (\r\n <NavItemsContext.Provider value={{ navItems, setNavItems, mobileVariant }}>\r\n <DesktopShell>{children}</DesktopShell>\r\n <MdShell>{children}</MdShell>\r\n {mobileVariant === 'sheet' ? (\r\n <MobileShell>{children}</MobileShell>\r\n ) : (\r\n <TabsMobileShell>{children}</TabsMobileShell>\r\n )}\r\n </NavItemsContext.Provider>\r\n );\r\n}\r\n\r\nexport const AppNavigation = Object.assign(AppNavigationRoot, {\r\n NavItems,\r\n ProjectSelector,\r\n PlanCard,\r\n CreditsCard,\r\n});\r\n"],"mappings":";AA4BI,SACE,KADF;AA1BJ,SAAoB,gBAAgB;AACpC,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAahC,SAAS,kBAAkB,EAAE,UAAU,gBAAgB,QAAQ,GAAuB;AACpF,QAAM,CAAC,UAAU,WAAW,IAAI,SAA0B,CAAC,CAAC;AAE5D,SACE,qBAAC,gBAAgB,UAAhB,EAAyB,OAAO,EAAE,UAAU,aAAa,cAAc,GACtE;AAAA,wBAAC,gBAAc,UAAS;AAAA,IACxB,oBAAC,WAAS,UAAS;AAAA,IAClB,kBAAkB,UACjB,oBAAC,eAAa,UAAS,IAEvB,oBAAC,mBAAiB,UAAS;AAAA,KAE/B;AAEJ;AAEO,MAAM,gBAAgB,OAAO,OAAO,mBAAmB;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const NavItemsContext = createContext({
|
|
4
|
+
navItems: [],
|
|
5
|
+
setNavItems: () => {
|
|
6
|
+
},
|
|
7
|
+
mobileVariant: "sheet"
|
|
8
|
+
});
|
|
9
|
+
function useNavItemsContext() {
|
|
10
|
+
return useContext(NavItemsContext);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
NavItemsContext,
|
|
14
|
+
useNavItemsContext
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=NavItemsContext.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/navigation/NavItemsContext.ts"],"sourcesContent":["'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { NavItemConfig } from './subcomponents/NavItems';\n\ninterface NavItemsContextValue {\n navItems: NavItemConfig[];\n setNavItems: (items: NavItemConfig[]) => void;\n mobileVariant: 'sheet' | 'tabs';\n}\n\nexport const NavItemsContext = createContext<NavItemsContextValue>({\n navItems: [],\n setNavItems: () => {},\n mobileVariant: 'sheet',\n});\n\nexport function useNavItemsContext(): NavItemsContextValue {\n return useContext(NavItemsContext);\n}\n"],"mappings":";AAEA,SAAS,eAAe,kBAAkB;AASnC,MAAM,kBAAkB,cAAoC;AAAA,EACjE,UAAU,CAAC;AAAA,EACX,aAAa,MAAM;AAAA,EAAC;AAAA,EACpB,eAAe;AACjB,CAAC;AAEM,SAAS,qBAA2C;AACzD,SAAO,WAAW,eAAe;AACnC;","names":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useEffect } from "react";
|
|
4
|
+
import { AppNavigationContext } from "../AppNavigationContext";
|
|
5
|
+
import { useNavItemsContext } from "../NavItemsContext";
|
|
6
|
+
function InternalTabsBar() {
|
|
7
|
+
const { navItems } = useNavItemsContext();
|
|
8
|
+
const tabsListRef = useRef(null);
|
|
9
|
+
const activeTabRef = useRef(null);
|
|
10
|
+
const activeLabel = navItems.find((i) => i.isActive)?.label;
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (activeTabRef.current && tabsListRef.current) {
|
|
13
|
+
const tabsList = tabsListRef.current;
|
|
14
|
+
const activeTabElement = activeTabRef.current;
|
|
15
|
+
const tabsListRect = tabsList.getBoundingClientRect();
|
|
16
|
+
const activeTabRect = activeTabElement.getBoundingClientRect();
|
|
17
|
+
const scrollLeft = activeTabElement.offsetLeft - tabsListRect.width / 2 + activeTabRect.width / 2;
|
|
18
|
+
tabsList.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" });
|
|
19
|
+
}
|
|
20
|
+
}, [activeLabel]);
|
|
21
|
+
if (!navItems.length) return null;
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
"div",
|
|
24
|
+
{
|
|
25
|
+
ref: tabsListRef,
|
|
26
|
+
className: "md:hidden fixed top-[72px] left-0 right-0 z-40 bg-white border-b border-gray-200 flex overflow-x-auto scrollbar-hide",
|
|
27
|
+
children: navItems.map((item) => /* @__PURE__ */ jsxs(
|
|
28
|
+
"button",
|
|
29
|
+
{
|
|
30
|
+
ref: item.isActive ? activeTabRef : void 0,
|
|
31
|
+
onClick: item.onClick,
|
|
32
|
+
className: `flex items-center gap-2 px-4 py-3 shrink-0 border-b-2 transition-colors ${item.isActive ? "border-gray-950 text-gray-950" : "border-transparent text-gray-500 hover:text-gray-900"}`,
|
|
33
|
+
children: [
|
|
34
|
+
item.icon && /* @__PURE__ */ jsx(item.icon, { size: 16, className: "shrink-0" }),
|
|
35
|
+
/* @__PURE__ */ jsx(
|
|
36
|
+
"span",
|
|
37
|
+
{
|
|
38
|
+
className: item.isActive ? "paragraph-small-semibold" : "paragraph-small-medium",
|
|
39
|
+
children: item.label
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
item.label
|
|
45
|
+
))
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function TabsMobileShell({ children }) {
|
|
50
|
+
return /* @__PURE__ */ jsxs(AppNavigationContext.Provider, { value: { breakpoint: "mobile" }, children: [
|
|
51
|
+
/* @__PURE__ */ jsx("div", { className: "hidden", children }),
|
|
52
|
+
/* @__PURE__ */ jsx(InternalTabsBar, {})
|
|
53
|
+
] });
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
TabsMobileShell
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=TabsMobileShell.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/navigation/shells/TabsMobileShell.tsx"],"sourcesContent":["'use client';\n\nimport { ReactNode, useRef, useEffect } from 'react';\nimport { AppNavigationContext } from '../AppNavigationContext';\nimport { useNavItemsContext } from '../NavItemsContext';\n\nfunction InternalTabsBar() {\n const { navItems } = useNavItemsContext();\n const tabsListRef = useRef<HTMLDivElement>(null);\n const activeTabRef = useRef<HTMLButtonElement>(null);\n\n const activeLabel = navItems.find((i) => i.isActive)?.label;\n\n // Scroll active tab into center when active item changes\n useEffect(() => {\n if (activeTabRef.current && tabsListRef.current) {\n const tabsList = tabsListRef.current;\n const activeTabElement = activeTabRef.current;\n const tabsListRect = tabsList.getBoundingClientRect();\n const activeTabRect = activeTabElement.getBoundingClientRect();\n const scrollLeft =\n activeTabElement.offsetLeft - tabsListRect.width / 2 + activeTabRect.width / 2;\n tabsList.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' });\n }\n }, [activeLabel]);\n\n if (!navItems.length) return null;\n\n return (\n <div\n ref={tabsListRef}\n className=\"md:hidden fixed top-[72px] left-0 right-0 z-40 bg-white border-b border-gray-200 flex overflow-x-auto scrollbar-hide\"\n >\n {navItems.map((item) => (\n <button\n key={item.label}\n ref={item.isActive ? activeTabRef : undefined}\n onClick={item.onClick}\n className={`flex items-center gap-2 px-4 py-3 shrink-0 border-b-2 transition-colors ${\n item.isActive\n ? 'border-gray-950 text-gray-950'\n : 'border-transparent text-gray-500 hover:text-gray-900'\n }`}\n >\n {item.icon && <item.icon size={16} className=\"shrink-0\" />}\n <span\n className={\n item.isActive ? 'paragraph-small-semibold' : 'paragraph-small-medium'\n }\n >\n {item.label}\n </span>\n </button>\n ))}\n </div>\n );\n}\n\nexport function TabsMobileShell({ children }: { children: ReactNode }) {\n return (\n <AppNavigationContext.Provider value={{ breakpoint: 'mobile' }}>\n {/*\n Children are hidden visually, but NavItems' useEffect still fires inside\n display:none — React effects are tied to commit phase, not layout visibility.\n This registers navItems in NavItemsContext so InternalTabsBar can read them.\n */}\n <div className=\"hidden\">{children}</div>\n <InternalTabsBar />\n </AppNavigationContext.Provider>\n );\n}\n"],"mappings":";AAkCQ,SAUgB,KAVhB;AAhCR,SAAoB,QAAQ,iBAAiB;AAC7C,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AAEnC,SAAS,kBAAkB;AACzB,QAAM,EAAE,SAAS,IAAI,mBAAmB;AACxC,QAAM,cAAc,OAAuB,IAAI;AAC/C,QAAM,eAAe,OAA0B,IAAI;AAEnD,QAAM,cAAc,SAAS,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AAGtD,YAAU,MAAM;AACd,QAAI,aAAa,WAAW,YAAY,SAAS;AAC/C,YAAM,WAAW,YAAY;AAC7B,YAAM,mBAAmB,aAAa;AACtC,YAAM,eAAe,SAAS,sBAAsB;AACpD,YAAM,gBAAgB,iBAAiB,sBAAsB;AAC7D,YAAM,aACJ,iBAAiB,aAAa,aAAa,QAAQ,IAAI,cAAc,QAAQ;AAC/E,eAAS,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,UAAU,GAAG,UAAU,SAAS,CAAC;AAAA,IACzE;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,MAAI,CAAC,SAAS,OAAQ,QAAO;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAU;AAAA,MAET,mBAAS,IAAI,CAAC,SACb;AAAA,QAAC;AAAA;AAAA,UAEC,KAAK,KAAK,WAAW,eAAe;AAAA,UACpC,SAAS,KAAK;AAAA,UACd,WAAW,2EACT,KAAK,WACD,kCACA,sDACN;AAAA,UAEC;AAAA,iBAAK,QAAQ,oBAAC,KAAK,MAAL,EAAU,MAAM,IAAI,WAAU,YAAW;AAAA,YACxD;AAAA,cAAC;AAAA;AAAA,gBACC,WACE,KAAK,WAAW,6BAA6B;AAAA,gBAG9C,eAAK;AAAA;AAAA,YACR;AAAA;AAAA;AAAA,QAhBK,KAAK;AAAA,MAiBZ,CACD;AAAA;AAAA,EACH;AAEJ;AAEO,SAAS,gBAAgB,EAAE,SAAS,GAA4B;AACrE,SACE,qBAAC,qBAAqB,UAArB,EAA8B,OAAO,EAAE,YAAY,SAAS,GAM3D;AAAA,wBAAC,SAAI,WAAU,UAAU,UAAS;AAAA,IAClC,oBAAC,mBAAgB;AAAA,KACnB;AAEJ;","names":[]}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect } from "react";
|
|
3
4
|
import { NavItem } from "../NavItem";
|
|
5
|
+
import { useNavItemsContext } from "../NavItemsContext";
|
|
4
6
|
function NavItems({ items }) {
|
|
7
|
+
const { setNavItems } = useNavItemsContext();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setNavItems(items);
|
|
10
|
+
}, [items, setNavItems]);
|
|
5
11
|
return /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: items.map((item) => /* @__PURE__ */ jsx(
|
|
6
12
|
NavItem,
|
|
7
13
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/navigation/subcomponents/NavItems.tsx"],"sourcesContent":["'use client';\r\n\r\nimport { ElementType } from 'react';\r\nimport { NavItem } from '../NavItem';\r\n\r\nexport interface NavItemConfig {\r\n label: string;\r\n isActive: boolean;\r\n icon: ElementType;\r\n onClick: () => void;\r\n}\r\n\r\ninterface NavItemsProps {\r\n items: NavItemConfig[];\r\n}\r\n\r\nexport function NavItems({ items }: NavItemsProps) {\r\n return (\r\n <div className=\"flex flex-col\">\r\n {items.map((item) => (\r\n <NavItem\r\n key={item.label}\r\n label={item.label}\r\n icon={item.icon}\r\n isActive={item.isActive}\r\n onClick={item.onClick}\r\n />\r\n ))}\r\n </div>\r\n );\r\n}\r\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/navigation/subcomponents/NavItems.tsx"],"sourcesContent":["'use client';\r\n\r\nimport { ElementType, useEffect } from 'react';\r\nimport { NavItem } from '../NavItem';\r\nimport { useNavItemsContext } from '../NavItemsContext';\r\n\r\nexport interface NavItemConfig {\r\n label: string;\r\n isActive: boolean;\r\n icon: ElementType;\r\n onClick: () => void;\r\n}\r\n\r\ninterface NavItemsProps {\r\n items: NavItemConfig[];\r\n}\r\n\r\nexport function NavItems({ items }: NavItemsProps) {\r\n const { setNavItems } = useNavItemsContext();\r\n\r\n useEffect(() => {\r\n setNavItems(items);\r\n }, [items, setNavItems]);\r\n\r\n return (\r\n <div className=\"flex flex-col\">\r\n {items.map((item) => (\r\n <NavItem\r\n key={item.label}\r\n label={item.label}\r\n icon={item.icon}\r\n isActive={item.isActive}\r\n onClick={item.onClick}\r\n />\r\n ))}\r\n </div>\r\n );\r\n}\r\n"],"mappings":";AA2BQ;AAzBR,SAAsB,iBAAiB;AACvC,SAAS,eAAe;AACxB,SAAS,0BAA0B;AAa5B,SAAS,SAAS,EAAE,MAAM,GAAkB;AACjD,QAAM,EAAE,YAAY,IAAI,mBAAmB;AAE3C,YAAU,MAAM;AACd,gBAAY,KAAK;AAAA,EACnB,GAAG,CAAC,OAAO,WAAW,CAAC;AAEvB,SACE,oBAAC,SAAI,WAAU,iBACZ,gBAAM,IAAI,CAAC,SACV;AAAA,IAAC;AAAA;AAAA,MAEC,OAAO,KAAK;AAAA,MACZ,MAAM,KAAK;AAAA,MACX,UAAU,KAAK;AAAA,MACf,SAAS,KAAK;AAAA;AAAA,IAJT,KAAK;AAAA,EAKZ,CACD,GACH;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -12,6 +12,10 @@ export interface AppMobileNavBarProps {
|
|
|
12
12
|
appIconIndicator?: boolean;
|
|
13
13
|
/** Shows the gray indicator bar below the GreatApps logo */
|
|
14
14
|
logoIndicator?: boolean;
|
|
15
|
+
/** Called when the GreatApps logo is clicked */
|
|
16
|
+
onLogoClick?: () => void;
|
|
17
|
+
/** Called when the app icon (e.g. GreatPages) is clicked */
|
|
18
|
+
onAppIconClick?: () => void;
|
|
15
19
|
/** Extra slot rendered after the app icon (e.g. project selector) */
|
|
16
20
|
extraLeftSlot?: ReactNode;
|
|
17
21
|
/** Required: the right-side action element (toggle button, sheet button, etc.) */
|
|
@@ -24,6 +28,8 @@ export function AppMobileNavBar({
|
|
|
24
28
|
appIconActive = false,
|
|
25
29
|
appIconIndicator = false,
|
|
26
30
|
logoIndicator = false,
|
|
31
|
+
onLogoClick,
|
|
32
|
+
onAppIconClick,
|
|
27
33
|
extraLeftSlot,
|
|
28
34
|
rightActionSlot,
|
|
29
35
|
menuItems = [],
|
|
@@ -34,7 +40,10 @@ export function AppMobileNavBar({
|
|
|
34
40
|
return (
|
|
35
41
|
<div className="fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200">
|
|
36
42
|
<div className="flex items-center gap-1.5">
|
|
37
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
className={`relative size-10 flex items-center justify-center rounded-lg transition-colors ${onLogoClick ? 'cursor-pointer hover:bg-gray-100' : ''}`}
|
|
45
|
+
onClick={onLogoClick}
|
|
46
|
+
>
|
|
38
47
|
<Image src="/icons/great-icon.svg" alt="GreatApps" width={28} height={28} />
|
|
39
48
|
{logoIndicator && (
|
|
40
49
|
<div className="absolute bottom-[-17px] left-1/2 -translate-x-1/2 w-5 h-[3px] bg-gray-950 rounded-t-xl" />
|
|
@@ -44,7 +53,8 @@ export function AppMobileNavBar({
|
|
|
44
53
|
<div
|
|
45
54
|
className={`size-10 flex items-center justify-center rounded-lg transition-colors duration-300 ${
|
|
46
55
|
appIconActive ? 'bg-pages-300' : 'bg-pages-50'
|
|
47
|
-
}`}
|
|
56
|
+
} ${onAppIconClick ? 'cursor-pointer hover:opacity-80' : ''}`}
|
|
57
|
+
onClick={onAppIconClick}
|
|
48
58
|
>
|
|
49
59
|
<Image src="/icons/icon-navbar.svg" alt="GreatPages" width={24} height={24} />
|
|
50
60
|
</div>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
+
import { ChevronLeft } from "lucide-react"
|
|
4
5
|
import { useMdSidebarStore } from "../../store/useMdSidebarStore"
|
|
5
6
|
import { cn } from "../../infra/utils/clsx"
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip"
|
|
6
8
|
|
|
7
9
|
function MdSideBarNavigation({
|
|
8
10
|
className,
|
|
@@ -26,9 +28,22 @@ function MdSideBarNavigation({
|
|
|
26
28
|
>
|
|
27
29
|
<div
|
|
28
30
|
data-slot="md-sidebar-navigation"
|
|
29
|
-
className={cn("w-[273px] h-full flex flex-col p-4 bg-white border-r border-gray-200", className)}
|
|
31
|
+
className={cn("relative w-[273px] h-full flex flex-col p-4 bg-white border-r border-gray-200", className)}
|
|
30
32
|
{...props}
|
|
31
33
|
>
|
|
34
|
+
{isExpanded && (
|
|
35
|
+
<Tooltip>
|
|
36
|
+
<TooltipTrigger asChild>
|
|
37
|
+
<button
|
|
38
|
+
onClick={collapse}
|
|
39
|
+
className="absolute top-[68px] -right-4 size-8 flex items-center justify-center rounded-full border border-gray-200 bg-white hover:bg-gray-100 cursor-pointer transition-colors z-40"
|
|
40
|
+
>
|
|
41
|
+
<ChevronLeft size={16} className="text-gray-500" />
|
|
42
|
+
</button>
|
|
43
|
+
</TooltipTrigger>
|
|
44
|
+
<TooltipContent side="right">Recolher menu</TooltipContent>
|
|
45
|
+
</Tooltip>
|
|
46
|
+
)}
|
|
32
47
|
{children}
|
|
33
48
|
</div>
|
|
34
49
|
</div>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ReactNode } from 'react';
|
|
3
|
+
import { ReactNode, useState } from 'react';
|
|
4
4
|
import { DesktopShell } from './shells/DesktopShell';
|
|
5
5
|
import { MdShell } from './shells/MdShell';
|
|
6
6
|
import { MobileShell } from './shells/MobileShell';
|
|
7
|
+
import { TabsMobileShell } from './shells/TabsMobileShell';
|
|
7
8
|
import { NavItems } from './subcomponents/NavItems';
|
|
8
9
|
import { ProjectSelector } from './subcomponents/ProjectSelector';
|
|
9
10
|
import { PlanCard } from './subcomponents/PlanCard';
|
|
10
11
|
import { CreditsCard } from './subcomponents/CreditsCard';
|
|
12
|
+
import { NavItemsContext } from './NavItemsContext';
|
|
13
|
+
import type { NavItemConfig } from './subcomponents/NavItems';
|
|
11
14
|
|
|
12
15
|
export type { NavItemConfig } from './subcomponents/NavItems';
|
|
13
16
|
export type { ProjectSelectorProps } from './subcomponents/ProjectSelector';
|
|
@@ -16,15 +19,22 @@ export type { CreditsCardProps } from './subcomponents/CreditsCard';
|
|
|
16
19
|
|
|
17
20
|
interface AppNavigationProps {
|
|
18
21
|
children: ReactNode;
|
|
22
|
+
mobileVariant?: 'sheet' | 'tabs';
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
function AppNavigationRoot({ children }: AppNavigationProps) {
|
|
25
|
+
function AppNavigationRoot({ children, mobileVariant = 'sheet' }: AppNavigationProps) {
|
|
26
|
+
const [navItems, setNavItems] = useState<NavItemConfig[]>([]);
|
|
27
|
+
|
|
22
28
|
return (
|
|
23
|
-
|
|
29
|
+
<NavItemsContext.Provider value={{ navItems, setNavItems, mobileVariant }}>
|
|
24
30
|
<DesktopShell>{children}</DesktopShell>
|
|
25
31
|
<MdShell>{children}</MdShell>
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
{mobileVariant === 'sheet' ? (
|
|
33
|
+
<MobileShell>{children}</MobileShell>
|
|
34
|
+
) : (
|
|
35
|
+
<TabsMobileShell>{children}</TabsMobileShell>
|
|
36
|
+
)}
|
|
37
|
+
</NavItemsContext.Provider>
|
|
28
38
|
);
|
|
29
39
|
}
|
|
30
40
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import type { NavItemConfig } from './subcomponents/NavItems';
|
|
5
|
+
|
|
6
|
+
interface NavItemsContextValue {
|
|
7
|
+
navItems: NavItemConfig[];
|
|
8
|
+
setNavItems: (items: NavItemConfig[]) => void;
|
|
9
|
+
mobileVariant: 'sheet' | 'tabs';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const NavItemsContext = createContext<NavItemsContextValue>({
|
|
13
|
+
navItems: [],
|
|
14
|
+
setNavItems: () => {},
|
|
15
|
+
mobileVariant: 'sheet',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function useNavItemsContext(): NavItemsContextValue {
|
|
19
|
+
return useContext(NavItemsContext);
|
|
20
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useRef, useEffect } from 'react';
|
|
4
|
+
import { AppNavigationContext } from '../AppNavigationContext';
|
|
5
|
+
import { useNavItemsContext } from '../NavItemsContext';
|
|
6
|
+
|
|
7
|
+
function InternalTabsBar() {
|
|
8
|
+
const { navItems } = useNavItemsContext();
|
|
9
|
+
const tabsListRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
const activeTabRef = useRef<HTMLButtonElement>(null);
|
|
11
|
+
|
|
12
|
+
const activeLabel = navItems.find((i) => i.isActive)?.label;
|
|
13
|
+
|
|
14
|
+
// Scroll active tab into center when active item changes
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (activeTabRef.current && tabsListRef.current) {
|
|
17
|
+
const tabsList = tabsListRef.current;
|
|
18
|
+
const activeTabElement = activeTabRef.current;
|
|
19
|
+
const tabsListRect = tabsList.getBoundingClientRect();
|
|
20
|
+
const activeTabRect = activeTabElement.getBoundingClientRect();
|
|
21
|
+
const scrollLeft =
|
|
22
|
+
activeTabElement.offsetLeft - tabsListRect.width / 2 + activeTabRect.width / 2;
|
|
23
|
+
tabsList.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' });
|
|
24
|
+
}
|
|
25
|
+
}, [activeLabel]);
|
|
26
|
+
|
|
27
|
+
if (!navItems.length) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
ref={tabsListRef}
|
|
32
|
+
className="md:hidden fixed top-[72px] left-0 right-0 z-40 bg-white border-b border-gray-200 flex overflow-x-auto scrollbar-hide"
|
|
33
|
+
>
|
|
34
|
+
{navItems.map((item) => (
|
|
35
|
+
<button
|
|
36
|
+
key={item.label}
|
|
37
|
+
ref={item.isActive ? activeTabRef : undefined}
|
|
38
|
+
onClick={item.onClick}
|
|
39
|
+
className={`flex items-center gap-2 px-4 py-3 shrink-0 border-b-2 transition-colors ${
|
|
40
|
+
item.isActive
|
|
41
|
+
? 'border-gray-950 text-gray-950'
|
|
42
|
+
: 'border-transparent text-gray-500 hover:text-gray-900'
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
{item.icon && <item.icon size={16} className="shrink-0" />}
|
|
46
|
+
<span
|
|
47
|
+
className={
|
|
48
|
+
item.isActive ? 'paragraph-small-semibold' : 'paragraph-small-medium'
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
{item.label}
|
|
52
|
+
</span>
|
|
53
|
+
</button>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function TabsMobileShell({ children }: { children: ReactNode }) {
|
|
60
|
+
return (
|
|
61
|
+
<AppNavigationContext.Provider value={{ breakpoint: 'mobile' }}>
|
|
62
|
+
{/*
|
|
63
|
+
Children are hidden visually, but NavItems' useEffect still fires inside
|
|
64
|
+
display:none — React effects are tied to commit phase, not layout visibility.
|
|
65
|
+
This registers navItems in NavItemsContext so InternalTabsBar can read them.
|
|
66
|
+
*/}
|
|
67
|
+
<div className="hidden">{children}</div>
|
|
68
|
+
<InternalTabsBar />
|
|
69
|
+
</AppNavigationContext.Provider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ElementType } from 'react';
|
|
3
|
+
import { ElementType, useEffect } from 'react';
|
|
4
4
|
import { NavItem } from '../NavItem';
|
|
5
|
+
import { useNavItemsContext } from '../NavItemsContext';
|
|
5
6
|
|
|
6
7
|
export interface NavItemConfig {
|
|
7
8
|
label: string;
|
|
@@ -15,6 +16,12 @@ interface NavItemsProps {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function NavItems({ items }: NavItemsProps) {
|
|
19
|
+
const { setNavItems } = useNavItemsContext();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setNavItems(items);
|
|
23
|
+
}, [items, setNavItems]);
|
|
24
|
+
|
|
18
25
|
return (
|
|
19
26
|
<div className="flex flex-col">
|
|
20
27
|
{items.map((item) => (
|