CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
app-header.tsx265 linesDownload Raw Back to ui
1'use client';2 3import React, { useState } from 'react';4import { Button } from '@/components/ui/button';5import { Badge } from '@/components/ui/badge';6import { Logo } from '@/components/ui/logo';7import { ChevronDown, ChevronUp, Menu } from 'lucide-react';8 9export interface HeaderAction {10  id: string;11  label: string;12  icon?: React.ComponentType<{ className?: string }>;13  onClick: () => void;14  variant?: 'default' | 'outline' | 'ghost' | 'secondary';15  size?: 'default' | 'sm' | 'lg' | 'icon';16  disabled?: boolean;17  content?: React.ReactNode;18  dataTourId?: string;19}20 21export interface ViewTab {22  id: string;23  label: string;24  icon?: React.ComponentType<{ className?: string }>;25}26 27interface AppHeaderProps {28  title?: string;29  subtitle?: string;30  badge?: string;31  onLogoClick?: () => void;32  actions?: HeaderAction[];33  mobileMenuContent?: React.ReactNode;34  desktopOnlyContent?: React.ReactNode; // Content shown only on desktop35  className?: string;36  leftText?: React.ReactNode; // Text to show next to logo on desktop, centered on mobile37  leftSubtext?: string; // Secondary line below leftText on mobile (e.g. active panel name)38  mobileVisibleActions?: string[]; // Action IDs to show outside dropdown on mobile39  viewTabs?: ViewTab[]; // View tabs for switching between sections40  activeViewTab?: string; // Currently active view tab41  onViewTabChange?: (tabId: string) => void; // Callback when view tab changes42  hideLogo?: boolean; // Hide the logo button (for use with sidebar)43  showMobileMenu?: boolean; // Show hamburger menu button on mobile (for sidebar)44  onMobileMenuClick?: () => void; // Callback when hamburger is clicked45  hideActionsOnMobile?: boolean; // Hide all actions on mobile (actions in sidebar instead)46  pageName?: string; // Page name to show in center on mobile (when showMobileMenu is true)47}48 49export function AppHeader({50  title,51  subtitle,52  badge,53  onLogoClick,54  actions = [],55  mobileMenuContent,56  desktopOnlyContent,57  className = '',58  leftText,59  leftSubtext,60  mobileVisibleActions = [],61  viewTabs,62  activeViewTab,63  onViewTabChange,64  hideLogo = false,65  showMobileMenu = false,66  onMobileMenuClick,67  hideActionsOnMobile = false,68  pageName69}: AppHeaderProps) {70  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);71 72  // Split actions into mobile-visible and dropdown-only73  const mobileVisibleActionsSet = new Set(mobileVisibleActions);74  const visibleOnMobile = hideActionsOnMobile ? [] : actions.filter(a => mobileVisibleActionsSet.has(a.id));75  const dropdownOnlyActions = hideActionsOnMobile ? [] : actions.filter(a => !mobileVisibleActionsSet.has(a.id));76 77  return (78    <div className={`border-b bg-card shadow-sm relative z-20 ${className}`}>79      <div className="px-3 py-2 flex items-center justify-between">80        {/* Left side - Logo + pageName (mobile when showMobileMenu is true) */}81        {showMobileMenu && (82          <div className="md:hidden flex items-center gap-3">83            <Logo width={24} height={24} />84            {pageName && <span className="text-sm font-semibold">{pageName}</span>}85          </div>86        )}87 88        {/* Left side - Logo + text (desktop), empty (mobile with sidebar) */}89        {!hideLogo && !showMobileMenu && (90          <button91            onClick={onLogoClick}92            className="flex items-center gap-2 p-1 pr-2 hover:ring-1 hover:ring-border rounded-sm transition-all"93          >94            <Logo width={24} height={24} />95            {leftText && (96              <div className="flex flex-col items-start min-w-0">97                <span className="font-semibold text-sm md:text-lg truncate max-w-[160px] md:max-w-none">{leftText}</span>98                {leftSubtext && <span className="text-xs text-muted-foreground md:hidden">{leftSubtext}</span>}99              </div>100            )}101          </button>102        )}103 104        {/* Center - View tabs or leftText/title (desktop only now) */}105        <div className="flex items-center gap-2 flex-1 justify-center md:justify-start md:ml-6">106          {viewTabs && viewTabs.length > 0 ? (107            /* Show view tabs as pill toggle */108            <div className="flex border rounded-full">109              {viewTabs.map((tab, index) => (110                <Button111                  key={tab.id}112                  variant={activeViewTab === tab.id ? 'secondary' : 'ghost'}113                  size="sm"114                  onClick={() => onViewTabChange?.(tab.id)}115                  className={`gap-2 ${116                    index === 0 ? 'rounded-r-none rounded-l-full' :117                    index === viewTabs.length - 1 ? 'rounded-l-none rounded-r-full' :118                    'rounded-none'119                  }`}120                >121                  {tab.icon && <tab.icon className="h-4 w-4" />}122                  {tab.label}123                </Button>124              ))}125            </div>126          ) : title ? (127            <>128              {title && <h1 className="text-lg md:text-xl font-semibold">{title}</h1>}129              {badge && <Badge variant="secondary">{badge}</Badge>}130            </>131          ) : null}132        </div>133 134        {/* Desktop - Show subtitle only if no leftText and no center title */}135        {!leftText && !title && subtitle && (136          <div className="hidden md:flex items-center flex-1 ml-6">137            <span className="text-sm text-muted-foreground">{subtitle}</span>138          </div>139        )}140 141        {/* Right side controls */}142        <div className="flex items-center gap-2">143          {/* Desktop actions */}144          <div className="hidden md:flex items-center gap-2">145            {actions.map((action) => (146              action.content ? (147                <div key={action.id}>{action.content}</div>148              ) : (149                <Button150                  key={action.id}151                  variant={action.variant || 'outline'}152                  size={action.size || 'sm'}153                  onClick={action.onClick}154                  disabled={action.disabled}155                  className="justify-start"156                  data-tour-id={action.dataTourId}157                >158                  {action.icon && <action.icon className="h-4 w-4 mr-2" />}159                  {action.label}160                </Button>161              )162            ))}163            {desktopOnlyContent}164          </div>165 166          {/* Mobile visible actions */}167          <div className="md:hidden flex items-center gap-2">168            {visibleOnMobile.map((action) => (169              action.content ? (170                <div key={action.id}>{action.content}</div>171              ) : (172                <Button173                  key={action.id}174                  variant={action.variant || 'outline'}175                  size={action.size || 'sm'}176                  onClick={action.onClick}177                  disabled={action.disabled}178                  className="h-8 px-3"179                  data-tour-id={action.dataTourId}180                >181                  {action.icon && <action.icon className="h-4 w-4 mr-2" />}182                  {action.label}183                </Button>184              )185            ))}186          </div>187 188          {/* Mobile menu toggle */}189          {(dropdownOnlyActions.length > 0 || mobileMenuContent) && (190            <Button191              variant="ghost"192              size="icon"193              onClick={() => setMobileMenuOpen(!mobileMenuOpen)}194              className="h-8 w-8 md:hidden"195            >196              {mobileMenuOpen ? (197                <ChevronUp className="h-4 w-4" />198              ) : (199                <ChevronDown className="h-4 w-4" />200              )}201            </Button>202          )}203 204          {/* Mobile hamburger menu (on right side) */}205          {showMobileMenu && (206            <Button207              variant="ghost"208              size="icon"209              onClick={onMobileMenuClick}210              className="md:hidden h-8 w-8"211            >212              <Menu className="h-5 w-5" />213            </Button>214          )}215        </div>216      </div>217      218      {/* Mobile dropdown menu */}219      {mobileMenuOpen && (dropdownOnlyActions.length > 0 || mobileMenuContent) && (220        <div className="md:hidden border-t bg-muted/30 px-4 py-4 space-y-3">221          {/* Show subtitle on mobile if no leftText and no center title */}222          {!leftText && !title && subtitle && (223            <div className="pb-2 border-b border-border/50">224              <p className="text-sm text-muted-foreground">{subtitle}</p>225            </div>226          )}227 228          {/* Mobile dropdown-only actions */}229          {dropdownOnlyActions.length > 0 && (230            <div className="space-y-2">231              {dropdownOnlyActions.map((action) => (232                action.content ? (233                  <div key={action.id}>{action.content}</div>234                ) : (235                  <Button236                    key={action.id}237                    variant={action.variant || 'outline'}238                    size={action.size || 'sm'}239                    onClick={() => {240                      action.onClick();241                      setMobileMenuOpen(false);242                    }}243                    disabled={action.disabled}244                    className="w-full justify-start"245                    data-tour-id={action.dataTourId}246                  >247                    {action.icon && <action.icon className="h-4 w-4 mr-2" />}248                    {action.label}249                  </Button>250                )251              ))}252            </div>253          )}254 255          {/* Custom mobile menu content */}256          {mobileMenuContent && (257            <div className="pt-2 border-t border-border/50">258              {mobileMenuContent}259            </div>260          )}261        </div>262      )}263    </div>264  );265}