diff --git a/src/__tests__/skill-detail-page.test.tsx b/src/__tests__/skill-detail-page.test.tsx
index 1e88304d5..29364d207 100644
--- a/src/__tests__/skill-detail-page.test.tsx
+++ b/src/__tests__/skill-detail-page.test.tsx
@@ -24,6 +24,10 @@ vi.mock("../lib/useAuthStatus", () => ({
useAuthStatus: () => useAuthStatusMock(),
}));
+vi.mock("../components/SkillDiffCard", () => ({
+ SkillDiffCard: () =>
,
+}));
+
describe("SkillDetailPage", () => {
const skillId = "skills:1" as Id<"skills">;
const ownerId = "users:1" as Id<"users">;
diff --git a/src/__tests__/skills-index.test.tsx b/src/__tests__/skills-index.test.tsx
index a09e716ae..c1145c499 100644
--- a/src/__tests__/skills-index.test.tsx
+++ b/src/__tests__/skills-index.test.tsx
@@ -301,6 +301,29 @@ describe("SkillsIndex", () => {
);
});
+ it("shows and clears the active capability tag filter", async () => {
+ searchMock = { tag: "crypto" };
+ render();
+ await act(async () => {});
+
+ const capabilityChip = screen.getByRole("button", { name: /crypto/i });
+ expect(capabilityChip).toBeTruthy();
+
+ await act(async () => {
+ fireEvent.click(capabilityChip);
+ });
+
+ expect(navigateMock).toHaveBeenCalled();
+ const lastCall = navigateMock.mock.calls.at(-1)?.[0] as {
+ replace?: boolean;
+ search: (prev: Record) => Record;
+ };
+ expect(lastCall.replace).toBe(true);
+ expect(lastCall.search({ tag: "crypto" })).toEqual({
+ tag: undefined,
+ });
+ });
+
it("shows load-more button when more results are available", async () => {
vi.stubGlobal("IntersectionObserver", undefined);
convexHttpMock.query.mockResolvedValue({
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 5d5989f3c..baa9f1a67 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -2,13 +2,13 @@ import { useAuthActions } from "@convex-dev/auth/react";
import { Link } from "@tanstack/react-router";
import { Menu, Monitor, Moon, Plus, Search, Sun } from "lucide-react";
import { useMemo, useRef } from "react";
-import { getUserFacingAuthError } from "../lib/authErrorMessage";
import { gravatarUrl } from "../lib/gravatar";
import { isModerator } from "../lib/roles";
import { getClawHubSiteUrl, getSiteMode, getSiteName } from "../lib/site";
import { applyTheme, useThemeMode } from "../lib/theme";
import { startThemeTransition } from "../lib/theme-transition";
-import { setAuthError, useAuthError } from "../lib/useAuthError";
+import { useAuthError } from "../lib/useAuthError";
+import { SignInButton } from "./SignInButton";
import { useAuthStatus } from "../lib/useAuthStatus";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { Button } from "./ui/button";
@@ -24,7 +24,7 @@ import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
export default function Header() {
const { isAuthenticated, isLoading, me } = useAuthStatus();
- const { signIn, signOut } = useAuthActions();
+ const { signOut } = useAuthActions();
const { mode, setMode } = useThemeMode();
const toggleRef = useRef(null);
const siteMode = getSiteMode();
@@ -37,7 +37,6 @@ export default function Header() {
const initial = (me?.displayName ?? me?.name ?? handle).charAt(0).toUpperCase();
const isStaff = isModerator(me);
const { error: authError, clear: clearAuthError } = useAuthError();
- const signInRedirectTo = getCurrentRelativeUrl();
const setTheme = (next: "system" | "light" | "dark") => {
startThemeTransition({
@@ -311,25 +310,14 @@ export default function Header() {
) : null}
-
+
>
)}
@@ -337,8 +325,3 @@ export default function Header() {
);
}
-
-function getCurrentRelativeUrl() {
- if (typeof window === "undefined") return "/";
- return `${window.location.pathname}${window.location.search}${window.location.hash}`;
-}
diff --git a/src/components/SignInButton.test.tsx b/src/components/SignInButton.test.tsx
new file mode 100644
index 000000000..9bb015e1d
--- /dev/null
+++ b/src/components/SignInButton.test.tsx
@@ -0,0 +1,84 @@
+/* @vitest-environment jsdom */
+
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { SignInButton } from "./SignInButton";
+
+const signInMock = vi.fn();
+const clearAuthErrorMock = vi.fn();
+const setAuthErrorMock = vi.fn();
+const getUserFacingAuthErrorMock = vi.fn();
+
+vi.mock("@convex-dev/auth/react", () => ({
+ useAuthActions: () => ({
+ signIn: signInMock,
+ }),
+}));
+
+vi.mock("../lib/useAuthError", () => ({
+ clearAuthError: () => clearAuthErrorMock(),
+ setAuthError: (message: string) => setAuthErrorMock(message),
+}));
+
+vi.mock("../lib/authErrorMessage", () => ({
+ getUserFacingAuthError: (error: unknown, fallback: string) =>
+ getUserFacingAuthErrorMock(error, fallback),
+}));
+
+describe("SignInButton", () => {
+ beforeEach(() => {
+ signInMock.mockReset();
+ clearAuthErrorMock.mockReset();
+ setAuthErrorMock.mockReset();
+ getUserFacingAuthErrorMock.mockReset();
+ getUserFacingAuthErrorMock.mockImplementation((_, fallback) => fallback);
+ window.history.replaceState(null, "", "/skills?q=test#top");
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("starts GitHub sign-in with the current relative URL by default", async () => {
+ signInMock.mockResolvedValue({ signingIn: true });
+
+ render(Sign in with GitHub);
+ fireEvent.click(screen.getByRole("button", { name: "Sign in with GitHub" }));
+
+ await waitFor(() => {
+ expect(signInMock).toHaveBeenCalledWith("github", {
+ redirectTo: "/skills?q=test#top",
+ });
+ });
+ expect(clearAuthErrorMock).toHaveBeenCalledTimes(1);
+ expect(setAuthErrorMock).not.toHaveBeenCalled();
+ });
+
+ it("surfaces a generic error when sign-in resolves without redirecting", async () => {
+ signInMock.mockResolvedValue({ signingIn: false });
+
+ render(Sign in with GitHub);
+ fireEvent.click(screen.getByRole("button", { name: "Sign in with GitHub" }));
+
+ await waitFor(() => {
+ expect(setAuthErrorMock).toHaveBeenCalledWith("Sign in failed. Please try again.");
+ });
+ });
+
+ it("surfaces user-facing auth errors when sign-in rejects", async () => {
+ const failure = new Error("oauth failed");
+ signInMock.mockRejectedValue(failure);
+ getUserFacingAuthErrorMock.mockReturnValue("GitHub auth unavailable");
+
+ render(Sign in with GitHub);
+ fireEvent.click(screen.getByRole("button", { name: "Sign in with GitHub" }));
+
+ await waitFor(() => {
+ expect(getUserFacingAuthErrorMock).toHaveBeenCalledWith(
+ failure,
+ "Sign in failed. Please try again.",
+ );
+ expect(setAuthErrorMock).toHaveBeenCalledWith("GitHub auth unavailable");
+ });
+ });
+});
diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx
new file mode 100644
index 000000000..c172f8795
--- /dev/null
+++ b/src/components/SignInButton.tsx
@@ -0,0 +1,48 @@
+import { useAuthActions } from "@convex-dev/auth/react";
+import type { ComponentProps } from "react";
+import { getUserFacingAuthError } from "../lib/authErrorMessage";
+import { clearAuthError, setAuthError } from "../lib/useAuthError";
+import { Button } from "./ui/button";
+
+type ButtonProps = ComponentProps;
+
+type SignInButtonProps = Omit & {
+ redirectTo?: string;
+};
+
+export function SignInButton({
+ redirectTo,
+ children = "Sign in with GitHub",
+ ...props
+}: SignInButtonProps) {
+ const { signIn } = useAuthActions();
+
+ return (
+
+ );
+}
+
+function getCurrentRelativeUrl() {
+ if (typeof window === "undefined") return "/";
+ return `${window.location.pathname}${window.location.search}${window.location.hash}`;
+}
diff --git a/src/components/SkillDetailPage.tsx b/src/components/SkillDetailPage.tsx
index 4bb3048f9..be2dfc13d 100644
--- a/src/components/SkillDetailPage.tsx
+++ b/src/components/SkillDetailPage.tsx
@@ -6,6 +6,7 @@ import { toast } from "sonner";
import { api } from "../../convex/_generated/api";
import type { Doc, Id } from "../../convex/_generated/dataModel";
import { canManageSkill, isModerator } from "../lib/roles";
+import { hasOwnProperty } from "../lib/hasOwnProperty";
import type { SkillBySlugResult, SkillPageInitialData } from "../lib/skillPage";
import { useAuthStatus } from "../lib/useAuthStatus";
import { ClientOnly } from "./ClientOnly";
@@ -37,13 +38,11 @@ type SkillDetailPageProps = {
type SkillFile = Doc<"skillVersions">["files"][number];
function formatReportError(error: unknown) {
- if (error && typeof error === "object" && "data" in error) {
+ if (hasOwnProperty(error, "data")) {
const data = (error as { data?: unknown }).data;
if (typeof data === "string" && data.trim()) return data.trim();
if (
- data &&
- typeof data === "object" &&
- "message" in data &&
+ hasOwnProperty(data, "message") &&
typeof (data as { message?: unknown }).message === "string"
) {
const message = (data as { message?: string }).message?.trim();
diff --git a/src/components/UserBadge.tsx b/src/components/UserBadge.tsx
index 3be52f86a..7f727f6e0 100644
--- a/src/components/UserBadge.tsx
+++ b/src/components/UserBadge.tsx
@@ -1,3 +1,4 @@
+import { hasOwnProperty } from "../lib/hasOwnProperty";
import type { PublicPublisher, PublicUser } from "../lib/publicUser";
type UserBadgeProps = {
@@ -17,11 +18,13 @@ export function UserBadge({
link = true,
showName = false,
}: UserBadgeProps) {
- const userName = user && "name" in user ? user.name?.trim() : undefined;
+ const userName = hasOwnProperty(user, "name") && typeof user.name === "string"
+ ? user.name.trim()
+ : undefined;
const displayName = user?.displayName?.trim() || userName || null;
const handle = user?.handle ?? fallbackHandle ?? null;
const href =
- user?.handle && "kind" in user
+ user?.handle && hasOwnProperty(user, "kind")
? user.kind === "org"
? `/orgs/${encodeURIComponent(user.handle)}`
: `/u/${encodeURIComponent(user.handle)}`
diff --git a/src/lib/categories.ts b/src/lib/categories.ts
new file mode 100644
index 000000000..b585e611d
--- /dev/null
+++ b/src/lib/categories.ts
@@ -0,0 +1,19 @@
+export type SkillCategory = {
+ slug: string;
+ label: string;
+ keywords: string[];
+};
+
+export const SKILL_CATEGORIES: SkillCategory[] = [
+ { slug: "mcp-tools", label: "MCP Tools", keywords: ["mcp", "tool", "server"] },
+ { slug: "prompts", label: "Prompts", keywords: ["prompt", "template", "system"] },
+ { slug: "workflows", label: "Workflows", keywords: ["workflow", "pipeline", "chain"] },
+ { slug: "dev-tools", label: "Dev Tools", keywords: ["dev", "debug", "lint", "test", "build"] },
+ { slug: "data", label: "Data & APIs", keywords: ["api", "data", "fetch", "http", "rest", "graphql"] },
+ { slug: "security", label: "Security", keywords: ["security", "scan", "auth", "encrypt"] },
+ { slug: "automation", label: "Automation", keywords: ["auto", "cron", "schedule", "bot"] },
+ { slug: "other", label: "Other", keywords: [] },
+];
+
+export const ALL_CATEGORY_KEYWORDS = SKILL_CATEGORIES.flatMap((c) => c.keywords);
+
diff --git a/src/lib/convexError.ts b/src/lib/convexError.ts
index c4ac3e399..259449f15 100644
--- a/src/lib/convexError.ts
+++ b/src/lib/convexError.ts
@@ -1,3 +1,5 @@
+import { hasOwnProperty } from "./hasOwnProperty";
+
type ConvexLikeErrorData =
| string
| {
@@ -24,9 +26,12 @@ export function getUserFacingConvexError(error: unknown, fallback: string) {
const candidates: string[] = [];
const maybe = error as ConvexLikeError;
- if (maybe && typeof maybe === "object" && "data" in maybe) {
+ if (hasOwnProperty(maybe, "data")) {
if (typeof maybe.data === "string") candidates.push(maybe.data);
- if (maybe.data && typeof maybe.data === "object" && typeof maybe.data.message === "string") {
+ if (
+ hasOwnProperty(maybe.data, "message") &&
+ typeof maybe.data.message === "string"
+ ) {
candidates.push(maybe.data.message);
}
}
diff --git a/src/lib/hasOwnProperty.ts b/src/lib/hasOwnProperty.ts
new file mode 100644
index 000000000..65262f9df
--- /dev/null
+++ b/src/lib/hasOwnProperty.ts
@@ -0,0 +1,6 @@
+export function hasOwnProperty(
+ value: unknown,
+ key: K,
+): value is Record {
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
+}
diff --git a/src/lib/packageApi.ts b/src/lib/packageApi.ts
index 2556eceab..ccd69e599 100644
--- a/src/lib/packageApi.ts
+++ b/src/lib/packageApi.ts
@@ -4,6 +4,7 @@ import type {
PackageVerificationSummary,
} from "clawhub-schema";
import { ApiRoutes } from "clawhub-schema/routes";
+import { hasOwnProperty } from "./hasOwnProperty";
import { getRequiredRuntimeEnv, getRuntimeEnv } from "./runtimeEnv";
export type PackageListItem = {
@@ -117,6 +118,11 @@ type PluginCatalogResult = {
nextCursor: string | null;
};
+type PackageCatalogBrowseResponse = {
+ items: PackageListItem[];
+ nextCursor: string | null;
+};
+
type PackageApiErrorOptions = {
status: number;
retryAfterSeconds?: number | null;
@@ -302,10 +308,17 @@ export async function fetchPluginCatalog(params: {
executesCode: params.executesCode,
limit: params.limit,
});
+ if (hasOwnProperty(response, "results") && Array.isArray(response.results)) {
+ return {
+ items: response.results.map((entry) => entry.package),
+ nextCursor: null,
+ };
+ }
+
+ const browseResponse = response as PackageCatalogBrowseResponse;
return {
- items:
- "results" in response ? response.results.map((entry) => entry.package) : response.items,
- nextCursor: "results" in response ? null : response.nextCursor,
+ items: browseResponse.items,
+ nextCursor: browseResponse.nextCursor,
};
}
diff --git a/src/routes/-settings.test.tsx b/src/routes/-settings.test.tsx
index f51184058..51665d730 100644
--- a/src/routes/-settings.test.tsx
+++ b/src/routes/-settings.test.tsx
@@ -5,17 +5,26 @@ import { Settings } from "./settings";
const useQueryMock = vi.fn();
const useMutationMock = vi.fn();
+const useAuthActionsMock = vi.fn();
vi.mock("convex/react", () => ({
useQuery: (...args: unknown[]) => useQueryMock(...args),
useMutation: (...args: unknown[]) => useMutationMock(...args),
}));
+vi.mock("@convex-dev/auth/react", () => ({
+ useAuthActions: () => useAuthActionsMock(),
+}));
+
describe("Settings", () => {
beforeEach(() => {
useQueryMock.mockReset();
useMutationMock.mockReset();
+ useAuthActionsMock.mockReset();
useMutationMock.mockReturnValue(vi.fn());
+ useAuthActionsMock.mockReturnValue({
+ signIn: vi.fn(),
+ });
});
it("skips token loading until auth has resolved", () => {
diff --git a/src/routes/cli/auth.tsx b/src/routes/cli/auth.tsx
index c97bce87f..cc4dcb727 100644
--- a/src/routes/cli/auth.tsx
+++ b/src/routes/cli/auth.tsx
@@ -1,14 +1,12 @@
-import { useAuthActions } from "@convex-dev/auth/react";
import { createFileRoute } from "@tanstack/react-router";
import { useMutation } from "convex/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { api } from "../../../convex/_generated/api";
import { Container } from "../../components/layout/Container";
-import { Button } from "../../components/ui/button";
+import { SignInButton } from "../../components/SignInButton";
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
-import { getUserFacingAuthError } from "../../lib/authErrorMessage";
import { getClawHubSiteUrl, normalizeClawHubSiteOrigin } from "../../lib/site";
-import { setAuthError, useAuthError } from "../../lib/useAuthError";
+import { useAuthError } from "../../lib/useAuthError";
import { useAuthStatus } from "../../lib/useAuthStatus";
export const Route = createFileRoute("/cli/auth")({
@@ -17,7 +15,6 @@ export const Route = createFileRoute("/cli/auth")({
function CliAuth() {
const { isAuthenticated, isLoading, me } = useAuthStatus();
- const { signIn } = useAuthActions();
const { error: authError, clear: clearAuthError } = useAuthError();
const createToken = useMutation(api.tokens.create);
@@ -35,7 +32,6 @@ function CliAuth() {
const label =
(decodeLabel(search.label_b64) ?? search.label ?? "CLI token").trim() || "CLI token";
const state = typeof search.state === "string" ? search.state.trim() : "";
- const signInRedirectTo = getCurrentRelativeUrl();
const safeRedirect = useMemo(() => isAllowedRedirectUri(redirectUri), [redirectUri]);
const registry = useMemo(() => {
@@ -139,23 +135,12 @@ function CliAuth() {
) : null}
-
+
@@ -214,8 +199,3 @@ function decodeLabel(value: string | undefined) {
return null;
}
}
-
-function getCurrentRelativeUrl() {
- if (typeof window === "undefined") return "/";
- return `${window.location.pathname}${window.location.search}${window.location.hash}`;
-}
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx
index 27d3aac0c..4e80503ca 100644
--- a/src/routes/dashboard.tsx
+++ b/src/routes/dashboard.tsx
@@ -18,6 +18,7 @@ import { api } from "../../convex/_generated/api";
import type { Doc } from "../../convex/_generated/dataModel";
import { EmptyState } from "../components/EmptyState";
import { Container } from "../components/layout/Container";
+import { SignInButton } from "../components/SignInButton";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { Card, CardContent } from "../components/ui/card";
@@ -120,7 +121,10 @@ function Dashboard() {
return (
- Sign in to access your dashboard.
+
+ Sign in to access your dashboard.
+ Sign in with GitHub
+
);
diff --git a/src/routes/import.tsx b/src/routes/import.tsx
index 95c830209..450bd40d0 100644
--- a/src/routes/import.tsx
+++ b/src/routes/import.tsx
@@ -5,6 +5,7 @@ import { toast } from "sonner";
import { api } from "../../convex/_generated/api";
import { EmptyState } from "../components/EmptyState";
import { Container } from "../components/layout/Container";
+import { SignInButton } from "../components/SignInButton";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { Card } from "../components/ui/card";
@@ -224,7 +225,11 @@ export function ImportGitHub() {
+ >
+ {!isLoading ? (
+ Sign in with GitHub
+ ) : null}
+
);
diff --git a/src/routes/publish-skill.tsx b/src/routes/publish-skill.tsx
index a159ef3b1..ba6848c7a 100644
--- a/src/routes/publish-skill.tsx
+++ b/src/routes/publish-skill.tsx
@@ -1,4 +1,3 @@
-import { useAuthActions } from "@convex-dev/auth/react";
import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
import {
PLATFORM_SKILL_LICENSE,
@@ -14,6 +13,7 @@ import { api } from "../../convex/_generated/api";
import { MAX_PUBLISH_FILE_BYTES, MAX_PUBLISH_TOTAL_BYTES } from "../../convex/lib/publishLimits";
import { EmptyState } from "../components/EmptyState";
import { Container } from "../components/layout/Container";
+import { SignInButton } from "../components/SignInButton";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardTitle } from "../components/ui/card";
@@ -44,7 +44,6 @@ export const Route = createFileRoute("/publish-skill")({
export function Upload() {
const { isAuthenticated, me } = useAuthStatus();
- const { signIn } = useAuthActions();
const { updateSlug } = useSearch({ from: "/publish-skill" });
const siteMode = getSiteMode();
const isSoulMode = siteMode === "souls";
@@ -347,8 +346,9 @@ export function Upload() {
void signIn("github") }}
- />
+ >
+ Sign in with GitHub
+
);
@@ -364,7 +364,7 @@ export function Upload() {
event.preventDefault();
setHasAttempted(true);
if (!validation.ready) {
- if (validationRef.current && "scrollIntoView" in validationRef.current) {
+ if (typeof validationRef.current?.scrollIntoView === "function") {
validationRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
}
return;
diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx
index 16ec70eaa..efa04feff 100644
--- a/src/routes/settings.tsx
+++ b/src/routes/settings.tsx
@@ -9,6 +9,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
+import { SignInButton } from "../components/SignInButton";
import {
Dialog,
DialogContent,
@@ -107,7 +108,10 @@ export function Settings() {
return (
- Sign in to access settings.
+
+ Sign in to access settings.
+ Sign in with GitHub
+
);
diff --git a/src/routes/skills/-SkillsToolbar.tsx b/src/routes/skills/-SkillsToolbar.tsx
index 125ea0945..38a002492 100644
--- a/src/routes/skills/-SkillsToolbar.tsx
+++ b/src/routes/skills/-SkillsToolbar.tsx
@@ -1,6 +1,22 @@
-import { ArrowDownUp, Check, Grid3X3, List, Search, X } from "lucide-react";
+import {
+ ArrowDownUp,
+ Check,
+ Database,
+ GitBranch,
+ Grid3X3,
+ List,
+ MessageSquare,
+ Package,
+ Plug,
+ Search,
+ Shield,
+ Wrench,
+ X,
+ Zap,
+} from "lucide-react";
import type { RefObject } from "react";
-import { SKILL_CAPABILITY_TAGS } from "../../../convex/lib/skillCapabilityTags";
+import { useMemo } from "react";
+import { SKILL_CATEGORIES, type SkillCategory } from "../../lib/categories";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import {
@@ -40,6 +56,17 @@ const SKILL_CAPABILITY_LABELS: Record = {
"posts-externally": "External posting",
};
+const CATEGORY_ICONS: Record = {
+ "mcp-tools": ,
+ prompts: ,
+ workflows: ,
+ "dev-tools": ,
+ data: ,
+ security: ,
+ automation: ,
+ other: ,
+};
+
export function SkillsToolbar({
searchInputRef,
query,
@@ -58,6 +85,26 @@ export function SkillsToolbar({
onToggleDir,
onToggleView,
}: SkillsToolbarProps) {
+ const activeCategory = useMemo(() => {
+ if (query === "__other__") return "other";
+ if (!query) return undefined;
+ return SKILL_CATEGORIES.find((c) =>
+ c.keywords.some((k) => k === query.trim().toLowerCase()),
+ )?.slug;
+ }, [query]);
+
+ const handleCategoryChange = (cat: SkillCategory | undefined) => {
+ if (!cat) {
+ onQueryChange("");
+ } else if (cat.slug === "other") {
+ onQueryChange("__other__");
+ } else if (cat.keywords[0]) {
+ onQueryChange(cat.keywords[0]);
+ } else {
+ onQueryChange("");
+ }
+ };
+
return (
{/* Search row */}
@@ -91,18 +138,30 @@ export function SkillsToolbar({
Clean only
-