Compare commits

...
Author SHA1 Message Date
Val Alexander e5be24fd13 fix: refine header and about responsiveness 2026-04-13 13:01:18 -05:00
4 changed files with 412 additions and 131 deletions
+7
View File
@@ -113,6 +113,7 @@ describe("Header", () => {
it("renders direct desktop theme family controls and plain Skills tab", () => {
siteModeMock.mockReturnValue("skills");
setThemeMock.mockClear();
setModeMock.mockClear();
render(<Header />);
@@ -127,6 +128,12 @@ describe("Header", () => {
fireEvent.click(screen.getByRole("button", { name: "Hub" }));
expect(setThemeMock).toHaveBeenCalledWith("hub");
fireEvent.click(screen.getByRole("button", { name: /Cycle theme family/i }));
expect(setThemeMock).toHaveBeenCalledWith("claw");
fireEvent.click(screen.getByRole("button", { name: /Cycle theme mode/i }));
expect(setModeMock).toHaveBeenCalledWith("light");
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
expect(screen.getAllByText("Skills")).toHaveLength(2);
+56 -1
View File
@@ -40,6 +40,13 @@ const NAV_ICONS: Record<NavIconName, ComponentType<{ size?: number; className?:
ghost: Ghost,
};
const THEME_FAMILY_ICONS: Record<string, ComponentType<{ size?: number; className?: string }>> = {
claw: Ghost,
hub: Plug,
};
const THEME_MODE_SEQUENCE: Array<"system" | "light" | "dark"> = ["system", "light", "dark"];
export default function Header() {
const { isAuthenticated, isLoading, me } = useAuthStatus();
const { signIn, signOut } = useAuthActions();
@@ -70,6 +77,8 @@ export default function Header() {
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const themeLabel = THEME_OPTIONS.find((option) => option.value === theme)?.label ?? "Claw";
const ThemeFamilyIcon = THEME_FAMILY_ICONS[theme] ?? Wrench;
const ThemeModeIcon = getThemeModeIcon(mode);
const setThemeMode = (next: "system" | "light" | "dark") => {
startThemeTransition({
@@ -89,6 +98,21 @@ export default function Header() {
setTheme(nextTheme);
};
const cycleThemeFamily = () => {
const currentIndex = Math.max(
0,
THEME_OPTIONS.findIndex((option) => option.value === theme),
);
const nextTheme = THEME_OPTIONS[(currentIndex + 1) % THEME_OPTIONS.length]?.value ?? "claw";
setThemeFamily(nextTheme);
};
const cycleThemeMode = () => {
const currentIndex = Math.max(0, THEME_MODE_SEQUENCE.indexOf(mode));
const nextMode = THEME_MODE_SEQUENCE[(currentIndex + 1) % THEME_MODE_SEQUENCE.length] ?? "system";
setThemeMode(nextMode);
};
const handleNavSearch = (e: React.FormEvent) => {
e.preventDefault();
const q = navSearchQuery.trim();
@@ -237,7 +261,6 @@ export default function Header() {
</button>
<div className="theme-toggle" ref={toggleRef}>
<div className="theme-picker-desktop" aria-label={`Theme family, current ${themeLabel}`}>
<span className="theme-picker-label">Theme</span>
<div className="theme-family-toggle" role="group" aria-label="Theme family">
{THEME_OPTIONS.map((option) => (
<button
@@ -253,6 +276,26 @@ export default function Header() {
))}
</div>
</div>
<div className="theme-cycle-group" aria-label="Theme controls">
<button
type="button"
className="theme-cycle-button theme-cycle-button-family"
onClick={cycleThemeFamily}
aria-label={`Cycle theme family. Current: ${themeLabel}`}
title={`Theme family: ${themeLabel}`}
>
<ThemeFamilyIcon className="h-4 w-4" aria-hidden="true" />
</button>
<button
type="button"
className="theme-cycle-button theme-cycle-button-mode"
onClick={cycleThemeMode}
aria-label={`Cycle theme mode. Current: ${mode}`}
title={`Theme mode: ${mode}`}
>
<ThemeModeIcon className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<ToggleGroup
className="theme-mode-toggle"
type="single"
@@ -410,3 +453,15 @@ function getCurrentRelativeUrl() {
if (typeof window === "undefined") return "/";
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
}
function getThemeModeIcon(mode: "system" | "light" | "dark") {
switch (mode) {
case "light":
return Sun;
case "dark":
return Moon;
case "system":
default:
return Monitor;
}
}
+95 -79
View File
@@ -84,88 +84,104 @@ export const Route = createFileRoute('/about')({
function AboutPage() {
return (
<main className="section about-page">
<div className="about-hero">
<div className="about-hero-copy">
<div className="skill-card-tags mb-3">
<Badge>About</Badge>
<Badge variant="accent">Policy</Badge>
<div className="about-bento">
<section className="about-panel about-panel-hero">
<div className="about-hero-copy">
<div className="skill-card-tags mb-3">
<Badge>About</Badge>
<Badge variant="accent">Policy</Badge>
</div>
<h1 className="about-title">What ClawHub will not host</h1>
<p className="about-lead">
ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to
evade defenses, scam people, invade privacy, or enable non-consensual behavior, it
does not belong here.
</p>
</div>
<h1 className="about-title">What ClawHub will not host</h1>
<p className="about-lead">
ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to evade
defenses, scam people, invade privacy, or enable non-consensual behavior, it does not
belong here.
</section>
<aside className="about-panel about-panel-callout">
<div className="about-callout">
<span className="about-callout-label">Moderation stance</span>
<p>
We judge end-to-end abuse patterns, not keyword theater. Useful tooling stays.
Predatory workflows get removed.
</p>
</div>
</aside>
<section className="about-panel about-panel-categories">
<div className="home-section-header">
<h2 className="home-section-title">Immediate rejection categories</h2>
</div>
<div className="about-grid">
{prohibitedCategories.map((category, index) => (
<article
key={category.title}
className={`about-rule-card${index % 3 === 0 ? ' about-rule-card-featured' : ''}`}
>
<h2>{category.title}</h2>
<p>{category.examples}</p>
</article>
))}
</div>
</section>
<section className="about-panel about-panel-patterns">
<div className="home-section-header">
<h2 className="home-section-title">Recent patterns we are explicitly not okay with</h2>
</div>
<div className="about-patterns">
{recentPatterns.map((pattern) => (
<div key={pattern} className="about-pattern">
{pattern}
</div>
))}
</div>
</section>
<section className="about-panel about-panel-enforcement">
<div>
<span className="about-callout-label">Enforcement</span>
<div className="management-sublist">
<div className="management-subitem">
We may hide, remove, or hard-delete violating skills.
</div>
<div className="management-subitem">
We may revoke tokens, soft-delete associated content, and ban repeat or severe
offenders.
</div>
<div className="management-subitem">
We do not guarantee warning-first enforcement for obvious abuse.
</div>
</div>
</div>
</section>
<section className="about-panel about-panel-actions">
<span className="about-callout-label">Next steps</span>
<p className="about-panel-copy">
If you are reviewing a borderline workflow, use the reviewer doc. If you are browsing,
stay in the public catalog.
</p>
</div>
<div className="about-callout">
<span className="about-callout-label">Moderation stance</span>
<p>
We judge end-to-end abuse patterns, not keyword theater. Useful tooling stays.
Predatory workflows get removed.
</p>
</div>
<div className="skill-card-tags">
<Button asChild variant="primary">
<Link to="/skills">
Browse Skills
</Link>
</Button>
<Button asChild>
<a
href="https://github.com/openclaw/clawhub/blob/main/docs/acceptable-usage.md"
target="_blank"
rel="noreferrer"
>
Reviewer Doc
</a>
</Button>
</div>
</section>
</div>
<section className="about-section">
<div className="home-section-header">
<h2 className="home-section-title">Immediate rejection categories</h2>
</div>
<div className="about-grid">
{prohibitedCategories.map((category) => (
<article key={category.title} className="about-rule-card">
<h2>{category.title}</h2>
<p>{category.examples}</p>
</article>
))}
</div>
</section>
<section className="about-section">
<div className="home-section-header">
<h2 className="home-section-title">Recent patterns we are explicitly not okay with</h2>
</div>
<div className="about-patterns">
{recentPatterns.map((pattern) => (
<div key={pattern} className="about-pattern">
{pattern}
</div>
))}
</div>
</section>
<section className="about-enforcement">
<div>
<span className="about-callout-label">Enforcement</span>
<div className="management-sublist">
<div className="management-subitem">
We may hide, remove, or hard-delete violating skills.
</div>
<div className="management-subitem">
We may revoke tokens, soft-delete associated content, and ban repeat or severe
offenders.
</div>
<div className="management-subitem">
We do not guarantee warning-first enforcement for obvious abuse.
</div>
</div>
</div>
<div className="skill-card-tags">
<Button asChild variant="primary">
<Link to="/skills">
Browse Skills
</Link>
</Button>
<Button asChild>
<a
href="https://github.com/openclaw/clawhub/blob/main/docs/acceptable-usage.md"
target="_blank"
rel="noreferrer"
>
Reviewer Doc
</a>
</Button>
</div>
</section>
</main>
);
}
+254 -51
View File
@@ -356,14 +356,14 @@
}
[data-theme-family="hub"] .navbar-top {
gap: 12px;
padding: 8px 0 10px;
gap: 16px;
padding: 12px 0;
}
[data-theme-family="hub"] .navbar-search {
max-width: 400px;
height: 32px;
border-radius: var(--r-sm);
max-width: none;
height: 38px;
border-radius: var(--r-md);
}
[data-theme-family="hub"] .navbar-search-mobile-trigger {
@@ -399,14 +399,14 @@
[data-theme-family="hub"] .brand {
font-family: var(--font-mono);
font-size: var(--fs-base);
font-size: var(--fs-md);
font-weight: 600;
text-transform: lowercase;
}
[data-theme-family="hub"] .brand-mark {
width: 24px;
height: 24px;
width: 32px;
height: 32px;
border-radius: var(--r-sm);
}
@@ -709,15 +709,17 @@ code {
}
.navbar-top {
display: flex;
display: grid;
grid-template-columns: minmax(148px, auto) minmax(320px, 1fr) minmax(320px, auto);
align-items: center;
gap: 16px;
padding: 12px 0;
min-height: 62px;
}
.navbar-search {
flex: 1;
max-width: 420px;
width: 100%;
max-width: none;
display: flex;
align-items: center;
gap: var(--space-2);
@@ -873,10 +875,13 @@ code {
align-items: center;
gap: 10px;
color: var(--ink);
min-width: 0;
line-height: 1;
}
.brand-name {
display: inline-flex;
min-width: 6.5ch;
}
.brand-mark {
@@ -1003,16 +1008,59 @@ code {
}
.nav-actions {
margin-left: auto;
display: flex;
gap: 8px;
align-items: center;
justify-self: end;
min-width: 0;
}
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
min-width: 276px;
justify-content: flex-end;
}
.theme-cycle-group {
display: none;
align-items: center;
gap: 8px;
width: 72px;
min-width: 72px;
grid-template-columns: repeat(2, 32px);
justify-content: end;
}
.theme-cycle-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--line);
border-radius: var(--r-pill);
background: var(--surface);
color: var(--ink-soft);
cursor: pointer;
transition:
border-color 0.15s ease,
background 0.15s ease,
color 0.15s ease,
box-shadow 0.15s ease;
}
.theme-cycle-button:hover {
color: var(--ink);
border-color: var(--border-ui-hover);
background: var(--surface-muted);
}
.theme-cycle-button:focus-visible {
outline: 2px solid var(--input-focus-border);
outline-offset: 2px;
}
.theme-mode-toggle button {
@@ -3956,20 +4004,29 @@ code {
}
.navbar-top {
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: 8px 0;
gap: 10px;
min-height: 0;
}
.navbar-search {
display: none;
display: flex;
order: 2;
flex: 1 1 0;
min-width: 0;
max-width: none;
height: 36px;
}
.navbar-search-mobile-trigger {
display: inline-flex;
display: none;
}
.navbar-search-mobile {
display: flex;
display: none;
}
.navbar-tabs {
@@ -3998,12 +4055,59 @@ code {
.nav-mobile {
display: inline-flex;
order: 1;
flex: 0 0 auto;
}
.brand {
display: none;
}
.theme-toggle {
display: none;
}
.nav-actions {
order: 3;
margin-left: auto;
flex: 0 0 auto;
gap: 0;
}
.nav-actions .btn {
width: 36px;
min-width: 36px;
height: 36px;
min-height: 36px;
padding: 0;
border-radius: var(--r-pill);
}
.sign-in-label,
.sign-in-provider {
display: none;
}
.user-trigger {
width: 36px;
min-width: 36px;
height: 36px;
padding: 0;
justify-content: center;
border-radius: var(--r-pill);
}
.user-trigger img,
.user-menu-fallback {
width: 24px;
height: 24px;
}
.user-trigger .mono,
.user-menu-chevron {
display: none;
}
.hero {
padding: 58px 18px 44px;
}
@@ -4080,11 +4184,6 @@ code {
}
@media (max-width: 520px) {
.brand {
font-size: 1.25rem;
gap: 0;
}
.nav-actions {
gap: 10px;
}
@@ -4095,13 +4194,13 @@ code {
min-height: 44px;
}
.sign-in-provider {
display: none;
}
.user-trigger .mono,
.user-menu-chevron {
display: none;
.nav-actions .btn,
.user-trigger {
width: 36px;
min-width: 36px;
height: 36px;
min-height: 36px;
padding: 0;
}
.search-bar {
@@ -7127,12 +7226,15 @@ html.theme-transition::view-transition-new(theme) {
gap: 24px;
}
.about-hero,
.about-enforcement {
.about-bento {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr);
grid-template-columns: 1fr;
gap: var(--space-5);
align-items: start;
}
.about-panel {
display: grid;
gap: var(--space-4);
padding: var(--space-5);
border: 1px solid var(--line);
border-radius: var(--r-xl);
@@ -7141,6 +7243,18 @@ html.theme-transition::view-transition-new(theme) {
color-mix(in srgb, var(--surface-muted) 58%, transparent);
}
.about-panel-hero,
.about-panel-enforcement,
.about-panel-actions {
align-content: start;
}
.about-panel-copy {
margin: 0;
color: var(--ink-soft);
line-height: 1.65;
}
.about-hero-copy {
display: flex;
flex-direction: column;
@@ -7186,12 +7300,7 @@ html.theme-transition::view-transition-new(theme) {
color: var(--ink-soft);
}
.about-section {
display: grid;
gap: var(--space-4);
}
.about-section .home-section-title {
.about-panel .home-section-title {
font-size: var(--fs-lg);
font-weight: 600;
color: var(--ink);
@@ -7201,7 +7310,7 @@ html.theme-transition::view-transition-new(theme) {
.about-patterns,
.souls-coming-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
grid-template-columns: 1fr;
gap: 14px;
}
@@ -7256,18 +7365,86 @@ html.theme-transition::view-transition-new(theme) {
gap: 20px;
}
@media (max-width: 860px) {
.about-hero,
.about-enforcement {
grid-template-columns: 1fr;
@media (min-width: 641px) {
.about-bento {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.about-callout {
order: -1;
.about-panel-hero {
grid-column: span 4;
}
.about-panel-callout {
grid-column: span 2;
}
.about-panel-categories,
.about-panel-patterns {
grid-column: 1 / -1;
}
.about-panel-enforcement,
.about-panel-actions {
grid-column: span 3;
}
.about-grid {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.about-rule-card {
grid-column: span 2;
}
.about-rule-card-featured {
grid-column: span 3;
}
.about-patterns {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.about-bento {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.about-panel-hero {
grid-column: span 8;
}
.about-panel-callout {
grid-column: span 4;
}
.about-panel-enforcement {
grid-column: span 7;
}
.about-panel-actions {
grid-column: span 5;
}
.about-grid {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.about-rule-card {
grid-column: span 4;
}
.about-rule-card-featured {
grid-column: span 6;
}
.about-patterns {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.about-bento,
.about-grid,
.about-patterns,
.souls-coming-grid {
@@ -7321,12 +7498,14 @@ html.theme-transition::view-transition-new(theme) {
align-items: center;
gap: 6px;
flex-shrink: 0;
min-width: 132px;
min-height: 32px;
padding: 4px 6px 4px 10px;
padding: 4px 6px;
border: 1px solid var(--line);
border-radius: var(--r-pill);
background: var(--surface);
white-space: nowrap;
overflow: hidden;
}
.theme-picker-label {
@@ -7339,20 +7518,24 @@ html.theme-transition::view-transition-new(theme) {
}
.theme-family-toggle {
display: inline-flex;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: center;
gap: 4px;
gap: 6px;
width: 112px;
flex-wrap: nowrap;
min-width: 0;
}
.theme-family-button {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
min-width: 0;
width: 100%;
height: 24px;
min-width: 0;
padding: 0 8px;
border-radius: 999px;
color: var(--ink-soft);
@@ -7379,15 +7562,35 @@ html.theme-transition::view-transition-new(theme) {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 34%, transparent);
}
[data-theme-family="hub"] .theme-picker-desktop,
[data-theme-family="hub"] .theme-family-button {
border-radius: 0;
}
.theme-mode-toggle {
min-width: 104px;
justify-content: center;
}
@media (max-width: 1100px) {
.theme-picker-label {
display: none;
}
}
@media (max-width: 1100px) and (min-width: 761px) {
.theme-picker-desktop,
.theme-mode-toggle {
display: none;
}
.theme-cycle-group {
display: grid;
}
}
@media (max-width: 900px) {
.theme-picker-desktop {
display: none;
}
}