diff --git a/docs/superpowers/plans/2026-07-22-amount-format-pr2.md b/docs/superpowers/plans/2026-07-22-amount-format-pr2.md new file mode 100644 index 000000000..66abd97a7 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-amount-format-pr2.md @@ -0,0 +1,377 @@ +# Amount Format — PR2 Implementation Plan (Convert remaining call sites) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Convert every remaining direct `numberUtils.prettyValue` call to the shared amount layer, so the user's format preference applies consistently across the whole wallet. + +**Architecture:** No new abstractions. PR1 built `formatAmount` / `resolveAmountFormat` (`src/utils/amount.js`), `useAmountFormat` (`src/hooks/`) and `` (`src/components/`). This PR is a mechanical sweep onto them. Behaviour is unchanged while the feature flag is off, which it is by default. + +**Tech Stack:** React 17 (mixed function + class components), Redux, `@hathor/wallet-lib`. + +**Spec:** `docs/superpowers/specs/2026-07-21-amount-format-design.md` +**Depends on:** PR1 (`raul-oliveira/feat/amount-format`) — branch from it, not from `master`. + +--- + +## Working agreement + +- **Do not commit per task.** Accumulate in the working tree; the controller commits once at the end. +- **Do not write render tests.** Jest 27 cannot parse `axios@1.7.7`'s ESM entry, so any test importing `@hathor/wallet-lib` fails; 7 of 9 suites already fail on `master`. Unit tests for pure functions are fine. +- **Test command:** `CI=true npx react-app-rewired test --watchAll=false 2>&1 | grep -E "^(PASS|FAIL)|^Test Suites:|^Tests:"`. Never pipe jest to `tail` and read `$?` — a pipeline returns the last command's exit code and falsely reports success. +- **Build/lint gate:** `npm run build` (plain, never `CI=true npm run build` — that fails on a pre-existing LavaMoat policy drift). +- **Baseline to match:** `7 failed, 4 passed, 11 total` / `33 passed, 33 total`. +- **`src/components/InputNumber.js` is never touched.** Editable inputs are always expanded. + +--- + +## The three APIs + +| API | Import | Use when | Returns | +| --- | --- | --- | --- | +| `` | `src/components/Amount.js` | The result is rendered as a React child | element | +| `useAmountFormat()` → `formatValue(v, {isNFT})` | `src/hooks/useAmountFormat.js` | **Function** component needing a string | string | +| `formatAmount(v, {decimalPlaces, isNFT, amountFormat})` | `src/utils/amount.js` | **Class** component or plain module needing a string | string | + +**Class components must mask the flag.** Add to `mapStateToProps`: + +```js + amountFormat: resolveAmountFormat( + state.amountFormat, + state.featureToggles[AMOUNT_FORMAT_FEATURE_TOGGLE] + ), +``` +importing `resolveAmountFormat` from `../utils/amount` and `AMOUNT_FORMAT_FEATURE_TOGGLE` from `../constants`. Never pass `state.amountFormat` raw — that bypasses the feature flag and was a real bug caught in PR1 review. + +**Putting a `` element into a template literal renders `[object Object]`.** Every site below is pre-classified; trust the classification but sanity-check the surrounding line before editing. + +--- + +## Task 1: Reown components (JSX-rendered strings) + +All five are **function** components; the helper lives inside the component, so use the hook. + +**Files:** `src/components/Reown/TransactionFees.js`, `Reown/NanoContractActions.js`, `Reown/modals/SendTransactionModal.js`, `Reown/modals/GetUtxosModal.js`, `Reown/modals/BaseNanoContractModal.js` + +- [ ] **Step 1: `TransactionFees.js`** + +Line 23 assigns `const formattedFee = numberUtils.prettyValue(fee);` and line 35 renders `{formattedFee} {symbol}`. Delete line 23 and replace line 35's content with: + +```jsx + +``` + +Import `Amount from '../Amount'`. Note the original called `prettyValue(fee)` with **no** decimal-places argument, so it used the lib default; `` uses the wallet's configured `decimalPlaces` from redux. That is the intended correction — call it out in your report. + +- [ ] **Step 2: `NanoContractActions.js`** + +Add `import { useAmountFormat } from '../../hooks/useAmountFormat';` and `const formatValue = useAmountFormat();` at the top of the component body. Replace the body of `formatAmount` (line ~91-93) so it returns: + +```js + return formatValue(amount, { isNFT: isNft }); +``` + +Keep the existing `isNft` variable exactly as it is computed today. + +- [ ] **Step 3: `SendTransactionModal.js`** + +Same pattern. Replace line ~119 inside `formatValue` with the hook's formatter — rename the local helper if it now collides with the hook result (e.g. call the hook result `formatAmountValue`). Preserve the existing `isNFT` computation. + +- [ ] **Step 4: `GetUtxosModal.js`** + +Same pattern for the `formatAmount` helper at line ~63-67, preserving its `isNFT` computation. + +- [ ] **Step 5: `BaseNanoContractModal.js`** + +Line 114 assigns into the `let displayValue` chain that other branches fill with template strings and which is rendered at line 137 as `{displayValue}`. It must stay a **string**. Add the hook and replace line 114 with: + +```js + displayValue = formatValue(value); +``` + +- [ ] **Step 6: Verify** + +Run the test suite and `npm run build`. Confirm the baseline is unchanged and no warning names these files. + +--- + +## Task 2: Reown `CreateTokenRequestData.js` (module-level helper — needs restructuring) + +**Files:** `src/components/Reown/CreateTokenRequestData.js` + +`formatAmount` is defined at **module scope** (line 17), outside the component, so it cannot call a hook. Its results feed template literals at lines 181 and 187, so it must keep returning a string. + +- [ ] **Step 1: Move the helper inside the component** + +Delete the module-level `const formatAmount = (amount) => {...}` at line 17. Inside the component body add: + +```js + const formatValue = useAmountFormat(); +``` +importing `useAmountFormat` from `../../hooks/useAmountFormat`. + +- [ ] **Step 2: Update the three call sites** + +- Line ~118: `` +- Line ~181: `` value={`${formatValue(data.deposit)} ${DEFAULT_TOKEN_SYMBOL}`} `` +- Line ~187: `` value={data.fee ? `${formatValue(data.fee)} ${DEFAULT_TOKEN_SYMBOL}` : '-'} `` + +If any of these sit outside the component body after the move, STOP and report — the restructuring assumption is wrong. + +- [ ] **Step 3: Verify** — suite + build unchanged. + +--- + +## Task 3: Atomic swap and nano contract screens (pure JSX) + +**Files:** `src/screens/atomic-swap/EditSwap.js`, `src/components/atomic-swap/ProposalBalanceTable.js`, `src/components/atomic-swap/ModalAtomicSend.js`, `src/screens/nano-contract/NanoContractDetail.js` + +- [ ] **Step 1: `EditSwap.js`** — two JSX sites. + +Line 137 → `` +Line 156 → `` + +Import `Amount from '../../components/Amount'`. + +- [ ] **Step 2: `ProposalBalanceTable.js`** — line 31 is JSX: + +```jsx + return {symbol} +``` + +Keep the symbol in its existing `` rather than moving it into ``; the bold styling is deliberate. + +- [ ] **Step 3: `ModalAtomicSend.js`** — line 66 is a **STRING** (it feeds `setAmount`, component state). Use the hook: + +```js + setAmount(formatValue(newAmount)); +``` + +- [ ] **Step 4: `NanoContractDetail.js`** — line 162 is JSX: + +```jsx +

Amount:

+``` + +- [ ] **Step 5: Verify** — suite + build unchanged. + +--- + +## Task 4: `NFTListElement.js` (class component, JSX) + +**Files:** `src/components/NFTListElement.js` + +Line 102 renders inside a conditional. NFT balances are integer-valued and the original hardcodes `0` decimal places, so pass `isNFT`: + +```jsx + { this.props.nftElement.balance.status === TOKEN_DOWNLOAD_STATUS.READY && } +``` + +- [ ] **Step 1:** Make the edit, importing `Amount from './Amount'`. +- [ ] **Step 2:** Verify — suite + build unchanged. No `mapStateToProps` change needed; `` reads redux itself. + +--- + +## Task 5: `ModalTokenImport.js` (STRING) + +**Files:** `src/components/ModalTokenImport.js` + +`ModalTokenImport` is a **function** component (`export default function ModalTokenImport({ onClose, manageDomLifecycle })`, line 59) and the balance helper containing line 271 is defined inside it, so the hook applies. + +- [ ] **Step 1: Add the hook** + +```js +import { useAmountFormat } from '../hooks/useAmountFormat'; +``` +and in the component body, next to the existing `useSelector` calls: + +```js + const formatValue = useAmountFormat(); +``` + +- [ ] **Step 2: Replace the return at line ~271** + +```js + return `${formatValue(total)} ${symbol}`; +``` + +The existing `const decimalPlaces = useSelector((state) => state.serverInfo.decimalPlaces);` at line 67 becomes unused if nothing else in the file references it — check, and remove it only if fully unused. + +- [ ] **Step 3:** Verify — suite + build unchanged. + +--- + +## Task 6: `tokens/TokenMint.js` and `tokens/TokenMelt.js` (class components, STRING) + +Both are class components whose values feed user-facing message strings. + +**Files:** `src/components/tokens/TokenMint.js`, `src/components/tokens/TokenMelt.js` + +- [ ] **Step 1: Add masked `amountFormat` to both `mapStateToProps`** + +Using the `resolveAmountFormat` snippet from the top of this plan. + +- [ ] **Step 2: Add a `formatValue` instance method to each class** + +```js + formatValue = (value) => formatAmount(value, { + decimalPlaces: this.props.decimalPlaces, + isNFT: this.isNFT(), + amountFormat: this.props.amountFormat, + }); +``` + +- [ ] **Step 3: `TokenMint.js`** + +Line 97 → `const prettyAmountValue = this.formatValue(this.state.amount);` + +Line 205 contains **two** amounts inside one template literal. The `getDepositAmount(...)` call is handled in Task 7; the second is the HTR balance, which is **HTR, not the token** — it must NOT receive the token's `isNFT`. Replace it with a bare `formatAmount` call: + +```js +formatAmount(this.props.htrBalance, { decimalPlaces: this.props.decimalPlaces, amountFormat: this.props.amountFormat }) +``` + +- [ ] **Step 4: `TokenMelt.js`** + +Line 90 → `const prettyAmountValue = this.formatValue(this.state.amount);` +Line 104 → `const prettyWalletAmount = this.formatValue(walletAmount);` + +- [ ] **Step 5: Verify** — suite + build unchanged. + +--- + +## Task 7: `utils/tokens.js` — thread the format through a plain module + +**Files:** `src/utils/tokens.js`, `src/screens/CreateToken.js`, `src/components/tokens/TokenMint.js` + +`getDepositAmount(mintAmount, depositPercent, decimalPlaces)` is a plain module function returning a **string**, consumed by template literals at `CreateToken.js:330` and `TokenMint.js:205`. It cannot read redux, so the caller must pass the format. + +- [ ] **Step 1: Add the parameter** + +```js + getDepositAmount(mintAmount, depositPercent, decimalPlaces, amountFormat) { + if (mintAmount) { + const deposit = hathorLib.tokensUtils.getDepositAmount(mintAmount, depositPercent); + return formatAmount(deposit, { decimalPlaces, amountFormat }); + } + return '0'; + }, +``` + +Import `formatAmount` from `./amount`. Update the JSDoc to document `amountFormat`. Note the deposit is always HTR, so no `isNFT`. + +Making `amountFormat` the last parameter keeps it optional — omitting it falls back to `AMOUNT_FORMAT_DEFAULT` (expanded), so any caller you miss degrades safely rather than crashing. + +- [ ] **Step 2: `CreateToken.js:330`** — a function component that already calls `useAmountFormat`. It needs the raw preference value, not the bound formatter, so read it alongside: + +```js + const amountFormat = useSelector(state => resolveAmountFormat( + state.amountFormat, + state.featureToggles[AMOUNT_FORMAT_FEATURE_TOGGLE] + )); +``` + +then pass `amountFormat` as the fourth argument. + +- [ ] **Step 3: `TokenMint.js:205`** — pass `this.props.amountFormat` (already masked by Task 6 Step 1) as the fourth argument. + +- [ ] **Step 4: Verify** — suite + build unchanged. + +--- + +## Task 8: `utils/nanoContracts.js` (returns mixed string | JSX) + +**Files:** `src/utils/nanoContracts.js` + +The enclosing function already returns JSX from a sibling branch (`` at line ~96), so its consumers must already render JSX. The Amount branch can therefore return an element. + +- [ ] **Step 1:** Replace line 92 with: + +```jsx + return ; +``` + +importing `Amount from '../components/Amount'`. The `decimalPlaces` parameter may become unused in this function — leave the signature alone, later cleanup removes it. + +- [ ] **Step 2: Confirm the assumption** + +Grep this function's call sites and verify every consumer renders the result as JSX. If **any** consumer puts it in a string, revert to threading `amountFormat` through as a parameter like Task 7 and report the deviation. + +- [ ] **Step 3: Verify** — suite + build unchanged. + +--- + +## Task 9: `screens/SendTokens.js` (STRING) + +**Files:** `src/screens/SendTokens.js` + +Line 147 builds `requiredAmount` for a user-facing message. Function component → use the hook. The amount is HTR (a fee total), so no `isNFT`: + +```js + const requiredAmount = formatValue(totalFee + outgoingHTR); +``` + +- [ ] **Step 1:** Add `useAmountFormat` and make the edit. +- [ ] **Step 2: Verify** — suite + build unchanged. + +--- + +## Task 10: Final sweep and verification + +- [ ] **Step 1: Confirm nothing is left behind** + +Run: `grep -rn "prettyValue" src/ | grep -v "InputNumber.js" | grep -v "__tests__" | grep -v "utils/amount.js"` + +Expected: **no output**. `src/utils/amount.js` legitimately retains the single wrapped call plus doc comments; `InputNumber.js` is intentionally excluded. + +- [ ] **Step 2: Confirm no raw preference reads** + +Run: `grep -rn "state.amountFormat" src/` + +Every hit must be inside `useAmountFormat.js`, wrapped in `resolveAmountFormat(...)`, in `Settings.js` (safe — gated at render), or in a test. + +- [ ] **Step 3: Remove dead imports** + +For every file touched, check whether `numberUtils` / `hathorLib` is still referenced. Remove the import only if fully unused; leave it otherwise. `npm run build` surfaces unused-variable warnings — treat any naming a touched file as a defect. + +- [ ] **Step 4: Full verification** + +Test suite: `7 failed, 4 passed, 11 total` / `33 passed, 33 total`, with the same 7 pre-existing failures. +Build: `Compiled with warnings.`, no warning naming a touched file. +`git status --short`: `package.json`, `package-lock.json`, `src/storage.js`, `src/components/InputNumber.js` all unmodified. + +- [ ] **Step 5: Extract strings** + +Run `make update_pot`. Expect little or no change — this PR adds no new user-facing strings. Report anything unexpected. + +--- + +## Call-site inventory (pre-classified) + +| File:line | Kind | Context | API | Notes | +| --- | --- | --- | --- | --- | +| `Reown/TransactionFees.js:23` | fn | JSX | `` | had no decimalPlaces arg | +| `Reown/NanoContractActions.js:93` | fn | JSX-rendered string | hook | keep `isNft` | +| `Reown/modals/SendTransactionModal.js:119` | fn | JSX-rendered string | hook | keep `isNFT` | +| `Reown/modals/GetUtxosModal.js:66` | fn | JSX-rendered string | hook | keep `isNFT` | +| `Reown/modals/BaseNanoContractModal.js:114` | fn | STRING | hook | shared `let displayValue` | +| `Reown/CreateTokenRequestData.js:20` | module-level helper | STRING | hook, after moving inside | feeds template literals | +| `screens/atomic-swap/EditSwap.js:137,156` | fn | JSX | `` | | +| `atomic-swap/ProposalBalanceTable.js:31` | fn | JSX | `` | keep `{symbol}` | +| `atomic-swap/ModalAtomicSend.js:66` | fn | STRING | hook | feeds `setAmount` | +| `screens/nano-contract/NanoContractDetail.js:162` | fn | JSX | `` | | +| `NFTListElement.js:102` | class | JSX | `` | hardcoded 0 decimals today | +| `ModalTokenImport.js:271` | fn | STRING | hook | helper is inside the component | +| `tokens/TokenMint.js:97` | class | STRING | `this.formatValue` | | +| `tokens/TokenMint.js:205` | class | STRING | bare `formatAmount` | **HTR balance — no isNFT** | +| `tokens/TokenMelt.js:90,104` | class | STRING | `this.formatValue` | | +| `utils/tokens.js:122` | module | STRING | param-threaded | **HTR deposit — no isNFT** | +| `utils/nanoContracts.js:92` | module | JSX | `` | sibling branch returns JSX | +| `screens/SendTokens.js:147` | fn | STRING | hook | **HTR fee — no isNFT** | + +--- + +## Out of scope + +- `src/components/InputNumber.js` — always expanded, never migrated. +- Removing now-unused `decimalPlaces` props and `mapStateToProps` entries — a later cleanup. +- Any layout or wrapping change — PRs 4–6. diff --git a/docs/superpowers/plans/2026-07-22-amount-format-pr3.md b/docs/superpowers/plans/2026-07-22-amount-format-pr3.md new file mode 100644 index 000000000..768856b50 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-amount-format-pr3.md @@ -0,0 +1,611 @@ +# Amount Format — PR3 Implementation Plan (Shared Radio components + Address Mode restyle) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Extract the duplicated radio-selector and save-button markup from the Amount Format and Address Mode modals into shared components, then restyle Address Mode onto them — without changing any of its behaviour. + +**Architecture:** Four new presentational components under `src/components/Radio/` plus `PreferenceSaveButton`, mirroring the decomposition shipped in wallet-mobile#893 but adapted to desktop React + SCSS + Bootstrap. Both modals become thin: they own state and behaviour, the shared components own presentation. + +**Spec:** `docs/superpowers/specs/2026-07-21-amount-format-design.md` +**Depends on:** PR2. Branch from it. + +--- + +## Working agreement + +- **Do not commit per task.** The controller commits once at the end. +- **Do not write render tests** — Jest 27 cannot parse `axios@1.7.7`'s ESM entry; 7 of 9 suites already fail on `master`. +- **Test command:** `CI=true npx react-app-rewired test --watchAll=false 2>&1 | grep -E "^(PASS|FAIL)|^Test Suites:|^Tests:"`. Never pipe jest to `tail` and read `$?`. +- **Build gate:** `npm run build` (plain, never `CI=true`). +- **Baseline:** `7 failed, 4 passed, 11 total` / `33 passed, 33 total`. +- **SCSS:** edit only `src/index.module.scss`, then `npm run build-css`. Never hand-edit `src/index.css`. +- **This is a refactor.** Amount Format must look pixel-identical afterwards. Address Mode changes appearance but **not behaviour**. + +--- + +## Reference: the mobile decomposition (wallet-mobile#893) + +Adapt this API; do not copy the React Native implementation. + +``` +RadioGroup({ value, onChange, options }) + options: Array<{ value, title, description?, hint?, badge?, disabled? }> + → a bordered card; renders a divider between consecutive options + +RadioOption({ selected, onPress, disabled, title, badge, description, hint }) + → radio circle + title row (title + optional badge pill) + optional description + optional italic hint + +RadioButton({ selected, disabled }) + → just the circle + +PreferenceSaveButton({ title, onPress, disabled }) + → the full-width primary save button +``` + +Desktop adaptations: `onPress` → `onClick`; RN `StyleSheet` → SCSS classes; `TouchableOpacity`/`View`/`Text` → `div`/`span`/`p`; the radio circle stays a real `` styled with `appearance: none` (as both modals already do) so keyboard and screen-reader behaviour is preserved. + +--- + +## Task 1: `RadioButton` + +**Files:** Create `src/components/Radio/RadioButton.js` + +- [ ] **Step 1: Write the component** + +```jsx +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * The radio circle itself. A real input so keyboard and assistive tech work; + * `appearance: none` in SCSS replaces the native rendering. + * + * @memberof Components + */ +function RadioButton({ id, name, selected, disabled, onChange }) { + return ( + + ); +} + +RadioButton.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, + disabled: PropTypes.bool, + onChange: PropTypes.func.isRequired, +}; + +RadioButton.defaultProps = { + disabled: false, +}; + +export default RadioButton; +``` + +--- + +## Task 2: `RadioOption` + +**Files:** Create `src/components/Radio/RadioOption.js` + +- [ ] **Step 1: Write the component** + +```jsx +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import RadioButton from './RadioButton'; + +/** + * A selectable row: radio circle, a title row carrying an optional badge pill, + * an optional description and an optional muted italic hint. + * + * @memberof Components + */ +function RadioOption({ name, value, selected, disabled, onSelect, title, badge, description, hint }) { + const inputId = `${name}-${value}`; + const handleSelect = () => { + if (!disabled) { + onSelect(value); + } + }; + + const renderBadge = () => { + if (!badge) { + return null; + } + return {badge}; + }; + + const renderDescription = () => { + if (!description) { + return null; + } + return

{description}

; + }; + + const renderHint = () => { + if (!hint) { + return null; + } + return {hint}; + }; + + return ( +
+ +
+
+ + {renderBadge()} +
+ {renderDescription()} + {renderHint()} +
+
+ ); +} + +RadioOption.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, + disabled: PropTypes.bool, + onSelect: PropTypes.func.isRequired, + title: PropTypes.node.isRequired, + badge: PropTypes.node, + description: PropTypes.node, + hint: PropTypes.node, +}; + +RadioOption.defaultProps = { + disabled: false, + badge: null, + description: null, + hint: null, +}; + +export default RadioOption; +``` + +Note the render helpers rather than inline ternaries — a project convention (`CLAUDE.md`: "Nas funcoes que renderizam JSX, evite usar if ternarios, prefira funcoes para isso"). + +--- + +## Task 3: `RadioGroup` and the barrel + +**Files:** Create `src/components/Radio/RadioGroup.js`, `src/components/Radio/index.js` + +- [ ] **Step 1: `RadioGroup.js`** + +```jsx +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import RadioOption from './RadioOption'; + +/** + * A bordered card grouping radio options, separated by dividers. Controlled: + * the caller owns the selected value and receives it back through onChange. + * + * @memberof Components + */ +function RadioGroup({ name, value, onChange, options }) { + return ( +
+ {options.map((option, index) => ( + + {index > 0 &&
} + +
+ ))} +
+ ); +} + +RadioGroup.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, + description: PropTypes.node, + hint: PropTypes.node, + badge: PropTypes.node, + disabled: PropTypes.bool, + })).isRequired, +}; + +export default RadioGroup; +``` + +- [ ] **Step 2: `index.js`** + +```js +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { default as RadioButton } from './RadioButton'; +export { default as RadioOption } from './RadioOption'; +export { default as RadioGroup } from './RadioGroup'; +``` + +--- + +## Task 4: `PreferenceSaveButton` + +**Files:** Create `src/components/PreferenceSaveButton.js` + +- [ ] **Step 1: Write the component** + +```jsx +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * The primary save button shared by the preference modals. Renders only the + * button; the caller supplies its own container and spacing. + * + * @memberof Components + */ +function PreferenceSaveButton({ title, onClick, disabled }) { + return ( + + ); +} + +PreferenceSaveButton.propTypes = { + title: PropTypes.node.isRequired, + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool, +}; + +PreferenceSaveButton.defaultProps = { + disabled: false, +}; + +export default PreferenceSaveButton; +``` + +--- + +## Task 5: Shared SCSS + +**Files:** Modify `src/index.module.scss` + +The `.amount-format-*` and `.address-mode-*` blocks currently duplicate the radio circle, option card, option typography and save button almost line for line. Hoist those into shared blocks. + +- [ ] **Step 1: Add the shared blocks** + +```scss +/* Shared radio selector, used by the preference modals */ +.radio-group { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 16px; + padding: 24px 16px; + + &-divider { + margin: 24px 0; + border-top: 1px solid #e5e7eb; + } +} + +.radio-option { + display: flex; + align-items: flex-start; + gap: 16px; + cursor: pointer; + + &--disabled { + cursor: default; + + .radio-option-title { + color: #c4c4c4; + cursor: default; + } + + .radio-option-description, + .radio-option-hint { + color: #b0b0b0; + } + + .radio-option-hint { + font-style: normal; + } + } + + &-body { + flex: 1; + min-width: 0; + } + + &-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + } + + &-title { + font-size: 16px; + font-weight: 600; + line-height: 20px; + color: #000; + margin-bottom: 4px; + cursor: pointer; + } + + &-description { + font-size: 12px; + line-height: 20px; + color: #979797; + margin-bottom: 0; + } + + &-hint { + font-size: 12px; + line-height: 20px; + font-style: italic; + color: #57606a; + } + + &-badge { + background: #f2f3f5; + border-radius: 8px; + width: 69px; + height: 23px; + font-size: 12px; + line-height: 20px; + color: #6b7280; + text-align: center; + flex-shrink: 0; + padding: 2px 0; + } +} + +.radio-button { + appearance: none; + -webkit-appearance: none; + width: 26px; + height: 26px; + border: 2px solid #c4c4c4; + border-radius: 50%; + margin: 0; + cursor: pointer; + position: relative; + flex-shrink: 0; + + &:checked { + border-color: $purpleHathor; + + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 13px; + height: 13px; + border-radius: 50%; + background: $purpleHathor; + } + } + + &:disabled { + border-color: #e0e0e0; + cursor: default; + } +} + +.preference-save-btn { + background: #e5e5e5; + border: none; + border-radius: 8px; + padding: 16px; + width: 226px; + color: #b0b0b0; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + cursor: default; + + &--active { + background: $purpleHathor; + color: white; + cursor: pointer; + + &:hover { + background: $purpleHathorHover; + } + } +} +``` + +- [ ] **Step 2: Delete the superseded declarations** + +Remove from the `.amount-format` block: `&-card`, `&-divider`, `&-option` (and all its nested rules including the `input[type=radio]` styling), `&-tag`, `&-save-btn`. **Keep** `&-modal .modal-dialog`, `&-description`, `&-preview-label`, `&-preview` and its children — those are Amount-Format-specific. + +Remove from the `.address-mode-*` rules: `.address-mode-option` (and its `input[type=radio]` styling and `--disabled` variants), `.address-mode-label`, `.address-mode-description`, `.address-mode-hint`, `.address-mode-save-btn` (and `--active`). **Keep** `.address-mode-alert` — the warning banner is unique to Address Mode. + +- [ ] **Step 3: Rebuild** — `npm run build-css`, then confirm `.radio-group`, `.radio-option`, `.radio-button` and `.preference-save-btn` are present in `src/index.css` and the deleted classes are gone. + +--- + +## Task 6: Recompose `ModalAmountFormat` — zero visual change + +**Files:** Modify `src/components/ModalAmountFormat.js` + +- [ ] **Step 1: Replace the hand-rolled options with `RadioGroup`** + +Delete the local `renderOption` helper. Import `RadioGroup from './Radio/RadioGroup'` and `PreferenceSaveButton from './PreferenceSaveButton'`, then render: + +```jsx + +``` + +- [ ] **Step 2: Replace the save button** + +```jsx + onSave(selectedFormat)} + /> +``` + +- [ ] **Step 3: Verify no visual change** + +The PREVIEW block, the description paragraph and the modal lifecycle stay exactly as they are. Compare against the Figma frame `318:2469` — spacing, the `Default` pill, the divider and the radio circle must be unchanged. + +--- + +## Task 7: Restyle `ModalAddressMode` — zero behaviour change + +**Files:** Modify `src/components/ModalAddressMode.js` + +This modal's behaviour is load-bearing. **Preserve all of it:** +- the `hasTxOutsideFirstAddress()` check on mount, and the `loading` state while it resolves +- `isSingleDisabled = (loading || hasTxOutside) && currentMode !== ADDRESS_MODE.SINGLE` +- the `.address-mode-alert` warning banner, shown only when `hasTxOutside`, including its `Learn more` link opened via `helpers.openExternalURL` +- save disabled unless `hasChanged && !loading` +- `onSave(selectedMode)` triggering the wallet reload in `Settings.js` + +- [ ] **Step 1: Replace the two hand-rolled option blocks with `RadioGroup`** + +```jsx + +``` + +The warning banner stays **below** the group, rendered by its existing conditional. + +- [ ] **Step 2: Replace the save button** with `PreferenceSaveButton`, keeping `disabled={!hasChanged || loading}`. + +- [ ] **Step 3: Replace the remaining inline styles** + +Delete the `style={{...}}` attributes the file currently uses for layout (the `display: flex` wrapper, the `marginBottom: 40` intro paragraph, the footer's `borderTop: 'none', marginTop: 12`) in favour of the shared classes plus a small `.address-mode-*` block for what remains unique. Do not introduce new inline styles. + +- [ ] **Step 4: Verify behaviour by reading** + +Re-read the file and confirm every behaviour in the list above still holds. Report each one explicitly. + +--- + +## Task 8: Verification + +- [ ] **Step 1: Suite and build** — baseline unchanged, no warning naming a touched file. +- [ ] **Step 2: `make update_pot`** — the strings are unchanged in wording, so expect only line-reference churn. Report anything else. +- [ ] **Step 3: Manual QA** (a human runs this; list it in the PR body) + - Amount Format modal is pixel-identical to before this PR. + - Address Mode: with a wallet that has transactions outside the first address, Single is disabled and greyed, the warning banner shows, and Save stays disabled. + - Address Mode: with a fresh wallet, Single is selectable, Save enables on change, and saving reloads the wallet into single-address mode. + - Keyboard: both modals are operable with Tab and the arrow keys, and the label click-target selects the option. + +--- + +## Out of scope + +- The `manageDomLifecycle` gap. **Both** modals currently call `$('#id').modal('show')` directly rather than using `GlobalModal`'s `manageDomLifecycle`, so Bootstrap's defaults apply and the modal can be dismissed by Escape or an outside click, silently discarding a selection. This predates the feature and now affects both. It is a reasonable follow-up but is deliberately not bundled here, to keep this PR a pure refactor. Raise it as a separate issue. +- Any amount-formatting change — done in PRs 1 and 2. diff --git a/docs/superpowers/plans/2026-07-22-amount-format-pr4.md b/docs/superpowers/plans/2026-07-22-amount-format-pr4.md new file mode 100644 index 000000000..cdeedc899 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-amount-format-pr4.md @@ -0,0 +1,170 @@ +# Amount Format — PR4 Implementation Plan (Home / dashboard wrapping) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Make long amounts on the dashboard wrap onto additional lines instead of clipping or overflowing — in the balance block and in the transaction history `Value` column. + +**Architecture:** Layout only. `` already carries `overflow-wrap: anywhere`; what is missing is containers that allow the wrap and produce the hanging indent from the design. No formatting logic changes. + +**Figma:** [Home / dashboard, node `372:1822`](https://www.figma.com/design/2xRserWXaSJAgkbiQlPzhr/Amounts-Update?node-id=372-1822) +**Depends on:** PR3. Branch from it. + +--- + +## Working agreement + +- **Do not commit per task.** The controller commits once at the end. +- **No render tests** — Jest 27 cannot parse `axios@1.7.7`'s ESM entry; 7 of 9 suites already fail on `master`. +- **Test command:** `CI=true npx react-app-rewired test --watchAll=false 2>&1 | grep -E "^(PASS|FAIL)|^Test Suites:|^Tests:"`. Never pipe jest to `tail` and read `$?`. +- **Build gate:** `npm run build` (plain, never `CI=true`). +- **Baseline:** `7 failed, 4 passed, 11 total` / `33 passed, 33 total`. +- **SCSS:** edit only `src/index.module.scss`, then `npm run build-css`. + +--- + +## Design values (measured from Figma — do not invent) + +**Balance rows** (`372:1845`–`372:1857`): each row is an independent flex row with `gap: 8px`. Label is `20px` weight 700, `line-height: 22px`, `#000`, `white-space: nowrap`. Value is `20px` weight 400, `line-height: 22px`, `#000`. The value box hugs its content — **no explicit max-width**. + +The hanging indent (continuation line aligning under the value's first character, not under the label) is a **free consequence of flex layout**: both lines live inside one flex item that begins after the label plus gap. Do not add an explicit indent or margin. + +**History `Value` column** (`372:2193`/`372:2195`): `20px` regular, `line-height: 22px`, `text-align: right`. Both lines of a wrapped value sit flush to the same right edge. + +**Two important caveats about the mock:** + +1. The wrapped values in Figma are **hand-authored line breaks** — two separate `

` nodes, each `white-space: nowrap`. The designer chose the break point to illustrate the effect; it is **not** a specification of where CSS should break. Implement live wrapping and let the break fall where the container width dictates. +2. The table's header columns and body columns are laid out independently in the mock (header pitch 334px; body columns content-hugged and differing per row). **Do not lift a fixed column width from Figma** — the design never committed to one. + +**Confirmed:** no `text-overflow`, no truncation, no reduced font size anywhere in the frame at any content length. The ellipsis in the ID column is literal text typed by the designer, not a CSS pattern to copy. + +--- + +## Explicitly out of scope + +The Figma frame also differs from the shipped app in ways this PR must **not** change: + +- Figma renders history values at `20px` in the default sans font; the app uses `font-family: monospace; font-size: 1.2rem` (`src/index.css`, `#token-history .value`). +- Figma uses `#41A922` / `#A92224` for received/sent; the app uses `#28a745` / `#dc3545`. + +The brief for this screen is wrapping only. Leave the font and colours alone and note the divergence in the PR description so the designer can confirm. + +--- + +## Task 1: Balance block wrapping + +**Files:** Modify `src/components/WalletBalance.js`, `src/index.module.scss` + +Today each row is `

Total:

`. In a plain paragraph the continuation line wraps back to the paragraph's left edge — under the label — instead of under the value. + +- [ ] **Step 1: Make each row a flex row** + +In `src/components/WalletBalance.js`, replace the three rows inside `renderBalance` with: + +```jsx +
+ {t`Total:`} + +
+
+ {t`Available:`} + +
+
+ {t`Locked:`} + +
+``` + +- [ ] **Step 2: Add the SCSS** + +Append to `src/index.module.scss`: + +```scss +/* Dashboard balance rows */ +.wallet-balance { + &-row { + display: flex; + align-items: flex-start; + gap: 8px; + // Bootstrap's paragraph spacing used to provide this; the rows are divs now. + margin-bottom: 1rem; + + // The value is the flex item, so its wrapped continuation lines align under + // the value's first character rather than under the label. This is the + // hanging indent from the design — it needs no explicit indent. + .amount { + min-width: 0; + } + } + + &-label { + white-space: nowrap; + flex-shrink: 0; + } +} +``` + +- [ ] **Step 3: Rebuild** — `npm run build-css`, confirm `.wallet-balance-row` is in `src/index.css`. + +- [ ] **Step 4: Check the spacing did not regress** + +The previous `

` elements inherited Bootstrap's `margin-bottom: 1rem`. The `margin-bottom` above restores it. Compare the rendered spacing against `master` and report any difference. + +--- + +## Task 2: History `Value` column wrapping + +**Files:** Modify `src/index.module.scss` + +The `Value` cell already renders ``, which carries `overflow-wrap: anywhere`. Two things can still defeat it: the cell collapsing to an unusably narrow width, and the table forcing a single line. + +- [ ] **Step 1: Add the column rules** + +Append to `src/index.module.scss`: + +```scss +/* Transaction history value column */ +#token-history .value { + // Long amounts wrap onto a second line, right-aligned, and are never + // shrunk or ellipsized. min-width stops the column collapsing so far that + // the value breaks earlier than it needs to. + min-width: 12rem; + white-space: normal; +} +``` + +Do **not** set an explicit `width` — the Figma columns are content-hugged and vary per row, so a fixed width would be inventing a value the design never specified. + +`text-align: right` and the monospace font are already applied by the existing `#token-history .value` rule in `src/index.css`; that rule is compiled from `src/index.module.scss`, so check whether the block already exists there and extend it rather than adding a duplicate selector. + +- [ ] **Step 2: Rebuild and confirm** — `npm run build-css`, then verify there is exactly one `#token-history .value` rule in the compiled output, with the merged declarations. + +--- + +## Task 3: Verification + +- [ ] **Step 1: Suite and build** — baseline unchanged, no warning naming a touched file. + +- [ ] **Step 2: Confirm no truncation remains** + +Run: `grep -rn "text-overflow\|white-space: *nowrap" src/index.module.scss` + +Review every hit. None may apply to an element that renders an amount. `.wallet-balance-label` legitimately uses `nowrap` — that is the label, not the value. + +- [ ] **Step 3: Manual QA** (a human runs this; list it in the PR body) + +Use a wallet holding a very large balance, or temporarily stub one, so values reach roughly `123,456,789,012,345,678.12345678`. + + - Dashboard `Total` / `Available` / `Locked`: the value wraps to a second line, and the second line starts under the **value**, not under the label. + - Narrow the window to roughly 900px: values still wrap, never clip, never ellipsize, never shrink. + - History `Value` column: long values wrap to two lines, both flush right, and the column does not collapse. + - Short values are unchanged from `master`. + - Row spacing in the balance block matches `master`. + +--- + +## Out of scope + +- Font family, font size and colour of history values (see above). +- The transaction overview modal — PR5. +- Deposit and fee labels — PR6. diff --git a/docs/superpowers/plans/2026-07-22-amount-format-pr5.md b/docs/superpowers/plans/2026-07-22-amount-format-pr5.md new file mode 100644 index 000000000..b20b17ef6 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-amount-format-pr5.md @@ -0,0 +1,134 @@ +# Amount Format — PR5 Implementation Plan (Transaction overview wrapping) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Stop the transaction overview modal clipping long amounts. Per-output amounts and the "You will pay" total wrap instead of being forced onto one line. + +**Architecture:** Layout only, on the modal's **current** structure. No formatting logic changes, no structural redesign. + +**Figma:** [as-is `318:4172`](https://www.figma.com/design/2xRserWXaSJAgkbiQlPzhr/Amounts-Update?node-id=318-4172) / [to-be `323:196`](https://www.figma.com/design/2xRserWXaSJAgkbiQlPzhr/Amounts-Update?node-id=323-196) +**Depends on:** PR4. Branch from it. + +--- + +## ⚠️ Read this before starting: one requirement is parked + +The original brief asked for the value font size to "adapt based on what the wallet-mobile does". Figma defines that precisely — but on an element the desktop modal **does not have**. + +Measured from Figma: + +| Element | as-is | to-be | +| --- | --- | --- | +| Headline amount (`323:193` / `323:249`) | **24px** SF Pro Text Heavy, black, centered | **16px**, same weight and colour | +| `available` sub-line (`323:194` / `323:250`) | 12px SF Pro Text Semibold, `#8E8E93` | unchanged | +| Total value (`323:291`) | 14px, single line | **14px, wraps to two lines — never shrinks** | + +So the design shrinks **only** a headline amount, 24px → 16px, and never shrinks the total. The shipped desktop modal (`src/components/ModalTransactionOverview.js`) has no headline amount — it opens straight into the per-output list — so those two numbers have nothing to apply to. + +**Decision pending.** Until it is made, this PR ships **wrapping only** and adds no adaptive font sizing. The three candidate resolutions are recorded in the "Open questions" section of `docs/superpowers/specs/2026-07-21-amount-format-design.md`. Do not invent a base/floor pair for the per-output rows. + +If you are executing this plan and the decision has since been made, stop and ask for an updated plan rather than improvising. + +--- + +## Working agreement + +- **Do not commit per task.** The controller commits once at the end. +- **No render tests** — Jest 27 cannot parse `axios@1.7.7`'s ESM entry. +- **Test command:** `CI=true npx react-app-rewired test --watchAll=false 2>&1 | grep -E "^(PASS|FAIL)|^Test Suites:|^Tests:"`. Never pipe jest to `tail` and read `$?`. +- **Build gate:** `npm run build` (plain, never `CI=true`). +- **Baseline:** `7 failed, 4 passed, 11 total` / `33 passed, 33 total`. +- **Do not restructure the modal.** No headline amount, no `To:` row, no collapsible fee breakdown, no `Privacy fee` line — the last implies a feature that does not exist on desktop. + +--- + +## Task 1: Per-output amounts wrap + +**Files:** Modify `src/components/ModalTransactionOverview.js` + +The output rows render an address on the left and an amount plus a direction arrow on the right. The amount `` sets `whiteSpace: 'nowrap'`, which is what clips long values today. + +- [ ] **Step 1: Locate the row** + +Find the amount `` inside `renderOutput` — it is the one carrying `fontWeight: 500, color: '#404040', marginLeft: '12px', whiteSpace: 'nowrap', fontSize: '14px'`. Line numbers have shifted across PRs 1–4; locate by content. + +- [ ] **Step 2: Let it wrap** + +Remove `whiteSpace: 'nowrap'` from that span's style object and add `overflowWrap: 'anywhere'` plus `minWidth: 0`. Keep every other declaration. + +The address span beside it already has `wordBreak: 'break-all', minWidth: 0, flex: 1`. Give the amount span a `flexShrink: 0` **only if** testing shows the address squeezing the amount to nothing; otherwise leave the flex behaviour alone and say so. + +- [ ] **Step 3: Keep the arrow on the last line** + +The direction arrow `` sits inside the same span. Confirm that when the amount wraps, the arrow stays adjacent to the final line rather than being orphaned. If it detaches badly, wrap the numeric part in its own `` and leave the arrow as a sibling — report if you needed this. + +--- + +## Task 2: "You will pay" total wraps + +**Files:** Modify `src/components/ModalTransactionOverview.js` + +Figma node `323:291` gives exact values for this element, and confirms it does **not** shrink: + +| Property | Value | +| --- | --- | +| font-size | `14px` | +| font-weight | `500` (semibold) | +| colour | `#404040` | +| line-height | `20px` | +| text-align | `right` | +| wrapping | `word-break: break-word`, wraps to as many lines as needed | + +- [ ] **Step 1: Update the value span** + +In `renderTotalPayment`, the value span currently sets `fontSize: '14px', fontWeight: 500, color: '#404040'`. Add `lineHeight: '20px'`, `textAlign: 'right'`, `overflowWrap: 'anywhere'` and `minWidth: 0`. + +- [ ] **Step 2: Let the flex row give it room** + +The wrapper is `

`. A flex item will not shrink below its content width by default, which would push the row wider than the modal. Give the label span `flexShrink: 0` and the value span `minWidth: 0` so the value is the element that wraps. + +- [ ] **Step 3: Check for a clipping ancestor** + +Figma's equivalent container (`323:290`) sets `overflow-clip`. **Do not reproduce that** — a clipping parent defeats wrapping entirely. Verify no ancestor of this row in the modal sets `overflow: hidden`; if one does, report it rather than changing shared modal CSS unilaterally. + +--- + +## Task 3: Network fee row + +**Files:** Modify `src/components/ModalTransactionOverview.js` + +`renderNetworkFeeValue` returns a span with `fontSize: '14px', fontWeight: 500, color: '#404040'`. It has no `nowrap`, so it should already wrap — but its flex row needs the same treatment as Task 2. + +- [ ] **Step 1:** Confirm the fee row's label has `flexShrink: 0` and the value has `minWidth: 0`. Add them if missing. +- [ ] **Step 2:** Leave the "No fee" pill alone — it is a fixed-width badge, not an amount. + +--- + +## Task 4: Verification + +- [ ] **Step 1: Suite and build** — baseline unchanged, no warning naming this file. + +- [ ] **Step 2: Confirm no `nowrap` remains on an amount** + +Run: `grep -n "nowrap" src/components/ModalTransactionOverview.js` + +Any remaining hit must be on a non-amount element. Report each with its purpose. + +- [ ] **Step 3: Manual QA** (a human runs this; list it in the PR body) + +Trigger the modal from Send Tokens with an amount near `123,456,789,012,345,678.12345678`: + + - The per-output amount wraps rather than clipping, and the direction arrow stays with the final line. + - "You will pay" wraps across lines, right-aligned, and does not widen the modal. + - With multiple tokens plus a fee, the joined total (`"… AAA + … HTR"`) wraps sensibly. + - The modal does not grow wider than its `modal-dialog` and no horizontal scrollbar appears. + - Short amounts look identical to `master`. + - The PIN field, Cancel and Confirm are unaffected. + +--- + +## Out of scope + +- **Adaptive font sizing** — parked, see the top of this plan. +- Any structural change to the modal. +- The `Privacy fee` line from the mock — no such feature on desktop. diff --git a/docs/superpowers/plans/2026-07-22-amount-format-pr6.md b/docs/superpowers/plans/2026-07-22-amount-format-pr6.md new file mode 100644 index 000000000..5f1f13177 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-amount-format-pr6.md @@ -0,0 +1,139 @@ +# Amount Format — PR6 Implementation Plan (Deposit / fee labels) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Stop the read-only amount labels on Create Deposit Token, Create Fee Token, Create NFT and Send Tokens from overflowing their containers when the amount is long. + +**Architecture:** Layout only. The amounts already route through `` / `formatAmount` from PR1. This PR gives their containers permission to wrap. **The editable amount input is not touched.** + +**Figma:** [Create deposit token / Send tokens, node `326:3120`](https://www.figma.com/design/2xRserWXaSJAgkbiQlPzhr/Amounts-Update?node-id=326-3120) +**Depends on:** PR5. Branch from it. + +--- + +## Working agreement + +- **Do not commit per task.** The controller commits once at the end. +- **No render tests** — Jest 27 cannot parse `axios@1.7.7`'s ESM entry. +- **Test command:** `CI=true npx react-app-rewired test --watchAll=false 2>&1 | grep -E "^(PASS|FAIL)|^Test Suites:|^Tests:"`. Never pipe jest to `tail` and read `$?`. +- **Build gate:** `npm run build` (plain, never `CI=true`). +- **Baseline:** `7 failed, 4 passed, 11 total` / `33 passed, 33 total`. + +--- + +## `src/components/InputNumber.js` is not modified + +Decided during design and unchanged: `InputNumber` accumulates keystrokes as BigInt and has no `maxLength`, so it **already** accepts any number of decimal places — the "support 8 decimal places in the inputs" requirement is functionally met today. + +A long typed value can overflow the field visually, but `` is single-line by definition and the caret is already pinned to the end (`InputNumber.updateCaretPosition`), so the digits being typed stay visible. Widening the field, shrinking the font and capping the length were all considered and rejected. **Only read-only labels are in scope.** + +For context, Figma's Amount input (`326:3146`) is `381px × 38px`, `1px solid #B7BFC7`, `border-radius: 4px`. Recorded so nobody re-derives it; not a change to make. + +--- + +## Design values (measured from Figma) + +The `Deposit: … HTR (20.00 HTR available)` label (`326:3153`): `16px`, `line-height: 22px`, colour `#000`, container hugs to `620px` inside a `716px` parent. + +**Caveat:** the mock shows this label as `white-space: nowrap`, on one line. That is not a specification that it must never wrap — it simply reflects the short sample value the designer used, which fits in the available width. With a realistic maximum it would overflow the card. Implement wrapping; the mock is consistent with that because at its sample length no wrap occurs. + +Two other frames in that Figma file sit loose on the canvas rather than inside the card's auto-layout (`326:3156`, `326:3159`, `326:3160`) and are designer scratch. Ignore them. + +--- + +## Task 1: Create Token / Create Fee Token labels + +**Files:** Modify `src/screens/CreateToken.js`, `src/index.module.scss` + +`CreateToken.js` builds `infoLabel` as a template string combining the deposit amount and the available balance, then renders it. The string itself is fine; the container must be allowed to wrap. + +- [ ] **Step 1: Find the label element** + +Locate where `infoLabel` (and the `requiredFeeAmountText` / `availableBalanceText` values) are rendered. Note the class or inline style on the containing element. + +- [ ] **Step 2: Give it a wrapping class** + +Add `className="amount-label"` to the element that renders the label text. If it already has classes, append rather than replace. + +- [ ] **Step 3: Add the SCSS** + +Append to `src/index.module.scss`: + +```scss +/* Read-only amount labels beside inputs (deposit, fee, available balance) */ +.amount-label { + // The label embeds amounts inside a sentence, so the break can land at a + // space normally; `anywhere` is the fallback for a single very long number + // that exceeds the container on its own. + overflow-wrap: anywhere; + white-space: normal; + max-width: 100%; +} +``` + +- [ ] **Step 4: Rebuild** — `npm run build-css`, confirm `.amount-label` is present in `src/index.css`. + +--- + +## Task 2: Create NFT labels + +**Files:** Modify `src/screens/CreateNFT.js` + +This screen renders three `

` rows — ` available:`, `Deposit:` and `Total:` — each already using ``, plus an `nftFee` string. + +- [ ] **Step 1:** Add `className="amount-label"` to each of those three `

` elements and to the element rendering `nftFee`. + +- [ ] **Step 2:** Confirm none of them sits inside a container with a fixed width or `overflow: hidden`. Report if one does. + +--- + +## Task 3: Send Tokens labels + +**Files:** Modify `src/components/SendTokensOne.js` + +Three read-only amount labels: the network fee row, the available-balance readout beside the token selector, and the "You'll pay" summary line built as a template string. + +- [ ] **Step 1:** Add `className="amount-label"` to each of the three containing elements. + +- [ ] **Step 2:** The available-balance readout sits next to the token `