From cbb7cc0c2bda20fc167ec99f223107927ec9d938 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 04:05:09 +0000 Subject: [PATCH 1/6] feat: add tooltip base component using react-aria-components Co-authored-by: bitnimble --- src/ui/base/tooltip/tooltip.module.css | 89 ++++++++++++++++++++++++++ src/ui/base/tooltip/tooltip.tsx | 48 ++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/ui/base/tooltip/tooltip.module.css create mode 100644 src/ui/base/tooltip/tooltip.tsx diff --git a/src/ui/base/tooltip/tooltip.module.css b/src/ui/base/tooltip/tooltip.module.css new file mode 100644 index 0000000..e95b425 --- /dev/null +++ b/src/ui/base/tooltip/tooltip.module.css @@ -0,0 +1,89 @@ +.tooltip { + background: var(--colorForeground); + color: var(--colorBackground); + padding: var(--gridBaseline) calc(var(--gridBaseline) * 1.5); + border-radius: 4px; + font-size: 14px; + max-width: 250px; +} + +.tooltip[data-placement='top'] { + margin-bottom: 8px; +} + +.tooltip[data-placement='bottom'] { + margin-top: 8px; +} + +.tooltip[data-placement='left'] { + margin-right: 8px; +} + +.tooltip[data-placement='right'] { + margin-left: 8px; +} + +.arrow { + position: absolute; +} + +.arrow svg { + fill: var(--colorForeground); + display: block; +} + +.tooltip[data-placement='top'] .arrow { + bottom: -4px; +} + +.tooltip[data-placement='top'] .arrow svg { + transform: rotate(180deg); +} + +.tooltip[data-placement='bottom'] .arrow { + top: -4px; +} + +.tooltip[data-placement='left'] .arrow { + right: -4px; +} + +.tooltip[data-placement='left'] .arrow svg { + transform: rotate(90deg); +} + +.tooltip[data-placement='right'] .arrow { + left: -4px; +} + +.tooltip[data-placement='right'] .arrow svg { + transform: rotate(-90deg); +} + +/* Entering animation */ +.tooltip[data-entering] { + animation: fadeIn 150ms ease-out; +} + +/* Exiting animation */ +.tooltip[data-exiting] { + animation: fadeOut 150ms ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/src/ui/base/tooltip/tooltip.tsx b/src/ui/base/tooltip/tooltip.tsx new file mode 100644 index 0000000..88bbb21 --- /dev/null +++ b/src/ui/base/tooltip/tooltip.tsx @@ -0,0 +1,48 @@ +'use client'; + +import React from 'react'; +import { + Tooltip as AriaTooltip, + TooltipTrigger, + OverlayArrow, + type TooltipProps as AriaTooltipProps, +} from 'react-aria-components'; +import styles from './tooltip.module.css'; + +type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'; + +export type TooltipProps = { + /** Content to display in the tooltip */ + content: React.ReactNode; + /** Position of the tooltip relative to the trigger */ + position?: TooltipPosition; + /** Delay in ms before showing the tooltip */ + delay?: number; + /** The trigger element */ + children: React.ReactElement; +}; + +const positionToPlacement: Record = { + top: 'top', + bottom: 'bottom', + left: 'left', + right: 'right', +}; + +export const Tooltip = (props: TooltipProps) => { + const { content, position = 'top', delay = 300, children } = props; + + return ( + + {children} + + + + + + + {content} + + + ); +}; From 3b2619ed875e715fc2b175b3707764a8ed5677a1 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:06:09 +0000 Subject: [PATCH 2/6] refactor: use react-aria Placement type directly Remove custom TooltipPosition type and positionToPlacement mapping in favor of using AriaTooltipProps['placement'] directly. Co-authored-by: bitnimble --- src/ui/base/tooltip/tooltip.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/ui/base/tooltip/tooltip.tsx b/src/ui/base/tooltip/tooltip.tsx index 88bbb21..ac9dace 100644 --- a/src/ui/base/tooltip/tooltip.tsx +++ b/src/ui/base/tooltip/tooltip.tsx @@ -9,33 +9,24 @@ import { } from 'react-aria-components'; import styles from './tooltip.module.css'; -type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'; - export type TooltipProps = { /** Content to display in the tooltip */ content: React.ReactNode; /** Position of the tooltip relative to the trigger */ - position?: TooltipPosition; + placement?: AriaTooltipProps['placement']; /** Delay in ms before showing the tooltip */ delay?: number; /** The trigger element */ children: React.ReactElement; }; -const positionToPlacement: Record = { - top: 'top', - bottom: 'bottom', - left: 'left', - right: 'right', -}; - export const Tooltip = (props: TooltipProps) => { - const { content, position = 'top', delay = 300, children } = props; + const { content, placement = 'top', delay = 300, children } = props; return ( {children} - + From 653ee8892f1473282ec0e37a4ec37263ac66b734 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:06:20 +0000 Subject: [PATCH 3/6] fix: use gridBaseline for all px values in tooltip CSS Co-authored-by: bitnimble --- src/ui/base/tooltip/tooltip.module.css | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ui/base/tooltip/tooltip.module.css b/src/ui/base/tooltip/tooltip.module.css index e95b425..ea0b6ec 100644 --- a/src/ui/base/tooltip/tooltip.module.css +++ b/src/ui/base/tooltip/tooltip.module.css @@ -2,25 +2,25 @@ background: var(--colorForeground); color: var(--colorBackground); padding: var(--gridBaseline) calc(var(--gridBaseline) * 1.5); - border-radius: 4px; + border-radius: calc(var(--gridBaseline) * 0.5); font-size: 14px; - max-width: 250px; + max-width: calc(var(--gridBaseline) * 32); } .tooltip[data-placement='top'] { - margin-bottom: 8px; + margin-bottom: var(--gridBaseline); } .tooltip[data-placement='bottom'] { - margin-top: 8px; + margin-top: var(--gridBaseline); } .tooltip[data-placement='left'] { - margin-right: 8px; + margin-right: var(--gridBaseline); } .tooltip[data-placement='right'] { - margin-left: 8px; + margin-left: var(--gridBaseline); } .arrow { @@ -33,7 +33,7 @@ } .tooltip[data-placement='top'] .arrow { - bottom: -4px; + bottom: calc(var(--gridBaseline) * -0.5); } .tooltip[data-placement='top'] .arrow svg { @@ -41,11 +41,11 @@ } .tooltip[data-placement='bottom'] .arrow { - top: -4px; + top: calc(var(--gridBaseline) * -0.5); } .tooltip[data-placement='left'] .arrow { - right: -4px; + right: calc(var(--gridBaseline) * -0.5); } .tooltip[data-placement='left'] .arrow svg { @@ -53,7 +53,7 @@ } .tooltip[data-placement='right'] .arrow { - left: -4px; + left: calc(var(--gridBaseline) * -0.5); } .tooltip[data-placement='right'] .arrow svg { From 609ac71cc6c9e13049e3b6ed325af92f9aa7ef30 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 06:33:05 +0000 Subject: [PATCH 4/6] refactor: simplify tooltip opacity animation to use transition Co-authored-by: bitnimble --- src/ui/base/tooltip/tooltip.module.css | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/ui/base/tooltip/tooltip.module.css b/src/ui/base/tooltip/tooltip.module.css index ea0b6ec..2c6ae04 100644 --- a/src/ui/base/tooltip/tooltip.module.css +++ b/src/ui/base/tooltip/tooltip.module.css @@ -5,6 +5,7 @@ border-radius: calc(var(--gridBaseline) * 0.5); font-size: 14px; max-width: calc(var(--gridBaseline) * 32); + transition: opacity 150ms ease-out; } .tooltip[data-placement='top'] { @@ -60,30 +61,10 @@ transform: rotate(-90deg); } -/* Entering animation */ .tooltip[data-entering] { - animation: fadeIn 150ms ease-out; + opacity: 0; } -/* Exiting animation */ .tooltip[data-exiting] { - animation: fadeOut 150ms ease-in; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } + opacity: 0; } From 7433b1280c85ce52307b642de128203108a73210 Mon Sep 17 00:00:00 2001 From: D Date: Sun, 18 Jan 2026 19:30:48 +1100 Subject: [PATCH 5/6] . --- AGENTS.md | 13 +++++++++++- bun.lock | 1 + src/app/page.tsx | 54 +++++++++++++++++++++++++++++------------------- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6576fb9..ffdb8d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,10 +16,21 @@ The codebase uses Docker to run third-party services locally (Meilisearch for se - Vanilla CSS modules - Zod for schemas +# General + +- If you need to add dependencies, add them with `bun install XYZ` and ensure that the bun.lock file is updated and part of the commit or PR. + # Typescript - Use `== null` (double equals null) instead of either `=== null` or `=== undefined` (triple equals null / undefined), and the same for `!=`. This is to make `null` and `undefined` mean the same thing everywhere in our codebase to avoid any potential serialisation/deserialisation confusion or issues. - Avoid use of `any` unless absolutely necessary. +- Prefer early-exit if statements rather than nested if statements. + +# CSS + +- Use design tokens present in the root layout as CSS variables +- Any metric should be based on a multiple of gridBaseline. Fractional multipliers are okay if necessary, e.g. calc(1.5 \* var(--gridBaseline)) +- Metrics that aren't dependent on "UI scale" should not be based on gridBaseline, e.g. font sizes, line heights, percentage border radii, 1px thin borders, etc. # Commands @@ -53,7 +64,7 @@ If you're receiving a request through an issue or PR comment, always: - Based on your max turn limit, you may need to break up your proposal into multiple steps. If so, report back to the requester with these multiple proposed stages and implement them one by one instead, after confirmation from the requester. - Most important - if the request is (verbatim and exactly) "test" with no other words other than the trigger phrase ping itself, respond with ONLY "meow", with no other words, context, or punctuation. - When creating a PR, always add the relevant CODEOWNER as a reviewer on the PR with the `-r` argument on `gh pr create`. -- When updating a PR to address review comments, always re-request a review from the person that reviewed it using `gh pr edit` using the `--add-reviewer` argument. +- When updating a PR to address review comments, always re-request a review from the person that reviewed it using `gh pr edit` using the `--add-reviewer` argument, and "Resolve" any review comments that were addressed via the Github pull request API. # English style diff --git a/bun.lock b/bun.lock index afedee7..f8bbe9e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "paradb", diff --git a/src/app/page.tsx b/src/app/page.tsx index f04a7d9..5cc83c8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -20,6 +20,7 @@ import styles from './page.module.css'; import { Search } from './search'; import useInfiniteScroll from 'hooks/useInfiniteScroll'; import { useSkeletonRef } from 'app/skeleton_provider'; +import { Tooltip } from 'ui/base/tooltip/tooltip'; export default function Page() { return ( @@ -212,27 +213,38 @@ const MapListTable = observer((props: { store: MapListStore; presenter: MapListP const DifficultyColorPills = (props: { difficulties: Difficulty[] }) => { const difficulties = new Set(props.difficulties.map((d) => parseDifficulty(d.difficultyName))); const color = (d: KnownDifficulty) => (difficulties.has(d) ? difficultyColors[d] : undefined); - return ( -
-
-
-
-
+ const tooltip = ( +
+ Difficulties:{' '} + {props.difficulties + .map((d) => d.difficultyName) + .filter((n) => !!n) + .join(', ')}
); + return ( + +
+
+
+
+
+
+
+ ); }; From a22ac215f91324dfa61613003ec5504c9187a0da Mon Sep 17 00:00:00 2001 From: D Date: Sun, 18 Jan 2026 20:36:04 +1100 Subject: [PATCH 6/6] . --- src/app/page.tsx | 53 ++++++++++---------------- src/ui/base/tooltip/tooltip.module.css | 30 ++++++++------- src/ui/base/tooltip/tooltip.tsx | 3 +- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 5cc83c8..7101cf1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -213,38 +213,27 @@ const MapListTable = observer((props: { store: MapListStore; presenter: MapListP const DifficultyColorPills = (props: { difficulties: Difficulty[] }) => { const difficulties = new Set(props.difficulties.map((d) => parseDifficulty(d.difficultyName))); const color = (d: KnownDifficulty) => (difficulties.has(d) ? difficultyColors[d] : undefined); - const tooltip = ( -
- Difficulties:{' '} - {props.difficulties - .map((d) => d.difficultyName) - .filter((n) => !!n) - .join(', ')} -
- ); return ( - -
-
-
-
-
-
-
+
+
+
+
+
+
); }; diff --git a/src/ui/base/tooltip/tooltip.module.css b/src/ui/base/tooltip/tooltip.module.css index 2c6ae04..40fa53d 100644 --- a/src/ui/base/tooltip/tooltip.module.css +++ b/src/ui/base/tooltip/tooltip.module.css @@ -1,11 +1,13 @@ .tooltip { - background: var(--colorForeground); - color: var(--colorBackground); + background: var(--colorBackground); + color: var(--colorForeground); padding: var(--gridBaseline) calc(var(--gridBaseline) * 1.5); border-radius: calc(var(--gridBaseline) * 0.5); + border: 1px solid var(--colorPurple); + box-sizing: border-box; font-size: 14px; max-width: calc(var(--gridBaseline) * 32); - transition: opacity 150ms ease-out; + transition: opacity 100ms ease-in-out; } .tooltip[data-placement='top'] { @@ -29,7 +31,7 @@ } .arrow svg { - fill: var(--colorForeground); + fill: var(--colorBackground); display: block; } @@ -37,30 +39,30 @@ bottom: calc(var(--gridBaseline) * -0.5); } -.tooltip[data-placement='top'] .arrow svg { - transform: rotate(180deg); -} - .tooltip[data-placement='bottom'] .arrow { - top: calc(var(--gridBaseline) * -0.5); + top: calc(var(--gridBaseline) * -1); } .tooltip[data-placement='left'] .arrow { right: calc(var(--gridBaseline) * -0.5); } -.tooltip[data-placement='left'] .arrow svg { - transform: rotate(90deg); +.tooltip[data-placement='right'] .arrow { + left: calc(var(--gridBaseline) * -1); } -.tooltip[data-placement='right'] .arrow { - left: calc(var(--gridBaseline) * -0.5); +.tooltip[data-placement='bottom'] .arrow svg { + transform: rotate(180deg); } -.tooltip[data-placement='right'] .arrow svg { +.tooltip[data-placement='left'] .arrow svg { transform: rotate(-90deg); } +.tooltip[data-placement='right'] .arrow svg { + transform: rotate(90deg); +} + .tooltip[data-entering] { opacity: 0; } diff --git a/src/ui/base/tooltip/tooltip.tsx b/src/ui/base/tooltip/tooltip.tsx index ac9dace..44f2c3a 100644 --- a/src/ui/base/tooltip/tooltip.tsx +++ b/src/ui/base/tooltip/tooltip.tsx @@ -8,6 +8,7 @@ import { type TooltipProps as AriaTooltipProps, } from 'react-aria-components'; import styles from './tooltip.module.css'; +import { colors } from '../design_system/design_tokens'; export type TooltipProps = { /** Content to display in the tooltip */ @@ -29,7 +30,7 @@ export const Tooltip = (props: TooltipProps) => { - + {content}