Skip to content

Commit 2831f04

Browse files
committed
Polish examples UI and update react package
Refine example apps' UI and add supporting config/code changes. Updates include: change floating footer CTA to "Show Cart" with a cart count badge and cursor styling; enhance reveal-header with a stateful tab switcher to toggle header reveal behaviors, add HeaderNav items, update title/subtitle, add icons, sticky variant selector, and several cursor/class tweaks on interactive buttons. Remove redundant "lint" scripts from apps/docs and apps/examples package.json files. Add new packages/react/eslint.config.js and apply multiple updates to packages/react source files (Header, context, motion, motion-css) and package.json. Update e2e test, TypeScript build info and pnpm lockfile.
1 parent 2fc459c commit 2831f04

13 files changed

Lines changed: 1262 additions & 57 deletions

File tree

apps/docs/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"scripts": {
66
"dev": "next dev --port 3002",
77
"build": "next build",
8-
"start": "next start --port 3002",
9-
"lint": "eslint ."
8+
"start": "next start --port 3002"
109
},
1110
"dependencies": {
1211
"@appshell/react": "workspace:*",

apps/examples/app/floating-footer/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,12 @@ export default function FloatingFooterPage() {
258258

259259
<Footer variant="floating" position="center">
260260
<button
261-
aria-label="Add to cart"
262-
className="flex items-center gap-2.5 rounded-full bg-primary px-7 py-3.5 text-sm font-semibold text-primary-foreground shadow-xl shadow-primary/30 transition-all hover:bg-primary/90 hover:shadow-2xl hover:shadow-primary/40 active:scale-[0.97]"
261+
aria-label="Show cart"
262+
className="flex items-center gap-2.5 rounded-full bg-primary px-7 py-3.5 text-sm font-semibold text-primary-foreground shadow-xl shadow-primary/30 transition-all hover:bg-primary/90 hover:shadow-2xl hover:shadow-primary/40 active:scale-[0.97] cursor-pointer"
263263
>
264264
<ShoppingCart className="size-5" />
265-
<span>Add to Cart</span>
265+
<span>Show Cart</span>
266+
<span className="flex items-center justify-center size-5 rounded-full bg-white/20 text-[11px] font-bold">3</span>
266267
</button>
267268
</Footer>
268269
</AppShell>

apps/examples/app/reveal-header/page.tsx

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"use client";
22

3-
import { AppShell, Header, Content, MotionProvider } from "@appshell/react";
3+
import { useState } from "react";
4+
import { AppShell, Header, Content, HeaderNav, HeaderNavItem, MotionProvider } from "@appshell/react";
45
import { framerMotionAdapter } from "@appshell/react/motion-framer";
5-
import { Search, SlidersHorizontal, Heart, Download, MapPin, Eye } from "lucide-react";
6+
import type { HeaderBehavior } from "@appshell/react";
7+
import { Search, SlidersHorizontal, Heart, Download, MapPin, Eye, Navigation, Type, SearchCheck, Layers } from "lucide-react";
68

79
const photos = [
810
{
@@ -123,18 +125,56 @@ function formatLikes(n: number) {
123125
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
124126
}
125127

128+
const tabs: { label: string; behavior: HeaderBehavior; icon: React.ReactNode; description: string }[] = [
129+
{
130+
label: "Nav",
131+
behavior: "reveal-nav",
132+
icon: <Navigation className="size-3.5" />,
133+
description: "Reveals the nav bar on scroll up",
134+
},
135+
{
136+
label: "Context",
137+
behavior: "reveal-context",
138+
icon: <Type className="size-3.5" />,
139+
description: "Reveals the title & subtitle on scroll up",
140+
},
141+
{
142+
label: "Search",
143+
behavior: "reveal-search",
144+
icon: <SearchCheck className="size-3.5" />,
145+
description: "Reveals the search row on scroll up",
146+
},
147+
{
148+
label: "All",
149+
behavior: "reveal-all",
150+
icon: <Layers className="size-3.5" />,
151+
description: "Reveals all rows on scroll up",
152+
},
153+
];
154+
126155
export default function RevealHeaderPage() {
156+
const [activeIndex, setActiveIndex] = useState(0);
157+
const activeBehavior = tabs[activeIndex].behavior;
158+
127159
return (
128160
<MotionProvider adapter={framerMotionAdapter}>
129161
<AppShell safeArea>
130162
<Header
131-
behavior="reveal-nav"
163+
behavior={activeBehavior}
132164
theme="light"
133165
logo={
134166
<span className="text-lg font-bold tracking-tight">Discover</span>
135167
}
136-
title="Explore"
137-
subtitle="Scroll down to see the header hide, scroll up to reveal it"
168+
nav={
169+
<HeaderNav>
170+
<HeaderNavItem label="Explore" active />
171+
<HeaderNavItem label="Collections" />
172+
<HeaderNavItem label="Photographers" />
173+
<HeaderNavItem label="Trending" />
174+
</HeaderNav>
175+
}
176+
title="Explore Photography"
177+
subtitle="Scroll down to hide the header, then scroll up to see which rows reveal"
138178
searchContent={
139179
<div className="flex items-center gap-2">
140180
<div className="relative flex-1">
@@ -147,7 +187,7 @@ export default function RevealHeaderPage() {
147187
</div>
148188
<button
149189
type="button"
150-
className="flex items-center gap-1.5 rounded-xl border border-gray-200/80 bg-white px-3.5 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-all"
190+
className="flex items-center gap-1.5 rounded-xl border border-gray-200/80 bg-white px-3.5 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-all cursor-pointer"
151191
>
152192
<SlidersHorizontal className="size-4" />
153193
<span className="hidden sm:inline">Filters</span>
@@ -157,12 +197,46 @@ export default function RevealHeaderPage() {
157197
actions={
158198
<button
159199
type="button"
160-
className="rounded-xl bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 transition-colors"
200+
className="rounded-xl bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 transition-colors cursor-pointer"
161201
>
162202
Upload
163203
</button>
164204
}
165205
/>
206+
207+
{/* Sticky tab switcher below header */}
208+
<div className="sticky top-14 z-40 w-full border-b border-gray-100 bg-white/90 backdrop-blur-xl">
209+
<div className="mx-auto max-w-5xl px-4 sm:px-6 py-3">
210+
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar">
211+
<span className="shrink-0 text-xs font-medium text-gray-400 uppercase tracking-wider mr-1">
212+
Reveal variant
213+
</span>
214+
{tabs.map((tab, i) => (
215+
<button
216+
key={tab.behavior}
217+
type="button"
218+
onClick={() => setActiveIndex(i)}
219+
className={`
220+
shrink-0 flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-sm font-medium
221+
transition-all duration-200 cursor-pointer
222+
${
223+
i === activeIndex
224+
? "bg-gray-900 text-white shadow-sm"
225+
: "bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-800"
226+
}
227+
`}
228+
>
229+
{tab.icon}
230+
{tab.label}
231+
</button>
232+
))}
233+
</div>
234+
<p className="mt-1.5 text-xs text-gray-400">
235+
{tabs[activeIndex].description}
236+
</p>
237+
</div>
238+
</div>
239+
166240
<Content className="pb-8">
167241
<div className="mx-auto max-w-5xl px-4 sm:px-6 pt-6">
168242
<div className="columns-1 sm:columns-2 lg:columns-3 gap-4 space-y-4">
@@ -202,14 +276,14 @@ export default function RevealHeaderPage() {
202276
<div className="absolute top-3 right-3 flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-all duration-500 ease-out translate-y-1 group-hover:translate-y-0">
203277
<button
204278
type="button"
205-
className="flex items-center justify-center size-8 rounded-xl bg-white/90 backdrop-blur-sm text-gray-700 hover:bg-white hover:text-red-500 transition-colors shadow-sm"
279+
className="flex items-center justify-center size-8 rounded-xl bg-white/90 backdrop-blur-sm text-gray-700 hover:bg-white hover:text-red-500 transition-colors shadow-sm cursor-pointer"
206280
aria-label="Like"
207281
>
208282
<Heart className="size-4" />
209283
</button>
210284
<button
211285
type="button"
212-
className="flex items-center justify-center size-8 rounded-xl bg-white/90 backdrop-blur-sm text-gray-700 hover:bg-white hover:text-gray-900 transition-colors shadow-sm"
286+
className="flex items-center justify-center size-8 rounded-xl bg-white/90 backdrop-blur-sm text-gray-700 hover:bg-white hover:text-gray-900 transition-colors shadow-sm cursor-pointer"
213287
aria-label="Download"
214288
>
215289
<Download className="size-4" />
@@ -236,7 +310,7 @@ export default function RevealHeaderPage() {
236310
{/* Floating variant indicator */}
237311
<div className="fixed bottom-4 left-4 z-50 flex items-center gap-1.5 rounded-full bg-black/70 backdrop-blur-sm px-3 py-1.5 text-[11px] font-mono text-white/80 shadow-lg">
238312
<span className="size-1.5 rounded-full bg-emerald-400 animate-pulse" />
239-
behavior=&quot;reveal-nav&quot; theme=&quot;light&quot;
313+
behavior=&quot;{activeBehavior}&quot; theme=&quot;light&quot;
240314
</div>
241315
</AppShell>
242316
</MotionProvider>

apps/examples/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"scripts": {
66
"dev": "next dev --port 3001",
77
"build": "next build",
8-
"start": "next start --port 3001",
9-
"lint": "eslint ."
8+
"start": "next start --port 3001"
109
},
1110
"dependencies": {
1211
"@appshell/react": "workspace:*",

apps/examples/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

e2e/footer-variants.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ test.describe("Footer variants", () => {
99
await expect(footer).toBeVisible();
1010

1111
// Scroll down — footer should hide
12-
await page.evaluate(() => window.scrollBy(0, 1200));
13-
await page.waitForTimeout(500);
12+
// Use scrollTo with a large value to ensure we're past the threshold on all viewports
13+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight / 2));
14+
await page.waitForTimeout(600);
1415
await expect(footer).not.toBeVisible();
1516

1617
// Scroll up — footer should reappear
@@ -43,7 +44,7 @@ test.describe("Footer variants", () => {
4344

4445
test("floating cart button is visible", async ({ page }) => {
4546
await page.goto("/floating-footer");
46-
const floatingButton = page.getByLabel("Add to cart");
47+
const floatingButton = page.getByLabel("Show cart");
4748
await expect(floatingButton).toBeVisible();
4849
});
4950

packages/react/eslint.config.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import js from "@eslint/js";
2+
import tseslint from "typescript-eslint";
3+
import reactHooks from "eslint-plugin-react-hooks";
4+
5+
export default tseslint.config(
6+
js.configs.recommended,
7+
...tseslint.configs.recommended,
8+
{
9+
plugins: {
10+
"react-hooks": reactHooks,
11+
},
12+
rules: {
13+
...reactHooks.configs.recommended.rules,
14+
"@typescript-eslint/no-unused-vars": [
15+
"error",
16+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
17+
],
18+
"@typescript-eslint/no-explicit-any": "warn",
19+
},
20+
},
21+
{
22+
ignores: ["dist/", "__tests__/", "scripts/"],
23+
}
24+
);

packages/react/package.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
},
3131
"./styles.css": "./dist/styles.css"
3232
},
33-
"files": ["dist", "README.md", "LICENSE"],
33+
"files": [
34+
"dist",
35+
"README.md",
36+
"LICENSE"
37+
],
3438
"sideEffects": false,
3539
"scripts": {
3640
"build": "tsup",
@@ -53,18 +57,22 @@
5357
}
5458
},
5559
"devDependencies": {
56-
"@testing-library/react": "^16.3.0",
60+
"@eslint/js": "^9.0.0",
5761
"@testing-library/jest-dom": "^6.6.0",
62+
"@testing-library/react": "^16.3.0",
5863
"@types/react": "^19.2.0",
5964
"@types/react-dom": "^19.2.0",
65+
"clsx": "^2.1.0",
66+
"eslint": "^9.0.0",
67+
"eslint-plugin-react-hooks": "^7.0.1",
6068
"framer-motion": "^12.6.0",
6169
"jsdom": "^26.1.0",
6270
"react": "^19.2.0",
6371
"react-dom": "^19.2.0",
6472
"tailwind-merge": "^3.3.0",
65-
"clsx": "^2.1.0",
6673
"tsup": "^8.4.0",
67-
"vitest": "^3.2.0",
68-
"typescript": "^5.7.3"
74+
"typescript": "^5.7.3",
75+
"typescript-eslint": "^8.56.1",
76+
"vitest": "^3.2.0"
6977
}
7078
}

packages/react/src/Header.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import {
44
memo,
5-
type ReactNode,
65
useEffect,
76
useRef,
87
useState,
@@ -11,7 +10,7 @@ import {
1110
import { cn } from "./cn";
1211
import { useMotion } from "./motion";
1312
import { useScrollDirection } from "./hooks/use-scroll-direction";
14-
import type { HeaderProps, HeaderBehavior } from "./types";
13+
import type { HeaderProps } from "./types";
1514

1615
const themeStyles = {
1716
light: {
@@ -117,19 +116,19 @@ export const Header = memo(function Header({
117116

118117
const toggleMobile = useCallback(() => setMobileOpen((o) => !o), []);
119118

120-
const MenuIcon = () => (
119+
const menuIcon = (
121120
<svg className="size-6" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
122121
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
123122
</svg>
124123
);
125124

126-
const CloseIcon = () => (
125+
const closeIcon = (
127126
<svg className="size-6" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
128127
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
129128
</svg>
130129
);
131130

132-
const NavRow = ({ isSticky = false }: { isSticky?: boolean }) => (
131+
const renderNavRow = (isSticky = false) => (
133132
<nav
134133
data-header-nav
135134
className={cn(
@@ -147,7 +146,7 @@ export const Header = memo(function Header({
147146
onClick={toggleMobile}
148147
aria-label={mobileOpen ? "Close menu" : "Open menu"}
149148
>
150-
{mobileOpen ? <CloseIcon /> : <MenuIcon />}
149+
{mobileOpen ? closeIcon : menuIcon}
151150
</button>
152151
)}
153152
{logo}
@@ -158,7 +157,7 @@ export const Header = memo(function Header({
158157
</nav>
159158
);
160159

161-
const ContextRow = () =>
160+
const renderContextRow = () =>
162161
title || subtitle ? (
163162
<div
164163
data-header-context
@@ -178,7 +177,7 @@ export const Header = memo(function Header({
178177
</div>
179178
) : null;
180179

181-
const SearchRow = () =>
180+
const renderSearchRow = () =>
182181
searchContent ? (
183182
<div
184183
data-header-search
@@ -193,7 +192,7 @@ export const Header = memo(function Header({
193192
</div>
194193
) : null;
195194

196-
const MobileMenuPanel = () => (
195+
const renderMobileMenuPanel = () => (
197196
<AnimatePresence>
198197
{mobileMenu && mobileOpen && (
199198
<motion.div
@@ -221,10 +220,10 @@ export const Header = memo(function Header({
221220
className
222221
)}
223222
>
224-
<NavRow />
225-
<ContextRow />
226-
<SearchRow />
227-
<MobileMenuPanel />
223+
{renderNavRow()}
224+
{renderContextRow()}
225+
{renderSearchRow()}
226+
{renderMobileMenuPanel()}
228227
</header>
229228
);
230229
}
@@ -239,10 +238,10 @@ export const Header = memo(function Header({
239238
className
240239
)}
241240
>
242-
<NavRow isSticky={behavior !== "static"} />
243-
<ContextRow />
244-
<SearchRow />
245-
<MobileMenuPanel />
241+
{renderNavRow(behavior !== "static")}
242+
{renderContextRow()}
243+
{renderSearchRow()}
244+
{renderMobileMenuPanel()}
246245
</header>
247246

248247
{hasRevealEffect && (
@@ -260,9 +259,9 @@ export const Header = memo(function Header({
260259
t.wrapper
261260
)}
262261
>
263-
{shouldShowInOverlay("nav") && <NavRow />}
264-
{shouldShowInOverlay("context") && <ContextRow />}
265-
{shouldShowInOverlay("search") && <SearchRow />}
262+
{shouldShowInOverlay("nav") && renderNavRow()}
263+
{shouldShowInOverlay("context") && renderContextRow()}
264+
{shouldShowInOverlay("search") && renderSearchRow()}
266265
</motion.div>
267266
)}
268267
</AnimatePresence>

0 commit comments

Comments
 (0)