|
| 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. |
0 commit comments