Skip to content

release: v0.5.3 - #135

Merged
logicalmechanism merged 92 commits into
mainfrom
release/v0.5.3
May 5, 2026
Merged

logicalmechanism merged 92 commits into
mainfrom
release/v0.5.3

Conversation

@logicalmechanism

Copy link
Copy Markdown
Contributor

Summary

Release v0.5.3 — desktop polish, history & activity, and updater UX.

Notable fix: reconcileWithOnChain now updates existing records' fields from on-chain instead of insert-only — closes a long-silent stale-field bug.

Full notes: CHANGELOG.md

Version bumped in lockstep across tauri.conf.json, Cargo.toml/Cargo.lock, root + fe + be package.json/package-lock.json, and aiken.toml.

Test plan

  • cd app/contracts && aiken check — all contract tests pass
  • cd app/snark && go test ./... -count=1 — gnark suite passes
  • cd app && python -m pytest -s -vv — Python CLI suite passes
  • cd app/gui && bash test.sh — fe + be vitest pass
  • cd app/gui && bash lint.sh — eslint, tsc, clippy, cargo fmt clean
  • cd app/gui && bash build.sh — production AppImage builds
  • Smoke test the AppImage: wallet unlock → node sync → marketplace, History activity rows, sales/purchase CSV export, updater download progress + cancel, comma-formatted bid input

🤖 Generated with Claude Code

logicalmechanism and others added 30 commits April 25, 2026 00:15
Adds bytes_per_sec to DownloadProgress (computed over a 1s rolling window
of cumulative-byte samples), and accepts an optional expected_size param
preferred over response.content_length() for percent. Both fix the
Settings → Updates progress display where percent jumped around and no
speed was shown — root cause was unreliable Content-Length on the GitHub
asset CDN redirect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New `UpdateContext` singleton wraps `useUpdateCheck` so a download
  kicked off by the Dashboard "update available" toast is visible when
  the user navigates to Settings → Updates. Provider mounted in
  main.tsx; Dashboard and UpdateSection now consume the context
  instead of instantiating their own hook copies.
- Toast Download action now starts the download AND navigates to
  /settings with `state.section = 'update'` so the existing progress
  UI does the showing.
- `useUpdateCheck` adds `bytes_per_sec` to the progress payload and an
  optional `expectedSize` arg passed through to the Rust command. The
  initial `downloading` state seeds `total_bytes` from `expectedSize`
  so the byte counters render correctly before the first event.
- `UpdateSection` renders speed (formatBytes/sec) and an ETA mm:ss
  computed from remaining bytes / rate; both hidden when rate is 0.
- Dashboard test mocks the new UpdateContext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous guard set `startupCheckedRef.current = true` in the effect
body before the timer fired. Under React.StrictMode (dev-only double
mount): first effect set ref=true and scheduled timer A; cleanup cleared
timer A; second effect saw ref already true and returned early. No
timer ever fired, so the Dashboard "update available" toast never
appeared in `tauri dev`. Production builds skip the double-invoke and
were unaffected.

Move the guard inside the timeout callback so StrictMode's setup →
cleanup → setup pattern correctly schedules a fresh timer that runs
the check exactly once.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 40-line scrollable preview is fine for a glance but painful to
actually read. Adds an "Expand" button next to the "Release notes:"
label that opens a max-w-2xl modal mirroring the DescriptionModal
pattern (focus trap, modal stack, Escape, backdrop click). The inline
preview stays for quick scanning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a Cancel button to the Settings → Updates downloading state. The
Rust side reads a shared `Arc<AtomicBool>` flag inside the chunk loop;
a new `cancel_update_download` Tauri command flips it. On the next
chunk boundary, `download_update` drops the file handle, removes the
.download tmp file, and returns `Err("cancelled")`.

The frontend hook recognizes the sentinel: instead of surfacing it as
an error, it restores the prior `available` state (info captured via
effect on every available transition — setState updaters in React 18
can run lazily, so a closure-time capture would still see null).
Without prior info the state falls back to idle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ownload-flow

Fix update toast Download action + accurate download progress
Surfaces non-protocol wallet txs (plain ADA send/receive) so the History tab
can show a complete picture of a payment credential's on-chain activity. Reuses
the existing HistoryRecord shape; classifies by whether the user appears in
inputs (send) or only outputs (receive). Excludes any tx that touches the
encryption/bidding contract addresses since those are surfaced by /history.
Cached for 60s with stale fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends TransactionType with 'send' and 'receive' for plain wallet activity,
adds English fallback labels and i18n entries (history.txType.send/receive)
across all 17 locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds chainApi.getWalletActivity, fetches it in parallel with the protocol
history during recovery, and dedupes by txHash before reconciling. The
filter dropdown now exposes Sent ADA / Received ADA so users can scope
the list to plain wallet movement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Refresh button only re-queried current encryption/bid UTxOs, so an
incoming receive (or outgoing send) wouldn't appear unless the user also
hit Recover. Activity is cached server-side for 60s, so calling it on
every refresh is cheap and matches the user's mental model: refresh shows
new things.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ults

Two related fixes:

1. Tauri loads Express from src-tauri/resources/be/dist/, not be/dist/.
   Backend changes were silently invisible until a manual `npm run bundle:be`
   refreshed the bundled copy. run.sh now does this automatically before
   `tauri dev`. CLAUDE.md updated to flag the same trap for future readers.

2. The activity endpoint cached empty results for 60s, which masked newly
   indexed receives. Empty responses are now uncached, and a structured log
   line on the empty path makes Koios-vs-filter issues debuggable from the
   server log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Koios /tx_info responses use 'tx_timestamp' (UNIX seconds), not 'block_time'.
The existing classifyTx and the new classifyActivityTx both did
'tx.block_time * 1000', which is NaN when block_time is undefined and
JSON-serializes as null.

Why this matters for the History tab: reconcileWithOnChain in
fe/src/services/transactionHistory.ts sorts records descending by timestamp
and slices to 100. With null timestamps coercing to 0, every wallet activity
record sorted to the very end and got truncated when local + on-chain rows
exceeded 100 — so receives never appeared. Date-range filters also dropped
them (null >= cutoff is false).

Now reads 'tx_timestamp ?? block_time ?? 0' so both endpoint shapes work
and the existing /history records also get real timestamps for the first
time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reconcileWithOnChain only added new records; it never updated existing
ones. So once a tx had been persisted with a stale field (e.g. timestamp 0
from the now-fixed block_time bug), later reconciles dropped the better
on-chain data on the floor. The receive at the top of /api/chain/activity
sat at position 73+ in the History tab because the localStorage copy still
held timestamp 0.

Now backfills timestamp / amountLovelace / counterparty / confirmedAtBlock
on existing records when the local copy is missing those values. Two new
tests cover the upgrade-from-stale-zero and the backfill paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The History tab list lives in its own scroll container (max-height
calc(100vh - 320px)), so the global ScrollToTop never appeared no matter
how far you scrolled inside the list. ScrollToTop now optionally accepts
a scrollContainer ref and binds to that element's scroll/scrollTo when
provided, falling back to window otherwise. HistoryTab hoists the
virtualizer's parentRef to the tab and passes it to both the virtualizer
and the button so the same scroll element drives both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y-monitor

Wallet activity monitor in History tab
Adds two new CSV formatter functions for tax-reporting exports:
salesToCSV joins seller listings with their bidsMap (highest pending
bid + accepted-bid sale amount) and purchasesToCSV joins buyer bids
with the encryption map for the seller PKH. Status labels follow the
draft spec (Open/Pending/Sold for sales; Active/Locked/Won/Cancelled/
Rejected for purchases). 21 new unit tests cover empty inputs, status
mapping, missing optional fields, and CSV escaping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an Export-as-CSV button to the MySales toolbar (next to Refresh)
that runs the currently filtered listings through salesToCSV and
saves via the native dialog. File name is veiled-sales-YYYY-MM-DD.csv.
Reuses the history.exportTitle/Aria/edTo/Failed translation keys —
no new locale strings required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mirrors the MySales export wiring: an Export-as-CSV button in the
purchases toolbar (after Refresh) runs the currently filtered+sorted
bids through purchasesToCSV and saves via the native dialog. File
name is veiled-purchases-YYYY-MM-DD.csv. Reuses the
history.exportTitle/Aria/edTo/Failed translation keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mocks salesToCSV/purchasesToCSV and exportTextFile, then asserts:
button is absent in the empty state, present and enabled when there
is data, and clicking it calls exportTextFile with a
veiled-{sales,purchases}-YYYY-MM-DD.csv filename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous export only covered live bid UTxOs, so once a buyer
decrypted their winning bid the row vanished from the CSV — useless
for tax records. Now the export also emits one row per re-encryption
UTxO the user owns or has since resold (Won/Resold), with the bid
amount recovered from local tx history (place-bid records keyed by
bid token name, captured during fetchData via bidSecretStorage).
Rows dedupe against active bids on encryptionToken to skip the brief
accepted-but-not-completed window. The export button moved to a
top-level action row so it stays visible when only purchased
encryptions remain. New tests cover the dedupe path, Won/Resold
status mapping, and bid-amount recovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the local-state salesToCSV / purchasesToCSV exports with
a single tax-records report sourced from the chain. The unifying
insight: every taxable event in PEACE is a re-encryption tx — the
post-SNARK transaction that consumes the bid UTxO, transfers the
encryption to the buyer, and pays the seller. That tx alone carries
the bid datum, the encryption datum, and the actual lovelace
exchanged, so it's the canonical event for both sides of the deal.

Backend: GET /api/chain/reencryption-history/:pkh intersects encryption
and bidding contract activity to find re-encryption candidates,
fetches tx_info, and emits one event per tx where the user was
either the bidder (output encryption datum's owner_vkh, parsed via
the existing parseEncryptionDatum) or the seller (first non-contract
input cred — the seller signs and provides fees). bidAmountLovelace
is read directly from the consumed bid input value;
futurePriceLovelace from the new encryption datum's new_price.
Re-sales by other users are filtered out. Cached for 60s, capped at
200 candidates per query, with stale-cache fallback.

Frontend: chainApi.getReencryptionHistory + reencryptionHistoryToCSV
produce one CSV with columns Date, Side (Sale|Purchase), Token Name,
Bid Amount (ADA), Future Price (ADA), Seller PKH, Buyer PKH, Tx
Hash, Block Height. Both MySales and MyPurchases tabs trigger the
same export (filename veiled-tax-records-{date}.csv), with the
button moved to a top-level row that stays visible in the empty
state — historical re-encryption events remain accessible even
when the user has no current listings or bids. The local
transactionHistory match (capped at 50 records) and CompletedPurchaseRow
machinery are removed; chain data is the source of truth.

Tests: 7 new backend tests cover Sale/Purchase classification, re-sale
exclusion, empty intersection, sort order, validation, and 503 path.
Frontend tests verify the button is reachable from the empty state and
that clicking it queries the chain endpoint and saves the unified CSV.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the user reported the tax CSV came back empty, the code couldn't
say *why* — backend errors were swallowed in chainApi, the
extractReencryptionEvent helper returned bare null on every skip path,
and contract inputs were matched only by payment_addr.bech32 (which
Koios doesn't always populate on inputs, in which case every candidate
silently dropped to "no bid input found" and the array came back []).

Backend (chain.ts):
- isBiddingInput / isContractInput now match by asset policy first
  (bid UTxOs carry biddingPolicyId, encryption UTxOs carry
  encryptionPolicyId — assets are reliable on inputs even when bech32
  is missing) with bech32 retained as a fallback.
- extractReencryptionEvent returns 'no-bid-input' | 'no-seller-input' |
  'no-buyer-output' | 'parse-fail' on skip so the route's summary
  log can attribute every dropped tx. Datum parse failures also log
  individually with the offending tx hash.
- Route logs candidate-scan counts (encryption/bidding tx totals,
  intersection size) and extraction summary (fetched, extracted,
  per-reason skips, matchedUser) — visible in `bash run.sh` console.

Frontend:
- chainApi.getReencryptionHistory now throws on backend failure (was
  silently returning []). Tab handlers already wrap in try/catch and
  surface "Export failed" via exportMessage.
- Both tab handlers log the event count to console.info before
  formatting, and show "No completed re-encryption events found on
  chain yet." when the array is empty — distinguishes empty-result
  from a failed call.

No test changes needed: the fixtures already populate asset_list with
contract policies, so the new asset-policy path is exercised by the
existing happy-path tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The contract address_txs intersection silently returned 0 candidates
when /address_txs paginated past the user's actual activity, leading
to empty CSV exports for users with multiple completed purchases. The
fundamental problem: scanning the whole chain blindly when we already
know which encryption tokens the user has bid on (locally) and which
txs they signed (from credential history).

Backend (POST /api/chain/reencryption-history/:pkh now takes
{ encryptionTokens: string[] } in the body):

- Buyer side: for each encryption token the user has bid on, walk
  its full asset history via /asset_txs. The user's purchase
  re-encryption is in there regardless of whether they still own the
  token (covers Won and Resold cases). Per-token failures are
  logged and skipped, never abort the whole query.
- Seller side: getCredentialTxs(pkh) returns every tx the user
  signed; the seller signs the re-encryption, so this captures all
  sales (including resales of items previously bought).
- Hash sets are deduped before the tx_info batch fetch, so a tx
  that surfaces on both sides costs one extraction. Cap at 500.

Reuses the existing getAssetTxs (originally added for decryption
levels) — I had nearly duplicated it; removed the duplicate.

Frontend:
- chainApi.getReencryptionHistory now POSTs and takes
  encryptionTokens. Both tabs gather them via listBidSecretTokens()
  before exporting (returns [] when locked or empty — handled).
- MySalesTab adds the bidSecretStorage import (already present in
  MyPurchases).

Tests rewritten to drive the new shape: getAssetTxs and
getCredentialTxs mocks, POST + body, dedup-across-sources test,
graceful per-token-failure test. Token name fixtures use hex
matching the route's validation regex.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 503 came from the outer try/catch around getTxInfoWithAssets —
Koios's POST /tx_info caps at ~50 hashes per call, but a heavy user's
getCredentialTxs(pkh) plus 6 tokens of asset history easily exceeds
that. Now the batch is split into chunks of 50 with per-chunk
try/catch, so a single oversized chunk (or one transient Koios
hiccup) only logs a warn and the rest of the export still goes
through.

The 503 response also now includes `detail` with the underlying
error message (cause logged with stack to backend already), and
apiFetch propagates code+message+detail in the thrown Error so the
WebView console shows what actually failed instead of a generic
"Unable to fetch re-encryption history".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two changes to make the empty-CSV case actually diagnosable from the
WebView console alone, no BE log spelunking:

1. Backend response now includes `meta` with candidate counts for
   every successful query: encryptionTokens, buyerSideHashes,
   sellerSideHashes, candidates, fetched, extracted, per-skip-reason
   counts, matchedUser. Both tab handlers log the whole block via
   console.info, and the empty-result toast now reads e.g.
   "(43 candidates, 12 re-encryption txs found, 0 matched your wallet)"
   so the failure mode is visible without restarting the BE.

2. PKH equality check is now lowercase on both sides. Hex is
   case-insensitive but JS string === isn't — if Koios ever returns
   uppercase creds while the wallet provides lowercase (or vice
   versa) every row would silently drop. Cheap insurance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnostic meta from a real wallet showed 14 candidate txs had bid
inputs but my "first non-contract input cred = seller" heuristic
couldn't find a wallet input on any of them, dropping the entire
buyer's purchase row in the process. PEACE re-encryption txs
evidently don't always carry a wallet-fee input cred in tx_info
(possibly paid via collateral or aggregator pattern).

Two changes:

1. Switch the primary seller detection to outputs: the seller
   is the only party who *receives* the bid lovelace, so a
   non-contract output whose value covers bidAmountLovelace is
   them. The input-based heuristic stays as a fallback for txs
   where outputs are split or aggregated.

2. Drop the hard requirement on sellerPkh — extraction now
   succeeds with sellerPkh = '' when neither strategy works.
   The buyer's tax row is the load-bearing one; missing seller
   is acceptable for purchases. Sales-side rows still need
   sellerPkh to match the user's PKH for inclusion, so the
   filter is unchanged for that direction.

Meta now reports `extractedNoSeller` (replacing the dropped
`skipNoSellerInput` counter) so the volume of seller-blank rows
is visible in the diagnostic output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The re-encryption-history extractor was returning 0 events because
Koios /tx_info silently strips inline_datum from outputs whose datum
exceeds its parser size cap (encryption datums carry BLS12-381 G1/G2
points + Capsule, well over the cap). When Koios kept the wrapper
(address, asset_list) but dropped the datum, the extractor counted
the output but couldn't parse it.

Two compounding bugs fixed:

1. Identify the encryption output by Koios's explicit `tx_index`
   field, not by array position. Array order can drift from on-chain
   tx_index when collateral_outputs are present, leading to refs
   pointing at the seller's wallet payment instead of the encryption
   UTxO.

2. When inline_datum is stripped from /tx_info, batch a second-pass
   /utxo_info call (`_extended: true`) which reliably returns
   inline_datum.bytes for any size, then decode via decodePlutusData.

Also:
- Parallelize per-token getAssetTxs (was sequential, pushed heavy
  bidders past the 30s request timeout).
- Allow 120s for /reencryption-history specifically — the multi-Koios-
  roundtrip walk legitimately needs more headroom than 30s.
- Split the overloaded `no-buyer-output` skip reason into specific
  buckets (no-encryption-output, encryption-output-no-datum,
  encryption-output-no-buyer-pkh, encryption-output-no-token-name)
  so future support can diagnose empty exports from the meta alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
logicalmechanism and others added 29 commits April 27, 2026 14:24
The 'Your bid will be locked... Why X ADA minimum?' hint sat 4px (mt-1)
below the input, which read as cramped against the input border. Bump
to 12px (mt-3) so the hint reads as a separate explanatory line rather
than a tail attached to the input.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the single phosphor-green accent with three semantic colors so the
visual hierarchy is no longer carried by one shouting hue:

  --accent       muted lilac  #9683c8  primary actions, focus rings, hover
                                       borders, active tab indicator
  --accent-text  soft mint    #7dd3a3  static values: prices, balance, stats
                                       counts (text/icons only, never fills)
  --link         warm sand    #d4a574  inline navigational links (URLs,
                                       "View full →" affordances)

Light theme gets darker variants for AA contrast on bright backgrounds.
tx-celebration glow switches to mint to match the "verified / completed"
semantic family. Status colors (success/warning/error) untouched.

JSX sweep applies each role:
  - prices, wallet balance, stats counts → accent-text
  - bid count pill on EncryptionCard → accent-text bg + text
  - TransactionLink (CardanoScan), Iagon docs, "Select all" → link

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes addressing the "Halloween vibe" feedback:

1. Drop --accent-text (soft mint). Static values — prices, balance, stats
   counts — now use --text-primary (white). Typography weight (font-semibold
   + tracking-tight + tnum) carries the "this is a hero number" hierarchy
   without color. Removes the green-next-to-lilac proximity that was reading
   as Halloween, while keeping the green energy reserved for status badges
   and the brief tx-celebration glow.

2. Bolder --accent: #9683c8 → #8b6dd6, and --accent-hover bumped from
   #b3a3d6 to #a78bfa so the hover state is clearly brighter. btn-primary
   now also picks up the shadow-glow on hover so it reads as pressable.
   tx-celebration glow switches to --success (green) since mint is gone.

Brand-emphasis pills (wallet balance, bid count) still use lilac
accent-muted bg + accent text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four small follow-ups picked up during manual review of the polish pass:

- Wrap .t-* typography classes in @layer components so per-instance
  Tailwind text-* utilities (text-3xl, text-lg, etc.) override the role
  defaults instead of losing on source-order. Switch the wordmark
  (WalletSetup, WalletUnlock), onboarding step title, and EmptyState
  title from inline `style={{ fontFamily: 'var(--font-display)' }}` to
  the .t-display class. Activates the previously-dead type tokens.

- Apply .field-invalid pattern to UpdatePriceModal and CreateListingModal
  price inputs (was only used in PlaceBidModal). Also adds tnum to both
  so digits don't wiggle on input.

- Theme tx-celebration glow via a new --tx-glow CSS variable instead of
  hardcoded rgba — light theme now gets a slightly dimmer glow that
  reads correctly against bright surfaces.

- Drop light-theme atmospheric noise opacity 0.04 → 0.022. The brighter
  the surface, the more visible the grain — at 0.04 it was reading as
  dirt on white card backgrounds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LibraryCard's no-image fallback rendered a bare 160px panel with no
margin or rounded corners, while ListingImage size="md" (used when an
imageLink is present) renders 160px + my-4 (32px) + rounded-md. The
grid's cards-with-images were ~32px taller than cards-without, breaking
the equal-height row that MarketplaceTab and MySalesTab already get.

EncryptionCard and SalesListingCard avoid this by always rendering
ListingImage and letting it handle the no-link branch internally
(same h-40 + my-4 + rounded-md). LibraryCard needs its custom
CategoryIcon fallback though, so instead align the wrapper styles to
match ListingImage's no-link branch exactly.

Drops the cardSize-based imgHeight variable (h-28 / h-40 / h-52) since
ListingImage at size="md" is hardcoded h-40 anyway — cardSize was only
varying the no-image path, which was the source of the inconsistency.
cardSize still controls inner padding and description line-clamp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…splay face

Three small alignments to the four shell stats cards:

- Active (selected) card now also gets the -translate-y-0.5 lift, not
  just the lilac glow. Hover and active had two different effects (lift
  vs glow) which read inconsistently. Active now does both — lifts AND
  glows — so it stays clearly distinct from hover (which only lifts).

- Apply card-stagger entrance (0/50/100/150ms cascade) to match the
  pattern already used by every Marketplace/Library/MySales/MyPurchases
  card. Shell now feels like a single coordinated reveal on dashboard
  load instead of four cards popping in instantly.

- Promote the four hero count numbers to the .t-display class —
  activates the display face (Bricolage Grotesque, falls back to Inter)
  on the most prominent numbers in the app shell. Drops the redundant
  font-semibold + tracking-tight since .t-display sets both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…utton wrapper

The previous "View full" affordance was the entire description block
wrapped in role=button + tabIndex + onKeyDown. Screen readers announced
the whole description text as a button, and the caret only became
visible on mouse hover (invisible to keyboard users).

Replace with the conventional pattern:
- <p> stays plain — screen readers hear it as paragraph text
- The "View full →" caret becomes a real <button> that is always
  visible, keyboard-focusable, and has its own aria-label
- The wrapper <div> keeps onClick + cursor-pointer for the mouse
  convenience of clicking anywhere on the description, but no a11y
  role/tabIndex — the inner button carries the semantics

Net result: "Description text. View full description, button."
instead of "View full description, button: <whole description>".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tern

Audit of the marketplace surface (MarketplaceTab + EncryptionCard +
filter helpers + PlaceBidModal) found the existing role split is
already followed correctly — no reassignments needed. PlaceBidModal's
"Listing Details" rows even use a refined three-tier hierarchy in one
line: muted label, secondary technical value (mono hash), primary
decision value (price).

Capture that hierarchy as the reference pattern in the token doc so
future code follows it. Also extend the descriptions to call out the
border cases ("readable-but-secondary values" for hashes, "inactive
chip states" for muted) that the audit confirmed.

No code changes — pure documentation update against the now-clean
marketplace surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the signature tx-celebration moment into wallet creation success.
Previously the user clicked "Create wallet" → wallet was minted → they
were navigated straight to Dashboard with no acknowledgement of the
milestone.

Now WalletSetup passes navigate state { justCreated: true }, and
Dashboard's existing nav-state effect fires a celebrate toast on first
mount and clears the state (so back-button doesn't re-celebrate).

To keep the API clean, add toast.celebrate(title, message?, action?)
to useToast — wraps addToast with variant='transaction' so the icon
gets the tx-celebration-icon animation. transactionSuccess() stays
reserved for actual on-chain tx success (it requires a txHash and
adds a CardanoScan link).

Translation keys use defaultValue fallbacks so the moment ships in
English now and can be localized in a separate i18n PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h-pilot

Frontend polish pilot — Marketplace, Library, Wallet, primitives
Promote 5 strings from defaultValue fallbacks to proper keys:
- dashboard:shell.walletCreatedTitle / walletCreatedBody
- card.openDescription / card.viewFull
- toast.queueOverflow (preserves {{count}} interpolation)

Other 17 locales pick these up via the translation backfill flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The polish pass left three template-literal aria-labels with hardcoded
English suffixes ("new bids", "accepted bids ready to decrypt",
"pending transactions"). Visually invisible — only screen-reader users
saw the English. Replace with i18next plural keys (_one / _other) so
the aria-label translates and reads grammatically at count == 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add no-restricted-syntax ESLint rule that flags string literals and
template literals in user-facing JSX attributes (aria-label,
aria-description, aria-roledescription, alt, title). Test files are
exempted; placeholder is intentionally not covered (numeric-only in
this app, locale-agnostic).

Backfill the 5 call sites the new rule surfaced:
- LayoutPopover card-size + column-count buttons (4 sites) now look up
  layoutPopover.cardSize{Small,Medium,Large} and the new
  layoutPopover.columnsCount plural key.
- StorageSection disk-usage segment tooltip uses storage.segmentTooltip
  with {{label}}, {{size}}, {{percent}} interpolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
formatAda + formatDate read i18next.language from the singleton and
pass it to toLocaleString / toLocaleDateString instead of `undefined`,
so number grouping and date format follow the user's selected app
language rather than the OS default. Falls back to undefined when
i18next isn't initialized — keeps unit tests stable.

formatRelativeTime swaps its hardcoded English buckets ("just now",
"5m ago", "2y ago") for Intl.RelativeTimeFormat with style:'narrow'.
English narrow CLDR matches the prior strings exactly except
"just now" → "now"; non-English locales now get their proper CLDR
forms (e.g. "5分钟前" instead of "5m ago"). Uses numeric:'auto' under
60s for the "now" form and numeric:'always' above so "1mo ago" / "1y
ago" don't collapse to "last mo." / "last yr."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…polish-keys

i18n: backfill EN keys for polish-pass defaultValue strings
Centralize Stripe-style thousands-separator formatting helpers in
formatAda.ts so currency-style inputs across the app can share a single
implementation. formatWithCommas preserves the user's decimal portion
verbatim (including trailing dots and trailing zeros) so it is safe to
use during typing as well as on blur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches the bid amount and future-price inputs from type=number to
type=text + inputMode=decimal so they can render thousands separators
at rest. Adds Stripe-style format-on-blur / parse-on-focus driven by
the shared formatWithCommas/stripCommas helpers, plus a setBidAmount /
setFuturePrice indirection so the Suggested / +10 / +25 / Max quick
actions also update the formatted display.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire UpdateBidModal's bid-amount and future-price inputs through the
shared formatWithCommas/stripCommas helpers so they render thousands
separators at rest and revert to a comma-free, fully-selected raw
string on focus. Auto-sync of the future-price field now mirrors the
display value too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire UpdatePriceModal's listing price input through the shared
formatWithCommas/stripCommas helpers so it renders thousands separators
at rest and reverts to a comma-free, fully-selected raw string on
focus, matching the Place/UpdateBid modals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the local formatPrice helper (Intl.NumberFormat-based) with
the shared formatWithCommas so all currency-style inputs use the same
formatting. Behavior tightens slightly: trailing zeros the user typed
("1.10") survive the round-trip instead of being trimmed to "1.1".
Also select-all on focus to match the bid/price modals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ImportListingModal carried its own copy of the local formatPrice helper
(Intl.NumberFormat-based, identical to the one CreateListingModal had).
Swap it for the shared formatWithCommas and select-all on focus so all
five currency-style inputs in the GUI use the same helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Card width depends on the column-count grid setting, but the price font
size was tied only to cardSize. Picking large + 4 columns gave a big
font in a narrow track, and a 21-character max-supply price like
"45,000,000,000.00 ADA" overflowed the card.

Wrap each card's <article> with @container and drive the price font
off the card's actual inline size. cardSize still acts as the user's
preferred tier — it caps the maximum font reached on a wide card —
but a narrow card always starts at text-base so the longest possible
ADA value fits regardless of column count.

Apply across EncryptionCard, SalesListingCard, MyPurchaseBidCard.
A new shared getPriceFontClass(cardSize) helper sits next to
getGridClasses in useTabFilterState since they're both card-grid
layout concerns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On the narrowest MySales card (large + 4 columns at xl viewport, the
article runs ~280px wide) the inline edit pencil inside the price plus
a longer storage-layer badge ("On-chain") wouldn't fit on the same row
as the price, even after the container-query font ramp. Both elements
were whitespace-nowrap + flex-shrink-0, so the row overflowed and the
price visually hugged the right edge of the card.

Switch the price/badge row to flex-col below @xs and back to flex-row
above it, so wide cards keep the original side-by-side hero layout
while narrow cards stack cleanly without overflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nput-commas

D16: thousands separators on bid/price number inputs
Adds @fontsource-variable/bricolage-grotesque (variable wght+opsz, latin
subset, ~131KB) and ships the woff2 in public/fonts/. Uncomments the
@font-face block in fonts.css so .t-display now renders in Bricolage
Grotesque instead of falling back to Inter.

Affected hero spots: WalletSetup title, WalletUnlock title, OnboardingOverlay
step titles, EmptyState titles, Dashboard stats counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Inter and JetBrains Mono woff2 files in public/fonts/ were dropped in
without a tracked npm dep — Bricolage was the only font with provenance
after the previous commit. Add @fontsource-variable/inter and
@fontsource-variable/jetbrains-mono so all three fonts are sourced the
same way and can be regenerated from package.json.

Inter files are byte-identical to the existing copies (no functional
change). JetBrains Mono files swap to the variable wght-normal version
(+12KB total across both subsets, gains the full 100-800 weight range).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…age-display-font

Install Bricolage Grotesque display font
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@logicalmechanism
logicalmechanism merged commit d093c95 into main May 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant