Skip to content

Commit e67801b

Browse files
authored
Merge pull request #89 from Benalex8797/feat/#83
Feat/#83
2 parents e4a295a + d89bd84 commit e67801b

17 files changed

Lines changed: 1738 additions & 17 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specId": "5cfd670f-fcc1-4b9c-8763-0eb9da6d7f95", "workflowType": "requirements-first", "specType": "feature"}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Design Document
2+
3+
## Overview
4+
5+
This feature enhances `KeySupplyBadge` by wrapping it in a rich tooltip that surfaces key price metadata — specifically the last-updated timestamp and the quote source. The implementation extends the existing `Tooltip` component to accept `React.ReactNode` content (backward-compatible), adds a `KeyPriceTooltipContent` data type, and composes a `KeyPriceTooltip` wrapper that formats and renders the metadata.
6+
7+
No visual changes are made to `KeySupplyBadge` itself. The tooltip is triggered by hover, keyboard focus, and mobile tap, and is fully accessible via `role="tooltip"` and `aria-describedby`.
8+
9+
## Architecture
10+
11+
The change is purely at the component layer — no new services, stores, or API calls are introduced.
12+
13+
```mermaid
14+
graph TD
15+
A[Consumer / Page] -->|passes tooltipContent prop| B[KeySupplyBadge]
16+
B -->|wraps with| C[Tooltip]
17+
C -->|renders trigger| D[KeySupplyBadge inner span]
18+
C -->|renders overlay| E[KeyPriceTooltipContent node]
19+
E --> F[formatRelativeTime]
20+
```
21+
22+
Key design decisions:
23+
24+
- The `Tooltip` component is extended in-place (widening `content` to `React.ReactNode`) rather than creating a parallel component, keeping the API surface small and all existing usages unaffected.
25+
- Relative-time formatting is a pure utility function, making it trivially testable without mounting any component.
26+
- `KeySupplyBadge` gains an optional `tooltipContent` prop; when absent the badge renders exactly as before.
27+
28+
## Components and Interfaces
29+
30+
### `Tooltip` (extended)
31+
32+
File: `src/components/ui/tooltip.tsx`
33+
34+
```ts
35+
interface TooltipProps {
36+
content: React.ReactNode; // widened from string — backward-compatible
37+
children: React.ReactNode;
38+
}
39+
```
40+
41+
The overlay `<div>` gains:
42+
43+
- a stable `id` (generated once via `useId`) so consumers can reference it with `aria-describedby`
44+
- `whitespace-normal` replaces `whitespace-nowrap` to support multi-line content
45+
- visibility is tracked in state so `aria-describedby` can be conditionally applied
46+
47+
### `KeyPriceTooltipContent` (new React node helper)
48+
49+
File: `src/components/common/KeySupplyBadge.tsx` (co-located, unexported)
50+
51+
Renders the two-line tooltip body from a `TooltipContent` object.
52+
53+
### `KeySupplyBadge` (extended)
54+
55+
```ts
56+
interface TooltipContent {
57+
lastUpdated?: string | null; // ISO 8601 timestamp
58+
quoteSource?: string | null;
59+
}
60+
61+
interface KeySupplyBadgeProps {
62+
supply?: number | null;
63+
className?: string;
64+
tooltipContent?: TooltipContent; // new optional prop
65+
}
66+
```
67+
68+
When `tooltipContent` is provided the badge is wrapped in `<Tooltip>`; otherwise it renders unchanged.
69+
70+
## Data Models
71+
72+
### `TooltipContent`
73+
74+
| Field | Type | Required | Description |
75+
| ------------- | ---------------- | -------- | ---------------------------------------- |
76+
| `lastUpdated` | `string \| null` | No | ISO 8601 timestamp of last price refresh |
77+
| `quoteSource` | `string \| null` | No | Name of the data provider / exchange |
78+
79+
### `formatRelativeTime(iso: string | null | undefined): string`
80+
81+
Pure function. Returns a human-readable relative string ("Updated 3 min ago") or `"Last updated: N/A"` when the input is absent or unparseable.
82+
83+
| Input | Output |
84+
| ------------------------------------ | --------------------- |
85+
| `"2024-01-15T10:30:00Z"` (3 min ago) | `"Updated 3 min ago"` |
86+
| `null` / `undefined` | `"Last updated: N/A"` |
87+
| Invalid date string | `"Last updated: N/A"` |
88+
89+
Relative buckets: seconds → "just now", minutes, hours, days.
90+
91+
## Correctness Properties
92+
93+
_A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees._
94+
95+
### Property 1: Tooltip visibility follows activation state
96+
97+
_For any_ `Tooltip` instance, the overlay should be visible after a hover or focus activation event fires on the wrapper, and hidden after the corresponding leave or blur event fires.
98+
99+
**Validates: Requirements 1.1, 1.2, 1.4**
100+
101+
---
102+
103+
### Property 2: Tooltip toggles on tap
104+
105+
_For any_ `Tooltip` instance, a tap/click event on the wrapper should toggle the overlay from hidden to visible (and from visible to hidden on a second tap).
106+
107+
**Validates: Requirements 1.3**
108+
109+
---
110+
111+
### Property 3: Relative time formatting
112+
113+
_For any_ valid ISO 8601 timestamp string, `formatRelativeTime` should return a non-empty string matching the pattern `"Updated <N> <unit> ago"` or `"just now"`. For any null, undefined, or unparseable input, it should return exactly `"Last updated: N/A"`.
114+
115+
**Validates: Requirements 2.1, 2.2**
116+
117+
---
118+
119+
### Property 4: Source label rendering
120+
121+
_For any_ non-empty `quoteSource` string, the rendered tooltip body should contain the text `"Source: <quoteSource>"`. For any null, undefined, or empty string, it should contain exactly `"Source: N/A"`.
122+
123+
**Validates: Requirements 3.1, 3.2**
124+
125+
---
126+
127+
### Property 5: Fully missing data renders all N/A fallbacks
128+
129+
_Example:_ When `TooltipContent` is `{}` (all fields absent), the rendered tooltip body should contain both `"Last updated: N/A"` and `"Source: N/A"`.
130+
131+
**Validates: Requirements 4.1**
132+
133+
---
134+
135+
### Property 6: Badge is unchanged without tooltipContent
136+
137+
_Example:_ Rendering `<KeySupplyBadge supply={42} />` (no `tooltipContent`) should produce output identical to the pre-feature baseline — no wrapper element, no tooltip overlay, same class names.
138+
139+
**Validates: Requirements 4.2, 6.3**
140+
141+
---
142+
143+
### Property 7: Tooltip overlay always carries role="tooltip"
144+
145+
_For any_ content value (string or ReactNode) passed to `Tooltip`, the rendered overlay element should have `role="tooltip"`.
146+
147+
**Validates: Requirements 5.1**
148+
149+
---
150+
151+
### Property 8: aria-describedby tracks tooltip visibility
152+
153+
_For any_ `Tooltip` instance, when the overlay is visible the trigger wrapper should have an `aria-describedby` attribute whose value matches the overlay's `id`; when the overlay is hidden the attribute should be absent or empty.
154+
155+
**Validates: Requirements 5.2, 5.3**
156+
157+
---
158+
159+
### Property 9: Tooltip overlay carries expected CSS classes
160+
161+
_For any_ content value passed to `Tooltip`, the rendered overlay element should include the classes for dark background (`bg-black`), rounded corners (`rounded-md`), and shadow (`shadow-md`).
162+
163+
**Validates: Requirements 6.1**
164+
165+
---
166+
167+
### Property 10: ReactNode content renders unchanged
168+
169+
_For any_ value passed as `content` to `Tooltip` (string or ReactNode), the rendered overlay should contain that value without modification or wrapping that alters its text content.
170+
171+
**Validates: Requirements 7.1, 7.2**
172+
173+
---
174+
175+
## Error Handling
176+
177+
| Scenario | Behavior |
178+
| --------------------------------------- | ------------------------------------------------------ |
179+
| `lastUpdated` is an invalid date string | `formatRelativeTime` returns `"Last updated: N/A"` |
180+
| `lastUpdated` is a future timestamp | Returns `"just now"` or a sensible fallback — no crash |
181+
| `quoteSource` is an empty string | Treated as absent; renders `"Source: N/A"` |
182+
| `tooltipContent` prop omitted entirely | Badge renders without any tooltip wrapper |
183+
| `content` prop is `null` or `undefined` | Tooltip renders an empty overlay — no crash |
184+
185+
All error paths are handled at the formatting/rendering layer; no exceptions are thrown to consumers.
186+
187+
## Testing Strategy
188+
189+
### Dual Testing Approach
190+
191+
Both unit tests and property-based tests are required and complementary.
192+
193+
- Unit tests cover specific examples, integration points, and edge cases.
194+
- Property tests verify universal invariants across many generated inputs.
195+
196+
### Property-Based Testing
197+
198+
Library: **fast-check** (TypeScript-native, works with Vitest/Jest).
199+
200+
Each property test runs a minimum of **100 iterations**.
201+
202+
Every test is tagged with a comment in the format:
203+
`// Feature: key-price-tooltip, Property <N>: <property_text>`
204+
205+
| Property | Test description | Generator inputs |
206+
| -------- | ------------------------------------------------- | ---------------------------------------- |
207+
| P1 | Tooltip shows on hover/focus, hides on leave/blur | Any ReactNode content |
208+
| P2 | Tooltip toggles on tap | Any ReactNode content |
209+
| P3 | `formatRelativeTime` output format | Random valid ISO strings; null/undefined |
210+
| P4 | Source label rendering | Random non-empty strings; null/empty |
211+
| P7 | `role="tooltip"` always present | Random string content |
212+
| P8 | `aria-describedby` tracks visibility | Random string content |
213+
| P9 | CSS classes always present | Random string content |
214+
| P10 | ReactNode content renders unchanged | Random strings and React elements |
215+
216+
### Unit Tests
217+
218+
- P5: Render `<KeyPriceTooltipContent tooltipContent={{}} />` → assert both N/A strings present.
219+
- P6: Render `<KeySupplyBadge supply={42} />` → assert no tooltip wrapper in output.
220+
- `formatRelativeTime` edge cases: future date, exactly 0 seconds ago, 59 seconds, 60 seconds, 59 minutes, 60 minutes, 23 hours, 24 hours.
221+
- Backward compatibility: existing `<Tooltip content="hello">` usage renders the string correctly.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Requirements Document
2+
3+
## Introduction
4+
5+
This feature enhances the `KeySupplyBadge` component by wrapping it in a rich tooltip that surfaces additional context about the key price — such as the last-updated timestamp and the quote source. The tooltip must be accessible (hover, keyboard focus, and mobile tap), handle missing/partial data gracefully, and remain visually unobtrusive within the existing UI.
6+
7+
## Glossary
8+
9+
- **Tooltip**: A transient overlay that appears on hover, keyboard focus, or mobile tap to display supplementary information without cluttering the primary UI.
10+
- **KeySupplyBadge**: The existing badge component (`src/components/common/KeySupplyBadge.tsx`) that displays the current key supply count.
11+
- **TooltipContent**: The structured data object passed to the tooltip, containing optional fields such as `lastUpdated` and `quoteSource`.
12+
- **Tooltip_Component**: The existing CSS-based tooltip primitive (`src/components/ui/tooltip.tsx`) used as the rendering foundation.
13+
- **Last_Updated**: An optional ISO 8601 timestamp indicating when the key price data was last refreshed.
14+
- **Quote_Source**: An optional string identifying the data provider or exchange from which the key price was sourced.
15+
16+
## Requirements
17+
18+
### Requirement 1: Tooltip Trigger Interactions
19+
20+
**User Story:** As a user, I want the key price tooltip to appear when I hover over, focus on, or tap the badge, so that I can access additional context without it being permanently visible.
21+
22+
#### Acceptance Criteria
23+
24+
1. WHEN a pointer device hovers over the `KeySupplyBadge`, THE `Tooltip_Component` SHALL display the tooltip overlay.
25+
2. WHEN keyboard focus moves to the `KeySupplyBadge` wrapper, THE `Tooltip_Component` SHALL display the tooltip overlay.
26+
3. WHEN a touch user taps the `KeySupplyBadge` on a mobile device, THE `Tooltip_Component` SHALL toggle the tooltip overlay.
27+
4. WHEN the pointer leaves the `KeySupplyBadge` or focus moves away, THE `Tooltip_Component` SHALL hide the tooltip overlay.
28+
29+
---
30+
31+
### Requirement 2: Tooltip Content — Last Updated
32+
33+
**User Story:** As a user, I want to see when the key price was last updated, so that I can judge the freshness of the data.
34+
35+
#### Acceptance Criteria
36+
37+
1. WHEN `TooltipContent.lastUpdated` is a valid ISO 8601 timestamp, THE `Tooltip_Component` SHALL display a human-readable relative time string (e.g. "Updated 3 min ago").
38+
2. IF `TooltipContent.lastUpdated` is absent or null, THEN THE `Tooltip_Component` SHALL display the text "Last updated: N/A" in place of the relative time string.
39+
40+
---
41+
42+
### Requirement 3: Tooltip Content — Quote Source
43+
44+
**User Story:** As a user, I want to see the source of the key price quote, so that I can evaluate the reliability of the data.
45+
46+
#### Acceptance Criteria
47+
48+
1. WHEN `TooltipContent.quoteSource` is a non-empty string, THE `Tooltip_Component` SHALL display the source label prefixed with "Source:" (e.g. "Source: CoinGecko").
49+
2. IF `TooltipContent.quoteSource` is absent, null, or an empty string, THEN THE `Tooltip_Component` SHALL display the text "Source: N/A" in place of the source label.
50+
51+
---
52+
53+
### Requirement 4: Graceful Handling of Fully Missing Data
54+
55+
**User Story:** As a user, I want the tooltip to remain functional even when no metadata is available, so that the badge never appears broken.
56+
57+
#### Acceptance Criteria
58+
59+
1. WHEN all fields in `TooltipContent` are absent or null, THE `Tooltip_Component` SHALL render the tooltip with all fields showing their "N/A" fallback values.
60+
2. THE `KeySupplyBadge` SHALL remain fully functional and visually unchanged when no `TooltipContent` is provided.
61+
62+
---
63+
64+
### Requirement 5: Accessibility
65+
66+
**User Story:** As a user relying on assistive technology, I want the tooltip to be accessible, so that I can consume its content with a screen reader.
67+
68+
#### Acceptance Criteria
69+
70+
1. THE `Tooltip_Component` SHALL render the tooltip overlay with `role="tooltip"`.
71+
2. THE `KeySupplyBadge` wrapper element SHALL include an `aria-describedby` attribute referencing the tooltip element's `id` when the tooltip is visible.
72+
3. WHEN the tooltip is hidden, THE `Tooltip_Component` SHALL ensure the tooltip overlay is not announced by screen readers.
73+
74+
---
75+
76+
### Requirement 6: Visual Design — Unobtrusive Appearance
77+
78+
**User Story:** As a user, I want the tooltip to be visually subtle and consistent with the existing UI, so that it does not distract from the primary content.
79+
80+
#### Acceptance Criteria
81+
82+
1. THE `Tooltip_Component` SHALL render the tooltip overlay using the existing dark background, rounded corners, and shadow styles already defined in `src/components/ui/tooltip.tsx`.
83+
2. THE `Tooltip_Component` SHALL support multi-line content within the tooltip overlay without truncating or overflowing the viewport.
84+
3. THE `KeySupplyBadge` SHALL NOT change its own visual appearance (size, color, or layout) as a result of this feature.
85+
86+
---
87+
88+
### Requirement 7: Tooltip Component API Extension
89+
90+
**User Story:** As a developer, I want the Tooltip component to accept structured React node content, so that I can render rich multi-line tooltips beyond a single string.
91+
92+
#### Acceptance Criteria
93+
94+
1. THE `Tooltip_Component` SHALL accept a `content` prop typed as `React.ReactNode` in addition to the existing `string` type, remaining backward-compatible with all current usages.
95+
2. WHEN `content` is a `React.ReactNode`, THE `Tooltip_Component` SHALL render it inside the tooltip overlay without modification.

0 commit comments

Comments
 (0)