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.
- MeetingQ4 GTM strategy review with sales leadership and channel partners
- Attendeesamantha.rodriguez@northwind-enterprise-solutions.com
- Calendar[NORTHWIND] Bi-weekly product sync — Engineering + Design
- Tagproduct/launch/jamie-mobile-public-release-prep
- Transcriptworkspaces/northwind/meetings/2026-04-21-roadmap-sync.json
- Fits as-iskickoff.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 meetingsProject planning for december — Q1 backend roadmap with platform teamNov 28
- Talk to Alex about the approach for meeting object creationProject planning for december — Q1 backend roadmap with platform teamNov 28
- Dock behaviour while live meeting[NORTHWIND] Daily Sync — Engineering, Design & Product — Nov 25Nov 25
- Fix search in meeting window[NORTHWIND] Daily Sync — Engineering, Design & Product — Nov 25Nov 25
- Design the tasks section for the new meeting view layoutCustomer feedback call — onboarding research with enterprise design partnersOct 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.
- SSamantha Rodriguez・samantha.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
- DDimitri Theodorakis-Walker・dimitri.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
- AAiko Yamamoto・aiko.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>
)
}