HextaUI

Marker

Des notes discrètes entre les contenus, comme les séparateurs de date et les événements système, avec des dates et heures fixes qui se lisent « Aujourd’hui » ou « Hier ».

Mira added Jun and Sol
Kicking off the marker component. Dates, events, the works.
Can the dates stick while you scroll?
Jun renamed the thread to “Marker”
They do now. Scroll up and the date turns into a pill.
Sol pinned a message
Docs are written.
Screenshots look right in dark mode too.
Sol merged #482 into main
New messages
Shipping it 🎉
"use client"

import * as React from "react"
import {
  IconGitMerge,
  IconPencil,
  IconPin,
  IconUserPlus,
} from "@tabler/icons-react"

import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"
import {
  Marker,
  MarkerContent,
  MarkerIcon,
  MarkerTime,
} from "@/components/ui/marker"

const day = 24 * 60 * 60 * 1000

export function MarkerDemo() {
  const threadRef = React.useRef<HTMLDivElement>(null)

  React.useLayoutEffect(() => {
    const thread = threadRef.current
    if (thread) {
      thread.scrollTop = thread.scrollHeight
    }
  }, [])

  return (
    <div
      ref={threadRef}
      tabIndex={0}
      role="region"
      aria-label="Conversation"
      className="h-96 w-full max-w-md overflow-y-auto overscroll-none rounded-xl border px-4 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
    >
      <section className="flex flex-col gap-3 pt-4 pb-3">
        <Marker variant="separator" sticky>
          <MarkerContent>
            <MarkerTime date={new Date(Date.now() - 2 * day)} />
          </MarkerContent>
        </Marker>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconUserPlus />
          </MarkerIcon>
          <MarkerContent>Mira added Jun and Sol</MarkerContent>
        </Marker>
        <Bubble variant="secondary">
          <BubbleContent>
            Kicking off the marker component. Dates, events, the works.
          </BubbleContent>
        </Bubble>
        <Bubble align="end">
          <BubbleContent>Can the dates stick while you scroll?</BubbleContent>
        </Bubble>
      </section>
      <section className="flex flex-col gap-3 py-3">
        <Marker variant="separator" sticky>
          <MarkerContent>
            <MarkerTime date={new Date(Date.now() - day)} />
          </MarkerContent>
        </Marker>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconPencil />
          </MarkerIcon>
          <MarkerContent>Jun renamed the thread to “Marker”</MarkerContent>
        </Marker>
        <Bubble variant="secondary">
          <BubbleContent>
            They do now. Scroll up and the date turns into a pill.
          </BubbleContent>
        </Bubble>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconPin />
          </MarkerIcon>
          <MarkerContent>Sol pinned a message</MarkerContent>
        </Marker>
      </section>
      <section className="flex flex-col gap-3 pt-3 pb-4">
        <Marker variant="separator" sticky>
          <MarkerContent>
            <MarkerTime date={new Date()} />
          </MarkerContent>
        </Marker>
        <BubbleGroup>
          <Bubble align="end">
            <BubbleContent>Docs are written.</BubbleContent>
          </Bubble>
          <Bubble align="end">
            <BubbleContent>
              Screenshots look right in dark mode too.
            </BubbleContent>
          </Bubble>
        </BubbleGroup>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconGitMerge className="text-success" />
          </MarkerIcon>
          <MarkerContent>
            Sol merged <a href="#">#482</a> into main
          </MarkerContent>
        </Marker>
        <Marker variant="separator">
          <MarkerContent>New messages</MarkerContent>
        </Marker>
        <Bubble variant="secondary">
          <BubbleContent>Shipping it 🎉</BubbleContent>
        </Bubble>
      </section>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/marker.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

import {
  Marker,
  MarkerContent,
  MarkerIcon,
  MarkerTime,
} from "@/components/ui/marker"
<Marker variant="separator" sticky>
  <MarkerContent>
    <MarkerTime date={message.sentAt} />
  </MarkerContent>
</Marker>
Marker
├── MarkerIcon
└── MarkerContent
    └── MarkerTime

Variantes

default est une note discrète, separator la centre entre deux filets et border la souligne comme un titre de section.

import { IconInfoCircle } from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerVariants() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Marker>
        <MarkerIcon>
          <IconInfoCircle />
        </MarkerIcon>
        <MarkerContent>Default: an icon and a short note.</MarkerContent>
      </Marker>
      <Marker variant="separator">
        <MarkerContent>Separator</MarkerContent>
      </Marker>
      <Marker variant="border">
        <MarkerContent>Border: a heading for the section below</MarkerContent>
      </Marker>
    </div>
  )
}

Dates collantes

Avec sticky, un marqueur reste en haut de sa zone de défilement. Une fois collé, ses lignes s'estompent et le label devient une pastille flottante pour rester lisible au-dessus des messages. Placez chaque jour dans sa propre section pour que la date suivante repousse la précédente.

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Marker, MarkerContent, MarkerTime } from "@/components/ui/marker"

const day = 24 * 60 * 60 * 1000

const days = [
  {
    date: new Date(Date.now() - 3 * day),
    messages: [
      "Kicking off the marker component today.",
      "Shadcn's version is a good base. Let's add sticky dates.",
      "Agreed, chats need them.",
    ],
  },
  {
    date: new Date(Date.now() - day),
    messages: [
      "Sticky dates work. They turn into a pill when they stick.",
      "Nice. Does it respect reduced motion?",
      "Yes, the pill just appears.",
    ],
  },
  {
    date: new Date(),
    messages: ["Docs are up.", "Shipping it.", "🎉"],
  },
]

export function MarkerSticky() {
  return (
    <div
      tabIndex={0}
      role="region"
      aria-label="Activity by day"
      className="h-80 w-full max-w-md overflow-y-auto overscroll-none rounded-xl border px-4 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
    >
      {days.map((group) => (
        <section
          key={group.date.toDateString()}
          className="flex flex-col gap-3 py-3"
        >
          <Marker variant="separator" sticky>
            <MarkerContent>
              <MarkerTime date={group.date} />
            </MarkerContent>
          </Marker>
          {group.messages.map((message, index) => (
            <Bubble
              key={message}
              align={index % 2 === 0 ? "start" : "end"}
              variant={index % 2 === 0 ? "secondary" : "default"}
            >
              <BubbleContent>{message}</BubbleContent>
            </Bubble>
          ))}
        </section>
      ))}
    </div>
  )
}

Dates relatives

<MarkerTime /> rend un <time> qui affiche Aujourd'hui, Hier, un jour de la semaine s'il date de moins d'une semaine, puis une date courte. Il se met à jour à minuit. Passez format pour votre propre formulation.

"use client"

import { Marker, MarkerContent, MarkerTime } from "@/components/ui/marker"

const day = 24 * 60 * 60 * 1000
const dates = [0, 1, 3, 40, 400].map((ago) => new Date(Date.now() - ago * day))

export function MarkerTimeDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      {dates.map((date) => (
        <Marker key={date.getTime()} variant="separator">
          <MarkerContent>
            <MarkerTime date={date} />
          </MarkerContent>
        </Marker>
      ))}
      <Marker variant="separator">
        <MarkerContent>
          <MarkerTime
            date={dates[0]}
            format={(date) =>
              date.toLocaleTimeString("en-US", {
                hour: "numeric",
                minute: "2-digit",
              })
            }
          />
        </MarkerContent>
      </Marker>
    </div>
  )
}

Activité

Rendez les marqueurs en éléments de liste pour un journal d'activité. Les liens à l'intérieur sont soulignés tant qu'ils ne sont pas survolés.

import {
  IconGitMerge,
  IconLock,
  IconPencil,
  IconTag,
} from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerEvents() {
  return (
    <ol className="flex w-full max-w-md flex-col gap-3">
      <Marker render={<li />}>
        <MarkerIcon>
          <IconTag />
        </MarkerIcon>
        <MarkerContent>
          Jun added the <a href="#">design</a> label
        </MarkerContent>
      </Marker>
      <Marker render={<li />}>
        <MarkerIcon>
          <IconPencil />
        </MarkerIcon>
        <MarkerContent>Mira renamed the pull request</MarkerContent>
      </Marker>
      <Marker render={<li />}>
        <MarkerIcon>
          <IconGitMerge className="text-success" />
        </MarkerIcon>
        <MarkerContent>Sol merged into main</MarkerContent>
      </Marker>
      <Marker render={<li />}>
        <MarkerIcon>
          <IconLock />
        </MarkerIcon>
        <MarkerContent>Conversation locked</MarkerContent>
      </Marker>
    </ol>
  )
}

Contenu long

Les notes passent à la ligne et les chaînes sans espace sont coupées au lieu d'élargir la mise en page. Les lignes de séparation gardent une longueur minimale de chaque côté.

import { IconAlertTriangle } from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerLongContent() {
  return (
    <div className="flex w-72 max-w-full flex-col gap-6">
      <Marker>
        <MarkerIcon>
          <IconAlertTriangle className="text-warning" />
        </MarkerIcon>
        <MarkerContent>
          Messages older than 90 days were archived to
          [email protected]
        </MarkerContent>
      </Marker>
      <Marker variant="separator">
        <MarkerContent>
          A separator label that is long enough to wrap onto two lines
        </MarkerContent>
      </Marker>
    </div>
  )
}

De droite à gauche

Les icônes et le texte suivent le sens de lecture.

import { IconUserPlus } from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-md flex-col gap-4">
      <Marker variant="separator">
        <MarkerContent>اليوم</MarkerContent>
      </Marker>
      <Marker>
        <MarkerIcon>
          <IconUserPlus />
        </MarkerIcon>
        <MarkerContent>أضافت ميرا جون إلى المحادثة</MarkerContent>
      </Marker>
    </div>
  )
}
  • Les marqueurs sont du texte simple, les lecteurs d'écran les lisent donc sur place. Les icônes leur sont masquées.
  • <MarkerTime /> conserve l'instant exact dans dateTime tout en affichant un libellé lisible.
  • La pastille collante apparaît sans mouvement quand la réduction des animations est activée.
PropTypePar défaut
variant
"default" | "separator" | "border""default"
stickySe colle en haut de la zone de défilement et devient une pastille une fois collé.
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="marker"Cible les marqueurs en CSS.
data-variantLa variante actuelle.
data-stickyPrésent quand sticky est activé.
data-stuckPrésent tant que le marqueur est collé en haut.
--marker-sticky-topDistance depuis le haut une fois collé. 0.5rem par défaut.
AttributDescription
data-slot="marker-content"Le texte. Devient la pastille une fois collé.
AttributDescription
data-slot="marker-icon"Une boîte d'icône de 16px, masquée aux lecteurs d'écran.
PropTypePar défaut
dateObligatoire. Tout ce que new Date() accepte.
Date | string | number–
localeFixe par défaut, pour que le serveur et le navigateur s'accordent.
Intl.LocalesArgument"en-US"
formatRemplace le libellé relatif par le vôtre.
(date: Date) => ReactNode–
AttributDescription
data-slot="marker-time"L'élément time, avec un dateTime ISO.