Middle Truncate

Usage

Drag the handle on the right to resize the container, the meeting metadata adapts in real time, keeping the meaningful prefix and suffix visible.

  • Meeting
    Q4 GTM strategy review with sales leadership and channel partners
  • Attendee
    samantha.rodriguez@northwind-enterprise-solutions.com
  • Calendar
    [NORTHWIND] Bi-weekly product sync — Engineering + Design
  • Tag
    product/launch/jamie-mobile-public-release-prep
  • Transcript
    workspaces/northwind/meetings/2026-04-21-roadmap-sync.json
  • Fits as-is
    kickoff.md
'use client'
 
import { cn } from '@jamie/ui'
import { MiddleTruncate } from '@jamie/ui/middle-truncate'
import { useCallback, useRef } from 'react'
 
const MIN_WIDTH = 200
 
const EXAMPLES = [
  {
    label: 'Meeting',
    value: 'Q4 GTM strategy review with sales leadership and channel partners'
  },
  {
    label: 'Attendee',
    value: 'samantha.rodriguez@northwind-enterprise-solutions.com'
  },
  {
    label: 'Calendar',
    value: '[NORTHWIND] Bi-weekly product sync — Engineering + Design'
  },
  {
    label: 'Tag',
    value: 'product/launch/jamie-mobile-public-release-prep'
  },
  {
    label: 'Transcript',
    value: 'workspaces/northwind/meetings/2026-04-21-roadmap-sync.json'
  },
  {
    label: 'Fits as-is',
    value: 'kickoff.md'
  }
]
 
export function MiddleTruncateDemo() {
  const containerRef = useRef<HTMLDivElement>(null)
  const handleRef = useRef<HTMLDivElement>(null)
 
  const handlePointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
    event.preventDefault()
    const container = containerRef.current
    const handle = handleRef.current
    if (!container) return
 
    const startX = event.clientX
    const startWidth = container.getBoundingClientRect().width
    const parentWidth =
      container.parentElement?.getBoundingClientRect().width ?? Number.POSITIVE_INFINITY
 
    let latestWidth = startWidth
    let rafId: number | null = null
 
    const previousCursor = document.body.style.cursor
    const previousUserSelect = document.body.style.userSelect
    document.body.style.cursor = 'ew-resize'
    document.body.style.userSelect = 'none'
    handle?.setAttribute('data-resizing', 'true')
 
    const flush = () => {
      rafId = null
      container.style.width = `${latestWidth}px`
    }
 
    const onMove = (moveEvent: PointerEvent) => {
      const delta = moveEvent.clientX - startX
      latestWidth = Math.min(parentWidth, Math.max(MIN_WIDTH, startWidth + delta))
      if (rafId === null) rafId = requestAnimationFrame(flush)
    }
 
    const onUp = () => {
      if (rafId !== null) {
        cancelAnimationFrame(rafId)
        rafId = null
      }
      container.style.width = `${latestWidth}px`
      document.body.style.cursor = previousCursor
      document.body.style.userSelect = previousUserSelect
      handle?.removeAttribute('data-resizing')
      window.removeEventListener('pointermove', onMove)
      window.removeEventListener('pointerup', onUp)
    }
 
    window.addEventListener('pointermove', onMove)
    window.addEventListener('pointerup', onUp)
  }, [])
 
  return (
    <div
      ref={containerRef}
      className="relative w-full max-w-full rounded-lg bg-secondary/50 px-4 py-3"
    >
      <ul className="flex flex-col divide-y divide-border/60">
        {EXAMPLES.map((example) => (
          <li key={example.label} className="flex items-center gap-4 py-2.5 first:pt-1 last:pb-1">
            <span className="shrink-0 basis-24 text-xs text-muted-foreground">{example.label}</span>
            <div className="min-w-0 flex-1">
              <MiddleTruncate text={example.value} className="text-sm" />
            </div>
          </li>
        ))}
      </ul>
      <div
        ref={handleRef}
        onPointerDown={handlePointerDown}
        className={cn(
          'group absolute left-full top-1/2 ml-2 -translate-y-1/2',
          'flex h-10 w-3 cursor-ew-resize touch-none select-none items-center justify-center'
        )}
      >
        <div
          className={cn(
            'h-8 w-1.5 rounded-full bg-border transition-colors',
            'group-hover:bg-foreground/40',
            'group-data-[resizing=true]:bg-foreground/60'
          )}
        />
      </div>
    </div>
  )
}

Mount the boot script

// app/layout.tsx
import { MiddleTruncateBoot } from "@jamie/ui/middle-truncate-boot";
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <MiddleTruncateBoot />
        {children}
      </body>
    </html>
  );
}

Give it a stable container width

The boot script reads clientWidth mid-parse. The container's width must come from an ancestor, not from a sibling that is parsed after.

// ✅ Fixed or already-parsed ancestor
<div className="w-92"><MiddleTruncate text={email} /></div>
 
// ❌ Width depends on a sibling parsed later — flashes
<div className="flex justify-between">
  <div className="flex-1 max-w-sm"><MiddleTruncate text={task} /></div>
  <div className="shrink-0">{right}</div>
</div>

Drop it in

import { MiddleTruncate } from '@jamie/ui/middle-truncate'
 
<MiddleTruncate text={longString} className="text-sm" />

Tasks List

Long meeting titles stay scannable from both ends without pushing the date column off-screen.

  • Ask Jozo to implement the backend for upcoming meetings
    Project planning for december — Q1 backend roadmap with platform team
    Nov 28
  • Talk to Alex about the approach for meeting object creation
    Project planning for december — Q1 backend roadmap with platform team
    Nov 28
  • Dock behaviour while live meeting
    [NORTHWIND] Daily Sync — Engineering, Design & Product — Nov 25
    Nov 25
  • Fix search in meeting window
    [NORTHWIND] Daily Sync — Engineering, Design & Product — Nov 25
    Nov 25
  • Design the tasks section for the new meeting view layout
    Customer feedback call — onboarding research with enterprise design partners
    Oct 14
import { MiddleTruncate } from '@jamie/ui/middle-truncate'
import { CircleIcon } from 'lucide-react'
 
const TASKS = [
  {
    text: 'Ask Jozo to implement the backend for upcoming meetings',
    meeting: 'Project planning for december — Q1 backend roadmap with platform team',
    date: 'Nov 28'
  },
  {
    text: 'Talk to Alex about the approach for meeting object creation',
    meeting: 'Project planning for december — Q1 backend roadmap with platform team',
    date: 'Nov 28'
  },
  {
    text: 'Dock behaviour while live meeting',
    meeting: '[NORTHWIND] Daily Sync — Engineering, Design & Product — Nov 25',
    date: 'Nov 25'
  },
  {
    text: 'Fix search in meeting window',
    meeting: '[NORTHWIND] Daily Sync — Engineering, Design & Product — Nov 25',
    date: 'Nov 25'
  },
  {
    text: 'Design the tasks section for the new meeting view layout',
    meeting: 'Customer feedback call — onboarding research with enterprise design partners',
    date: 'Oct 14'
  }
]
 
export function MiddleTruncateTasksDemo() {
  return (
    <div className="w-full max-w-3xl rounded-md border bg-card">
      <ul className="divide-y divide-border">
        {TASKS.map((task) => (
          <li key={task.text} className="flex items-center justify-between gap-8 px-3 py-2.5">
            <div className="flex max-w-64 shrink-0 items-center gap-3">
              <CircleIcon className="size-4 shrink-0 text-muted-foreground" />
              <MiddleTruncate text={task.text} className="min-w-0 flex-1 text-sm" />
            </div>
            <div className="flex shrink-0 items-center gap-3">
              <div className="w-32 md:w-44">
                <MiddleTruncate text={task.meeting} className="text-xs text-muted-foreground" />
              </div>
              <span className="text-xs text-muted-foreground tabular-nums">{task.date}</span>
            </div>
          </li>
        ))}
      </ul>
    </div>
  )
}

Speakers Popover

The email gets middle-truncated so the username and the company domain both stay visible inside the narrow popover.

  • S
    Samantha Rodriguezsamantha.rodriguez@northwind-enterprise-solutions.com

    ...so for the Q4 launch we want to make sure both engineering and design are aligned on the new tasks flow before we ship to the public

  • D
    Dimitri Theodorakis-Walkerdimitri.theodorakis@platform.northwind-enterprise.com

    ...the most important thing is the migration path for existing teams who have been using the old artifacts view since launch

  • A
    Aiko Yamamotoaiko.yamamoto@meetjamie.ai

    ...let's also confirm the rollout plan behind the feature flag and make sure we have proper telemetry on the meeting source column

import { Avatar, AvatarFallback } from '@jamie/ui/avatar'
import { MiddleTruncate } from '@jamie/ui/middle-truncate'
 
const SPEAKERS = [
  {
    initials: 'S',
    userId: 'speaker-samantha',
    name: 'Samantha Rodriguez',
    email: 'samantha.rodriguez@northwind-enterprise-solutions.com',
    clipText:
      'so for the Q4 launch we want to make sure both engineering and design are aligned on the new tasks flow before we ship to the public'
  },
  {
    initials: 'D',
    userId: 'speaker-dimitri',
    name: 'Dimitri Theodorakis-Walker',
    email: 'dimitri.theodorakis@platform.northwind-enterprise.com',
    clipText:
      'the most important thing is the migration path for existing teams who have been using the old artifacts view since launch'
  },
  {
    initials: 'A',
    userId: 'speaker-aiko',
    name: 'Aiko Yamamoto',
    email: 'aiko.yamamoto@meetjamie.ai',
    clipText:
      "let's also confirm the rollout plan behind the feature flag and make sure we have proper telemetry on the meeting source column"
  }
]
 
export function MiddleTruncateSpeakersDemo() {
  return (
    <div className="w-92 max-w-full rounded-md border bg-popover p-2 shadow-md">
      <ul className="flex flex-col">
        {SPEAKERS.map((speaker) => (
          <li
            key={speaker.email}
            className="flex flex-col rounded-md hover:bg-surface-container p-1.5 [&+&]:mt-1 cursor-default"
          >
            <div className="flex h-6.5 items-center gap-2.5">
              <Avatar className="size-6 text-xs">
                <AvatarFallback userId={speaker.userId}>{speaker.initials}</AvatarFallback>
              </Avatar>
              <div className="flex min-w-0 flex-1 items-center gap-1.5 pr-2">
                <span className="shrink-0 text-sm font-medium">{speaker.name}</span>
                <span className="text-xs text-muted-foreground">・</span>
                <MiddleTruncate
                  text={speaker.email}
                  className="min-w-0 flex-1 text-xs text-muted-foreground"
                />
              </div>
            </div>
            <p className="mt-2 line-clamp-2 px-2 text-xs text-muted-foreground">
              ...{speaker.clipText}
            </p>
          </li>
        ))}
      </ul>
    </div>
  )
}