Skip to content
Merged
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
4 changes: 2 additions & 2 deletions app/src/components/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ export function FilterBar({
'&:focus': isSearchExpanded ? {} : { outline: `2px solid ${colors.primary}`, outlineOffset: 2 },
}}
>
<Tooltip title={isSearchExpanded ? '' : 'search'}>
<Tooltip title={isSearchExpanded ? '' : '.find()'}>
<SearchIcon
className="search-icon"
sx={{
Expand All @@ -469,7 +469,7 @@ export function FilterBar({
id="filter-search"
name="filter-search"
inputProps={{ 'aria-label': selectedCategory ? `Search ${FILTER_LABELS[selectedCategory]}` : 'Search filters' }}
placeholder={selectedCategory ? FILTER_LABELS[selectedCategory] : ''}
placeholder={selectedCategory ? FILTER_LABELS[selectedCategory] : '.find(_)'}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
Expand Down
34 changes: 26 additions & 8 deletions app/src/components/HeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ export function HeroSection({ potd = null }: HeroSectionProps) {
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.3s backwards',
}}
>
<PrimaryCta to="/plots" label="browse plots" />
<PrimaryCta to="/plots" subject="plots" verb="browse" ariaLabel="Browse plots" />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
<SecondaryLink to="/mcp" label="or connect via mcp" />
<SecondaryLink href="https://github.com/MarkusNeusinger/anyplot" label="or clone on github" external />
<SecondaryLink to="/mcp" subject="mcp" verb="connect" ariaLabel="Connect via MCP" />
<SecondaryLink href="https://github.com/MarkusNeusinger/anyplot" subject="github" verb="clone" ariaLabel="Clone on GitHub" external />
</Box>
</Box>

Expand All @@ -183,11 +183,12 @@ export function HeroSection({ potd = null }: HeroSectionProps) {
);
}

function PrimaryCta({ to, label }: { to: string; label: string }) {
function PrimaryCta({ to, subject, verb, ariaLabel }: { to: string; subject: string; verb: string; ariaLabel: string }) {
return (
<Box
component={RouterLink}
to={to}
aria-label={ariaLabel}
sx={{
textDecoration: 'none',
fontFamily: typography.mono,
Expand All @@ -201,24 +202,34 @@ function PrimaryCta({ to, label }: { to: string; label: string }) {
bgcolor: 'var(--ink)',
color: 'var(--bg-page)',
transition: 'all 0.2s',
'& .cta-subject': { opacity: 0.55, transition: 'opacity 0.2s' },
'&:hover': { bgcolor: colors.primary, color: '#FFF' },
'&:hover .cta-subject': { opacity: 0.8 },
'&:focus-visible': { outline: `2px solid ${colors.primary}`, outlineOffset: 2 },
}}
>
{label} <Box component="span">→</Box>
<Box component="span">
<Box component="span" className="cta-subject">{subject}</Box>
{`.${verb}()`}
</Box>{' '}
<Box component="span">→</Box>
</Box>
);
}

function SecondaryLink({
to,
href,
label,
subject,
verb,
ariaLabel,
external,
}: {
to?: string;
href?: string;
label: string;
subject: string;
verb: string;
ariaLabel: string;
external?: boolean;
}) {
const linkProps = external
Expand All @@ -228,6 +239,7 @@ function SecondaryLink({
return (
<Box
{...linkProps}
aria-label={ariaLabel}
sx={{
textDecoration: 'none',
fontFamily: typography.mono,
Expand All @@ -237,13 +249,19 @@ function SecondaryLink({
alignItems: 'center',
gap: 0.5,
transition: 'color 0.2s',
'& .link-subject': { opacity: 0.7, transition: 'opacity 0.2s' },
'& .arrow': { transition: 'transform 0.2s' },
'&:hover': { color: colors.primary },
'&:hover .link-subject': { opacity: 1 },
'&:hover .arrow': { transform: 'translateX(3px)' },
'&:focus-visible': { outline: `2px solid ${colors.primary}`, outlineOffset: 2, borderRadius: '2px' },
}}
>
{label}&nbsp;<Box component="span" className="arrow">→</Box>
<Box component="span">
<Box component="span" className="link-subject">{subject}</Box>
{`.${verb}()`}
</Box>
&nbsp;<Box component="span" className="arrow">→</Box>
</Box>
);
}
2 changes: 1 addition & 1 deletion app/src/components/ImageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export const ImageCard = memo(function ImageCard({
pointerEvents: 'none',
zIndex: 2,
}}>
{'>>> copied'}
{'>>> .copied'}
</Box>
)}
{/* Copy button - appears on hover */}
Expand Down
12 changes: 6 additions & 6 deletions app/src/components/SpecDetailView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,26 @@ describe('SpecDetailView', () => {
expect(screen.queryByText('1/1')).not.toBeInTheDocument();
});

it('shows ">>> copied" overlay when codeCopied matches current library', () => {
it('shows ">>> .copied" overlay when codeCopied matches current library', () => {
render(
<SpecDetailView {...defaultProps} codeCopied="matplotlib" />,
);
expect(screen.getByText('>>> copied')).toBeInTheDocument();
expect(screen.getByText('>>> .copied')).toBeInTheDocument();
});

it('shows ">>> downloaded" overlay when downloadDone matches current library', () => {
it('shows ">>> .downloaded" overlay when downloadDone matches current library', () => {
render(
<SpecDetailView {...defaultProps} downloadDone="matplotlib" />,
);
expect(screen.getByText('>>> downloaded')).toBeInTheDocument();
expect(screen.getByText('>>> .downloaded')).toBeInTheDocument();
});

it('does not show overlay when codeCopied does not match current library', () => {
render(
<SpecDetailView {...defaultProps} codeCopied="plotly" />,
);
expect(screen.queryByText('>>> copied')).not.toBeInTheDocument();
expect(screen.queryByText('>>> downloaded')).not.toBeInTheDocument();
expect(screen.queryByText('>>> .copied')).not.toBeInTheDocument();
expect(screen.queryByText('>>> .downloaded')).not.toBeInTheDocument();
});

it('toggles zoom on click via aria-label change', async () => {
Expand Down
12 changes: 6 additions & 6 deletions app/src/components/SpecDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export function SpecDetailView({
</Box>

<Box sx={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 0.5 }}>
<Tooltip title="Show static preview" disableFocusListener>
<Tooltip title=".preview()" disableFocusListener>
<IconButton
onClick={() => {
onViewModeChange('preview');
Expand All @@ -240,7 +240,7 @@ export function SpecDetailView({
<ImageOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Open Raw HTML" disableFocusListener>
<Tooltip title=".raw()" disableFocusListener>
<IconButton
onClick={() => window.open(currentImpl.preview_html, '_blank', 'noopener,noreferrer')}
aria-label="Open raw HTML"
Expand Down Expand Up @@ -329,7 +329,7 @@ export function SpecDetailView({
pointerEvents: 'none',
zIndex: 2,
}}>
{codeCopied === currentImpl.library_id ? '>>> copied' : '>>> downloaded'}
{codeCopied === currentImpl.library_id ? '>>> .copied' : '>>> .downloaded'}
</Box>
)}

Expand All @@ -338,7 +338,7 @@ export function SpecDetailView({
sx={{ position: 'absolute', top: 8, right: 8, display: zoomed ? 'none' : 'flex', gap: 0.5 }}
>
{currentImpl && (
<Tooltip title="Copy Code" disableFocusListener>
<Tooltip title=".copy()" disableFocusListener>
<IconButton
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onCopyCode(currentImpl); }}
aria-label="Copy code"
Expand All @@ -350,7 +350,7 @@ export function SpecDetailView({
</Tooltip>
)}
{currentImpl && (
<Tooltip title="Download PNG" disableFocusListener>
<Tooltip title=".download()" disableFocusListener>
<IconButton
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onDownload(currentImpl); }}
aria-label="Download PNG"
Expand All @@ -362,7 +362,7 @@ export function SpecDetailView({
</Tooltip>
)}
{interactiveAvailable && (
<Tooltip title="Show Interactive" disableFocusListener>
<Tooltip title=".open()" disableFocusListener>
<IconButton
onClick={() => {
onViewModeChange('interactive');
Expand Down
12 changes: 6 additions & 6 deletions app/src/components/SpecOverview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,25 +157,25 @@ describe('SpecOverview', () => {
expect(skeleton).toBeInTheDocument();
});

it('shows ">>> copied" overlay when codeCopied matches a library_id', () => {
it('shows ">>> .copied" overlay when codeCopied matches a library_id', () => {
render(
<SpecOverview {...defaultProps} codeCopied="matplotlib" />,
);
expect(screen.getByText('>>> copied')).toBeInTheDocument();
expect(screen.getByText('>>> .copied')).toBeInTheDocument();
});

it('shows ">>> downloaded" overlay when downloadDone matches a library_id', () => {
it('shows ">>> .downloaded" overlay when downloadDone matches a library_id', () => {
render(
<SpecOverview {...defaultProps} downloadDone="plotly" />,
);
expect(screen.getByText('>>> downloaded')).toBeInTheDocument();
expect(screen.getByText('>>> .downloaded')).toBeInTheDocument();
});

it('does not show overlay when codeCopied does not match any library_id', () => {
render(
<SpecOverview {...defaultProps} codeCopied="nonexistent" />,
);
expect(screen.queryByText('>>> copied')).not.toBeInTheDocument();
expect(screen.queryByText('>>> downloaded')).not.toBeInTheDocument();
expect(screen.queryByText('>>> .copied')).not.toBeInTheDocument();
expect(screen.queryByText('>>> .downloaded')).not.toBeInTheDocument();
});
});
8 changes: 4 additions & 4 deletions app/src/components/SpecOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ function ImplementationCard({
pointerEvents: 'none',
zIndex: 2,
}}>
{codeCopied === impl.library_id ? '>>> copied' : '>>> downloaded'}
{codeCopied === impl.library_id ? '>>> .copied' : '>>> .downloaded'}
</Box>
)}

Expand All @@ -221,7 +221,7 @@ function ImplementationCard({
transition: 'opacity 0.2s',
}}
>
<Tooltip title="Copy Code" disableFocusListener>
<Tooltip title=".copy()" disableFocusListener>
<IconButton
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onCopyCode(impl); }}
aria-label="Copy code"
Expand All @@ -234,7 +234,7 @@ function ImplementationCard({
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Download PNG" disableFocusListener>
<Tooltip title=".download()" disableFocusListener>
<IconButton
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onDownload(impl); }}
aria-label="Download PNG"
Expand All @@ -248,7 +248,7 @@ function ImplementationCard({
</IconButton>
</Tooltip>
{impl.preview_html && (
<Tooltip title="Open Interactive" disableFocusListener>
<Tooltip title=".open()" disableFocusListener>
<IconButton
component={Link}
to={`${specPath(specId, impl.language, impl.library_id)}?view=interactive`}
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/SpecTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export function SpecTabs({
{!overviewMode && (
<TabPanel value={tabIndex} index={0}>
<Box sx={{ position: 'relative' }}>
<Tooltip title={copied ? 'Copied!' : 'Copy code'}>
<Tooltip title={copied ? '.copied' : '.copy()'}>
<IconButton
onClick={handleCopy}
aria-label="Copy code"
Expand Down
9 changes: 7 additions & 2 deletions app/src/components/ToolbarActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ describe('PlotsLink', () => {
expect(link).toHaveAttribute('href', '/plots');
});

it('has a "plots" tooltip', async () => {
it('has a "plots.list()" tooltip', async () => {
const user = userEvent.setup();
render(<PlotsLink />);

const link = screen.getByRole('link');
await user.hover(link);

expect(await screen.findByText('plots')).toBeInTheDocument();
expect(await screen.findByText('plots.list()')).toBeInTheDocument();
});

it('has a descriptive aria-label', () => {
render(<PlotsLink />);
expect(screen.getByRole('link')).toHaveAttribute('aria-label', 'Browse plots');
});
});

Expand Down
5 changes: 3 additions & 2 deletions app/src/components/ToolbarActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ interface ToolbarActionsProps {
*/
export function PlotsLink() {
return (
<Tooltip title="plots">
<Tooltip title="plots.list()">
<Box
component={Link}
to="/plots"
aria-label="Browse plots"
sx={{
display: 'flex',
alignItems: 'center',
Expand Down Expand Up @@ -62,7 +63,7 @@ export function GridSizeToggle({ imageSize, onImageSizeChange, onTrackEvent }: T
};

return (
<Tooltip title={imageSize === 'normal' ? 'compact view' : 'normal view'}>
<Tooltip title={imageSize === 'normal' ? '.compact()' : '.normal()'}>
<Box
role="button"
tabIndex={0}
Expand Down
20 changes: 19 additions & 1 deletion docs/reference/style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,25 @@ Three variants. All buttons read as method calls in mono — there is no ambigui
.btn-action:hover::before { color: var(--ok-green); }
```

Examples: `.copy()`, `.open()`, `.download()`, `.preview()`, `.share()`, `.fork()`. The leading `.` is part of the visual language — it signals "this is a method on the thing in front of you" without saying so. No icons needed for common actions.
Examples: `.copy()`, `.open()`, `.download()`, `.preview()`, `.share()`, `.fork()`, `.raw()`. The leading `.` is part of the visual language — it signals "this is a method on the thing in front of you" without saying so. No icons needed for common actions.

#### 7.4.1 Subject-Prefixed Method Calls

Two forms of the pseudo-function style, used in different contexts:

1. **Implicit subject — `.verb()`** — when a card or surrounding surface visually provides the subject (plot card, detail pane). See the button examples above.
2. **Explicit subject — `subject.verb()`** — when the action stands alone, detached from a carrier. Format follows `any.plot()`: the subject is the object/namespace (`--ink-muted`), `.verb()` is the method (`--ink`, green on hover).

Examples of the explicit form:

- Hero CTAs: `plots.browse()`, `plots.find()`
- Secondary links: `github.clone()`, `mcp.connect()`
- Search prompt: `❯ plots.find(_▌)` (or bare `.find()` inside a detail pane)
- Page-level fallbacks: `page.miss()`, `results.empty()`

Pluralisation: prefer the natural plural for collections (`plots.browse()`, `libs.list()`) and the singular for a specific item (`plot.open()`, `spec.copy()`). Keep everything lowercase — the wordmark-style capitalisation stops at the logo.

**Accessibility**: the pseudo-function style is a *visual* convention — it belongs on visible labels, tooltip titles, and placeholder text. `aria-label` and any hidden `<label>` fallback for assistive tech must stay in readable human language (`"Copy code"`, `"Browse plots"`, `"Zoom in"`), because screen readers announce punctuation literally (`.copy()` → "dot copy open close paren"). Icon-only controls should always carry a descriptive `aria-label` even when their tooltip uses the method-call style.

**Hero CTA (filled, only on landing hero)**

Expand Down
Loading