Skip to content

Commit e4be4de

Browse files
committed
feat(web): <Monitor> wrapper primitive (collapsible panel with pinned dot)
1 parent 9ffa8a2 commit e4be4de

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

web/src/monitors/Monitor.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useState } from 'react';
2+
import type { CSSProperties, ReactNode } from 'react';
3+
4+
interface MonitorProps {
5+
title: string;
6+
count?: number;
7+
pinned: boolean;
8+
defaultOpen?: boolean;
9+
children: ReactNode;
10+
}
11+
12+
const wrap: CSSProperties = {
13+
borderBottom: '1px solid var(--hair)',
14+
};
15+
16+
const header: CSSProperties = {
17+
height: 32,
18+
display: 'flex',
19+
alignItems: 'center',
20+
gap: 8,
21+
padding: '0 12px',
22+
fontFamily: 'var(--ff-sans)',
23+
fontSize: 11,
24+
color: 'var(--ink-2)',
25+
cursor: 'pointer',
26+
userSelect: 'none',
27+
background: 'var(--bg-page)',
28+
};
29+
30+
export function Monitor({ title, count, pinned, defaultOpen = false, children }: MonitorProps) {
31+
const [open, setOpen] = useState<boolean>(pinned || defaultOpen);
32+
return (
33+
<section data-monitor={title} data-pinned={pinned} data-collapsed={!open} style={wrap}>
34+
<header
35+
onClick={() => setOpen((v) => !v)}
36+
style={header}
37+
>
38+
<span aria-hidden style={{ fontSize: 9, color: 'var(--ink-3)', width: 8 }}>
39+
{open ? '▾' : '▸'}
40+
</span>
41+
{pinned && (
42+
<span
43+
data-pinned-dot
44+
aria-hidden
45+
style={{
46+
display: 'inline-block',
47+
width: 5,
48+
height: 5,
49+
borderRadius: '50%',
50+
background: 'var(--acc)',
51+
}}
52+
/>
53+
)}
54+
<span style={{ fontWeight: 500, letterSpacing: '0.06em', textTransform: 'uppercase' }}>
55+
{title}
56+
</span>
57+
{count !== undefined && (
58+
<span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--ink-3)', fontFamily: 'var(--ff-mono)' }}>
59+
{count}
60+
</span>
61+
)}
62+
</header>
63+
{open && (
64+
<div style={{ padding: '8px 12px 12px', background: 'var(--bg-elev)' }}>
65+
{children}
66+
</div>
67+
)}
68+
</section>
69+
);
70+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen, fireEvent } from '../_helpers/render';
3+
import { Monitor } from '@/monitors/Monitor';
4+
5+
describe('<Monitor>', () => {
6+
it('renders title + optional count', () => {
7+
render(
8+
<Monitor title="Selected" count={3} pinned>
9+
<div>body content</div>
10+
</Monitor>,
11+
);
12+
expect(screen.getByText('Selected')).toBeInTheDocument();
13+
expect(screen.getByText('3')).toBeInTheDocument();
14+
expect(screen.getByText('body content')).toBeInTheDocument();
15+
});
16+
17+
it('omits count display when count is undefined', () => {
18+
render(
19+
<Monitor title="Health" pinned={false}>
20+
<div>body</div>
21+
</Monitor>,
22+
);
23+
expect(screen.getByText('Health')).toBeInTheDocument();
24+
expect(screen.queryByText(/^\d+$/)).not.toBeInTheDocument();
25+
});
26+
27+
it('renders pinned dot when pinned=true', () => {
28+
const { container } = render(
29+
<Monitor title="X" pinned>
30+
<div>body</div>
31+
</Monitor>,
32+
);
33+
expect(container.firstChild).toHaveAttribute('data-pinned', 'true');
34+
expect(container.querySelector('[data-pinned-dot]')).not.toBeNull();
35+
});
36+
37+
it('starts collapsed by default unless defaultOpen', () => {
38+
const { container, rerender } = render(
39+
<Monitor title="X" pinned={false}>
40+
<div>body</div>
41+
</Monitor>,
42+
);
43+
expect(container.firstChild).toHaveAttribute('data-collapsed', 'true');
44+
expect(screen.queryByText('body')).not.toBeInTheDocument();
45+
46+
rerender(
47+
<Monitor title="X" pinned={false} defaultOpen>
48+
<div>body</div>
49+
</Monitor>,
50+
);
51+
expect(screen.getByText('body')).toBeInTheDocument();
52+
});
53+
54+
it('toggles open/closed on header click', () => {
55+
const { container } = render(
56+
<Monitor title="X" pinned={false}>
57+
<div>body</div>
58+
</Monitor>,
59+
);
60+
expect(screen.queryByText('body')).not.toBeInTheDocument();
61+
fireEvent.click(screen.getByText('X'));
62+
expect(screen.getByText('body')).toBeInTheDocument();
63+
expect(container.firstChild).toHaveAttribute('data-collapsed', 'false');
64+
});
65+
});

0 commit comments

Comments
 (0)