Kind was the only facet and it earned its row less than it cost: the surface is already in every card's name, and the search covers all kinds. The paste-URL hint moves onto the start-from-scratch line.
Unified · Split
| @@ -19,7 +19,6 @@ import { Button } from "../components/button"; · 18 unmodified lines | |||
| 19 | import { Input } from "../components/input"; | 19 | import { Input } from "../components/input"; |
| 20 | import { PageContainer, PageHeader } from "../components/page"; | 20 | import { PageContainer, PageHeader } from "../components/page"; |
| 21 | import { Skeleton } from "../components/skeleton"; | 21 | import { Skeleton } from "../components/skeleton"; |
| 22 | import { cn } from "../lib/utils"; | ||
| 23 | import { useExecutorDocumentTitle } from "../lib/document-title"; | 22 | import { useExecutorDocumentTitle } from "../lib/document-title"; |
| 24 | import { | 23 | import { |
| 25 | availableCatalogKinds, | 24 | availableCatalogKinds, |
| @@ -298,29 +297,6 @@ function CardSkeleton(props: { readonly index: number }) { · 272 unmodified lines | |||
| 298 | ); | 297 | ); |
| 299 | } | 298 | } |
| 300 | 299 | ||
| 301 | function KindChip(props: { | ||
| 302 | readonly label: string; | ||
| 303 | readonly active: boolean; | ||
| 304 | readonly onSelect: () => void; | ||
| 305 | }) { | ||
| 306 | return ( | ||
| 307 | // oxlint-disable-next-line react/forbid-elements -- a filter chip, not a Button variant | ||
| 308 | <button | ||
| 309 | type="button" | ||
| 310 | onClick={props.onSelect} | ||
| 311 | aria-pressed={props.active} | ||
| 312 | className={cn( | ||
| 313 | "rounded-full border px-3 py-1 text-xs font-medium transition-colors", | ||
| 314 | props.active | ||
| 315 | ? "border-foreground/20 bg-foreground text-background" | ||
| 316 | : "border-border text-muted-foreground hover:bg-accent hover:text-foreground", | ||
| 317 | )} | ||
| 318 | > | ||
| 319 | {props.label} | ||
| 320 | </button> | ||
| 321 | ); | ||
| 322 | } | ||
| 323 | |||
| 324 | // --------------------------------------------------------------------------- | 300 | // --------------------------------------------------------------------------- |
| 325 | // Page | 301 | // Page |
| 326 | // --------------------------------------------------------------------------- | 302 | // --------------------------------------------------------------------------- |
| @@ -333,7 +309,6 @@ export function IntegrationBrowsePage() { · 6 unmodified lines | |||
| 333 | const installed = useAtomValue(integrationsOptimisticAtom); | 309 | const installed = useAtomValue(integrationsOptimisticAtom); |
| 334 | 310 | ||
| 335 | const [query, setQuery] = useState(""); | 311 | const [query, setQuery] = useState(""); |
| 336 | const [kind, setKind] = useState<CatalogKind | null>(null); | ||
| 337 | const [detecting, setDetecting] = useState(false); | 312 | const [detecting, setDetecting] = useState(false); |
| 338 | const [error, setError] = useState<string | null>(null); | 313 | const [error, setError] = useState<string | null>(null); |
| 339 | // Resolve-on-click failures belong on the card that was clicked — a page-top | 314 | // Resolve-on-click failures belong on the card that was clicked — a page-top |
| @@ -357,7 +332,7 @@ export function IntegrationBrowsePage() { · 17 unmodified lines | |||
| 357 | // Nothing to exclude now that the registry is the only source. | 332 | // Nothing to exclude now that the registry is the only source. |
| 358 | const excludeDomains = useMemo(() => new Set<string>(), []); | 333 | const excludeDomains = useMemo(() => new Set<string>(), []); |
| 359 | 334 | ||
| 360 | const catalog = useCatalogBrowse({ query: listQuery, ...(kind ? { kind } : {}) }); | 335 | const catalog = useCatalogBrowse({ query: listQuery }); |
| 361 | const catalogEntries = useMemo( | 336 | const catalogEntries = useMemo( |
| 362 | () => filterCatalogEntries(catalog.entries, { excludeDomains, availableKinds }), | 337 | () => filterCatalogEntries(catalog.entries, { excludeDomains, availableKinds }), |
| 363 | [catalog.entries, excludeDomains, availableKinds], | 338 | [catalog.entries, excludeDomains, availableKinds], |
| @@ -541,7 +516,6 @@ export function IntegrationBrowsePage() { · 177 unmodified lines | |||
| 541 | const rows: Row[] = []; | 516 | const rows: Row[] = []; |
| 542 | for (const entry of allPresets) { | 517 | for (const entry of allPresets) { |
| 543 | if (entry.preset.url !== undefined || entry.preset.endpoint !== undefined) continue; | 518 | if (entry.preset.url !== undefined || entry.preset.endpoint !== undefined) continue; |
| 544 | if (kind !== null && entry.pluginKey !== kind) continue; | ||
| 545 | if (text.length > 0) { | 519 | if (text.length > 0) { |
| 546 | const corpus = | 520 | const corpus = |
| 547 | `${entry.preset.name} ${entry.preset.summary ?? ""} ${entry.preset.family ?? ""} ${entry.pluginLabel}`.toLowerCase(); | 521 | `${entry.preset.name} ${entry.preset.summary ?? ""} ${entry.preset.family ?? ""} ${entry.pluginLabel}`.toLowerCase(); |
| @@ -566,7 +540,7 @@ export function IntegrationBrowsePage() { · 18 unmodified lines | |||
| 566 | }); | 540 | }); |
| 567 | } | 541 | } |
| 568 | return rows; | 542 | return rows; |
| 569 | }, [allPresets, kind, text, pickPreset, isAdded]); | 543 | }, [allPresets, text, pickPreset, isAdded]); |
| 570 | 544 | ||
| 571 | // --- Catalog rows: one per (service, surface) ----------------------------- | 545 | // --- Catalog rows: one per (service, surface) ----------------------------- |
| 572 | const catalogRows = useMemo<readonly Row[]>(() => { | 546 | const catalogRows = useMemo<readonly Row[]>(() => { |
| @@ -688,27 +662,10 @@ export function IntegrationBrowsePage() { · 115 unmodified lines | |||
| 688 | ) : null} | 662 | ) : null} |
| 689 | </div> | 663 | </div> |
| 690 | 664 | ||
| 691 | <div className="mb-3 flex flex-wrap items-center gap-1.5"> | ||
| 692 | <KindChip label="All" active={kind === null} onSelect={() => setKind(null)} /> | ||
| 693 | {availableKinds.map((candidate) => ( | ||
| 694 | <KindChip | ||
| 695 | key={candidate} | ||
| 696 | label={CATALOG_KIND_LABEL[candidate]} | ||
| 697 | active={kind === candidate} | ||
| 698 | onSelect={() => setKind(candidate)} | ||
| 699 | /> | ||
| 700 | ))} | ||
| 701 | <span className="ml-auto shrink-0 text-xs text-muted-foreground"> | ||
| 702 | Can't find it? Paste its URL above. | ||
| 703 | </span> | ||
| 704 | </div> | ||
| 705 | |||
| 706 | {/* Above the results, not below: an endless list has no reachable | 665 | {/* Above the results, not below: an endless list has no reachable |
| 707 | bottom, and this is the escape hatch for exactly the person the | 666 | bottom, and this is the escape hatch for exactly the person the |
| 708 | list is failing. One quiet line — the label carries the action, so | 667 | list is failing. One quiet line; the label carries the action, so |
| 709 | the chips can stay as bare format names without colliding with the | 668 | the chips stay bare format names. */} |
| 710 | facet pills above (those FILTER, these CREATE, and the plus mark + | ||
| 711 | leading verb keep that legible). */} | ||
| 712 | <div className="mb-8 flex flex-wrap items-center gap-x-3 gap-y-2"> | 669 | <div className="mb-8 flex flex-wrap items-center gap-x-3 gap-y-2"> |
| 713 | <span className="shrink-0 text-xs text-muted-foreground">Start from scratch:</span> | 670 | <span className="shrink-0 text-xs text-muted-foreground">Start from scratch:</span> |
| 714 | <div className="flex shrink-0 items-center gap-1.5"> | 671 | <div className="flex shrink-0 items-center gap-1.5"> |
| @@ -734,6 +691,9 @@ export function IntegrationBrowsePage() { · 19 unmodified lines | |||
| 734 | ), | 691 | ), |
| 735 | )} | 692 | )} |
| 736 | </div> | 693 | </div> |
| 694 | <span className="ml-auto shrink-0 text-xs text-muted-foreground"> | ||
| 695 | Can't find it? Paste its URL above. | ||
| 696 | </span> | ||
| 737 | </div> | 697 | </div> |
| 738 | 698 | ||
| 739 | {error ? ( | 699 | {error ? ( |