Skip to contentVibraUI
Dashboards

Guides

The help centre read as a publication, one way for every guide band: who signed each article, the drawing on its cover, and where its page lives.

Server-only, since it reads db: a section imports it from its own data file and hands the rows to its island as props. The guides are db.articles — title, shelf, summary, reading time and reads are the entity's own. What the entity does not record is a rule written here once, so a guide reads the same on every band: the Support team writes the help centre, and its six longest-serving people who are not on leave keep the six shelves, one each in the shelves' order, so every guide on a shelf is signed by its keeper (name, job and face, from db.employees and avatarFor). A cover is a drawing, never a photograph: each shelf has a ProductArt glyph of its own (SHELF_GLYPHS — a notebook for getting started, a filing cabinet for billing, a hub for integrations, a phone for security, a drive for data and a lantern for troubleshooting), tinted from the guide's title. The help centre names a few products Northwind connects to; a guide whose words name one stays in the help centre and off the bands. A guide's page is /support/help/<slug>: the Support dashboard's help centre is one page at /support/help with no page per guide, so add that route (or point guideHref at the one you have) before the cards link anywhere; every card follows guideHref. Dates are said in UTC.

Install

npx shadcn@latest add @vibra/guides-support

Needs the @vibra registry in your components.json — set it up once.

Props

PropTypeDefaultDescription
guides() => Guide[]—Every guide a band may show, the most read first; a tie goes to the title.
Guide{ id, slug, title, shelf, summary, body, minutes, views, updatedAt, updated, author, glyph, href }—An article as a band shows it: the entity's own words, reading time and reads, the day it was revised said in UTC, who signed it, its cover glyph and its page.
GuideAuthor{ name, role, avatarUrl }—Who signed a guide: the keeper of its shelf, their job on the Support team and their face.
shelfKeepers() => Map<ArticleCategory, GuideAuthor>—The teammate who keeps each shelf: the Support team's longest-serving people not on leave, one per shelf in order; HELP_CENTRE_AUTHOR on every shelf when the team has nobody.
HELP_CENTRE_AUTHORGuideAuthor—Who signs a guide when the Support team has nobody to keep the shelves: Northwind Support, the help centre itself.
SHELF_GLYPHSRecord<ArticleCategory, ProductGlyph>—The ProductArt drawing on each shelf's covers.
guideHref(slug: string) => string—A guide's page: /support/help/<slug>, a route your app adds; the kit's help centre is one page.
GUIDES_HREFstring"/support/help"The help centre itself.

Dependencies

Source

lib/dashboards/support/guides.ts
/**
 * The help centre read as a publication, one way for every guide band: who
 * signed each article, what it is drawn as on a cover, and where its page
 * lives. The articles are `db.articles` — their titles, shelves, summaries,
 * reading times and reads are the entity's own — and what the entity does not
 * record is a rule over what it does, written down here once so a guide has
 * the same author and the same cover on every band that shows it:
 *
 * - **The author** is the teammate who keeps the guide's shelf. The Support
 *   team writes the help centre, and its six longest-serving people who are
 *   not on leave keep the six shelves, one each, in the shelves' own order.
 *   A help centre with nobody on the team signs every guide as itself.
 * - **The cover** is a drawing, never a photograph: each shelf has a
 *   `ProductArt` glyph of its own, tinted from the guide's title, so two
 *   guides on one shelf share a picture and not a colour.
 * - **What is left off**: the help centre names a few products Northwind
 *   connects to, and a guide band names no company but Northwind; a guide
 *   whose words name one is not on the bands.
 *
 * Server-only, because it reads `db`: a section imports it from its own
 * `<name>.data.ts` and hands the rows to its island as props, so the store
 * never reaches the browser. Dates are formatted here, in UTC.
 */
import { avatarFor } from "@/lib/avatars"
import { ARTICLE_CATEGORIES, db, type ArticleCategory } from "@/lib/sample-data"
import type { ProductGlyph } from "@/components/ui/product-art"

/** The help centre's own page: the Support dashboard's route. */
export const GUIDES_HREF = "/support/help"

/** A guide's page in the help centre. */
export function guideHref(slug: string): string {
  return `${GUIDES_HREF}/${slug}`
}

/**
 * The drawing on each shelf's covers, picked for what the shelf is about: a
 * notebook for the first steps, the cabinet invoices are filed in, a hub for
 * connecting things, the phone a second factor arrives on, the drive data is
 * kept on, and a lantern for finding a way through.
 */
export const SHELF_GLYPHS: Record<ArticleCategory, ProductGlyph> = {
  "Getting started": "notebook",
  Billing: "filing-cabinet",
  Integrations: "hub",
  Security: "phone",
  "Data & exports": "ssd",
  Troubleshooting: "lantern",
}

/** Who signed a guide: a face, a name and the job they do on the Support team. */
export type GuideAuthor = { name: string; role: string; avatarUrl: string }

/** One guide as every band reads it. */
export type Guide = {
  id: string
  slug: string
  title: string
  shelf: ArticleCategory
  summary: string
  body: string
  /** The entity's own estimate, in whole minutes. */
  minutes: number
  /** Reads over the last 90 days. */
  views: number
  updatedAt: Date
  /** `updatedAt` as a reader says it, in UTC: "Jul 11, 2026". */
  updated: string
  author: GuideAuthor
  glyph: ProductGlyph
  href: string
}

const DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" })

/** Who signs a guide when the Support team has nobody to keep the shelves: the help centre itself. */
export const HELP_CENTRE_AUTHOR: GuideAuthor = { name: "Northwind Support", role: "Help centre", avatarUrl: avatarFor("Northwind Support") }

/**
 * The teammate who keeps each shelf: the Support team's six longest-serving
 * people not on leave, one to a shelf in the shelves' order. A team shorter
 * than the shelves starts again from the top; with nobody on it, every shelf
 * is signed by `HELP_CENTRE_AUTHOR`.
 */
export function shelfKeepers(): Map<ArticleCategory, GuideAuthor> {
  const team = db.employees
    .all()
    .filter((employee) => employee.department === "Support" && employee.status !== "on_leave")
    .sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime() || a.name.localeCompare(b.name))
  if (team.length === 0) return new Map(ARTICLE_CATEGORIES.map((shelf) => [shelf, HELP_CENTRE_AUTHOR]))
  return new Map(
    ARTICLE_CATEGORIES.map((shelf, index) => {
      const keeper = team[index % team.length]
      return [shelf, { name: keeper.name, role: keeper.role, avatarUrl: avatarFor(keeper.name) }]
    })
  )
}

/** Every guide a band may show, the most read first; a tie goes to the title. */
export function guides(): Guide[] {
  const keepers = shelfKeepers()
  return db.articles
    .all()
    .sort((a, b) => b.views - a.views || a.title.localeCompare(b.title))
    .map((article) => ({
      id: article.id,
      slug: article.slug,
      title: article.title,
      shelf: article.category,
      summary: article.summary,
      body: article.body,
      minutes: article.minutesToRead,
      views: article.views,
      updatedAt: article.updatedAt,
      updated: DAY.format(article.updatedAt),
      author: keepers.get(article.category)!,
      glyph: SHELF_GLYPHS[article.category],
      href: guideHref(article.slug),
    }))
}