Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/emcn-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
- **`ChipDatePicker`** — chip-styled date field.
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.
- **`OverflowText`** — the canonical single-line overflow treatment for read-only human labels and titles. It owns `min-w-0`, fade-only clipping (never an ellipsis), the conditional 18px edge mask, and the full-value floating tooltip; consumers pass only layout/typography through `className`. `overflowTextClipClass` and `overflowTextFadeClass` are the complete base/faded treatments for the rare component that must own measurement itself; never pair either with `truncate`, `text-ellipsis`, or hover-time mask removal. Use `DropdownMenuItemLabel` for a menu label beside icons, checks, or actions. A non-editable `Combobox` passes the full visual value through `overlayLabel`; the combobox owns the visual overlay's fade and keeps its one accessible tooltip on the interactive layer. Keep ordinary `truncate` only for editable values, code/log/path content, dense or virtualized grids, and rich composite content that cannot supply a plain tooltip label. Multiline copy uses an intentional `line-clamp-*` treatment instead.
- **`OverflowText`** — the canonical single-line overflow treatment for read-only human labels and titles. It owns `min-w-0`, fade-only clipping (never an ellipsis), the conditional 16px edge mask, and the full-value floating tooltip; consumers pass only layout/typography through `className`. `overflowTextClipClass` and `overflowTextFadeClass` are the complete base/faded treatments for the rare component that must own measurement itself; never pair either with `truncate`, `text-ellipsis`, or hover-time mask removal. Use `DropdownMenuItemLabel` for a menu label beside icons, checks, or actions. A non-editable `Combobox` passes the full visual value through `overlayLabel`; the combobox owns the visual overlay's fade and keeps its one accessible tooltip on the interactive layer. Keep ordinary `truncate` only for editable values, code/log/path content, dense or virtualized grids, and rich composite content that cannot supply a plain tooltip label. Multiline copy uses an intentional `line-clamp-*` treatment instead.

## Modal keyboard defaults

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ function BreadcrumbLocationPopover({
border
className={cn(
POPOVER_ANIMATION_CLASSES,
'bg-[var(--bg)] p-1.5 text-[var(--text-body)] shadow-sm'
'bg-[var(--bg)] p-1.5 text-[var(--text-body)] shadow-sm dark:[--surface-hover:var(--surface-4)]'
)}
onMouseEnter={openPopover}
onMouseLeave={scheduleClose}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
chipContentLabelClass,
chipVariants,
cn,
dropdownMenuRowClass,
OverflowText,
POPOVER_ANIMATION_CLASSES,
Popover,
PopoverAnchor,
PopoverContent,
PopoverItem,
PopoverSection,
Tooltip,
} from '@sim/emcn'
import { Check, Pencil, Pin, Plus, Trash } from '@sim/emcn/icons'
Expand All @@ -27,7 +27,7 @@ export const ALL_ROWS_VIEW_LABEL = 'All'
/** Matches the breadcrumb location popover's hover-intent grace period. */
const POPOVER_CLOSE_DELAY_MS = 120

/** Rendered width of one action button (`p-1` + `size-3` glyph) plus its `gap-0.5`.
/** Rendered width of one 20px action button plus its `gap-0.5`.
* The row reserves `actionCount` of these, so keep it in step with the button
* classes below — the overlay is absolutely positioned and can't size the spacer. */
const VIEW_ACTION_SLOT_PX = 22
Expand Down Expand Up @@ -131,21 +131,18 @@ export const ViewsMenu = memo(function ViewsMenu({
side='bottom'
align='start'
sideOffset={6}
minWidth={240}
maxWidth={320}
minWidth={220}
maxWidth={280}
maxHeight={420}
border
className={cn(
POPOVER_ANIMATION_CLASSES,
'bg-[var(--bg)] p-1.5 text-[var(--text-body)] shadow-sm'
'bg-[var(--bg)] p-1 text-[var(--text-body)] shadow-sm dark:bg-[var(--bg)]'
Comment thread
waleedlatif1 marked this conversation as resolved.
)}
onMouseEnter={openPopover}
onMouseLeave={scheduleClose}
onFocusCapture={cancelScheduledClose}
>
<PopoverSection className='px-1.5 py-0.5 text-[var(--text-muted)] text-xs'>
Views
</PopoverSection>
<div className='flex flex-col gap-0.5'>
{!hasDefaultView && (
<ViewRow
Expand Down Expand Up @@ -194,13 +191,10 @@ export const ViewsMenu = memo(function ViewsMenu({
</div>
{canEdit && (
<>
<div className='my-1 h-px bg-[var(--border)]' />
<PopoverItem
onClick={() => runAndClose(onNewView)}
className='h-7 items-center gap-1.5 px-1.5 py-0 text-xs'
>
<div className='my-1 h-px bg-[var(--border-1)]' />
<PopoverItem onClick={() => runAndClose(onNewView)} className={dropdownMenuRowClass}>
<span className='flex size-[14px] shrink-0 items-center justify-center'>
<Plus className='size-3 text-[var(--text-icon)]' />
<Plus className='size-[14px] text-[var(--text-icon)]' />
</span>
<OverflowText label='New view' className='flex-1 text-left' />
</PopoverItem>
Expand Down Expand Up @@ -268,10 +262,10 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
<PopoverItem
active={isActive}
onClick={onSelect}
className='h-7 min-w-0 flex-1 items-center gap-1.5 px-1.5 py-0 text-xs'
className={cn(dropdownMenuRowClass, 'flex-1')}
>
<span className='flex size-[14px] shrink-0 items-center justify-center'>
{isActive && <Check className='size-3 text-[var(--text-icon)]' />}
{isActive && <Check className='size-[14px] text-[var(--text-icon)]' />}
</span>
<OverflowText label={label} className='flex-1 text-left' />
{actionCount > 0 && (
Expand All @@ -283,7 +277,7 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
)}
</PopoverItem>
{actionCount > 0 && (
<div className='pointer-events-none absolute right-1.5 flex items-center gap-0.5'>
<div className='pointer-events-none absolute right-1 flex items-center gap-0.5'>
{actions?.map((action) => {
// Disabled via aria-disabled, not the `disabled` attribute: the button
// must keep receiving hover and focus events so the tooltip can explain
Expand Down Expand Up @@ -311,7 +305,7 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
: 'group-focus-within/view:opacity-100 group-hover/view:opacity-100'
)}
>
<action.icon className='size-3' />
<action.icon className='size-[14px]' />
</Button>
)
return action.disabledReason ? (
Expand Down Expand Up @@ -344,7 +338,7 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
defaultState.onSetDefault?.()
}}
>
<Pin className={cn('size-3', defaultState.isDefault && 'fill-current')} />
<Pin className={cn(defaultState.isDefault ? 'size-3 fill-current' : 'size-[14px]')} />
</Button>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('OverflowText', () => {
expect(label.classList.contains('text-sm')).toBe(true)
expect(label.className).toContain('-webkit-mask-image:linear-gradient')
expect(label.className).toContain('mask-image:linear-gradient')
expect(label.className).toContain('100%_-_16px')

act(() => {
label.dispatchEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

/** Complete fade-only clipping treatment for measured special cases. */
export const overflowTextFadeClass =
'overflow-hidden text-clip whitespace-nowrap [-webkit-mask-image:linear-gradient(to_right,black_calc(100%_-_18px),transparent)] [mask-image:linear-gradient(to_right,black_calc(100%_-_18px),transparent)]'
'overflow-hidden text-clip whitespace-nowrap [-webkit-mask-image:linear-gradient(to_right,black_calc(100%_-_16px),transparent)] [mask-image:linear-gradient(to_right,black_calc(100%_-_16px),transparent)]'

/** Fade-free clipping for externally measured labels and rich-content overflow exceptions. */
export const overflowTextClipClass = 'block min-w-0 overflow-hidden text-clip whitespace-nowrap'
Expand Down
Loading