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
30 changes: 18 additions & 12 deletions docs/UI-Design-System.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,19 @@ size tailored specifically for inline use inside FiltersSidebar.

**Props:**

| Prop | Type | Default | Description |
| ----------------- | ---------------------------------------- | ----------- | ------------------------------------------------------------------------- |
| `variant?` | `"empty" \| "filtered" \| "error"` | `"empty"` | Which semantic state to render. |
| `size?` | `"default" \| "compact"` | `"default"` | Full-size (marketplace results) vs condensed (FiltersSidebar inline). |
| `title?` | `string` | per variant | Override the default heading. |
| `message?` | `string` | per variant | Override the default subtitle. |
| `onClearFilters?` | `() => void` | — | Shown only when `variant === "filtered"`. Renders the Clear CTA. |
| `onRetry?` | `() => void \| Promise<void>` | — | Shown only when `variant === "error"`. Handles async loading + aria-busy. |
| `action?` | `{ label: string; onClick: () => void }` | — | Optional custom CTA button rendered before any variant-specific actions. |
| Prop | Type | Default | Description |
| ----------------- | -------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------- |
| `variant?` | `"empty" \| "api-detail" \| "filtered" \| "error" \| "plan-badge" \| "risk-gauge" \| "quota-banner"` | `"empty"` | Which semantic state to render. |
| `size?` | `"default" \| "compact"` | `"default"` | Full-size (marketplace results) vs condensed (FiltersSidebar inline). |
| `title?` | `string` | per variant | Override the default heading. |
| `message?` | `string` | per variant | Override the default subtitle. |
| `description?` | `string` | — | Deprecated: use `message` instead. |
| `onClearFilters?` | `() => void` | — | Shown only when `variant === "filtered"`. Renders the Clear CTA. |
| `onRetry?` | `() => void \| Promise<void>` | — | Shown only when `variant === "error"`. Handles async loading + aria-busy. |
| `action?` | `{ label: string; onClick: () => void }` | — | Optional custom CTA button rendered before any variant-specific actions. |
| `secondaryAction?`| `{ label: string; onClick: () => void }` | — | Optional secondary CTA button rendered after the primary action. |
| `loading?` | `boolean` | `false` | Renders the loading skeleton variant. |
| `copyable?` | `boolean` | `false` | When true, renders a copy-to-clipboard button for the message text. |

**Visual Spec (v7):**

Expand Down Expand Up @@ -561,7 +565,7 @@ Enhanced in v7 with an inline, context-aware **zero-results EmptyState** that ap
| `clearFilters` | `() => void` | (req) | Reset every filter to its default/empty state. |
| `favoritesOnly?` | `boolean` | `false` | Whether the "Favorites only" toggle is checked. |
| `toggleFavoritesOnly?` | `() => void` | no-op | Toggle the `favoritesOnly` state. |
| `resultCount?` | `number` | — | **(v7)** Live count of results after filtering. When `0` + active filters, renders the inline EmptyState. |
| `resultCount?` | `number` | — | **(v7)** Live count of results after filtering. When `0`, renders the inline EmptyState (filtered if filters active, empty otherwise). |

**Visual Spec:**

Expand All @@ -570,7 +574,7 @@ Enhanced in v7 with an inline, context-aware **zero-results EmptyState** that ap
- Price range: Two `<input type="number">` fields with a `WarningIcon` + error message when `min > max`.
- Popularity: Accessible `Dropdown` component.
- **Zero-results inline block (v7):** Wrapped in a `12px` padding-top + `1px solid var(--line)` top-border separator so it visually groups with "results feedback" rather than the Favorites controls above. Contains:
- `<EmptyState variant="filtered" size="compact" onClearFilters={clearFilters} />`
- `<EmptyState variant={hasActiveFilters ? "filtered" : "empty"} size="compact" onClearFilters={hasActiveFilters ? clearFilters : undefined} />`
- Wrapper carries `role="status"` + `aria-live="polite"` for assistive-tech announcements.
- Clear button: Ghost button style, always present at the bottom.

Expand All @@ -579,7 +583,9 @@ Enhanced in v7 with an inline, context-aware **zero-results EmptyState** that ap
- Default: All filter sections expanded.
- Collapsed: Section header shows rotated chevron; panel is `hidden`.
- Price error: Both price inputs gain `filter-input--invalid` class; alert paragraph appears with `role="alert"`.
- **Zero-results (v7):** When `resultCount === 0` AND at least one filter is active (categories, price bounds, non-any popularity, or favorites-only), the inline EmptyState mounts with a token-based top-border separator above it.
- **Zero-results (v7):** When `resultCount === 0`:
- AND at least one filter is active → renders `variant="filtered"` with a "Clear filters" CTA.
- AND no filters are active → renders `variant="empty"` with the generic "No APIs available" message.
- Focused: Standard focus ring on all inputs and buttons.

**Responsive Behavior:**
Expand Down
2 changes: 1 addition & 1 deletion src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ export default function EmptyState({
}: EmptyStateProps) {
const resolvedMessage = message ?? description;
const resolvedAction = action;
const { copied, handleCopy } = useCopy();
const { copy: handleCopy, copied } = useCopy();

if (loading) {
return (
Expand Down
44 changes: 44 additions & 0 deletions src/components/FiltersSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,50 @@ describe("FiltersSidebar", () => {
expect(within(dialog).getByTestId("filters-zero-results")).toBeTruthy();
expect(within(dialog).getByTestId("empty-state-filtered")).toBeTruthy();
});

it("renders EmptyState when resultCount=0 and no filters active (genuine empty marketplace)", () => {
render(<FiltersSidebar {...baseProps} resultCount={0} />);
const block = screen.getByTestId("filters-zero-results");
expect(block).toBeTruthy();
expect(
block.querySelector('[data-testid="empty-state-empty"]'),
).toBeTruthy();
});

it("does not render clear-filters CTA when no filters are active", () => {
render(<FiltersSidebar {...baseProps} resultCount={0} />);
expect(screen.queryByTestId("empty-state-clear-filters")).toBeNull();
});

it("renders sidebar controls normally when resultCount > 0", () => {
render(<FiltersSidebar {...baseProps} resultCount={10} />);
expect(screen.getByText("Categories")).toBeTruthy();
expect(screen.getByText("Price range")).toBeTruthy();
expect(screen.queryByTestId("filters-zero-results")).toBeNull();
});

it("renders EmptyState illustration for empty variant inside sidebar", () => {
render(<FiltersSidebar {...baseProps} resultCount={0} />);
const emptyState = screen.getByTestId("empty-state-empty");
expect(emptyState.querySelector("svg")).toBeTruthy();
expect(emptyState.getAttribute("data-size")).toBe("compact");
});

it("uses compact size for both empty and filtered variants inside sidebar", () => {
const { rerender } = render(
<FiltersSidebar {...baseProps} resultCount={0} />,
);
expect(screen.getByTestId("empty-state-empty").getAttribute("data-size")).toBe("compact");

rerender(
<FiltersSidebar
{...baseProps}
selectedCategories={new Set(["AI/ML"])}
resultCount={0}
/>,
);
expect(screen.getByTestId("empty-state-filtered").getAttribute("data-size")).toBe("compact");
});
});

describe("empty-state illustration (v7) visual refinements", () => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/FiltersSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,7 @@ export default function FiltersSidebar({
The wrapper is aria-live="polite" so screen readers announce the
zero-results state when filters narrow the count to 0. */}
{typeof resultCount === "number" &&
resultCount === 0 &&
hasActiveFilters && (
resultCount === 0 && (
<div
data-testid="filters-zero-results"
style={{
Expand All @@ -507,7 +506,7 @@ export default function FiltersSidebar({
aria-live="polite"
>
<EmptyState
variant="filtered"
variant={hasActiveFilters ? "filtered" : "empty"}
size="compact"
onClearFilters={hasActiveFilters ? handleClearFilters : undefined}
/>
Expand Down
1 change: 0 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2662,7 +2662,6 @@ code,
font-size: 0.65rem;
}
}
}

/* QuotaBanner layout and styles */
.quota-banner {
Expand Down
Loading