From f98532f0a0b15d169c0f1c534cc89e68d892b302 Mon Sep 17 00:00:00 2001
From: Bradley Axen
Date: Sat, 18 Jul 2026 23:23:14 -0700
Subject: [PATCH] refine(desktop): interlocking honeycomb for Community Home
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Design refinement pass on the Community Home surface (visual only —
nullable selection, join/create/remove, and navigation are unchanged):
- Rebuild the hex grid as a true interlocking honeycomb: pointy-top
hexes that tessellate, ResizeObserver-driven sizing, and balanced
rows (e.g. 5 tiles render 3+2, not 4+1) instead of a staggered grid
with large gaps.
- Fix hierarchy: communities now read as elevated hero tiles; join and
create are lighter, additive cells rather than the heaviest elements.
- Square the icon tiles, strengthen tile contrast/borders, and use a
clip-path-safe drop shadow for real elevation.
- Polish typography, hover/focus affordances (lift + primary outline +
"Enter" hint), and add ease-out motion with reduced-motion fallbacks.
Co-Authored-By: Claude Opus 4.8 (1M context)
Signed-off-by: Bradley Axen
---
.../features/communities/ui/CommunityHome.tsx | 239 +++++++++++++-----
1 file changed, 178 insertions(+), 61 deletions(-)
diff --git a/desktop/src/features/communities/ui/CommunityHome.tsx b/desktop/src/features/communities/ui/CommunityHome.tsx
index 6583398326..25b6d801a0 100644
--- a/desktop/src/features/communities/ui/CommunityHome.tsx
+++ b/desktop/src/features/communities/ui/CommunityHome.tsx
@@ -5,8 +5,8 @@ import * as React from "react";
import { ThemeGrainientBackground } from "@/app/ThemeGrainientBackground";
import type { Community } from "@/features/communities/types";
import { useCommunityIcons } from "@/features/communities/useCommunityIcons";
+import { useElementWidth } from "@/shared/hooks/use-mobile";
import { getInitials } from "@/shared/lib/initials";
-import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import {
AlertDialog,
@@ -28,41 +28,98 @@ import {
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
const CREATE_COMMUNITY_URL = "https://app.builderlab.xyz/signup?returnTo=/buzz";
+
+// Pointy-top hexagon: flat vertical sides (so hexes in a row touch along their
+// edges) and points at top/bottom (so alternating rows nest into each other).
const HEX_CLIP_PATH =
- "polygon(25% 6.7%, 75% 6.7%, 100% 50%, 75% 93.3%, 25% 93.3%, 0 50%)";
+ "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)";
+
+// Pointy-top geometry, as ratios of the hex width W.
+// height = W / 0.866 (≈ 1.1547·W)
+// row nest = rows overlap vertically by 1/4 of the hex height.
+const HEX_ASPECT = 0.866; // width / height
+const ROW_OVERLAP = 0.2887; // 0.25 · (1 / 0.866) — negative margin between rows
+
+/** Pick a hex width + column cap that keeps regular hexagons at any width. */
+function honeycombLayout(containerWidth: number): {
+ hexWidth: number;
+ maxColumns: number;
+} {
+ if (containerWidth >= 860) return { hexWidth: 176, maxColumns: 4 };
+ if (containerWidth >= 640) return { hexWidth: 156, maxColumns: 4 };
+ if (containerWidth >= 440) return { hexWidth: 140, maxColumns: 3 };
+ return { hexWidth: 118, maxColumns: 2 };
+}
+
+/** Split items into honeycomb rows given how many fit per row. */
+function chunkRows(items: T[], perRow: number): T[][] {
+ const rows: T[][] = [];
+ for (let i = 0; i < items.length; i += perRow) {
+ rows.push(items.slice(i, i + perRow));
+ }
+ return rows;
+}
+
+/** Shared shell: a fixed-width regular hexagon with a hairline edge + fill. */
+function HexShell({
+ children,
+ edgeClassName,
+ fillClassName,
+ width,
+}: {
+ children: React.ReactNode;
+ edgeClassName: string;
+ fillClassName: string;
+ width: number;
+}) {
+ return (
+ <>
+
+
+ {children}
+
+ >
+ );
+}
function CommunityHex({
community,
iconUrl,
onOpen,
onRemove,
+ width,
}: {
community: Community;
iconUrl: string | null;
onOpen: () => void;
onRemove: () => void;
+ width: number;
}) {
return (
@@ -110,34 +167,36 @@ function ActionHex({
icon,
onClick,
testId,
+ width,
}: {
label: string;
detail: string;
icon: React.ReactNode;
onClick: () => void;
testId: string;
+ width: number;
}) {
return (
);
}
@@ -158,6 +217,68 @@ export function CommunityHome({
const iconsByCommunity = useCommunityIcons(communities);
const [communityToRemove, setCommunityToRemove] =
React.useState(null);
+ const [gridRef, gridWidth] = useElementWidth();
+
+ const { hexWidth, maxColumns } = honeycombLayout(gridWidth);
+ // How many hexes fit per row, leaving room for the half-hex stagger.
+ const tileCount = communities.length + 2;
+ const measuredColumns =
+ gridWidth > 0
+ ? Math.floor((gridWidth - hexWidth / 2) / hexWidth)
+ : maxColumns;
+ const columnCap = Math.max(1, Math.min(maxColumns, measuredColumns));
+ // Balance the last row: e.g. 5 tiles in a 4-wide comb becomes 3+2, not 4+1.
+ const rowCount = Math.ceil(tileCount / columnCap);
+ const perRow = Math.max(1, Math.ceil(tileCount / rowCount));
+
+ // Build the honeycomb: community tiles first, then the additive actions.
+ // Each tile carries a stable key so rows can be keyed by content, not index.
+ const tiles: { key: string; node: React.ReactNode }[] = [
+ ...communities.map((community) => ({
+ key: community.id,
+ node: (
+ onOpenCommunity(community.id)}
+ onRemove={() => setCommunityToRemove(community)}
+ width={hexWidth}
+ />
+ ),
+ })),
+ {
+ key: "__join",
+ node: (
+ }
+ key="__join"
+ label="Join a community"
+ onClick={onJoinCommunity}
+ testId="community-home-join"
+ width={hexWidth}
+ />
+ ),
+ },
+ {
+ key: "__create",
+ node: (
+ ✦}
+ key="__create"
+ label="Create a community"
+ onClick={() => void openUrl(CREATE_COMMUNITY_URL)}
+ testId="community-home-create"
+ width={hexWidth}
+ />
+ ),
+ },
+ ];
+
+ const rows = chunkRows(tiles, perRow);
+ const hasStagger = rows.length > 1;
return (
Buzz communities
-
+
Where do you want to go?
@@ -195,42 +316,38 @@ export function CommunityHome({
*:nth-child(even)]:translate-y-[45%] sm:[&>*:nth-child(even)]:translate-y-0",
- "sm:[&>*:nth-child(3n+2)]:translate-y-[45%] md:[&>*:nth-child(3n+2)]:translate-y-0",
- "md:[&>*:nth-child(even)]:translate-y-[45%]",
- )}
+ className="mt-14 flex flex-col items-center"
+ ref={gridRef}
>
- {communities.map((community) => (
- onOpenCommunity(community.id)}
- onRemove={() => setCommunityToRemove(community)}
- />
- ))}
- }
- label="Join a community"
- onClick={onJoinCommunity}
- testId="community-home-join"
- />
- ✦}
- label="Create a community"
- onClick={() => void openUrl(CREATE_COMMUNITY_URL)}
- testId="community-home-create"
- />
+ {rows.map((row, rowIndex) => {
+ const isOffsetRow = rowIndex % 2 === 1;
+ // Nest each row up into the previous one and stagger alternating
+ // rows by half a hex so the pointed edges interlock.
+ const translateX = hasStagger
+ ? isOffsetRow
+ ? hexWidth / 4
+ : -hexWidth / 4
+ : 0;
+ return (
+ tile.key).join("|")}
+ style={{
+ marginTop:
+ rowIndex === 0 ? 0 : -(hexWidth / HEX_ASPECT) * ROW_OVERLAP,
+ transform: `translateX(${translateX}px)`,
+ }}
+ >
+ {row.map((tile) => tile.node)}
+
+ );
+ })}
{communities.length === 0 ? (
-
- This is your community home. It will stay available whenever you
- want a neutral place to join, create, or switch communities.
+
+ This is your community home. It stays available whenever you want a
+ neutral place to join, create, or switch communities.
) : null}