2 Commits
Author SHA1 Message Date
openhands 31a89c505d fix(studio): count pages, not items, in organize-imports button
CI / check (push) Successful in 2m25s
CI / deploy (push) Successful in 1m14s
CI / publish-container (push) Successful in 51s
The "Create" CTA used totalSelected (the sum of furni items across
approved groups) as its plural count, so with many imported items it
claimed to create thousands of pages. One approved group creates exactly
one page, so the label now counts approved groups instead.
2026-09-12 18:08:10 +02:00
openhands fd7b29a5f8 feat(site): pro hero with animated ring and floating stat chips
- hero-ring: continuously shifting gradient hairline around the hero
  frame (mask-drawn, reduced-motion safe).
- glass-chip: frosted floating pills under the CTAs that bob on a
  staggered loop, live online counter keeps ticking for the Online chip.
- Taller hero for more presence on desktop.
2026-09-12 18:08:09 +02:00
3 changed files with 143 additions and 2 deletions

No files matched your search

+62 -1
View File
@@ -248,8 +248,9 @@ export default async function Home() {
}}
/>
<AmbientOrbs />
<div className="hero-ring" aria-hidden="true" style={{ zIndex: 3 }} />
<div
className="relative flex flex-col items-center gap-5 px-4 py-10 text-center sm:gap-8 sm:px-6 sm:py-12 md:px-12 md:py-20"
className="relative flex flex-col items-center gap-5 px-4 py-12 text-center sm:gap-8 sm:px-6 sm:py-14 md:px-12 md:py-24"
style={{ zIndex: 2 }}
>
<div
@@ -331,6 +332,66 @@ export default async function Home() {
{th("login")}
</Link>
</div>
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-2.5">
{[
{
value: users,
label: tp("statsCitizens"),
icon: ICON_FRIENDS,
live: false,
},
{
value: rooms,
label: tp("statsRooms"),
icon: ICON_CATALOG,
live: false,
},
{
value: online,
label: tp("statsOnline"),
icon: ICON_NAV_ME,
live: true,
},
].map((s) => (
<div key={s.label} className="glass-chip">
<Image
src={s.icon}
alt=""
width={12}
height={12}
unoptimized
className="opacity-70"
/>
<span
className="glass-chip-dot"
style={{
background: s.live
? "var(--color-primary)"
: "color-mix(in srgb, var(--color-text-muted) 55%, transparent)",
boxShadow: s.live
? "0 0 10px color-mix(in srgb, var(--color-primary) 70%, transparent)"
: "none",
}}
/>
<span
className="glass-chip-value"
style={{
color: "var(--color-text-readable)",
fontSize: "13px",
}}
>
{s.value === null ? (
<span className="text-xs">{te("unavailable")}</span>
) : s.live ? (
<LiveOnlineCounter initial={s.value} />
) : (
<AnimatedCounter value={s.value} />
)}
</span>
<span className="glass-chip-label">{s.label}</span>
</div>
))}
</div>
</div>
</div>
</Reveal>
+73
View File
@@ -2262,4 +2262,77 @@ html {
.brand-halo {
animation: none;
}
.hero-ring,
.glass-chip {
animation: none;
}
}
/* ── Hero pro touches ──────────────────────────────────────────────── */
/* Continuously shifting gradient hairline drawn on the hero border. */
.hero-ring {
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(
120deg,
color-mix(in srgb, var(--color-primary) 70%, transparent) 0%,
color-mix(in srgb, var(--color-accent) 60%, transparent) 35%,
var(--color-surface) 50%,
color-mix(in srgb, var(--color-accent) 60%, transparent) 65%,
color-mix(in srgb, var(--color-primary) 70%, transparent) 100%
);
background-size: 260% 260%;
animation: gradient-shift 14s ease-in-out infinite;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
}
/* Frosted floating stat chips that bob gently on the hero. */
.glass-chip {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.45rem 0.85rem;
border-radius: 999px;
background: color-mix(in srgb, var(--color-surface) 38%, transparent);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent);
box-shadow:
0 10px 26px -10px color-mix(in srgb, #000 30%, transparent),
inset 0 1px 0 0 color-mix(in srgb, #fff 12%, transparent);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
animation: float 5s ease-in-out infinite;
}
.glass-chip:nth-child(2) {
animation-delay: -1.7s;
}
.glass-chip:nth-child(3) {
animation-delay: -3.4s;
}
.glass-chip-dot {
width: 0.45rem;
height: 0.45rem;
border-radius: 999px;
flex-shrink: 0;
}
.glass-chip-value {
font-weight: 900;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.glass-chip-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--color-text-muted);
}
@@ -229,6 +229,13 @@ export function OrganizeImportsDialog({
[groups, approved],
);
// One approved group becomes exactly one page, so the "Create … pages"
// label must count groups, not the furni items inside them.
const selectedGroupsCount = useMemo(
() => groups.filter((g) => approved[g.name]).length,
[groups, approved],
);
const parentOptions = useMemo(() => {
const options: RootPage[] = [];
for (const p of rootPages) {
@@ -980,7 +987,7 @@ export function OrganizeImportsDialog({
)}
{moveMode
? t("move", { count: totalSelected })
: t("create", { count: totalSelected })}
: t("create", { count: selectedGroupsCount })}
</Button>
</div>
</DialogContent>