Viana Kitv0.1.4

Blocks

Page Title

Standard page header block with auto-generated breadcrumbs, an h1 title, optional subtitle, and a right-side action slot. Shown by default on most pages via AppDashboard.

page.tsx
KA

Site

Stay up to date to everything in your network

Page content


Import

tsx
// Recommended — via AppDashboard pageTitle prop
import { AppDashboard } from "@/components/blocks/AppDashboard"

// Standalone use
import { AppPageTitle } from "@/components/blocks/AppPageTitle"

Examples

With actions

Pass any elements to actions — buttons, selects, or text. The slot is right-aligned and empty by default.

page.tsx
KA

Site

Stay up to date to everything in your network

Page content

Hidden

Pass pageTitle={false} to suppress the block entirely for pages that don't need a title header.

page.tsx
KA

Page content (no title block)

Standalone

AppPageTitle can be used directly in custom layouts outside of AppDashboard.

example.tsx

Site

Stay up to date to everything in your network

With icon

Pass any ReactNode to the icon prop — an <img>, SVG, or Lucide icon. The icon renders to the left of the title stack and is optional.

page.tsx
KA
Dashboard

Hi Kevin! Welcome to Viana

Stay up to date to everything in your network

Page content

Primary title color

Set titleColor="primary" to render the h1 in the brand blue color. The default is "default" which uses text-foreground.

example.tsx

Hi Kevin! Welcome to Viana

Stay up to date to everything in your network

API Reference

PropTypeDefaultDescription
titlestringautoPage heading rendered as an <h1>. Auto-derived from the last pathname segment when omitted.
subtitlestringOptional description rendered below the heading.
breadcrumbsAppPageTitleBreadcrumb[]autoExplicit breadcrumb trail. Auto-generated from window.location.pathname when omitted.
actionsReactNodeRight-side slot. Accepts buttons, selects, or any elements. Empty by default.
iconReactNodeOptional icon rendered to the left of the title stack. Accepts <img>, SVG, or Lucide icons.
titleColor"default" | "primary""default"Controls the h1 color. "default" uses text-foreground; "primary" uses the brand blue.
hiddenbooleanfalseHides the entire block.
classNamestringExtra classes on the root element.

When used via AppDashboard, pass pageTitle={AppPageTitleProps} or pageTitle={false} to suppress.

Breadcrumb auto-generation

When breadcrumbs is omitted, the component reads window.location.pathname on mount and converts each path segment into a readable label. Segments are split on - and _, then title-cased (site-settingsSite Settings). The last segment has no link (current page). Pass an explicit breadcrumbs array to override when the URL structure doesn't match the desired labels.

Source

src/components/blocks/AppPageTitle.tsx
"use client"

import * as React from "react"
import { cn } from "../../lib/utils"
import {
  AppBreadcrumb,
  AppBreadcrumbItem,
  AppBreadcrumbLink,
  AppBreadcrumbList,
  AppBreadcrumbPage,
  AppBreadcrumbSeparator,
} from "../primitives/AppBreadcrumb"

export type AppPageTitleBreadcrumb = {
  label: string
  href?: string
}

export type AppPageTitleProps = {
  title?: string
  subtitle?: string
  breadcrumbs?: AppPageTitleBreadcrumb[]
  actions?: React.ReactNode
  icon?: React.ReactNode
  titleColor?: "default" | "primary"
  hidden?: boolean
  className?: string
}

function AppPageTitle({ title, subtitle, breadcrumbs, actions, hidden = false, className }: AppPageTitleProps) {
  const [autoCrumbs, setAutoCrumbs] = React.useState<AppPageTitleBreadcrumb[]>([])

  React.useEffect(() => {
    if (breadcrumbs === undefined) {
      const segments = window.location.pathname.split("/").filter(Boolean)
      setAutoCrumbs(segments.map((seg, i) => ({
        label: seg.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
        href: i < segments.length - 1 ? "/" + segments.slice(0, i + 1).join("/") : undefined,
      })))
    }
  }, [breadcrumbs])

  if (hidden) return null

  const crumbs = breadcrumbs ?? autoCrumbs

  return (
    <div className={cn("mb-6 flex items-start justify-between gap-4", className)}>
      <div className="space-y-1 min-w-0">
        {crumbs.length > 0 && (
          <AppBreadcrumb>
            <AppBreadcrumbList>
              {crumbs.map((crumb, i) => (
                <React.Fragment key={i}>
                  <AppBreadcrumbItem>
                    {crumb.href
                      ? <AppBreadcrumbLink href={crumb.href}>{crumb.label}</AppBreadcrumbLink>
                      : <AppBreadcrumbPage>{crumb.label}</AppBreadcrumbPage>}
                  </AppBreadcrumbItem>
                  {i < crumbs.length - 1 && <AppBreadcrumbSeparator />}
                </React.Fragment>
              ))}
            </AppBreadcrumbList>
          </AppBreadcrumb>
        )}
        <h1 className="text-2xl font-bold tracking-tight text-foreground">{title}</h1>
        {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
      </div>
      {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
    </div>
  )
}
AppPageTitle.displayName = "AppPageTitle"

export { AppPageTitle }