Styled marker with icon and content sub-builders and variant support.

Preview
Style
Switched to a new branch
Thinking...
Conversation compacted
Explored 4 files
src/demo/views/marker.ts58 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { Marker } from '../../generated/registry/ui/marker'
import { spinner } from '../../generated/registry/ui/spinner'
import { icon } from '../../generated/registry/lib/icons'
import { GitBranch, Search } from 'lucide'

import { defineSlice } from '../slice'
import type { Message, Model } from '../assemble'

export const markerView = (model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('flex w-full max-w-sm flex-col gap-8 py-12')],
    [
      Marker<Message>(
        {},
        [
          Marker.icon<Message>({}, [icon(h, GitBranch)], h),
          Marker.content<Message>({}, ['Switched to a new branch'], h),
        ],
        h,
      ),
      h.div(
        [h.Attribute('role', 'status')],
        [
          Marker<Message>(
            {},
            [
              Marker.icon<Message>({}, [spinner<Message>({}, h)], h),
              Marker.content<Message>({ className: 'shimmer' }, ['Thinking...'], h),
            ],
            h,
          ),
        ],
      ),
      Marker<Message>(
        { variant: 'separator' },
        [Marker.content<Message>({}, ['Conversation compacted'], h)],
        h,
      ),
      Marker<Message>(
        {},
        [
          Marker.icon<Message>({}, [icon(h, Search)], h),
          Marker.content<Message>({}, ['Explored 4 files'], h),
        ],
        h,
      ),
    ],
  )

export const slice = defineSlice({
  fields: {},
  init: {},
  messages: [],
  handlers: () => ({}),
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/marker

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/marker.ts68 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

type Child = Html | string

import { cn } from '@/lib/utils'

export const markerVariantKeys = ['default', 'separator', 'border'] as const
export type MarkerVariant = (typeof markerVariantKeys)[number]

export const markerVariants: Record<MarkerVariant, string> = {
  default: '',
  separator: 'before:h-px before:min-w-0 before:flex-1 before:bg-border after:h-px after:min-w-0 after:flex-1 after:bg-border before:mr-1 after:ml-1',
  border: 'border-b border-border pb-2',
}

export const markerClass = 'gap-2 text-sm text-muted-foreground [a]:hover:text-foreground [a]:underline-offset-3 [a]:underline [&_svg:not([class*=\'size-\'])]:size-4 min-h-4 text-left group/marker relative flex w-full items-center'

export const markerIconClass = 'size-4 [&_svg:not([class*=\'size-\'])]:size-4 shrink-0'

export const markerContentClass = 'group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3 min-w-0 wrap-break-word'

type StyleConfig = Readonly<{ className?: string; variant?: MarkerVariant }>

const markerContainer = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [
      h.Class(cn(markerClass, markerVariants[config.variant ?? 'default'], config.className)),
      h.DataAttribute('slot', 'marker'),
      h.DataAttribute('variant', config.variant ?? 'default'),
    ],
    children,
  )

const markerIcon = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.span(
    [
      h.Class(cn(markerIconClass, config.className)),
      h.AriaHidden(true),
      h.DataAttribute('slot', 'marker-icon'),
    ],
    children,
  )

const markerContent = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.span(
    [h.Class(cn(markerContentClass, config.className)), h.DataAttribute('slot', 'marker-content')],
    children,
  )

/** Styled marker — `Marker.icon` and `Marker.content` sub-builders. Mirrors the
 *  shadcn v4 `marker.tsx`. */
export const Marker = Object.assign(markerContainer, {
  icon: markerIcon,
  content: markerContent,
})