mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b96de5012 | ||
|
|
f44beaa3ab |
@@ -252,7 +252,7 @@ describe("SkillsIndex", () => {
|
||||
});
|
||||
|
||||
const titles = Array.from(
|
||||
document.querySelectorAll(".skills-row-title > span:first-child"),
|
||||
document.querySelectorAll(".skills-table-name > span:first-child"),
|
||||
).map((node) => node.textContent);
|
||||
|
||||
expect(titles[0]).toBe("Older High Score");
|
||||
|
||||
+127
-132
@@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchPluginCatalog, type PackageListItem } from "../../lib/packageApi";
|
||||
import { familyLabel, packageCapabilityLabel } from "../../lib/packageLabels";
|
||||
import { familyLabel } from "../../lib/packageLabels";
|
||||
|
||||
type PluginSearchState = {
|
||||
q?: string;
|
||||
@@ -97,90 +97,91 @@ export function PluginsIndex() {
|
||||
Plugins
|
||||
</h1>
|
||||
<p className="section-subtitle" style={{ marginBottom: 0 }}>
|
||||
Code plugins and bundle plugins for OpenClaw.
|
||||
Browse the plugin catalog.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="card" style={{ display: "grid", gap: 12, marginBottom: 18 }}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
style={{ display: "grid", gap: 12 }}
|
||||
>
|
||||
<form
|
||||
className="skills-toolbar"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="skills-search">
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Search plugins"
|
||||
className="skills-search-input"
|
||||
placeholder="Search plugins…"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
||||
<select
|
||||
className="input"
|
||||
value={search.family ?? ""}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value as PluginSearchState["family"] | "";
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
family: value || undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">All plugins</option>
|
||||
<option value="code-plugin">Code plugins</option>
|
||||
<option value="bundle-plugin">Bundle plugins</option>
|
||||
</select>
|
||||
<label className="tag" style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={search.verified ?? false}
|
||||
onChange={(event) => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
verified: event.target.checked || undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
Verified only
|
||||
</label>
|
||||
<label className="tag" style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={search.executesCode ?? false}
|
||||
onChange={(event) => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
executesCode: event.target.checked || undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
Executes code
|
||||
</label>
|
||||
<Link className="btn" to="/plugins/new">
|
||||
Publish Plugin
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skills-toolbar-row">
|
||||
<select
|
||||
className="skills-sort"
|
||||
value={search.family ?? ""}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value as PluginSearchState["family"] | "";
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
family: value || undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
aria-label="Filter by type"
|
||||
>
|
||||
<option value="">All plugins</option>
|
||||
<option value="code-plugin">Code plugins</option>
|
||||
<option value="bundle-plugin">Bundle plugins</option>
|
||||
</select>
|
||||
<button
|
||||
className="search-filter-button"
|
||||
type="button"
|
||||
aria-pressed={search.verified ?? false}
|
||||
onClick={() => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
verified: prev.verified ? undefined : true,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Verified
|
||||
</button>
|
||||
<button
|
||||
className="search-filter-button"
|
||||
type="button"
|
||||
aria-pressed={search.executesCode ?? false}
|
||||
onClick={() => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
q: query.trim() || undefined,
|
||||
executesCode: prev.executesCode ? undefined : true,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Executes code
|
||||
</button>
|
||||
<Link className="btn btn-primary" to="/plugins/new">
|
||||
Publish
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="card">No plugins match that filter.</div>
|
||||
@@ -192,73 +193,67 @@ export function PluginsIndex() {
|
||||
key={item.name}
|
||||
to="/plugins/$name"
|
||||
params={{ name: item.name }}
|
||||
className="skill-card"
|
||||
className="card skill-card"
|
||||
>
|
||||
<div className="skill-card-tags">
|
||||
<span className="tag">{familyLabel(item.family)}</span>
|
||||
{item.executesCode ? (
|
||||
<span className="tag tag-accent">
|
||||
{packageCapabilityLabel(item.family, item.executesCode)}
|
||||
<span className="tag tag-compact">{familyLabel(item.family)}</span>
|
||||
{item.isOfficial ? (
|
||||
<span className="tag tag-compact tag-accent">
|
||||
<VerifiedBadge /> Verified
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="skill-card-title">{item.displayName}</div>
|
||||
<div className="skills-row-slug">{item.name}</div>
|
||||
<div className="skill-card-summary">
|
||||
<h3 className="skill-card-title">{item.displayName}</h3>
|
||||
<p className="skill-card-summary">
|
||||
{item.summary ?? "No summary provided."}
|
||||
</div>
|
||||
<div className="skill-card-footer skill-card-footer-inline">
|
||||
<div className="stat">
|
||||
{item.ownerHandle ? `by ${item.ownerHandle}` : "community plugin"}
|
||||
{item.latestVersion ? ` · v${item.latestVersion}` : ""}
|
||||
</div>
|
||||
{item.isOfficial ? <VerifiedBadge /> : null}
|
||||
</p>
|
||||
<div className="skill-card-footer" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span className="stat">
|
||||
{item.ownerHandle ? `by ${item.ownerHandle}` : "community"}
|
||||
</span>
|
||||
{item.latestVersion ? (
|
||||
<span className="stat">v{item.latestVersion}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{!search.q && (search.cursor || nextCursor) ? (
|
||||
<div
|
||||
className="card"
|
||||
style={{ display: "flex", gap: 12, flexWrap: "wrap", justifyContent: "space-between", marginTop: 18 }}
|
||||
style={{ display: "flex", gap: 12, justifyContent: "center", marginTop: 22 }}
|
||||
>
|
||||
<div className="section-subtitle" style={{ margin: 0 }}>
|
||||
Browsing {items.length} plugin{items.length === 1 ? "" : "s"} per page.
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
||||
{search.cursor ? (
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
First page
|
||||
</button>
|
||||
) : null}
|
||||
{nextCursor ? (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: nextCursor,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Next page
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{search.cursor ? (
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: undefined,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
First page
|
||||
</button>
|
||||
) : null}
|
||||
{nextCursor ? (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
cursor: nextCursor,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Next page
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -78,49 +78,44 @@ export function SkillsResults({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="skills-list">
|
||||
<div className="skills-table">
|
||||
<div className="skills-table-header">
|
||||
<span>Skill</span>
|
||||
<span>Summary</span>
|
||||
<span>Author</span>
|
||||
<span className="skills-table-stats">Stats</span>
|
||||
</div>
|
||||
{sorted.map((entry) => {
|
||||
const skill = entry.skill;
|
||||
const clawdis = entry.latestVersion?.parsed?.clawdis;
|
||||
const isPlugin = Boolean(clawdis?.nix?.plugin);
|
||||
const platforms = getPlatformLabels(clawdis?.os, clawdis?.nix?.systems);
|
||||
const ownerHandle = entry.owner?.handle ?? entry.ownerHandle ?? null;
|
||||
const skillHref = buildSkillHref(skill, ownerHandle);
|
||||
return (
|
||||
<Link key={skill._id} className="skills-row" to={skillHref}>
|
||||
<div className="skills-row-main">
|
||||
<div className="skills-row-title">
|
||||
<span>{skill.displayName}</span>
|
||||
<span className="skills-row-slug">/{skill.slug}</span>
|
||||
<Link key={skill._id} className="skills-table-row" to={skillHref}>
|
||||
<span className="skills-table-name">
|
||||
<span>
|
||||
{skill.displayName}
|
||||
{getSkillBadges(skill).map((badge) => (
|
||||
<span key={badge} className="tag">
|
||||
{badge}
|
||||
</span>
|
||||
<span key={badge} className="tag tag-compact">{badge}</span>
|
||||
))}
|
||||
{isPlugin ? (
|
||||
<span className="tag tag-accent tag-compact">Plugin bundle (nix)</span>
|
||||
) : null}
|
||||
{platforms.map((label) => (
|
||||
<span key={label} className="tag tag-compact">
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="skills-row-summary">
|
||||
{skill.summary ?? "No summary provided."}
|
||||
</div>
|
||||
<div className="skills-row-owner">
|
||||
<UserBadge
|
||||
user={entry.owner}
|
||||
fallbackHandle={ownerHandle}
|
||||
prefix="by"
|
||||
link={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skills-row-metrics">
|
||||
</span>
|
||||
{entry.latestVersion?.version ? (
|
||||
<span className="skills-table-version">v{entry.latestVersion.version}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="skills-table-summary">
|
||||
{skill.summary ?? "No summary provided."}
|
||||
</span>
|
||||
<span className="skills-table-author">
|
||||
<UserBadge
|
||||
user={entry.owner}
|
||||
fallbackHandle={ownerHandle}
|
||||
prefix=""
|
||||
link={false}
|
||||
/>
|
||||
</span>
|
||||
<span className="skills-table-stats">
|
||||
<SkillMetricsRow stats={skill.stats} />
|
||||
</div>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
+143
-2
@@ -248,6 +248,30 @@ code {
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.theme-toggle button {
|
||||
all: unset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--ink-soft);
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.theme-toggle button[data-state="on"] {
|
||||
opacity: 1;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.user-trigger {
|
||||
@@ -288,6 +312,7 @@ code {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
@@ -1217,12 +1242,14 @@ code {
|
||||
appearance: none;
|
||||
border: 1px solid var(--border-ui);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 10px 14px;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
padding: 10px 20px 10px 14px;
|
||||
background: rgba(255, 255, 255, 0.45) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%236b5549' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 7px center;
|
||||
background-size: 10px;
|
||||
color: var(--ink);
|
||||
font-weight: 650;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .skills-sort {
|
||||
@@ -1270,6 +1297,119 @@ code {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.skills-table {
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.skills-table-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1fr) minmax(0, 2fr) 120px 140px;
|
||||
gap: 12px;
|
||||
padding: 10px 18px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 650;
|
||||
color: var(--ink-soft);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(42, 31, 25, 0.03);
|
||||
}
|
||||
|
||||
.skills-table-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1fr) minmax(0, 2fr) 120px 140px;
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-bottom: 1px solid var(--line);
|
||||
transition: background 0.15s ease;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skills-table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.skills-table-row:hover {
|
||||
background: rgba(255, 107, 74, 0.07);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .skills-table-row:hover {
|
||||
background: rgba(232, 106, 71, 0.11);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .skills-table-header {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.skills-table-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.skills-table-name > span:first-child {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.skills-table-summary {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.skills-table-author {
|
||||
font-size: 0.85rem;
|
||||
color: var(--ink-soft);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skills-table-stats {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skills-table-version {
|
||||
font-size: 0.85rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.skills-table-header,
|
||||
.skills-table-row {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) 100px;
|
||||
}
|
||||
.skills-table-stats {
|
||||
display: none;
|
||||
}
|
||||
.skills-table-header span:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.skills-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -1386,6 +1526,7 @@ code {
|
||||
|
||||
.skill-card {
|
||||
min-height: 176px;
|
||||
box-shadow: 0 1px 3px rgba(42, 31, 25, 0.08);
|
||||
}
|
||||
|
||||
.skill-card-tags {
|
||||
|
||||
Reference in New Issue
Block a user