mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 16:38:49 +00:00
style(webui): refine skills mobile experience
This commit is contained in:
parent
655cd80651
commit
9b5a5c5302
@ -146,7 +146,12 @@ export function SkillsCatalogSettings({ skills }: { skills: SkillSummary[] }) {
|
||||
className="h-9 rounded-[11px] bg-background pl-9 text-[13px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-[10px] bg-muted/65 p-1">
|
||||
<div
|
||||
className={cn(
|
||||
"flex max-w-full items-center gap-1 overflow-x-auto rounded-[10px] bg-muted/65 p-1",
|
||||
"scrollbar-thin scrollbar-track-transparent sm:w-auto",
|
||||
)}
|
||||
>
|
||||
{([
|
||||
["all", t("settings.skills.filterAll", { defaultValue: "All" }), skills.length],
|
||||
[
|
||||
@ -165,7 +170,7 @@ export function SkillsCatalogSettings({ skills }: { skills: SkillSummary[] }) {
|
||||
type="button"
|
||||
onClick={() => setInstalledFilter(filter)}
|
||||
className={cn(
|
||||
"rounded-[8px] px-2.5 py-1 text-[11px] font-medium transition-colors",
|
||||
"shrink-0 whitespace-nowrap rounded-[8px] px-2.5 py-1 text-[11px] font-medium transition-colors",
|
||||
installedFilter === filter
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
@ -314,6 +319,7 @@ function SkillDetailSheet({
|
||||
const [actionError, setActionError] = useState("");
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !skill) return;
|
||||
@ -323,6 +329,7 @@ function SkillDetailSheet({
|
||||
setLoadFailed(false);
|
||||
setActionError("");
|
||||
setDeleteOpen(false);
|
||||
setDescriptionExpanded(false);
|
||||
fetchSkillDetail(token, skill.name)
|
||||
.then((payload) => {
|
||||
if (!cancelled) setDetail(payload);
|
||||
@ -349,6 +356,7 @@ function SkillDetailSheet({
|
||||
: activeSkill.available
|
||||
? t("settings.skills.statusEnabled", { defaultValue: "Enabled" })
|
||||
: t("settings.skills.statusNeedsSetup", { defaultValue: "Needs setup" });
|
||||
const descriptionExpandable = activeSkill.description.length > 240;
|
||||
|
||||
const toggleEnabled = async () => {
|
||||
setActionBusy(true);
|
||||
@ -399,12 +407,25 @@ function SkillDetailSheet({
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-[min(34rem,calc(100vw-1rem))] max-w-none gap-0 overflow-hidden p-0 sm:max-w-none"
|
||||
closeButtonClassName={cn(
|
||||
"right-2 top-2 inline-flex h-10 w-10 items-center justify-center rounded-full opacity-100",
|
||||
"text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
||||
"sm:right-3 sm:top-3",
|
||||
)}
|
||||
className={cn(
|
||||
"w-full max-w-none gap-0 overflow-hidden border-l-0 p-0",
|
||||
"sm:w-[min(34rem,calc(100vw-1rem))] sm:max-w-none sm:border-l",
|
||||
)}
|
||||
>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-5 sm:py-5",
|
||||
"pb-[max(1rem,env(safe-area-inset-bottom))]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3 pr-8">
|
||||
<div className="min-w-0 flex-1">
|
||||
<SheetTitle className="truncate text-[20px] font-semibold">
|
||||
<SheetTitle className="truncate text-[19px] font-semibold sm:text-[20px]">
|
||||
{activeSkill.name}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
@ -427,9 +448,26 @@ function SkillDetailSheet({
|
||||
{statusLabel}
|
||||
</Pill>
|
||||
</div>
|
||||
<p className="mt-3 text-[13px] leading-5 text-muted-foreground">
|
||||
<p
|
||||
className={cn(
|
||||
"mt-3 text-[13px] leading-5 text-muted-foreground",
|
||||
descriptionExpandable && !descriptionExpanded && "line-clamp-5",
|
||||
)}
|
||||
>
|
||||
{activeSkill.description}
|
||||
</p>
|
||||
{descriptionExpandable ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={descriptionExpanded}
|
||||
onClick={() => setDescriptionExpanded((value) => !value)}
|
||||
className="mt-1.5 min-h-8 rounded-full text-[12px] font-medium text-foreground/70 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{descriptionExpanded
|
||||
? t("settings.skills.showLess", { defaultValue: "Show less" })
|
||||
: t("settings.skills.showMore", { defaultValue: "Show more" })}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -444,7 +482,7 @@ function SkillDetailSheet({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 space-y-5">
|
||||
<div className="flex items-center justify-between gap-4 border-y border-border/45 px-1 py-3.5">
|
||||
<div className="flex min-h-16 items-start justify-between gap-3 border-y border-border/45 px-1 py-3.5">
|
||||
<div>
|
||||
<p className="text-[13px] font-medium text-foreground">
|
||||
{t("settings.skills.enabledControl", { defaultValue: "Use this skill" })}
|
||||
@ -467,15 +505,20 @@ function SkillDetailSheet({
|
||||
disabled={actionBusy}
|
||||
onClick={() => void toggleEnabled()}
|
||||
className={cn(
|
||||
"relative h-6 w-11 shrink-0 rounded-full transition-colors",
|
||||
"relative -mr-1 h-10 w-14 shrink-0 rounded-full",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
"disabled:cursor-wait disabled:opacity-60",
|
||||
enabled ? "bg-foreground" : "bg-muted-foreground/30",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-background shadow-sm transition-transform",
|
||||
"absolute left-1.5 top-2 h-6 w-11 rounded-full transition-colors",
|
||||
enabled ? "bg-foreground" : "bg-muted-foreground/30",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute left-2 top-2.5 h-5 w-5 rounded-full bg-background shadow-sm transition-transform",
|
||||
enabled ? "translate-x-5" : "translate-x-0",
|
||||
)}
|
||||
/>
|
||||
@ -498,7 +541,7 @@ function SkillDetailSheet({
|
||||
{detail ? <RawInstructionsBlock markdown={detail.raw_markdown} /> : null}
|
||||
|
||||
{deletable ? (
|
||||
<div className="flex items-center justify-between gap-4 border-t border-border/45 pt-5">
|
||||
<div className="flex flex-col items-stretch gap-3 border-t border-border/45 pt-5 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<div>
|
||||
<p className="text-[13px] font-medium text-foreground">
|
||||
{t("settings.skills.deleteTitle", { defaultValue: "Delete skill" })}
|
||||
@ -511,11 +554,14 @@ function SkillDetailSheet({
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={actionBusy}
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
className="shrink-0 rounded-full"
|
||||
className={cn(
|
||||
"h-10 w-full shrink-0 rounded-full border-destructive/20 text-destructive",
|
||||
"hover:bg-destructive/8 hover:text-destructive sm:h-9 sm:w-auto",
|
||||
)}
|
||||
>
|
||||
<Trash2 className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||
{t("settings.skills.deleteAction", { defaultValue: "Delete" })}
|
||||
@ -571,7 +617,7 @@ function RawInstructionsBlock({ markdown }: { markdown: string }) {
|
||||
|
||||
return (
|
||||
<details className="group rounded-[18px] border border-border/45 bg-muted/20 px-3 py-3">
|
||||
<summary className="flex cursor-pointer select-none items-center justify-between gap-3 text-[13px] font-medium text-foreground/90 transition-colors hover:text-foreground">
|
||||
<summary className="flex min-h-11 cursor-pointer select-none items-center justify-between gap-3 text-[13px] font-medium text-foreground/90 transition-colors hover:text-foreground">
|
||||
<span>
|
||||
{t("settings.skills.instructionsTitle", { defaultValue: "Skill instructions" })}
|
||||
</span>
|
||||
@ -657,7 +703,7 @@ function RequirementsSection({
|
||||
})}
|
||||
title={option.label}
|
||||
onClick={() => void copyCommand(option.command)}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[8px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[9px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-7 sm:w-7"
|
||||
>
|
||||
{copiedCommand === option.command ? (
|
||||
<Check className="h-3.5 w-3.5 text-emerald-600" aria-hidden />
|
||||
@ -689,7 +735,7 @@ function RequirementsSection({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRefresh}
|
||||
className="mt-3 h-8 rounded-full bg-background/60 px-3 text-[11px]"
|
||||
className="mt-3 h-9 rounded-full bg-background/60 px-3 text-[11px]"
|
||||
>
|
||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||
{t("settings.skills.checkAgain", { defaultValue: "Check again" })}
|
||||
|
||||
@ -198,7 +198,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
|
||||
|
||||
{query.trim().length < 2 ? (
|
||||
<section className="overflow-hidden rounded-[22px] bg-settings-surface">
|
||||
<div className="flex items-center justify-between border-b border-border/45 px-5 py-4">
|
||||
<div className="flex flex-col items-start gap-2 border-b border-border/45 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
||||
<div>
|
||||
<h2 className="text-[14px] font-semibold">
|
||||
{t("settings.skills.marketplaceTrendingTitle", {
|
||||
@ -371,9 +371,9 @@ function MarketplaceSkillRow({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-3 px-2 py-3.5">
|
||||
<div className="flex min-w-0 items-center gap-2 px-1 py-3.5 sm:gap-3 sm:px-2">
|
||||
{skill.rank ? (
|
||||
<span className="w-7 shrink-0 text-right font-mono text-[12px] tabular-nums text-muted-foreground/65">
|
||||
<span className="w-6 shrink-0 text-right font-mono text-[11px] tabular-nums text-muted-foreground/65 sm:w-7 sm:text-[12px]">
|
||||
#{skill.rank}
|
||||
</span>
|
||||
) : null}
|
||||
@ -390,7 +390,7 @@ function MarketplaceSkillRow({
|
||||
name: skill.name,
|
||||
defaultValue: "Open {{name}} on skills.sh",
|
||||
})}
|
||||
className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
||||
</a>
|
||||
@ -418,7 +418,10 @@ function MarketplaceSkillRow({
|
||||
variant={installed ? "secondary" : "default"}
|
||||
disabled={installed || installBusy || installSupported === false}
|
||||
onClick={() => onSelect(skill)}
|
||||
className={cn("min-w-[92px] rounded-full", installed && "text-emerald-700")}
|
||||
className={cn(
|
||||
"min-w-[82px] rounded-full px-2.5 sm:min-w-[92px] sm:px-3",
|
||||
installed && "text-emerald-700",
|
||||
)}
|
||||
title={
|
||||
installSupported === false
|
||||
? t("settings.skills.marketplaceNpxRequired", {
|
||||
@ -512,7 +515,7 @@ function TrendingSkeleton() {
|
||||
<div className="h-3.5 w-48 max-w-[55%] animate-pulse rounded bg-muted" />
|
||||
<div className="h-3 w-32 max-w-[40%] animate-pulse rounded bg-muted/70" />
|
||||
</div>
|
||||
<div className="h-8 w-[92px] animate-pulse rounded-full bg-muted" />
|
||||
<div className="h-8 w-[82px] animate-pulse rounded-full bg-muted sm:w-[92px]" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -57,13 +57,24 @@ const sheetVariants = cva(
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
closeButtonClassName?: string;
|
||||
showCloseButton?: boolean;
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, showCloseButton = true, ...props }, ref) => (
|
||||
>((
|
||||
{
|
||||
side = "right",
|
||||
className,
|
||||
children,
|
||||
closeButtonClassName,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<DialogPrimitive.Content
|
||||
@ -78,7 +89,14 @@ const SheetContent = React.forwardRef<
|
||||
>
|
||||
{children}
|
||||
{showCloseButton ? (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity",
|
||||
"hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"disabled:pointer-events-none",
|
||||
closeButtonClassName,
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@ -378,6 +378,12 @@ describe("App layout", () => {
|
||||
});
|
||||
|
||||
it("opens Skills from the main sidebar", async () => {
|
||||
const longSkillDescription = [
|
||||
"Work with GitHub repositories, issues, pull requests, releases, workflows,",
|
||||
"and code search through the GitHub CLI.",
|
||||
"Use this skill for repository maintenance, review automation, release preparation,",
|
||||
"and other GitHub workflows that need authenticated command-line access.",
|
||||
].join(" ");
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/settings/cli-apps": { apps: [], installed_count: 0, catalog_updated_at: "2026-04-18" },
|
||||
@ -413,7 +419,7 @@ describe("App layout", () => {
|
||||
},
|
||||
"/api/webui/skills/github": {
|
||||
name: "github",
|
||||
description: "Work with GitHub.",
|
||||
description: longSkillDescription,
|
||||
source: "builtin",
|
||||
enabled: true,
|
||||
deletable: false,
|
||||
@ -501,6 +507,13 @@ describe("App layout", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Open details for github" }));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "github" })).toBeInTheDocument();
|
||||
const showMore = await screen.findByRole("button", { name: "Show more" });
|
||||
expect(showMore).toHaveAttribute("aria-expanded", "false");
|
||||
fireEvent.click(showMore);
|
||||
expect(screen.getByRole("button", { name: "Show less" })).toHaveAttribute(
|
||||
"aria-expanded",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByText("Setup required")).toBeInTheDocument();
|
||||
expect(screen.getByText("brew install gh")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Unavailable reason")).not.toBeInTheDocument();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user