From c52b8be76b9facee37e44feb8a8817092d9c157a Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Tue, 14 Apr 2026 00:59:04 +0800 Subject: [PATCH 1/2] docs(repo): add contributor guidelines --- AGENTS.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..068c41d3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# Repository Guidelines + +## Project Structure & Module Organization +This repository is a Next.js 16 app using the App Router and TypeScript. Primary code lives in `src/`: routes in `src/app`, reusable UI in `src/components`, server/client integrations in `src/services`, shared hooks in `src/hooks`, and utility helpers in `src/lib` and `src/utils`. GraphQL operations and generated types live under `src/schema` and `src/gql`. Static files are in `public/`, Jest setup and mocks are in `test/`, and parser-focused tests also live beside source in `src/store/clippings/__tests__`. + +## Build, Test, and Development Commands +Use `pnpm` for all local work. + +- `pnpm dev`: run the app locally on port `3101` with Turbo mode. +- `pnpm build`: run GraphQL codegen, then create a production build. +- `pnpm start`: serve the built app with `PORT`. +- `pnpm test`: run the Jest suite in `jest-environment-jsdom`. +- `pnpm test:update`: refresh snapshots, for example `parser.test.ts.snap`. +- `pnpm lint` / `pnpm lint:fix`: check or fix issues with `oxlint`. +- `pnpm format` / `pnpm format:check`: apply or verify formatting with `oxfmt`. +- `pnpm codegen`: regenerate GraphQL client artifacts after schema or `.graphql` changes. + +## Coding Style & Naming Conventions +Follow the existing TypeScript + React style: 2-space indentation, single quotes, and semicolons omitted. Prefer PascalCase for React components (`AuthPageShell.tsx`), camelCase for utilities and hooks (`use-screen-size.ts` is an existing exception), and route folders that match URL structure such as `src/app/dash/[userid]/...`. Keep generated files like `src/schema/generated.tsx` and `src/gql/*` out of manual edits. + +## Testing Guidelines +Jest collects coverage from `src/**/*.{js,ts,jsx,tsx}` except generated schema output. Name tests `*.test.ts` or place them in `__tests__/`. Keep fast unit tests close to the code when practical, and use `test/setup.ts` plus `test/mocks/` for shared setup. Run `pnpm test` before opening a PR; run `pnpm test:update` only when snapshot changes are intentional. + +## Commit & Pull Request Guidelines +Recent history favors Conventional Commit-style messages such as `refactor(auth): extract AuthPageShell...` and `refactor(ui): ...`. Use a clear type/scope prefix like `feat(dash):`, `fix(upload):`, or `refactor(search):`. PRs should explain the user-visible change, note any schema or config updates, link the issue when relevant, and include screenshots for UI work in `src/app` or `src/components`. + +## Configuration Tips +Runtime behavior depends on environment variables such as `PORT`, `GIT_COMMIT`, `NEXT_PUBLIC_PP_TOKEN`, and Redis/telemetry values used in deployment. Avoid committing secrets, and document any new env var in the PR description. From 7bdc3a102b4060be541e47f8454f4e663e10a386 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Tue, 14 Apr 2026 01:48:06 +0800 Subject: [PATCH 2/2] style(navbar): polish sizing for compact and consistent layout Standardize all interactive elements to h-8 baseline, shrink logo from 44px to 32px, remove slogan line, replace "More" text button with ellipsis icon, strip heavy card wrapper from profile area, and reduce avatar from 40px to 28px. Also fix pre-existing TS errors for dynamic href strings. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/navigation-bar/authed.tsx | 304 ++++++++---------- src/components/navigation-bar/avatar.tsx | 8 +- .../navigation-bar/navigation-bar.module.css | 22 -- .../navigation-bar/navigation-bar.tsx | 177 +++++----- 4 files changed, 243 insertions(+), 268 deletions(-) delete mode 100644 src/components/navigation-bar/navigation-bar.module.css diff --git a/src/components/navigation-bar/authed.tsx b/src/components/navigation-bar/authed.tsx index f54f99ec..eb233fd4 100644 --- a/src/components/navigation-bar/authed.tsx +++ b/src/components/navigation-bar/authed.tsx @@ -1,32 +1,28 @@ 'use client' import Tooltip from '@annatarhe/lake-ui/tooltip' -import { LogOut, Search, Settings, Smartphone, User } from 'lucide-react' +import { Ellipsis, LogOut, QrCode, Search, Settings, User } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useId, useRef, useState } from 'react' import toast from 'react-hot-toast' import { checkIsPremium } from '@/compute/user' import { useTranslation } from '@/i18n/client' -import { cn } from '@/utils/cn' +import { cn } from '@/lib/utils' import profile from '@/utils/profile' -import LinkIndicator from '../link-indicator' import AvatarOnNavigationBar from './avatar' import { onCleanServerCookie } from './logout' -// Custom Dropdown component type DropdownProps = { - trigger: React.ReactNode children: React.ReactNode } -const Dropdown = ({ trigger, children }: DropdownProps) => { +function Dropdown({ children }: DropdownProps) { const [isOpen, setIsOpen] = useState(false) const dropdownRef = useRef(null) - - const toggleDropdown = () => setIsOpen(!isOpen) + const menuId = useId() useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -38,48 +34,86 @@ const Dropdown = ({ trigger, children }: DropdownProps) => { } } + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) + document.addEventListener('keydown', handleEscape) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + document.removeEventListener('keydown', handleEscape) + } }, []) return (
-
{trigger}
- - {isOpen && ( + + + {isOpen ? (
- -
{children}
- )} + ) : null}
) } -// Custom Divider component -const Divider = ({ className = '' }: { className?: string }) => ( -
-) +type MenuItemProps = { + href?: string + onClick?: () => void + tone?: 'default' | 'danger' + icon: React.ReactNode + children: React.ReactNode +} + +function MenuItem(props: MenuItemProps) { + const { href, onClick, tone = 'default', icon, children } = props + const className = cn( + 'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm transition-colors', + tone === 'danger' + ? 'text-red-600 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-500/10' + : 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-900' + ) + + const iconClassName = cn( + 'shrink-0', + tone === 'danger' + ? 'text-red-500 dark:text-red-300' + : 'text-slate-500 dark:text-slate-400' + ) + + if (href) { + return ( + + {icon} + {children} + + ) + } + + return ( + + ) +} type LoggedNavigationBarProps = { profile: { @@ -106,132 +140,80 @@ function LoggedNavigationBar(props: LoggedNavigationBarProps) { }, [router, t]) return ( - + {t('app.menu.settings')} + + + } + > + {t('app.menu.loginByQRCode.title')} + + +
+ + } + > + {t('app.menu.logout.title')} + +
+ + + ) } diff --git a/src/components/navigation-bar/avatar.tsx b/src/components/navigation-bar/avatar.tsx index fa8d1d54..ef54315c 100644 --- a/src/components/navigation-bar/avatar.tsx +++ b/src/components/navigation-bar/avatar.tsx @@ -17,8 +17,10 @@ function AvatarOnNavigationBar(props: AvatarOnNavigationBarProps) { return (
avatar
) diff --git a/src/components/navigation-bar/navigation-bar.module.css b/src/components/navigation-bar/navigation-bar.module.css deleted file mode 100644 index d6b159c1..00000000 --- a/src/components/navigation-bar/navigation-bar.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.navbar { -} -.navbar + div { - flex: 1; -} - -.navbar::before { - content: ''; - /* width: calc(90% + 10px); */ - height: 100%; - position: absolute; - top: 0; - left: 0; - z-index: -1; - background-color: rgba(272, 242, 242, 0.5); - /* background-color: blur-background; */ - filter: blur(5px); - transform: scale(1.1); - left: -5px; - top: -5px; - overflow: hidden; -} diff --git a/src/components/navigation-bar/navigation-bar.tsx b/src/components/navigation-bar/navigation-bar.tsx index 2f9851ee..c2034ac4 100644 --- a/src/components/navigation-bar/navigation-bar.tsx +++ b/src/components/navigation-bar/navigation-bar.tsx @@ -14,38 +14,29 @@ import { cn } from '@/lib/utils' import type { ProfileQuery } from '@/schema/generated' import { getUserSlug } from '@/utils/profile.utils' -import LinkIndicator from '../link-indicator' -import { useCtrlP } from '../searchbar/hooks' import SearchBar from '../searchbar/searchbar' import LoggedNavigationBar from './authed' import LoginByQRCode from './login-by-qrcode' -import styles from './navigation-bar.module.css' - const leftMenu = [ { - emoji: () => , + icon: BookOpen, alt: 'read', dest: (id: number | string) => `/dash/${id}/home`, targetSegment: 'home', }, { - emoji: () => , + icon: LayoutGrid, alt: 'square', dest: (id: number | string) => `/dash/${id}/square`, targetSegment: 'square', }, { - emoji: () => , + icon: Upload, alt: 'upload', dest: (id: number | string) => `/dash/${id}/upload`, targetSegment: 'upload', }, - // { - // emoji: () => , - // alt: 'my', - // dest: (id: number | string) => `/dash/${id}/profile`, - // }, ] type NavigationBarProps = { @@ -54,83 +45,105 @@ type NavigationBarProps = { function NavigationBar(props: NavigationBarProps) { const { myProfile: profile } = props - - const { visible, setVisible } = useCtrlP() - const onSearchbarClose = useCallback(() => { - setVisible(false) - }, [setVisible]) - + const { t } = useTranslation(undefined, 'navigation') + const activeSegment = useSelectedLayoutSegment() + const [searchVisible, setSearchVisible] = useState(false) const [loginByQRCodeModalVisible, setLoginByQRCodeModalVisible] = useState(false) - const { t } = useTranslation(undefined, 'navigation') - // const homeLink = getMyHomeLink(profile) - const activeSegment = useSelectedLayoutSegment() + const onSearchbarClose = useCallback(() => { + setSearchVisible(false) + }, []) + + const profileSlug = profile ? getUserSlug(profile) : null + const brandHref = profileSlug ? `/dash/${profileSlug}/home` : '/' return ( - + - + ) }