From af43dab0ad7d21f37d83f1445effdfdc8ebe8333 Mon Sep 17 00:00:00 2001 From: Marco de Jongh Date: Thu, 25 Dec 2025 13:10:04 +1100 Subject: [PATCH] feat: Add comprehensive UI polish with cyan theme and design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create design token system with consistent colors, shadows, spacing, and typography - Add Ant Design theme configuration with ConfigProvider - Create new Logo component with climbing board icon - Update all components to use theme tokens instead of hardcoded colors - Add smooth transitions, hover effects, and card shadows - Improve global CSS with scrollbar styling, focus states, and animations - Use cyan color palette (#06B6D4) to match climbing board aesthetics - Prevent text selection on cards and horizontal scrolling issues - Update logbook stats to use Ant Design components - Fix saved configurations layout with CSS Grid for 2 cards per row 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 81 ++++---- .../[angle]/list/layout-client.module.css | 4 + .../components/board-page/angle-selector.tsx | 5 +- .../web/app/components/board-page/header.tsx | 19 +- .../components/board-page/share-button.tsx | 30 +-- packages/web/app/components/brand/logo.tsx | 84 ++++++++ .../app/components/climb-card/climb-card.tsx | 23 ++- packages/web/app/components/index.css | 189 ++++++++++++++++++ .../loading/animated-board-loading.tsx | 14 +- .../app/components/logbook/logbook-stats.tsx | 88 ++++---- .../queue-control/queue-control-bar.tsx | 13 +- .../queue-control/queue-list-item.tsx | 18 +- .../setup-wizard/board-config-preview.tsx | 8 +- .../consolidated-board-config.tsx | 40 ++-- packages/web/app/layout.tsx | 7 +- packages/web/app/theme/antd-theme.ts | 135 +++++++++++++ packages/web/app/theme/theme-config.ts | 126 ++++++++++++ 17 files changed, 746 insertions(+), 138 deletions(-) create mode 100644 packages/web/app/components/brand/logo.tsx create mode 100644 packages/web/app/theme/antd-theme.ts create mode 100644 packages/web/app/theme/theme-config.ts diff --git a/package-lock.json b/package-lock.json index 8ceec9e2..3386c6c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2986,7 +2986,7 @@ "version": "3.9.3", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@pkgr/core": { @@ -4395,7 +4395,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5861,7 +5861,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -6795,7 +6795,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -7947,7 +7947,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/gel/-/gel-2.2.0.tgz", "integrity": "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@petamoriken/float16": "^3.8.7", @@ -7968,7 +7968,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "devOptional": true, + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -7978,7 +7978,7 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7991,7 +7991,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -9713,7 +9713,7 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -10240,7 +10240,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -10566,6 +10566,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10585,6 +10586,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -10980,6 +10982,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, "license": "MIT" }, "node_modules/scroll-into-view-if-needed": { @@ -11201,7 +11204,7 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11866,7 +11869,7 @@ "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -11889,12 +11892,12 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -11906,12 +11909,12 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -11923,12 +11926,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -11940,12 +11943,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -11957,12 +11960,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -11974,12 +11977,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -11991,12 +11994,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -12008,12 +12011,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -12025,12 +12028,12 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12042,12 +12045,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12059,12 +12062,12 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12076,12 +12079,12 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12093,12 +12096,12 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12110,12 +12113,12 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12127,12 +12130,12 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12144,12 +12147,12 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12161,12 +12164,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -12178,12 +12181,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -12195,12 +12198,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -12212,12 +12215,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -12229,12 +12232,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -12246,12 +12249,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -12263,12 +12266,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -12280,12 +12283,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -12297,12 +12300,12 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -12314,12 +12317,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -12328,7 +12331,7 @@ "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.module.css b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.module.css index ef9a5b65..d4f036d3 100644 --- a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.module.css +++ b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.module.css @@ -2,6 +2,8 @@ background: transparent; height: 100%; position: relative; + overflow-x: hidden; + max-width: 100%; } .mainContent { @@ -9,6 +11,8 @@ padding: 0; margin-right: 0; transition: margin-right 0.2s; + overflow-x: hidden; + max-width: 100%; } .sider { diff --git a/packages/web/app/components/board-page/angle-selector.tsx b/packages/web/app/components/board-page/angle-selector.tsx index 06c160a8..8e710706 100644 --- a/packages/web/app/components/board-page/angle-selector.tsx +++ b/packages/web/app/components/board-page/angle-selector.tsx @@ -8,6 +8,7 @@ import useSWR from 'swr'; import { ANGLES } from '@/app/lib/board-data'; import { BoardName, Climb } from '@/app/lib/types'; import { ClimbStatsForAngle } from '@/app/lib/data/queries'; +import { themeTokens } from '@/app/theme/theme-config'; const { Text, Title } = Typography; @@ -131,8 +132,8 @@ export default function AngleSelector({ boardName, currentAngle, currentClimb }: hoverable onClick={() => handleAngleChange(angle)} style={{ - backgroundColor: angle === currentAngle ? '#e6f7ff' : undefined, - borderColor: angle === currentAngle ? '#1890ff' : undefined, + backgroundColor: angle === currentAngle ? themeTokens.semantic.selected : undefined, + borderColor: angle === currentAngle ? themeTokens.colors.primary : undefined, minHeight: currentClimb && !isLoading ? (hasStats ? '160px' : '60px') : '60px', }} > diff --git a/packages/web/app/components/board-page/header.tsx b/packages/web/app/components/board-page/header.tsx index 1706d817..769bacb5 100644 --- a/packages/web/app/components/board-page/header.tsx +++ b/packages/web/app/components/board-page/header.tsx @@ -2,8 +2,6 @@ import React from 'react'; import { Flex, Button, Dropdown, MenuProps } from 'antd'; import { Header } from 'antd/es/layout/layout'; -import Title from 'antd/es/typography/Title'; -import Link from 'next/link'; import { useSession, signIn, signOut } from 'next-auth/react'; import SearchButton from '../search-drawer/search-button'; import SearchClimbNameInput from '../search-drawer/search-climb-name-input'; @@ -16,7 +14,9 @@ import { useBoardProvider } from '../board-provider/board-provider-context'; import { useQueueContext } from '../graphql-queue'; import { UserOutlined, LogoutOutlined, LoginOutlined, PlusOutlined, MoreOutlined } from '@ant-design/icons'; import AngleSelector from './angle-selector'; +import Logo from '../brand/logo'; import styles from './header.module.css'; +import Link from 'next/link'; type BoardSeshHeaderProps = { boardDetails: BoardDetails; @@ -60,23 +60,20 @@ export default function BoardSeshHeader({ boardDetails, angle }: BoardSeshHeader ]; return (
- + {/* Logo - Fixed to left */} - - - <Link href="/" style={{ textDecoration: 'none', color: 'inherit' }}> - BS - </Link> - + + {/* Center Section - Mobile only */} diff --git a/packages/web/app/components/board-page/share-button.tsx b/packages/web/app/components/board-page/share-button.tsx index 35920362..ca634308 100644 --- a/packages/web/app/components/board-page/share-button.tsx +++ b/packages/web/app/components/board-page/share-button.tsx @@ -15,6 +15,7 @@ import { usePartyContext } from '../party-manager/party-context'; import { useBackendUrl } from '../connection-manager/connection-settings-context'; import { useQueueContext } from '../graphql-queue'; import { BackendSetupPanel } from './backend-setup-panel'; +import { themeTokens } from '@/app/theme/theme-config'; const { Text } = Typography; @@ -109,16 +110,16 @@ export const ShareBoardButton = () => {
🎮
- + Board Controller Connected
@@ -146,7 +147,7 @@ export const ShareBoardButton = () => { {/* Connecting */} {isConnecting && ( - + Connecting to backend... {backendUrl} @@ -162,14 +163,14 @@ export const ShareBoardButton = () => { gap="small" style={{ padding: '12px', - background: '#f6ffed', - border: '1px solid #b7eb8f', - borderRadius: '6px', + background: themeTokens.colors.successBg, + border: `1px solid ${themeTokens.colors.success}`, + borderRadius: themeTokens.borderRadius.md, }} > - +
- + Connected to Backend
@@ -198,9 +199,12 @@ export const ShareBoardButton = () => { justify="space-between" align="center" style={{ - background: user.id === currentUserId ? '#e6f7ff' : '#f5f5f5', + background: + user.id === currentUserId + ? themeTokens.semantic.selected + : themeTokens.neutral[100], padding: '8px 12px', - borderRadius: '8px', + borderRadius: themeTokens.borderRadius.md, width: '100%', }} > @@ -211,7 +215,7 @@ export const ShareBoardButton = () => {
{user.isLeader && ( - + )} ))} diff --git a/packages/web/app/components/brand/logo.tsx b/packages/web/app/components/brand/logo.tsx new file mode 100644 index 00000000..198219b6 --- /dev/null +++ b/packages/web/app/components/brand/logo.tsx @@ -0,0 +1,84 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { themeTokens } from '@/app/theme/theme-config'; + +type LogoProps = { + size?: 'sm' | 'md' | 'lg'; + showText?: boolean; + linkToHome?: boolean; +}; + +const sizes = { + sm: { icon: 24, fontSize: 14, gap: 6 }, + md: { icon: 28, fontSize: 16, gap: 8 }, + lg: { icon: 36, fontSize: 20, gap: 10 }, +}; + +export const Logo = ({ size = 'md', showText = true, linkToHome = true }: LogoProps) => { + const { icon, fontSize, gap } = sizes[size]; + + const logoContent = ( +
+ + {/* Board background with rounded corners */} + + + {/* Climbing holds pattern - arranged like a real board */} + {/* Top row */} + + + + {/* Middle section */} + + + + + {/* Bottom row */} + + + + {showText && ( + + BoardSesh + + )} +
+ ); + + if (linkToHome) { + return ( + + {logoContent} + + ); + } + + return logoContent; +}; + +export default Logo; diff --git a/packages/web/app/components/climb-card/climb-card.tsx b/packages/web/app/components/climb-card/climb-card.tsx index b3f495f7..cd60aa66 100644 --- a/packages/web/app/components/climb-card/climb-card.tsx +++ b/packages/web/app/components/climb-card/climb-card.tsx @@ -7,6 +7,7 @@ import { CopyrightOutlined } from '@ant-design/icons'; import ClimbCardCover from './climb-card-cover'; import { Climb, BoardDetails } from '@/app/lib/types'; import ClimbCardActions from './climb-card-actions'; +import { themeTokens } from '@/app/theme/theme-config'; type ClimbCardProps = { climb?: Climb; @@ -23,17 +24,21 @@ const ClimbCard = ({ climb, boardDetails, onCoverClick, selected, actions }: Cli const cardTitle = climb ? (
{/* LEFT: Name, Angle, Benchmark */} -
+
{climb.name} @ {climb.angle}° - {climb.benchmark_difficulty !== null && } + {climb.benchmark_difficulty !== null && ( + + )}
{/* RIGHT: Difficulty, Quality */} -
+
{climb.difficulty && climb.quality_average && climb.quality_average !== '0' ? ( `${climb.difficulty} ★${climb.quality_average}` ) : ( - project + + project + )}
@@ -45,11 +50,15 @@ const ClimbCard = ({ climb, boardDetails, onCoverClick, selected, actions }: Cli - {/* TODO: Make a link to the list with the setter_name filter */} - {climb ? `By ${climb.setter_username} - ${climb.ascensionist_count} ascents` : null} +
+ {climb ? `By ${climb.setter_username} - ${climb.ascensionist_count} ascents` : null} +
{cover}
); diff --git a/packages/web/app/components/index.css b/packages/web/app/components/index.css index 8c4a1264..bccefb0b 100644 --- a/packages/web/app/components/index.css +++ b/packages/web/app/components/index.css @@ -1,7 +1,29 @@ +/* BoardSesh Global Styles */ + +/* Base styles */ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #F9FAFB; + color: #1F2937; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; +} + +/* Prevent horizontal overflow */ +html, body { + max-width: 100vw; + overflow-x: hidden; +} + +/* Prevent horizontal scroll on layout components */ +.ant-layout, +.ant-layout-content, +.ant-row { + max-width: 100%; + overflow-x: hidden; } /* Prevent double-tap zoom globally while preserving pinch-to-zoom */ @@ -9,6 +31,107 @@ html { touch-action: manipulation; } +/* Smooth transitions for interactive elements */ +a, +button, +.ant-btn, +.ant-card, +.ant-input, +.ant-select, +.ant-select-selector { + transition: all 0.2s ease; +} + +/* Card hover enhancement */ +.ant-card { + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.ant-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* Prevent text selection on interactive cards */ +.ant-card { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Button hover polish - subtle lift effect */ +.ant-btn:not(:disabled):not(.ant-btn-text):hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.ant-btn:not(:disabled):active { + transform: translateY(0); +} + +/* Selected state styling utility class */ +.selected-item { + background-color: #ECFEFF !important; + border-color: #06B6D4 !important; +} + +/* Custom scrollbar for webkit browsers */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #F3F4F6; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #D1D5DB; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #9CA3AF; +} + +/* Firefox scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: #D1D5DB #F3F4F6; +} + +/* Focus styles for accessibility */ +:focus-visible { + outline: 2px solid #06B6D4; + outline-offset: 2px; +} + +/* Remove default focus outline when using mouse */ +:focus:not(:focus-visible) { + outline: none; +} + +/* Header shadow utility */ +.header-shadow { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +/* Bottom bar shadow utility */ +.bottom-bar-shadow { + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08); +} + +/* Drawer transitions */ +.ant-drawer-content-wrapper { + transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1) !important; +} + +/* Improve modal animations */ +.ant-modal { + animation-duration: 0.2s; +} + /* Ensure all interactive elements have minimum 16px font to prevent iOS auto-zoom on focus */ @media (max-width: 768px) { input, @@ -29,4 +152,70 @@ html { .ant-dropdown-trigger { font-size: 16px !important; } + + /* Slightly reduce card hover effect on mobile for performance */ + .ant-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transform: none; + } + + /* Disable button lift on mobile */ + .ant-btn:not(:disabled):hover { + transform: none; + } +} + +/* Loading skeleton animation improvement */ +.ant-skeleton-content .ant-skeleton-title, +.ant-skeleton-content .ant-skeleton-paragraph > li { + background: linear-gradient(90deg, #F3F4F6 25%, #E5E7EB 37%, #F3F4F6 63%); + background-size: 400% 100%; + animation: skeleton-loading 1.4s ease infinite; +} + +@keyframes skeleton-loading { + 0% { + background-position: 100% 50%; + } + 100% { + background-position: 0 50%; + } +} + +/* Tag styling enhancement */ +.ant-tag { + border-radius: 4px; + font-weight: 500; +} + +/* Rate/star styling */ +.ant-rate-star { + transition: transform 0.15s ease; +} + +.ant-rate-star:hover { + transform: scale(1.1); +} + +/* Collapse panel styling */ +.ant-collapse-header { + font-weight: 500 !important; +} + +/* Badge styling */ +.ant-badge-count { + font-weight: 600; + box-shadow: 0 0 0 2px #fff; +} + +/* Override Atlaskit drag-and-drop indicator color to match theme */ +[data-drop-indicator-edge] { + --indicator-color: #06B6D4 !important; + background-color: #06B6D4 !important; +} + +/* Alternative selector for drop indicator line */ +.pragmatic-drop-indicator, +[class*="drop-indicator"] { + background-color: #06B6D4 !important; } diff --git a/packages/web/app/components/loading/animated-board-loading.tsx b/packages/web/app/components/loading/animated-board-loading.tsx index 448a0047..29959b50 100644 --- a/packages/web/app/components/loading/animated-board-loading.tsx +++ b/packages/web/app/components/loading/animated-board-loading.tsx @@ -5,6 +5,7 @@ import { Typography } from 'antd'; import BoardRenderer from '../board-renderer/board-renderer'; import { BoardDetails } from '@/app/lib/types'; import { LitUpHoldsMap } from '../board-renderer/types'; +import { themeTokens } from '@/app/theme/theme-config'; const { Text } = Typography; @@ -43,7 +44,8 @@ const AnimatedBoardLoading: React.FC = ({ isVisible, const sweepWidth = 60; // 60 degree sweep arc const holdsMap: LitUpHoldsMap = {}; - const colors = ['#4ECDC4', '#45B7D1', '#96CEB4']; + // Use theme colors for the animation - primary, secondary, and success for variety + const colors = [themeTokens.colors.primary, themeTokens.colors.secondary, themeTokens.colors.success]; for (const hold of boardDetails.holdsData) { // Calculate angle from center (in degrees, 0-360) @@ -133,8 +135,8 @@ const AnimatedBoardLoading: React.FC = ({ isVisible,
@@ -143,12 +145,12 @@ const AnimatedBoardLoading: React.FC = ({ isVisible, {currentMessage} diff --git a/packages/web/app/components/logbook/logbook-stats.tsx b/packages/web/app/components/logbook/logbook-stats.tsx index 577074b3..aeaa3366 100644 --- a/packages/web/app/components/logbook/logbook-stats.tsx +++ b/packages/web/app/components/logbook/logbook-stats.tsx @@ -11,10 +11,14 @@ import { ArcElement, TooltipItem, } from 'chart.js'; +import { Button, Space, DatePicker, Typography } from 'antd'; import dayjs from 'dayjs'; import isoWeek from 'dayjs/plugin/isoWeek'; +import { themeTokens } from '@/app/theme/theme-config'; dayjs.extend(isoWeek); +const { Text } = Typography; + ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement); const optionsBar = { @@ -319,42 +323,58 @@ export const LogBookStats: React.FC<{ boardName: string; userId: string }> = ({ } }, [filteredLogbook]); - const buttonStyle = (btnTimeframe: string) => ({ - marginRight: '10px', - backgroundColor: timeframe === btnTimeframe ? '#007bff' : '#f8f9fa', - color: timeframe === btnTimeframe ? '#fff' : '#000', - border: '1px solid #007bff', - padding: '5px 10px', - cursor: 'pointer', - }); - return ( -
-

LogBook Stats

-
- - - - - +
+ + LogBook Stats + +
+ + + + + + + {timeframe === 'custom' && ( -
- - +
+ + From: + setFromDate(date ? date.format('YYYY-MM-DD') : '')} + /> + To: + setToDate(date ? date.format('YYYY-MM-DD') : '')} + /> +
)}
diff --git a/packages/web/app/components/queue-control/queue-control-bar.tsx b/packages/web/app/components/queue-control/queue-control-bar.tsx index 3828290f..1ad929ab 100644 --- a/packages/web/app/components/queue-control/queue-control-bar.tsx +++ b/packages/web/app/components/queue-control/queue-control-bar.tsx @@ -13,6 +13,7 @@ import { TickButton } from '../logbook/tick-button'; import ClimbThumbnail from '../climb-card/climb-thumbnail'; import { AscentStatus } from './queue-list-item'; import { CopyrightOutlined } from '@ant-design/icons'; +import { themeTokens } from '@/app/theme/theme-config'; import styles from './queue-control-bar.module.css'; const { Title, Text } = Typography; @@ -51,7 +52,7 @@ const QueueControlBar: React.FC = ({ boardDetails, angle }: Que = ({ boardDetails, angle }: Que }} style={{ width: '100%', - boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', // Add subtle shadow for separation + boxShadow: themeTokens.shadows.lg, + borderRadius: 0, + borderTop: `1px solid ${themeTokens.neutral[200]}`, }} > @@ -136,7 +139,11 @@ const QueueControlBar: React.FC = ({ boardDetails, angle }: Que }); }} type={currentClimb?.mirrored ? 'primary' : 'default'} - style={currentClimb?.mirrored ? { backgroundColor: '#722ed1', borderColor: '#722ed1' } : undefined} + style={ + currentClimb?.mirrored + ? { backgroundColor: themeTokens.colors.purple, borderColor: themeTokens.colors.purple } + : undefined + } icon={} /> ) : null} diff --git a/packages/web/app/components/queue-control/queue-list-item.tsx b/packages/web/app/components/queue-control/queue-list-item.tsx index d0caa3a8..b36978e1 100644 --- a/packages/web/app/components/queue-control/queue-list-item.tsx +++ b/packages/web/app/components/queue-control/queue-list-item.tsx @@ -13,6 +13,7 @@ import { TickButton } from '../logbook/tick-button'; import ClimbThumbnail from '../climb-card/climb-thumbnail'; import { useBoardProvider } from '../board-provider/board-provider-context'; import { CopyrightOutlined } from '@ant-design/icons'; +import { themeTokens } from '@/app/theme/theme-config'; const { Text } = Typography; @@ -45,7 +46,7 @@ export const AscentStatus = ({ climbUuid }: { climbUuid: ClimbUuid }) => { {/* Regular ascent icon */} {hasSuccessfulAscent ? (
- +
) : null} {/* Mirrored ascent icon */} @@ -57,11 +58,11 @@ export const AscentStatus = ({ climbUuid }: { climbUuid: ClimbUuid }) => { left: '2px', }} > - +
) : null} {!hasSuccessfulMirroredAscent && !hasSuccessfulAscent ? ( - + ) : null}
); @@ -69,9 +70,9 @@ export const AscentStatus = ({ climbUuid }: { climbUuid: ClimbUuid }) => { // Single icon for non-mirroring boards return hasSuccessfulAscent ? ( - + ) : ( - + ); }; @@ -126,7 +127,11 @@ const QueueListItem: React.FC = ({
= ({ MozUserSelect: 'none', msUserSelect: 'none', userSelect: 'none', + borderLeft: isCurrent ? `3px solid ${themeTokens.colors.primary}` : undefined, }} onDoubleClick={() => setCurrentClimbQueueItem(item)} > diff --git a/packages/web/app/components/setup-wizard/board-config-preview.tsx b/packages/web/app/components/setup-wizard/board-config-preview.tsx index 311ba5c6..b7b6b698 100644 --- a/packages/web/app/components/setup-wizard/board-config-preview.tsx +++ b/packages/web/app/components/setup-wizard/board-config-preview.tsx @@ -122,7 +122,7 @@ export default function BoardConfigPreview({ config, onDelete, boardConfigs }: B if (isLoading) { return ( - + ); @@ -130,10 +130,11 @@ export default function BoardConfigPreview({ config, onDelete, boardConfigs }: B if (!boardDetails) { return ( - + } onClick={handleDelete} danger size="small" />} > @@ -154,10 +155,11 @@ export default function BoardConfigPreview({ config, onDelete, boardConfigs }: B } return ( - + -
- - - BoardSesh - - - Configure your climbing board - +
+ +
+
+ +
+ + Configure your climbing board + +
{savedConfigurations.length > 0 && ( <> @@ -454,7 +468,7 @@ const ConsolidatedBoardConfig = ({ boardConfigs }: ConsolidatedBoardConfigProps) key: 'saved', label: `Saved Configurations (${savedConfigurations.length})`, children: ( - +
{savedConfigurations.map((config) => ( ))} - +
), }, ]} @@ -563,7 +577,7 @@ const ConsolidatedBoardConfig = ({ boardConfigs }: ConsolidatedBoardConfigProps) -
+
You can login after reaching the board page
@@ -634,7 +648,9 @@ const ConsolidatedBoardConfig = ({ boardConfigs }: ConsolidatedBoardConfigProps) ) : selectedBoard && selectedLayout && selectedSize && selectedSets.length > 0 ? (
-
Loading preview...
+
+ Loading preview... +
) : ( diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx index 3600e1a4..ed4c51a0 100644 --- a/packages/web/app/layout.tsx +++ b/packages/web/app/layout.tsx @@ -1,10 +1,11 @@ // app/layout.tsx (or app/_app.tsx if you are using a global layout) import React from 'react'; import { AntdRegistry } from '@ant-design/nextjs-registry'; -import { App } from 'antd'; +import { App, ConfigProvider } from 'antd'; import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/next'; import SessionProviderWrapper from './components/providers/session-provider'; +import { antdTheme } from './theme/antd-theme'; import './components/index.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -17,7 +18,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {children} + + {children} + diff --git a/packages/web/app/theme/antd-theme.ts b/packages/web/app/theme/antd-theme.ts new file mode 100644 index 00000000..24c03e9a --- /dev/null +++ b/packages/web/app/theme/antd-theme.ts @@ -0,0 +1,135 @@ +import type { ThemeConfig } from 'antd'; +import { themeTokens } from './theme-config'; + +export const antdTheme: ThemeConfig = { + token: { + // Primary colors + colorPrimary: themeTokens.colors.primary, + colorSuccess: themeTokens.colors.success, + colorWarning: themeTokens.colors.warning, + colorError: themeTokens.colors.error, + colorInfo: themeTokens.colors.secondary, + + // Typography + fontFamily: themeTokens.typography.fontFamily, + fontSize: themeTokens.typography.fontSize.base, + + // Border radius + borderRadius: themeTokens.borderRadius.md, + borderRadiusLG: themeTokens.borderRadius.lg, + borderRadiusSM: themeTokens.borderRadius.sm, + + // Shadows + boxShadow: themeTokens.shadows.sm, + boxShadowSecondary: themeTokens.shadows.md, + + // Background colors + colorBgContainer: themeTokens.semantic.surface, + colorBgLayout: themeTokens.semantic.background, + colorBgElevated: themeTokens.semantic.surfaceElevated, + + // Text colors + colorText: themeTokens.neutral[800], + colorTextSecondary: themeTokens.neutral[500], + colorTextTertiary: themeTokens.neutral[400], + colorTextQuaternary: themeTokens.neutral[300], + + // Border colors + colorBorder: themeTokens.neutral[200], + colorBorderSecondary: themeTokens.neutral[100], + + // Link colors + colorLink: themeTokens.colors.primary, + colorLinkHover: themeTokens.colors.primaryHover, + colorLinkActive: themeTokens.colors.primaryActive, + + // Control heights + controlHeight: 40, + controlHeightSM: 32, + controlHeightLG: 48, + + // Motion + motionDurationFast: '0.1s', + motionDurationMid: '0.2s', + motionDurationSlow: '0.3s', + }, + + components: { + Button: { + borderRadius: themeTokens.borderRadius.md, + controlHeight: 40, + controlHeightSM: 32, + controlHeightLG: 48, + paddingContentHorizontal: 16, + fontWeight: themeTokens.typography.fontWeight.medium, + }, + + Card: { + borderRadiusLG: themeTokens.borderRadius.lg, + boxShadowTertiary: themeTokens.shadows.sm, + paddingLG: 20, + }, + + Input: { + borderRadius: themeTokens.borderRadius.md, + controlHeight: 40, + paddingInline: 12, + }, + + Select: { + borderRadius: themeTokens.borderRadius.md, + controlHeight: 40, + }, + + Drawer: { + borderRadiusLG: themeTokens.borderRadius.lg, + }, + + Modal: { + borderRadiusLG: themeTokens.borderRadius.lg, + }, + + Layout: { + headerBg: themeTokens.semantic.surface, + bodyBg: themeTokens.semantic.background, + siderBg: themeTokens.semantic.surface, + }, + + Menu: { + borderRadius: themeTokens.borderRadius.md, + itemBorderRadius: themeTokens.borderRadius.sm, + }, + + Dropdown: { + borderRadiusLG: themeTokens.borderRadius.md, + }, + + Form: { + labelFontSize: themeTokens.typography.fontSize.sm, + verticalLabelPadding: '0 0 8px', + }, + + Typography: { + titleMarginBottom: '0.5em', + titleMarginTop: 0, + }, + + Collapse: { + borderRadiusLG: themeTokens.borderRadius.md, + headerBg: themeTokens.neutral[50], + }, + + Tabs: { + cardBg: themeTokens.neutral[50], + itemSelectedColor: themeTokens.colors.primary, + }, + + Tag: { + borderRadiusSM: themeTokens.borderRadius.sm, + }, + + Rate: { + starColor: '#FBBF24', // Amber-400 for stars + }, + }, +}; diff --git a/packages/web/app/theme/theme-config.ts b/packages/web/app/theme/theme-config.ts new file mode 100644 index 00000000..861130b3 --- /dev/null +++ b/packages/web/app/theme/theme-config.ts @@ -0,0 +1,126 @@ +// Design tokens for BoardSesh +// This file defines the design system used throughout the application + +export const themeTokens = { + // Brand colors - Cyan palette to match climbing board aesthetics + colors: { + primary: '#06B6D4', // Cyan-500 - lighter primary + primaryHover: '#0891B2', // Cyan-600 + primaryActive: '#0E7490', // Cyan-700 + secondary: '#22D3EE', // Cyan-400 + success: '#10B981', // Emerald-500 + successBg: '#ECFDF5', + warning: '#F59E0B', + warningBg: '#FFFBEB', + error: '#EF4444', + errorBg: '#FEF2F2', + purple: '#7C3AED', // For mirror button + purpleHover: '#6D28D9', + }, + + // Neutral palette + neutral: { + 50: '#F9FAFB', + 100: '#F3F4F6', + 200: '#E5E7EB', + 300: '#D1D5DB', + 400: '#9CA3AF', + 500: '#6B7280', + 600: '#4B5563', + 700: '#374151', + 800: '#1F2937', + 900: '#111827', + }, + + // Semantic colors + semantic: { + selected: '#ECFEFF', // Cyan-50 - lighter cyan + selectedBorder: '#06B6D4', // Cyan-500 - matches primary + background: '#F9FAFB', + surface: '#FFFFFF', + surfaceElevated: '#FFFFFF', + }, + + // Shadows + shadows: { + xs: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)', + xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', + inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)', + }, + + // Typography + typography: { + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + fontSize: { + xs: 12, + sm: 14, + base: 16, + lg: 18, + xl: 20, + '2xl': 24, + '3xl': 30, + }, + fontWeight: { + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + }, + lineHeight: { + tight: 1.25, + normal: 1.5, + relaxed: 1.75, + }, + }, + + // Spacing scale + spacing: { + 0: 0, + 1: 4, + 2: 8, + 3: 12, + 4: 16, + 5: 20, + 6: 24, + 8: 32, + 10: 40, + 12: 48, + 16: 64, + }, + + // Border radius + borderRadius: { + none: 0, + sm: 4, + md: 8, + lg: 12, + xl: 16, + full: 9999, + }, + + // Transitions + transitions: { + fast: '150ms ease', + normal: '200ms ease', + slow: '300ms ease', + }, + + // Z-index scale + zIndex: { + dropdown: 1000, + sticky: 1020, + fixed: 1030, + modal: 1040, + popover: 1050, + tooltip: 1060, + }, +} as const; + +// Type exports for use in components +export type ThemeTokens = typeof themeTokens; +export type ColorTokens = typeof themeTokens.colors; +export type NeutralTokens = typeof themeTokens.neutral;