feat: dashboard v2 #525
Conversation
Remove light/dark dual themes, prefers-color-scheme hooks, and the showUSDeBanner setting. Consolidate variant palettes and semantic color tokens, and simplify button hover styles with color-mix. BREAKING CHANGE: theme prop no longer accepts lightMode/darkMode; pass a flat theme object instead.
Add getYieldOpportunities for batched yield loading with per-id cache fallback. Improve token selector with selected state and yield counts, and polish dashboard earn details with conditional charts, risk tooltip, and tighter layout spacing.
…metadata Use API statistics and mechanics for TVL, fees, and lockup. Simplify reward rate formatting by removing typed variable-rate handling. Add useTokenListYields to batch-fetch yields and display the best rate per token in the earn flow.
Replace getBaseYieldType with getExtendedYieldType, remove the liquid-staking abstraction, and add isStakingYieldType, isUnstakeYieldType, and isDepositYieldType helpers. Update position details, review flows, and i18n for new API yield types.
Populate Yield.provider via ProvidersControllerGetProvider instead of legacy fallback metadata, and update consumers to use yield.provider directly.
Split the dashboard earn tab into Stake, DeFi, and RWA categories with filtered yield selection. Enhance earn details with category-specific metrics, provider badges, and inline validator selection.
Replace pill-style triggers with summary cards showing TVL, commission, website, and status. Show yield strategy output details in the reward section, remove StakedVia, and sync generated API types for validators, KYC, and new networks.
Introduce KYC verification gating for RWA yields with status hook, gate card, and verification modal across earn, review, and position flows. Extract earn details into dedicated model, formatters, and subcomponents.
Introduce a position-details model with metric cards, balance breakdown, and detail rows, replacing the reused earn details view. Filter dashboard yield category tabs to categories with available yields and refine earn details KPI ordering and i18n types.
Add stake/unstake action tabs with in-panel staking routes in dashboard position details. Group positions and filter activity by yield category, and refine dashboard scroll layout and Utila theme tokens.
|
📝 WalkthroughWalkthroughThis PR simplifies theme configuration, adds provider-aware yield and KYC data handling, introduces dashboard earn-details and manage views, refactors activity and position-detail flows, updates selection UIs and review screens, expands translations, and adds tests plus documentation for the new behavior. ChangesWidget dashboard earn and KYC refresh
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx (1)
536-542:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSilent return when KYC gate is blocking lacks user feedback.
When
kycGateIsBlockingis true, the mutation function returns early without providing any feedback or state update. While the button is disabled (line 677), users who somehow trigger the mutation (e.g., via keyboard, rapid clicking before state updates) will experience a silent no-op.Consider adding explicit handling or ensuring the UI clearly communicates the blocking state before this point is reached.
💡 Suggested improvement
if (!isConnected) return openConnectModal?.(); - if (kycGateIsBlocking) return; + if (kycGateIsBlocking) { + // KYC gate UI already shows blocking state + // Consider logging or analytics here if needed + return; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx` around lines 536 - 542, The mutationFn currently returns silently when kycGateIsBlocking is true; update mutationFn to explicitly surface feedback instead of a silent no-op by invoking the existing user-visible mechanism (for example call a toast/notification helper or set a local error state like setKycBlockingError that the UI reads) when kycGateIsBlocking is true, then return; keep checks for validation.hasErrors, stakeEnterRequestDto.isNothing(), and isConnected/openConnectModal as-is but ensure the new feedback call (e.g., showKycBlockingToast or setKycBlockingError) is executed before returning so users get immediate feedback if they trigger the mutation while KYC is blocking.packages/widget/src/hooks/api/use-yield-opportunity/index.ts (1)
30-45:⚠️ Potential issue | 🟠 MajorRemove
queryClientfrom the TanStackqueryKey.
packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.tsshowsqueryFnalready receivesqueryClientin its params, so it should be passed toqueryFnonly. Including thequeryClientobject reference inqueryKeymakes cache identity depend on an internal, non-serializable instance and can cause incorrect/unnecessary cache splitting/refetching.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/hooks/api/use-yield-opportunity/index.ts` around lines 30 - 45, The queryKey currently includes the non-serializable queryClient instance causing cache identity issues; update the getKey invocation used in the use-yield-opportunity hook so it only receives serializable inputs (e.g., yieldId, isLedgerLive, apiClient, integrationId) and remove queryClient from that key, while still passing queryClient into the queryFn call (the queryFn defined in get-yield-opportunity.ts should continue to receive queryClient via its params); update references to getKey and the queryFn invocation so queryClient is excluded from the key but provided to queryFn.
🧹 Nitpick comments (20)
packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx (2)
484-502: ⚖️ Poor tradeoffDashboard category selection changes the selected token.
When a user selects a different dashboard yield category,
onDashboardYieldCategorySelectdispatches bothtoken/selectandyield/selectactions (lines 500-501). This means selecting a category can change which token is currently selected, which might be unexpected UX if the user had intentionally selected a specific token.Consider whether category selection should:
- Filter the current token's yields by category (keeping token stable), or
- Show the highest-yielding opportunity across all tokens in that category (current behavior)
The current behavior may be intentional for the dashboard flow, but worth confirming with UX/product requirements.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx` around lines 484 - 502, The handler onDashboardYieldCategorySelect currently dispatches both "token/select" and "yield/select", which changes the selected token when switching categories; update it to preserve the user's current token selection: first try to find targetYield within the currently selected token (use selectedStakeData/selected token id and filter availableYieldsById for yieldDto.token.id === selectedToken?.id and matching category) and only dispatch { type: "yield/select", data: targetYield } if found; if no matching yield exists for the current token, then fall back to the existing behavior (select the highest-yielding target across all tokens and dispatch both { type: "token/select", data: targetYield.token } and { type: "yield/select", data: targetYield }). Ensure to reference onDashboardYieldCategorySelect, selectedDashboardYieldCategory, selectedStakeData, dashboardYields, dispatch, "token/select", and "yield/select".
311-318: ⚡ Quick winDashboard category filter could result in empty yield list.
When
dashboardVariantis active andselectedDashboardYieldCategoryis set, the yields are filtered to match only that category (lines 312-318). If no yields match the selected category for the current token, users will see an empty opportunity list with no explanation.Consider adding empty-state handling or validation that the selected category has available yields before applying the filter.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx` around lines 311 - 318, When dashboardVariant is true and selectedDashboardYieldCategory is applied, dashboardFilteredDtos may become empty leaving users with a blank opportunity list; update the logic around dashboardFilteredDtos (and where filteredDtos, getDashboardYieldCategory, selectedDashboardYieldCategory are used) to validate the filtered result and handle emptiness: either fallback to the unfiltered filteredDtos, clear/reset selectedDashboardYieldCategory to a default/available category, or set an explicit empty-state flag that the UI can render with messaging; ensure this validation occurs immediately after the filter is computed so the consuming UI can display a helpful empty state or revert the filter.packages/widget/src/components/molecules/kyc-gate-card/verification-modal.tsx (1)
68-74: ⚡ Quick winConsider adding sandbox attribute to iframe for defense-in-depth.
The iframe loads an external KYC verification URL. While
referrerPolicyis configured, consider adding asandboxattribute with appropriate permissions (e.g.,sandbox="allow-forms allow-scripts allow-same-origin") to limit potential security risks.🔒 Proposed security enhancement
<iframe className={verificationModalFrameStyle} data-testid="kyc-verification-iframe" referrerPolicy="strict-origin-when-cross-origin" + sandbox="allow-forms allow-scripts allow-same-origin allow-popups" src={url} title={t("details.kyc_gate.modal.iframe_title")} />Note: Adjust sandbox permissions based on actual KYC provider requirements.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/components/molecules/kyc-gate-card/verification-modal.tsx` around lines 68 - 74, The iframe rendering the KYC verification URL (the element with data-testid="kyc-verification-iframe" and src={url}) lacks a sandbox attribute; add a sandbox attribute to the iframe (e.g., sandbox="allow-forms allow-scripts allow-same-origin") to restrict capabilities as a defense-in-depth measure, and adjust the permission list to match the KYC provider's actual requirements so the verification flow still works.packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts (1)
164-170: 💤 Low valueHardcoded iframe background may not adapt to theme.
The KYC verification iframe has a hardcoded white background (
#ffffff). This is likely intentional to match third-party KYC provider content, but it may create a visual mismatch in dark-themed widgets. If the iframe content supports dark themes, consider making this configurable or document the reasoning for the fixed color.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts` around lines 164 - 170, The verificationModalFrameStyle currently hardcodes the iframe background to "`#ffffff`", which breaks dark themes; update verificationModalFrameStyle to read its background from a theme or CSS variable (e.g., use a theme token, prop-derived value, or a --kyc-iframe-bg CSS variable) and fall back to "`#ffffff`" if unspecified, or add a short comment/documentation explaining why a fixed white background is required when the KYC provider cannot support dark mode.packages/widget/src/utils/formatters.ts (1)
107-115: 💤 Low valueConsider locale configurability for compact formatters.
The
Intl.NumberFormatinstances hardcode"en-US"locale. While this ensures consistent formatting across deployments, it doesn't respect user locale preferences. If locale-aware formatting is desired in the future, these formatters will need to be refactored.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/utils/formatters.ts` around lines 107 - 115, compactUsdFormatter and compactNumberFormatter currently hardcode the "en-US" locale; update them to be locale-configurable by either (a) accepting a locale parameter or (b) deriving the locale from the environment (e.g., undefined to use runtime/default locale or a passed-in locale string). Modify the definitions of compactUsdFormatter and compactNumberFormatter (or replace them with factory functions like createCompactUsdFormatter/createCompactNumberFormatter) so callers can supply locale; ensure options (notation: "compact", maximumFractionDigits: 0) are preserved and used when constructing new Intl.NumberFormat instances.packages/widget/src/components/atoms/icons/arrow.tsx (1)
21-40: ⚡ Quick winConsider adding accessibility attributes.
The Arrow SVG lacks semantic information for screen readers. Consider adding
aria-hidden="true"(if purely decorative and adjacent text provides context) orrole="img"witharia-labelwhen the arrow conveys meaning.♿ Example with aria-hidden for decorative use
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" style={{🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/components/atoms/icons/arrow.tsx` around lines 21 - 40, The Arrow SVG lacks accessibility attributes; update the Arrow component's <svg> element to support accessibility by adding either aria-hidden="true" when decorative or role="img" plus an aria-label prop when it conveys meaning. Modify the component signature to accept an optional ariaLabel (or decorative/ariaHidden boolean) and use that to conditionally render aria-hidden or role="img" and aria-label on the <svg> (the element using size, rotation[direction], color and vars.color.text). Ensure the default remains decorative if no label is provided.packages/widget/src/components/atoms/select-modal/index.tsx (1)
252-252: Clarifyappearance: "plain"behavior forSelectModalItemvariants
variant={{ appearance: "plain", ...variant }}allows a caller-providedvariant.appearanceto override"plain"(sinceItemContainerVariantssupportsappearance: "card" | "plain"). In the currentSelectModalItemusage, thevariantprop only sets{ type: ..., hover: "enabled" }, so"plain"remains in effect. If the intent is to always force"plain"for modal items, switch the order to placeappearanceafter the spread (or preventappearancefrom being part of the passed variant).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/components/atoms/select-modal/index.tsx` at line 252, The code currently spreads caller-provided variant into variant={{ appearance: "plain", ...variant }}, which allows callers to override appearance; change this to enforce plain by applying appearance after the spread (e.g., variant={{ ...variant, appearance: "plain" }}) in the SelectModalItem JSX (or alternatively ensure callers never pass appearance by typing or stripping it before spread) so the ItemContainerVariants appearance is always "plain" for modal items.packages/widget/src/components/molecules/summary-item/index.css.ts (1)
61-72: 💤 Low valueMinor redundancy in border color styling.
Line 64 sets
borderColor: "transparent"in the atoms, then Line 67 immediately overrides it withborderColor: utilaPalette.border. While this works correctly, consider whether thetransparentassignment is necessary or if it serves as an intentional fallback.🧹 Optional simplification if the transparent value isn't needed
utila: [ atoms({ background: "summaryItemBackground", - borderColor: "transparent", }), { borderColor: utilaPalette.border, borderRadius: "8px", borderWidth: "1px", borderStyle: "solid", }, ],🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/components/molecules/summary-item/index.css.ts` around lines 61 - 72, The utila style array currently sets borderColor twice (first via atoms with borderColor: "transparent", then overridden by borderColor: utilaPalette.border), so remove the redundant "transparent" assignment from the atoms call (the utila array that includes atoms({...}) and the object with borderColor: utilaPalette.border) unless you intended a fallback; update the atoms invocation to omit borderColor while keeping borderRadius, borderWidth, and borderStyle in the following object so the effective border color comes only from utilaPalette.border.packages/widget/src/providers/api/api-client.ts (1)
245-245: ⚡ Quick winDocument the breaking API change.
The method rename from
withRunOptionstowithOptionsis a breaking change for any external consumers. Ensure this is documented in release notes or migration guides.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/providers/api/api-client.ts` at line 245, The public API rename from withRunOptions to withOptions is breaking; add a short-term compatibility shim and document the change: add a deprecated alias function named withRunOptions that calls withOptions(options) (reference bindApiClients and ApiClientOptions) so existing consumers don't break, and update release notes/migration guide to describe the rename, the deprecation, and recommended migration to withOptions.packages/widget/src/hooks/api/use-yield-kyc-gate.ts (1)
40-45: 💤 Low valueConsider clarifying unreachable fallback values.
The empty-string fallbacks on lines 43-44 (
selectedYield?.id ?? ""andaddress ?? "") are technically unreachable becausequeryEnabledensures both values are truthy before the query runs. These fallbacks exist only to satisfy TypeScript's type checker. While this is safe, it may be clearer to add a comment or use a type assertion to signal that these branches are unreachable in practice.Optional: Make the safety guarantee explicit
queryFn: ({ signal }) => apiClient .withOptions({ signal }) - .yield.KycControllerGetStatus(selectedYield?.id ?? "", { - params: { address: address ?? "" }, + .yield.KycControllerGetStatus(selectedYield!.id, { + params: { address: address! }, }),Or add a comment:
// queryEnabled ensures selectedYield and address are defined queryFn: ({ signal }) => apiClient .withOptions({ signal }) .yield.KycControllerGetStatus(selectedYield?.id ?? "", { params: { address: address ?? "" }, }),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/hooks/api/use-yield-kyc-gate.ts` around lines 40 - 45, The empty-string fallbacks on selectedYield?.id and address are only present for TS but are unreachable due to queryEnabled; either add a short clarifying comment above the queryFn (e.g. “queryEnabled ensures selectedYield and address are defined”) or replace the fallbacks with non-null assertions (selectedYield!.id and address!) in the call to apiClient.withOptions(...).yield.KycControllerGetStatus so the intent is explicit; keep references to queryFn, selectedYield, address and the KycControllerGetStatus call when making the change.packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx (2)
29-59: ⚡ Quick winConsider extracting provider name logic into a helper function.
The provider name resolution logic (lines 54-59) chains multiple fallbacks and could benefit from extraction for testability and clarity.
♻️ Optional refactor suggestion
const getProviderDisplayName = ( selectedProvider: ProviderDetailsItem | undefined, selectedValidators: ValidatorDto[], yieldDto: YieldDto ): string => { return ( selectedProvider?.name ?? selectedValidators[0]?.name ?? selectedValidators[0]?.address ?? yieldDto.provider?.name ?? yieldDto.providerId ); };Then use it:
- const providerName = - selectedProvider?.name ?? - selectedValidatorsArr[0]?.name ?? - selectedValidatorsArr[0]?.address ?? - yieldDto.provider?.name ?? - yieldDto.providerId; + const providerName = getProviderDisplayName( + selectedProvider, + selectedValidatorsArr, + yieldDto + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx` around lines 29 - 59, The provider name resolution in ProviderSelectionCard currently inlines a multi-fallback expression into the providerName constant; extract that logic into a small helper (e.g., getProviderDisplayName) that accepts selectedProvider, selectedValidatorsArr (or selectedValidators), and yieldDto and returns the resolved string; replace the inlined expression with a call to getProviderDisplayName(selectedProvider, selectedValidatorsArr, yieldDto) so the resolution is testable and clearer while keeping the original fallback order (selectedProvider?.name, first selected validator name, first validator address, yieldDto.provider?.name, yieldDto.providerId).
188-220: ⚡ Quick winExtract ExternalLinkIcon to shared component.
The
ExternalLinkIconSVG is duplicated between this file andintegration-docs-link.tsx(lines 27-59). Extract it to a shared icon component (e.g., incomponents/atoms/icons/) to follow DRY principles.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx` around lines 188 - 220, Extract the duplicated ExternalLinkIcon SVG into a single shared React component named ExternalLinkIcon (e.g., components/atoms/icons/ExternalLinkIcon) and export it as the default (or named) component; then remove the inline ExternalLinkIcon declarations from provider-selection-card.tsx and integration-docs-link.tsx and replace them with an import of the shared ExternalLinkIcon component and use it in the same places. Ensure the new component preserves the same props/attributes (aria-hidden, className, width/height, viewBox, and SVG paths) so existing usages (ExternalLinkIcon in provider-selection-card.tsx and the copy in integration-docs-link.tsx) continue to render identically. Also update any import paths and run a quick lint/compile to confirm no type or prop regressions.packages/widget/src/domain/types/reward-rate.ts (1)
10-22: Reduce the breaking-change concern forrewardType: no in-repo dependency onRewardTypes
RewardTypesstill exists inpackages/widget/src/generated/api/legacy.ts, but there are no usages of theRewardTypestype inpackages/widget/src(only generated declarations).RewardRateBreakdownItem.rewardTypeisstring | undefined, andRewardRateBreakdownuses onlyitem.keyanditem.rate(it never readsitem.rewardType).- Other
rewardTypehandling in this repo already treats it asstring | undefined(e.g.,getRewardTypeFormatted(rewardType: string | undefined)), and there are no references togetRewardTypeFromRateType.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/domain/types/reward-rate.ts` around lines 10 - 22, The repo should avoid depending on the generated RewardTypes enum — keep RewardRateBreakdownItem.rewardType typed as string | undefined (not RewardTypes) and ensure all code that consumes RewardRateBreakdown (and functions like getRewardTypeFormatted) accept string | undefined; remove any imports/uses of RewardTypes or getRewardTypeFromRateType in widget/src and confirm RewardRateBreakdown only reads item.key and item.rate so no runtime changes are required. Use the symbols RewardRateBreakdownItem, rewardType, RewardRateBreakdown, getRewardTypeFormatted, getRewardTypeFromRateType and RewardTypes to locate and update/remove references.packages/widget/src/pages/details/activity-page/hooks/use-activity-page.tsx (1)
53-57: ⚡ Quick winConsider refactoring type assertion for better type safety.
The type assertion to access
activityActions.data.pages[0].totalis fragile and couples this code to the internal structure of the query result. IfuseActivityActions()changes its data structure, this will break silently.Consider either:
- Exposing
totaldirectly from theuseActivityActions()hook with proper typing- Using a type guard function instead of an assertion
- Adding a comment explaining the coupling and why it's necessary
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages/details/activity-page/hooks/use-activity-page.tsx` around lines 53 - 57, The current fragile type assertion on activityActions.data used to compute apiTotal should be replaced with a safer contract: update the useActivityActions hook to return a typed total (e.g. include a total: number | undefined property) and then read activityActions.total when computing apiTotal; alternatively, add a small type guard (e.g. isActivityActionsData(data): data is { pages: { total?: number }[] }) and use it to safely access pages[0].total. Locate the apiTotal calculation (variable name apiTotal) and the useActivityActions hook (function name useActivityActions) and implement one of these fixes so the component no longer relies on an unsafe assertion of activityActions.data.packages/widget/src/pages/details/activity-page/components/activity-filters/styles.css.ts (1)
52-55: ⚡ Quick winInconsistent variant styling approach.
The
filterCountrecipe mixesatoms({ background: "..." })(line 53) with an inline object{ background: "..." }(line 54) for the same property. For consistency and maintainability, prefer usingatoms()for both variants or use inline objects for both.♻️ Suggested fix for consistency
variants: { state: { default: atoms({ background: "backgroundMuted" }), - active: { background: "rgba(255, 255, 255, 0.2)" }, + active: atoms({ background: "backgroundMuted" }), // or define a theme token for this translucent white }, },Note: If the translucent white is intentional and not available in the theme, consider adding a dedicated theme token rather than hardcoding the rgba value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages/details/activity-page/components/activity-filters/styles.css.ts` around lines 52 - 55, The filterCount recipe's state variants use mixed styles: default uses atoms({ background: "backgroundMuted" }) while active uses an inline { background: "rgba(255, 255, 255, 0.2)" }; update the active variant to use atoms(...) for consistency (or convert both to inline objects) — e.g., change the active background to an atoms token (or add a theme token for the translucent white and reference it via atoms) so both state variants use the same styling approach; locate this in the filterCount recipe's state -> default/active definitions.packages/widget/src/pages-dashboard/position-details/components/styles.css.ts (1)
63-112: ⚡ Quick winHardcoded color values bypass the theme system.
The
metricCardandmetricValueTextrecipes use hardcoded hex colors for theactionandclaimtone variants (lines 77, 80, 104-105). These colors won't adapt to theme changes or variant overrides.💡 Consider using theme tokens
Add semantic color tokens to your theme contract for these tones:
// In theme contract metricActionBg: string; metricClaimBg: string; metricActionText: string; metricClaimText: string;Then reference them via
vars.color.*oratoms({ background: "metricActionBg" }). This allows theme variants to customize these colors and keeps all color management centralized.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages-dashboard/position-details/components/styles.css.ts` around lines 63 - 112, Replace the hardcoded hex colors in the metricCard and metricValueText recipes with theme tokens so colors respond to theme overrides: update the tone.action and tone.claim entries in metricCard to use your theme variables (e.g., atoms({ background: "metricActionBg" }) and atoms({ background: "metricClaimBg" }) or vars.color.metricActionBg/metricClaimBg) and update the tone.action and tone.claim entries in metricValueText to use theme text tokens (e.g., atoms({ color: "metricActionText" }) / vars.color.metricActionText and metricClaimText / vars.color.metricClaimText); leave defaultVariants intact and ensure the corresponding tokens are added to the theme contract if missing.packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts (1)
61-91: ⚡ Quick winHardcoded color values bypass the theme system.
The badge styles use hardcoded hex colors (e.g.,
#FEF1CF,#9A4F0E,#E8F9EF,#15803D,#DCFCE7) instead of theme tokens fromvars.colororatoms(). This prevents these colors from adapting to theme variants or future palette changes.💡 Consider using theme tokens
If these are one-off colors for specific badge types, consider adding semantic tokens to the theme contract (e.g.,
badgeWarningBg,badgeWarningText,badgeSuccessBg,badgeSuccessText) and referencing them here. This centralizes color management and enables theme-wide updates.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts` around lines 61 - 91, Replace hardcoded hex colors in headerBadge, headerAutoBadge, headerBadgeText and autoBadge with theme tokens (e.g., use vars.color or atoms color tokens) so badges follow the theme; if the tokens don't exist, add semantic tokens like badgeWarningBg, badgeWarningText, badgeSuccessBg, badgeSuccessText to the theme contract and reference those (or appropriate atoms keys) in the style definitions instead of literals.packages/widget/src/hooks/api/use-yield-validators.ts (1)
74-80: 💤 Low valueDocument the
suppressRichErrorsparameter.The new optional
suppressRichErrorsparameter controls error handling behavior but lacks inline documentation. Consider adding a JSDoc comment explaining when and why to suppress rich errors.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/hooks/api/use-yield-validators.ts` around lines 74 - 80, Add a JSDoc comment for the optional parameter suppressRichErrors on the useYieldValidators hook: describe that it toggles whether the hook returns rich/structured error objects vs. generic errors (when true it suppresses detailed error payloads for simpler consumers or to avoid leaking internals), note default behavior (false/undefined returns rich errors), and indicate typical use-cases (e.g., UI components that only need a message or tests). Place the comment immediately above the parameter in the function signature where suppressRichErrors is declared so maintainers can quickly see its purpose and expected values.packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx (1)
333-344: ⚡ Quick winInconsistent null-handling between cooldown and warmup/lockup facts.
getCooldownFactalways returns a fact (even whendays === 0), whereasgetWarmupFactandgetLockupFactreturnnullwhen their values are undefined/null. This inconsistency means a zero-day cooldown will still appear in the detail rows, while missing warmup/lockup won't.Consider aligning behavior: either return
nullwhendays === 0for cooldown, or always return facts for warmup/lockup with zero/unavailable values.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx` around lines 333 - 344, getCooldownFact currently always returns a fact (even when days === 0) which is inconsistent with getWarmupFact and getLockupFact that return null for missing values; update getCooldownFact to return null when getYieldCooldownPeriod(yieldDto) is undefined or when days === 0 (so it mirrors getWarmupFact/getLockupFact behavior), keep id "cooldown" and use formatCooldownDays(days, t) only when returning a fact, and ensure any callers that map facts handle nullable returns the same way they do for warmup/lockup.packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts (1)
77-87: 💤 Low valueConsider surfacing provider fetch failures more explicitly.
fetchYieldProvidercatches all errors and returnsundefined, which silently drops provider details when the fetch fails. While logging to console helps debugging, downstream code won't know if the provider is genuinely missing or if the fetch failed.Consider returning a discriminated result (e.g.,
{ success: boolean; provider?: YieldProviderDetails; error?: Error }) if distinguishing between these cases matters.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts` around lines 77 - 87, The current try/catch inside fetchYieldProvider swallows errors and returns undefined; update fetchYieldProvider (the function that calls queryClient.fetchQuery with queryKey getProviderKey(providerId) and queryFn client.yield.ProvidersControllerGetProvider) to return a discriminated result object like { success: boolean; provider?: YieldProviderDetails; error?: Error } instead of undefined on failure: on success return { success: true, provider }, on failure catch the error and return { success: false, error } (and stop using console.log). Adjust the function's return type accordingly and update callers to handle the new shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts`:
- Around line 22-31: The CSS uses color-mix() for the kyc-gate-card variants
(objects named default/pending/rejected) without fallbacks; add a plain fallback
background and borderColor before the color-mix declaration or wrap the
color-mix pair inside an `@supports`(rule: color-mix(...)) block so browsers
without color-mix still get a solid color. Update the objects named default,
pending, and rejected in styles.css.ts (and mirror the same change in
button/styles.css.ts and earn-details/styles.css.ts) to set borderColor and
background to the solid hex first, then the color-mix value or guarded by
`@supports`.
In `@packages/widget/src/hooks/use-yield-meta-info.tsx`:
- Line 56: In use-yield-meta-info, stop relying on getYieldProviderDetails for
provider because y.provider is optional and providerName already falls back to
y.metadata.name—use y.provider only when present and keep the existing fallback
logic for providerName; confirm getExtendedYieldType(y) is the single source of
truth for yieldType and that the existing switch(yieldType) covers all generated
YieldType cases (leave the default to return ifNotFound for unexpected strings);
and restore any missing metadata previously shown in extra by merging
Tezos-specific details.extra_tezos with reward-type and withdrawal-period (when
present) so extra includes both def.extra_tezos and the reward/withdrawal fields
as needed.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/components/earn-details-header.tsx`:
- Line 109: The translation lookup uses i18next pluralization by calling
t("positions.via", { providerName, count }), so add plural keys
positions.via_one and positions.via_other to both language translation files
(English and French) instead of a single positions.via entry; ensure the string
templates include the same interpolation variables (e.g. providerName) expected
by the component (referenced in earn-details-header.tsx) so the one/other
variants render correctly for count === 1 and other counts.
In `@packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts`:
- Around line 300-305: The globalStyle call that targets
`${chartContainer}:is(:focus, :focus-visible, :focus-within), ${chartContainer}
*:is(:focus, :focus-visible, :focus-within)` removes all focus outlines and
breaks keyboard accessibility; update this by removing the blanket outline:
"none" rule and instead either (a) delete this globalStyle so default focus
remains, or (b) replace it with explicit :focus-visible styling for interactive
elements inside chartContainer (e.g. tooltips, data points, range buttons) to
add a clear, visible custom outline, or (c) if the chart lib already provides
focus visuals, narrow the selector to avoid overriding them; locate the
globalStyle usage and chartContainer constant to apply the chosen fix.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/use-yield-reward-rate-history.ts`:
- Around line 61-82: The useMemo wrapper around the data computation must be
removed; replace the const data = useMemo(...) with a plain synchronous
computation that directly maps, filters, converts, and sorts (preserving the
existing logic: new Date(item.timestamp), BigNumber(item.rewardRate), skip
invalid date or non-finite rewardRate, produce {date, timestamp, value:
rewardRate.times(100).toNumber()} and sort by date) so it no longer uses useMemo
or depends on React memoization. Locate the const named data in
use-yield-reward-rate-history (and references to query.data) and move the
mapping/flatMap/sort logic out of useMemo into a simple const data = (query.data
?? []).flatMap(...).sort(...) so behavior is unchanged but useMemo is removed.
- Around line 48-57: The cast to the local RewardRateHistoryResponse weakens
type-safety—stop forcing (response as RewardRateHistoryResponse) and use the
generated API DTO types instead: treat the call to
YieldsControllerGetYieldRewardRateHistory as returning
RewardRateHistoryResponseDto (or use RewardRateHistoryResponseDto["items"]) and
return response.items ?? [] so the items retain their generated type
(RewardRateSnapshotDto). Update the return typing of the hook to use that
DTO/items type and remove the unnecessary cast.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/use-yield-tvl-history.ts`:
- Around line 50-77: The code uses useMemo to build the data array in the
use-yield-tvl-history module (const data =
useMemo<RewardRateHistoryPoint[]>(...) referencing useMemo, query.data,
BigNumber), which violates the guideline disallowing useMemo for
render-performance only; remove the useMemo wrapper and compute the transformed
array directly (map/flatMap + sort) into the const data so the component returns
a fresh array each render, keeping the same transformation logic (date parsing,
tvlUsd check, BigNumber toNumber, NaN/finite guards) and preserving the
RewardRateHistoryPoint shape and sort order.
In
`@packages/widget/src/pages-dashboard/overview/positions/hooks/use-grouped-positions.ts`:
- Around line 36-46: The code builds integrationIds from positions and passes
them into useQueries without validating for null/undefined; update the
integrationIds creation to filter out falsy/null values (e.g., only include ids
where id != null) so integrationIds only contains valid integrationId strings
before calling useQueries, ensuring the queries array (and queryKey entries like
["yield-opportunity", yieldId, isLedgerLive]) never receives null/undefined
yieldId and avoiding unnecessary/invalid queries in useQueries and queryFn.
In
`@packages/widget/src/pages/details/activity-page/hooks/use-action-list-item.ts`:
- Around line 73-167: Remove unnecessary useMemo wrappers around pure
computations: replace useMemo calls for direction (getDirection), tokenSymbol
(logic using action.yieldData and getActionInputToken), amount
(Maybe.fromNullable(...).map(...)), amountSign, title (switch using t and
action.actionData.type), and the timestamp block producing
timestampAbsolute/timestampRelative with plain const declarations so these
values are recomputed each render; keep the same computation logic and
referenced helpers (getDirection, ICON_TYPE_MAP, getActionInputToken, Maybe,
defaultFormattedNumber, t, getActivityDayKind, getActivityRelativeTime,
capitalizeFirstLetters) and preserve existing dependency usage (action, locale,
t) but remove useMemo imports/usages accordingly.
In
`@packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx`:
- Around line 55-57: When selection is disabled (canSelect === false) we should
avoid calling useTokenListYields with the real tokenBalances because it computes
query inputs and may trigger network work; update the calls to
useTokenListYields so they receive an empty array when canSelect is false (e.g.
replace data?.tokenBalances ?? [] with canSelect ? (data?.tokenBalances ?? []) :
[]) and make the same change for the second occurrence around the block
referenced by lines 61-81 so the hook only runs queries when selection is
enabled.
In `@packages/widget/src/pages/details/earn-page/styles.css.ts`:
- Around line 125-140: The styles selectorSummaryBadge and selectorSummaryActive
currently hardcode colors (`#EAF7EE` and `#137333`); replace those hex values with
semantic theme tokens (e.g., use vars.colors.success.surface /
vars.colors.success.fg or the appropriate surface/foreground tokens from your
design system) and, if necessary, add dedicated tokens for badge background and
text so theme packs and dark mode variants pick them up; update both
selectorSummaryBadge and selectorSummaryActive to reference those vars tokens
(and keep existing tokens for borderRadius, fontSize, fontWeight), and ensure
the chosen tokens provide sufficient contrast in dark mode or add a dark-mode
variant token.
In `@packages/widget/src/providers/theme-wrapper.tsx`:
- Around line 17-39: finalTheme is always derived from lightTheme (via
merge(structuredClone(lightTheme), theme)) and automatic system dark-mode
selection (prefers-color-scheme / matchMedia) appears removed; update the README
and widget CHANGELOG to state this breaking change and that consumers must now
pass a dark theme explicitly via SettingsProps["theme"] (hint: show an example
using useSettings() or passing darkTheme), and clearly document that finalTheme
no longer auto-switches and how to re-enable system-based switching if desired
(e.g., reintroduce matchMedia logic around finalTheme).
In `@packages/widget/src/translation/French/translations.json`:
- Around line 48-113: The English translations file is missing two keys present
in French: add "dashboard.summary_item.rewards_summary_current_enabled_tooltip"
and "dashboard.summary_item.rewards_summary_zero_tooltip" to
packages/widget/src/translation/English/translations.json with appropriate
English copy matching the intent of the French entries; ensure the keys are
placed under the dashboard.summary_item object (matching the structure used in
French) so lookup for rewards_summary_current_enabled_tooltip and
rewards_summary_zero_tooltip succeeds at runtime.
---
Outside diff comments:
In `@packages/widget/src/hooks/api/use-yield-opportunity/index.ts`:
- Around line 30-45: The queryKey currently includes the non-serializable
queryClient instance causing cache identity issues; update the getKey invocation
used in the use-yield-opportunity hook so it only receives serializable inputs
(e.g., yieldId, isLedgerLive, apiClient, integrationId) and remove queryClient
from that key, while still passing queryClient into the queryFn call (the
queryFn defined in get-yield-opportunity.ts should continue to receive
queryClient via its params); update references to getKey and the queryFn
invocation so queryClient is excluded from the key but provided to queryFn.
In `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx`:
- Around line 536-542: The mutationFn currently returns silently when
kycGateIsBlocking is true; update mutationFn to explicitly surface feedback
instead of a silent no-op by invoking the existing user-visible mechanism (for
example call a toast/notification helper or set a local error state like
setKycBlockingError that the UI reads) when kycGateIsBlocking is true, then
return; keep checks for validation.hasErrors, stakeEnterRequestDto.isNothing(),
and isConnected/openConnectModal as-is but ensure the new feedback call (e.g.,
showKycBlockingToast or setKycBlockingError) is executed before returning so
users get immediate feedback if they trigger the mutation while KYC is blocking.
---
Nitpick comments:
In `@packages/widget/src/components/atoms/icons/arrow.tsx`:
- Around line 21-40: The Arrow SVG lacks accessibility attributes; update the
Arrow component's <svg> element to support accessibility by adding either
aria-hidden="true" when decorative or role="img" plus an aria-label prop when it
conveys meaning. Modify the component signature to accept an optional ariaLabel
(or decorative/ariaHidden boolean) and use that to conditionally render
aria-hidden or role="img" and aria-label on the <svg> (the element using size,
rotation[direction], color and vars.color.text). Ensure the default remains
decorative if no label is provided.
In `@packages/widget/src/components/atoms/select-modal/index.tsx`:
- Line 252: The code currently spreads caller-provided variant into variant={{
appearance: "plain", ...variant }}, which allows callers to override appearance;
change this to enforce plain by applying appearance after the spread (e.g.,
variant={{ ...variant, appearance: "plain" }}) in the SelectModalItem JSX (or
alternatively ensure callers never pass appearance by typing or stripping it
before spread) so the ItemContainerVariants appearance is always "plain" for
modal items.
In `@packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts`:
- Around line 164-170: The verificationModalFrameStyle currently hardcodes the
iframe background to "`#ffffff`", which breaks dark themes; update
verificationModalFrameStyle to read its background from a theme or CSS variable
(e.g., use a theme token, prop-derived value, or a --kyc-iframe-bg CSS variable)
and fall back to "`#ffffff`" if unspecified, or add a short comment/documentation
explaining why a fixed white background is required when the KYC provider cannot
support dark mode.
In
`@packages/widget/src/components/molecules/kyc-gate-card/verification-modal.tsx`:
- Around line 68-74: The iframe rendering the KYC verification URL (the element
with data-testid="kyc-verification-iframe" and src={url}) lacks a sandbox
attribute; add a sandbox attribute to the iframe (e.g., sandbox="allow-forms
allow-scripts allow-same-origin") to restrict capabilities as a defense-in-depth
measure, and adjust the permission list to match the KYC provider's actual
requirements so the verification flow still works.
In `@packages/widget/src/components/molecules/summary-item/index.css.ts`:
- Around line 61-72: The utila style array currently sets borderColor twice
(first via atoms with borderColor: "transparent", then overridden by
borderColor: utilaPalette.border), so remove the redundant "transparent"
assignment from the atoms call (the utila array that includes atoms({...}) and
the object with borderColor: utilaPalette.border) unless you intended a
fallback; update the atoms invocation to omit borderColor while keeping
borderRadius, borderWidth, and borderStyle in the following object so the
effective border color comes only from utilaPalette.border.
In `@packages/widget/src/domain/types/reward-rate.ts`:
- Around line 10-22: The repo should avoid depending on the generated
RewardTypes enum — keep RewardRateBreakdownItem.rewardType typed as string |
undefined (not RewardTypes) and ensure all code that consumes
RewardRateBreakdown (and functions like getRewardTypeFormatted) accept string |
undefined; remove any imports/uses of RewardTypes or getRewardTypeFromRateType
in widget/src and confirm RewardRateBreakdown only reads item.key and item.rate
so no runtime changes are required. Use the symbols RewardRateBreakdownItem,
rewardType, RewardRateBreakdown, getRewardTypeFormatted,
getRewardTypeFromRateType and RewardTypes to locate and update/remove
references.
In `@packages/widget/src/hooks/api/use-yield-kyc-gate.ts`:
- Around line 40-45: The empty-string fallbacks on selectedYield?.id and address
are only present for TS but are unreachable due to queryEnabled; either add a
short clarifying comment above the queryFn (e.g. “queryEnabled ensures
selectedYield and address are defined”) or replace the fallbacks with non-null
assertions (selectedYield!.id and address!) in the call to
apiClient.withOptions(...).yield.KycControllerGetStatus so the intent is
explicit; keep references to queryFn, selectedYield, address and the
KycControllerGetStatus call when making the change.
In
`@packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts`:
- Around line 77-87: The current try/catch inside fetchYieldProvider swallows
errors and returns undefined; update fetchYieldProvider (the function that calls
queryClient.fetchQuery with queryKey getProviderKey(providerId) and queryFn
client.yield.ProvidersControllerGetProvider) to return a discriminated result
object like { success: boolean; provider?: YieldProviderDetails; error?: Error }
instead of undefined on failure: on success return { success: true, provider },
on failure catch the error and return { success: false, error } (and stop using
console.log). Adjust the function's return type accordingly and update callers
to handle the new shape.
In `@packages/widget/src/hooks/api/use-yield-validators.ts`:
- Around line 74-80: Add a JSDoc comment for the optional parameter
suppressRichErrors on the useYieldValidators hook: describe that it toggles
whether the hook returns rich/structured error objects vs. generic errors (when
true it suppresses detailed error payloads for simpler consumers or to avoid
leaking internals), note default behavior (false/undefined returns rich errors),
and indicate typical use-cases (e.g., UI components that only need a message or
tests). Place the comment immediately above the parameter in the function
signature where suppressRichErrors is declared so maintainers can quickly see
its purpose and expected values.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx`:
- Around line 29-59: The provider name resolution in ProviderSelectionCard
currently inlines a multi-fallback expression into the providerName constant;
extract that logic into a small helper (e.g., getProviderDisplayName) that
accepts selectedProvider, selectedValidatorsArr (or selectedValidators), and
yieldDto and returns the resolved string; replace the inlined expression with a
call to getProviderDisplayName(selectedProvider, selectedValidatorsArr,
yieldDto) so the resolution is testable and clearer while keeping the original
fallback order (selectedProvider?.name, first selected validator name, first
validator address, yieldDto.provider?.name, yieldDto.providerId).
- Around line 188-220: Extract the duplicated ExternalLinkIcon SVG into a single
shared React component named ExternalLinkIcon (e.g.,
components/atoms/icons/ExternalLinkIcon) and export it as the default (or named)
component; then remove the inline ExternalLinkIcon declarations from
provider-selection-card.tsx and integration-docs-link.tsx and replace them with
an import of the shared ExternalLinkIcon component and use it in the same
places. Ensure the new component preserves the same props/attributes
(aria-hidden, className, width/height, viewBox, and SVG paths) so existing
usages (ExternalLinkIcon in provider-selection-card.tsx and the copy in
integration-docs-link.tsx) continue to render identically. Also update any
import paths and run a quick lint/compile to confirm no type or prop
regressions.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx`:
- Around line 333-344: getCooldownFact currently always returns a fact (even
when days === 0) which is inconsistent with getWarmupFact and getLockupFact that
return null for missing values; update getCooldownFact to return null when
getYieldCooldownPeriod(yieldDto) is undefined or when days === 0 (so it mirrors
getWarmupFact/getLockupFact behavior), keep id "cooldown" and use
formatCooldownDays(days, t) only when returning a fact, and ensure any callers
that map facts handle nullable returns the same way they do for warmup/lockup.
In `@packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts`:
- Around line 61-91: Replace hardcoded hex colors in headerBadge,
headerAutoBadge, headerBadgeText and autoBadge with theme tokens (e.g., use
vars.color or atoms color tokens) so badges follow the theme; if the tokens
don't exist, add semantic tokens like badgeWarningBg, badgeWarningText,
badgeSuccessBg, badgeSuccessText to the theme contract and reference those (or
appropriate atoms keys) in the style definitions instead of literals.
In
`@packages/widget/src/pages-dashboard/position-details/components/styles.css.ts`:
- Around line 63-112: Replace the hardcoded hex colors in the metricCard and
metricValueText recipes with theme tokens so colors respond to theme overrides:
update the tone.action and tone.claim entries in metricCard to use your theme
variables (e.g., atoms({ background: "metricActionBg" }) and atoms({ background:
"metricClaimBg" }) or vars.color.metricActionBg/metricClaimBg) and update the
tone.action and tone.claim entries in metricValueText to use theme text tokens
(e.g., atoms({ color: "metricActionText" }) / vars.color.metricActionText and
metricClaimText / vars.color.metricClaimText); leave defaultVariants intact and
ensure the corresponding tokens are added to the theme contract if missing.
In
`@packages/widget/src/pages/details/activity-page/components/activity-filters/styles.css.ts`:
- Around line 52-55: The filterCount recipe's state variants use mixed styles:
default uses atoms({ background: "backgroundMuted" }) while active uses an
inline { background: "rgba(255, 255, 255, 0.2)" }; update the active variant to
use atoms(...) for consistency (or convert both to inline objects) — e.g.,
change the active background to an atoms token (or add a theme token for the
translucent white and reference it via atoms) so both state variants use the
same styling approach; locate this in the filterCount recipe's state ->
default/active definitions.
In `@packages/widget/src/pages/details/activity-page/hooks/use-activity-page.tsx`:
- Around line 53-57: The current fragile type assertion on activityActions.data
used to compute apiTotal should be replaced with a safer contract: update the
useActivityActions hook to return a typed total (e.g. include a total: number |
undefined property) and then read activityActions.total when computing apiTotal;
alternatively, add a small type guard (e.g. isActivityActionsData(data): data is
{ pages: { total?: number }[] }) and use it to safely access pages[0].total.
Locate the apiTotal calculation (variable name apiTotal) and the
useActivityActions hook (function name useActivityActions) and implement one of
these fixes so the component no longer relies on an unsafe assertion of
activityActions.data.
In `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx`:
- Around line 484-502: The handler onDashboardYieldCategorySelect currently
dispatches both "token/select" and "yield/select", which changes the selected
token when switching categories; update it to preserve the user's current token
selection: first try to find targetYield within the currently selected token
(use selectedStakeData/selected token id and filter availableYieldsById for
yieldDto.token.id === selectedToken?.id and matching category) and only dispatch
{ type: "yield/select", data: targetYield } if found; if no matching yield
exists for the current token, then fall back to the existing behavior (select
the highest-yielding target across all tokens and dispatch both { type:
"token/select", data: targetYield.token } and { type: "yield/select", data:
targetYield }). Ensure to reference onDashboardYieldCategorySelect,
selectedDashboardYieldCategory, selectedStakeData, dashboardYields, dispatch,
"token/select", and "yield/select".
- Around line 311-318: When dashboardVariant is true and
selectedDashboardYieldCategory is applied, dashboardFilteredDtos may become
empty leaving users with a blank opportunity list; update the logic around
dashboardFilteredDtos (and where filteredDtos, getDashboardYieldCategory,
selectedDashboardYieldCategory are used) to validate the filtered result and
handle emptiness: either fallback to the unfiltered filteredDtos, clear/reset
selectedDashboardYieldCategory to a default/available category, or set an
explicit empty-state flag that the UI can render with messaging; ensure this
validation occurs immediately after the filter is computed so the consuming UI
can display a helpful empty state or revert the filter.
In `@packages/widget/src/providers/api/api-client.ts`:
- Line 245: The public API rename from withRunOptions to withOptions is
breaking; add a short-term compatibility shim and document the change: add a
deprecated alias function named withRunOptions that calls withOptions(options)
(reference bindApiClients and ApiClientOptions) so existing consumers don't
break, and update release notes/migration guide to describe the rename, the
deprecation, and recommended migration to withOptions.
In `@packages/widget/src/utils/formatters.ts`:
- Around line 107-115: compactUsdFormatter and compactNumberFormatter currently
hardcode the "en-US" locale; update them to be locale-configurable by either (a)
accepting a locale parameter or (b) deriving the locale from the environment
(e.g., undefined to use runtime/default locale or a passed-in locale string).
Modify the definitions of compactUsdFormatter and compactNumberFormatter (or
replace them with factory functions like
createCompactUsdFormatter/createCompactNumberFormatter) so callers can supply
locale; ensure options (notation: "compact", maximumFractionDigits: 0) are
preserved and used when constructing new Intl.NumberFormat instances.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| background: "color-mix(in oklab, #5147f3 8%, transparent)", | ||
| borderColor: "#5147f3", | ||
| }, | ||
| pending: { | ||
| background: "color-mix(in oklab, #ffc21b 16%, transparent)", | ||
| borderColor: "#ffc21b", | ||
| }, | ||
| rejected: { | ||
| background: "color-mix(in oklab, #ff3b1f 10%, transparent)", | ||
| borderColor: "#ff3b1f", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there's a browserslist config or documented browser support requirements
fd -t f 'browserslist|\.browserslistrc|package\.json' | xargs grep -l "browserslist\|targets" 2>/dev/null | head -5Repository: stakekit/widget
Length of output: 41
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Browser support config files =="
fd -t f -a '(\.browserslistrc|browserslist(\.config)?\.json|browserslist(\.rc)?|package\.json|postcss\.config\.js|postcss\.config\..+|next\.config\.js|vite\.config\..+|webpack\.config\..+|tailwind\.config\..+)$' .
echo
echo "== Files mentioning browserslist/targets =="
fd -t f 'package\.json|\.browserslistrc|browserslist\.config\.json' . \
| xargs -r rg -n --hidden --no-ignore-vcs -S "browserslist|targets" \
|| true
echo
echo "== Any CSS fallbacks around color-mix usage =="
fd -t f -g'*.css.ts' -g'*.css' -g'*.ts' -g'*.tsx' . \
| xargs -r rg -n --hidden --no-ignore-vcs -S "color-mix\(" \
|| trueRepository: stakekit/widget
Length of output: 1194
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Grep for browserslist/targets config in package.json files =="
fd -t f -a 'package\.json' . -0 | xargs -0 rg -n --hidden -S '"browserslist"|browserslist|targets\s*[:=]' || true
echo
echo "== Grep for color-mix usage in widget sources (including *.css.ts) =="
fd -t f -a '\.(css\.ts|ts|tsx|css)$' packages/widget/src . -0 | xargs -0 rg -n --hidden -S 'color-mix\(' || true
echo
echo "== Show the exact code around color-mix in the reviewed file =="
FILE="packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts"
if [ -f "$FILE" ]; then
sed -n '1,120p' "$FILE" | nl -ba | sed -n '1,80p'
else
echo "Missing $FILE"
fiRepository: stakekit/widget
Length of output: 1878
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Line count for reviewed file =="
wc -l packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts
echo
echo "== Reviewed file (lines 1-120) =="
awk '{printf "%4d %s\n", NR, $0}' packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts | sed -n '1,120p'
echo
echo "== Search for browser targets in root + widget package.json =="
for f in package.json packages/widget/package.json; do
echo "--- $f ---"
rg -n '"browserslist"|browserslist|targets\s*[:=]|target\s*[:=]|esbuild|`@babel/preset-env`|autoprefixer|caniuse' "$f" || true
done
echo
echo "== Search for browserslist config files =="
fd -t f 'browserslist(\..+)?|\.browserslistrc|browserslist\.config\.json' . || true
echo
echo "== Search for `@supports/`@fallback patterns around color-mix in widget sources =="
fd -t f '\.(css\.ts|css)$' packages/widget/src -0 | xargs -0 rg -n --hidden -S '`@supports`|color-mix\(' || trueRepository: stakekit/widget
Length of output: 4443
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package.json (root) =="
wc -l package.json
sed -n '1,200p' package.json
echo
echo "== packages/widget/package.json =="
wc -l packages/widget/package.json
sed -n '1,220p' packages/widget/package.json
echo
echo "== postcss.config.js =="
wc -l packages/widget/postcss.config.js
sed -n '1,200p' packages/widget/postcss.config.js
echo
echo "== Vite targets (search) =="
fd -t f 'vite\.config\..*\.ts|vite\.config\.ts' packages/widget/vite -0 | xargs -0 rg -n -S 'target|targets|browserslist|esbuild|autoprefixer' || true
echo
echo "== Any usage/fallback for color-mix with `@supports` =="
fd -t f '\.(css\.ts|css)$' packages/widget/src -0 | xargs -0 rg -n --hidden -S '`@supports`|color-mix\(' || trueRepository: stakekit/widget
Length of output: 7576
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search widget for browserslist/targets hints =="
rg -n --hidden -S "browserslist|browsers\.list|target(s)?\s*[:=]|esbuild\s*[:=]|`@babel/preset-env`|autoprefixer|postcss-preset-env|preset-env|caniuse" packages/widget || true
echo
echo "== Inspect vite.config.base.ts for build/target/esbuild settings =="
wc -l packages/widget/vite/vite.config.base.ts
sed -n '1,220p' packages/widget/vite/vite.config.base.ts
echo
echo "== Search for `@supports/fallback` patterns using color-mix =="
fd -t f '\.(css\.ts|css)$' packages/widget/src -0 | xargs -0 rg -n --hidden -S '`@supports`|color-mix\(' || true
echo
echo "== Search for any postcss plugins that could transform color-mix =="
rg -n --hidden -S "postcss-preset-env|cssnano|postcss-color-mix|color-mix|postcss.*preset" packages/widget/postcss.config.js packages/widget -g'*.js' -g'*.ts' || trueRepository: stakekit/widget
Length of output: 7380
Add a fallback or align browser targets for color-mix()
color-mix() is used for backgrounds in packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts (and appears elsewhere in packages/widget/src/components/atoms/button/styles.css.ts and packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts). This repo doesn’t define browserslist/browser targets (Vite/postcss only runs autoprefixer), and there’s no @supports/fallback around these declarations—so styling may break in browsers that don’t support color-mix. Add a fallback background/border color (or wrap with @supports) to keep the UI consistent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/widget/src/components/molecules/kyc-gate-card/styles.css.ts` around
lines 22 - 31, The CSS uses color-mix() for the kyc-gate-card variants (objects
named default/pending/rejected) without fallbacks; add a plain fallback
background and borderColor before the color-mix declaration or wrap the
color-mix pair inside an `@supports`(rule: color-mix(...)) block so browsers
without color-mix still get a solid color. Update the objects named default,
pending, and rejected in styles.css.ts (and mirror the same change in
button/styles.css.ts and earn-details/styles.css.ts) to set borderColor and
background to the solid hex first, then the color-mix value or guarded by
`@supports`.
| .map((t) => t.symbol) | ||
| .join(", "); | ||
| const provider = getYieldProviderDetails(y); | ||
| const provider = y.provider; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify y.provider is safely accessed and check old helper behavior
# Check if getYieldProviderDetails is still defined and what it does
ast-grep --pattern $'export const getYieldProviderDetails = ($$$) => {
$$$
}'
# Find all direct y.provider accesses to see the pattern
rg -n "\.provider" packages/widget/src/hooks/use-yield-meta-info.tsx
# Check Yield type definition to see if provider is optional
ast-grep --pattern $'type Yield = {
$$$
provider
$$$
}'Repository: stakekit/widget
Length of output: 97
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate helper definition/usages
rg -n "getYieldProviderDetails" -S .
# Inspect the exact file and surrounding code near the provider access
sed -n '1,220p' packages/widget/src/hooks/use-yield-meta-info.tsx
# Find where the Yield type (and provider field optionality) is defined in this repo
rg -n "type Yield\s*=|\binterface Yield\b|\bexport (type|interface) Yield\b" -S packages/widget/src
rg -n "provider\??\s*:" -S packages/widget/src
# Check generated/api/yield exports (best-effort) for provider optionality
rg -n "generated/api/yield" -S packages/widget/src/hooks/use-yield-meta-info.tsx
rg -n "yield['\"]|\byield\b" -S packages/widget/src | head -n 50Repository: stakekit/widget
Length of output: 41
🏁 Script executed:
#!/bin/bash
set -u
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
echo "== Provider access context =="
sed -n '1,120p' "$FILE" | nl -ba | sed -n '1,120p'
echo
echo "== Around line ~56 =="
sed -n '45,80p' "$FILE" | nl -ba
echo
echo "== Search for getYieldProviderDetails in repo =="
rg -n "getYieldProviderDetails" packages/widget/src || true
rg -n "getYieldProviderDetails" . || true
echo
echo "== Imports possibly related to Yield/provider =="
rg -n "import.*yield|from .*yield|getExtendedYieldType|getBaseYieldType|provider" "$FILE" || true
echo
echo "== Find Yield type/provider field definition in widget/src =="
rg -n "type Yield\b|interface Yield\b|export (type|interface) Yield\b" packages/widget/src || true
rg -n "provider\??\s*:" packages/widget/src || true
echo
echo "== Search generated/api/yield types for provider optionality (best-effort) =="
rg -n "provider\??\s*:" -S packages/widget/src/generated/api/yield 2>/dev/null || true
rg -n "provider\??\s*:" -S generated/api/yield 2>/dev/null || trueRepository: stakekit/widget
Length of output: 8062
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
YIELDS_FILE="packages/widget/src/domain/types/yields.ts"
GEN_YIELD="packages/widget/src/generated/api/yield.ts"
echo "== use-yield-meta-info.tsx around provider line =="
sed -n '40,90p' "$FILE"
echo
echo "== domain/types/yields.ts =="
sed -n '1,120p' "$YIELDS_FILE"
echo
echo "== Look for getYieldProviderDetails in domain/types/yields.ts =="
rg -n "getYieldProviderDetails" "$YIELDS_FILE" || true
echo
echo "== generated/api/yield.ts provider optionality (snippets) =="
rg -n "readonly provider\?\:" "$GEN_YIELD" | head -n 20Repository: stakekit/widget
Length of output: 5428
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
YIELDS_FILE="packages/widget/src/domain/types/yields.ts"
echo "== Around extra/yieldType switch (lines ~110-240) =="
sed -n '110,240p' "$FILE" | nl -ba | sed -n '1,160p'
echo
echo "== getExtendedYieldType implementation =="
rg -n "getExtendedYieldType" "$YIELDS_FILE"
sed -n '120,220p' "$YIELDS_FILE" | nl -ba | sed -n '1,160p'
echo
echo "== getBaseYieldType presence/impl =="
rg -n "getBaseYieldType" "$YIELDS_FILE" || true
rg -n "getBaseYieldType" packages/widget/src || true
echo
echo "== Locate the yieldType switch in use-yield-meta-info.tsx =="
rg -n "switch\s*\(\s*yieldType\s*\)" "$FILE" || true
rg -n "yieldType\s*===|case\s+|ifNotFound" "$FILE" | head -n 80
echo
echo "== Check for getYieldProviderDetails in repo (best-effort) =="
rg -n "getYieldProviderDetails" packages/widget/src || true
rg -n "getYieldProviderDetails" . || trueRepository: stakekit/widget
Length of output: 173
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
YIELDS_FILE="packages/widget/src/domain/types/yields.ts"
echo "== Imports for use-yield-meta-info.tsx =="
sed -n '1,40p' "$FILE" | cat -n
echo
echo "== Provider usage context (around lines ~50-70) =="
sed -n '45,80p' "$FILE" | cat -n
echo
echo "== getExtendedYieldType + any base type helpers in yields.ts =="
rg -n "getExtendedYieldType|getBaseYieldType" "$YIELDS_FILE"
sed -n '80,200p' "$YIELDS_FILE" | cat -n
echo
echo "== Domain type: Yield provider optionality =="
sed -n '1,60p' "$YIELDS_FILE" | cat -n
echo
echo "== YieldType switch in use-yield-meta-info.tsx =="
rg -n "switch\s*\(\s*yieldType\s*\)" "$FILE" || true
# Show a chunk likely containing the switch (adjust if needed)
sed -n '110,240p' "$FILE" | cat -n
echo
echo "== Check for mention of liquid-staking / native_staking / pooled_staking / fixed_yield cases in use-yield-meta-info.tsx =="
rg -n "liquid-staking|native_staking|pooled_staking|fixed_yield|real_world_asset|concentrated_liquidity_pool|liquidity_pool" "$FILE" || true
echo
echo "== getYieldProviderDetails presence =="
rg -n "getYieldProviderDetails" packages/widget/src || trueRepository: stakekit/widget
Length of output: 16102
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
YIELDS_FILE="packages/widget/src/domain/types/yields.ts"
echo "== getBaseYieldType references =="
rg -n "getBaseYieldType" "$YIELDS_FILE" packages/widget/src || true
echo
echo "== use-yield-meta-info.tsx: switch (yieldType) full block (approx) =="
# print a broader chunk to include default/return-ifNotFound handling
sed -n '115,260p' "$FILE" | cat -n
echo
echo "== use-yield-meta-info.tsx: any default case in switch =="
rg -n "default:" "$FILE" || true
echo
echo "== getExtendedYieldType logic (full signature area) =="
rg -n "export const getExtendedYieldType" "$YIELDS_FILE"
sed -n '280,380p' "$YIELDS_FILE" | cat -nRepository: stakekit/widget
Length of output: 10362
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
YIELDS_FILE="packages/widget/src/domain/types/yields.ts"
echo "== use-yield-meta-info.tsx: switch default branch context (around ~250-290) =="
sed -n '240,320p' "$FILE" | cat -n
echo
echo "== yields.ts: getYieldTypeLabels map coverage (continue from where it left off) =="
# show the rest of the map in getYieldTypeLabels
sed -n '140,260p' "$YIELDS_FILE" | cat -n
echo
echo "== yields.ts: any helpers validating supported yield types =="
rg -n "YieldTypeLabelsMap|ExtendedYieldType|isNativeStaking|isPooledStaking|mechanics\.type" "$YIELDS_FILE"Repository: stakekit/widget
Length of output: 7284
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) helper existence
echo "== getYieldProviderDetails occurrences (whole repo) =="
rg -n "getYieldProviderDetails" . || true
echo
# 2) liquid-staking / other yield types
echo "== liquid_staking occurrences (whole repo) =="
rg -n "liquid[_-]staking" . || true
echo
echo "== concentrated_liquidity_pool / fixed_yield / liquidity_pool occurrences (whole repo) =="
rg -n "concentrated_liquidity_pool|fixed_yield|liquidity_pool|real_world_asset|restaking|vault|lending" packages/widget/src/generated/api/yield.ts | head -n 50 || true
echo
# 3) inspect ApiYieldType definition (best-effort)
echo "== Find ApiYieldType (YieldType) union in generated/api/yield.ts =="
rg -n "export type YieldType" packages/widget/src/generated/api/yield.ts || true
rg -n "YieldType\s*=" packages/widget/src/generated/api/yield.ts || true
# show nearby lines where YieldType union is defined (limit to avoid huge output)
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("packages/widget/src/generated/api/yield.ts")
txt = p.read_text(encoding="utf-8", errors="ignore").splitlines()
for i,line in enumerate(txt):
if "export type YieldType" in line:
start=max(0,i-5); end=min(len(txt), i+120)
print("---- context around:", i+1, "----")
print("\n".join(f"{j+1}:{txt[j]}" for j in range(start,end)))
break
else:
print("No 'export type YieldType' found.")
PYRepository: stakekit/widget
Length of output: 8225
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/hooks/use-yield-meta-info.tsx"
YIELDS_FILE="packages/widget/src/domain/types/yields.ts"
echo "== use-yield-meta-info.tsx: extra construction area (~100-170) =="
sed -n '95,190p' "$FILE" | cat -n
echo
echo "== use-yield-meta-info.tsx: Tezos extra field logic (look for removed reward type/withdrawal period) =="
rg -n "extra_tezos|extra\"|reward|withdraw" "$FILE" | head -n 50
echo
echo "== yields.ts: look for liquid staking mention/mapping =="
rg -n "liquid[_-]staking|liquid staking|liquidStaking" "$YIELDS_FILE" || true
echo
echo "== yields.ts: getExtendedYieldType related helpers (native/pooled staking detection) =="
sed -n '380,520p' "$YIELDS_FILE" | cat -nRepository: stakekit/widget
Length of output: 10296
Adjust provider/yield-type concerns in use-yield-meta-info; re-check extra metadata regression
packages/widget/src/hooks/use-yield-meta-info.tsxline 56 readsy.provider, but it’s optional (provider?: ...), andproviderNamealready falls back toy.metadata.namewhenprovideris missing—so this is safe withoutgetYieldProviderDetails.getExtendedYieldType(y)can only produce locally-derived"native_staking"/"pooled_staking"or the generated APIYieldTypeunion; theswitch (yieldType)covers all resulting cases, withdefaultreturningifNotFoundfor anything unexpected (notably,"liquid-staking"is not in the generatedYieldTypeunion).extrais now onlydetails.extra_tezosfor Tezos (def.extra/ Tezos-specific extra). If reward-type and withdrawal-period metadata was previously shown inextra, it’s now removed and should be restored if still desired.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/widget/src/hooks/use-yield-meta-info.tsx` at line 56, In
use-yield-meta-info, stop relying on getYieldProviderDetails for provider
because y.provider is optional and providerName already falls back to
y.metadata.name—use y.provider only when present and keep the existing fallback
logic for providerName; confirm getExtendedYieldType(y) is the single source of
truth for yieldType and that the existing switch(yieldType) covers all generated
YieldType cases (leave the default to return ifNotFound for unexpected strings);
and restore any missing metadata previously shown in extra by merging
Tezos-specific details.extra_tezos with reward-type and withdrawal-period (when
present) so extra includes both def.extra_tezos and the reward/withdrawal fields
as needed.
| className={styles.headerProviderLabelText} | ||
| variant={{ weight: "normal" }} | ||
| > | ||
| {t("positions.via", { providerName, count: 1 })} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify positions.via translation key
echo "=== English translation ==="
cat packages/widget/src/translation/English/translations.json | jq -r '.positions.via // "KEY NOT FOUND"'
echo "=== French translation ==="
cat packages/widget/src/translation/French/translations.json | jq -r '.positions.via // "KEY NOT FOUND"'Repository: stakekit/widget
Length of output: 142
Update check to i18next plural suffix keys (positions.via_one / positions.via_other)
The current check for a literal positions.via key fails in both packages/widget/src/translation/English/translations.json and packages/widget/src/translation/French/translations.json. Since earn-details-header.tsx calls t("positions.via", { ..., count: 1 }), ensure positions.via_one exists in both languages (and positions.via_other as well for non-1 counts).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/components/earn-details-header.tsx`
at line 109, The translation lookup uses i18next pluralization by calling
t("positions.via", { providerName, count }), so add plural keys
positions.via_one and positions.via_other to both language translation files
(English and French) instead of a single positions.via entry; ensure the string
templates include the same interpolation variables (e.g. providerName) expected
by the component (referenced in earn-details-header.tsx) so the one/other
variants render correctly for count === 1 and other counts.
Source: Coding guidelines
| globalStyle( | ||
| `${chartContainer}:is(:focus, :focus-visible, :focus-within), ${chartContainer} *:is(:focus, :focus-visible, :focus-within)`, | ||
| { | ||
| outline: "none", | ||
| } | ||
| ); |
There was a problem hiding this comment.
Removing focus indicators degrades keyboard accessibility.
The globalStyle suppresses all focus outlines within the chart container. This makes it impossible for keyboard users to see which element has focus.
If the chart is non-interactive, this may be acceptable. However, if users can interact with chart elements (tooltips, data points, range buttons), you must provide visible focus indicators. Consider one of these alternatives:
- Remove this rule and style
:focus-visiblestates explicitly for interactive elements - If the chart library draws its own focus indicators, verify they're visible
- If suppressing browser defaults, add custom focus styling to replace them
♻️ Example: Custom focus styling
globalStyle(
`${chartContainer}:is(:focus, :focus-visible, :focus-within), ${chartContainer} *:is(:focus, :focus-visible, :focus-within)`,
{
- outline: "none",
+ outline: `2px solid ${vars.color.text}`,
+ outlineOffset: "2px",
}
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| globalStyle( | |
| `${chartContainer}:is(:focus, :focus-visible, :focus-within), ${chartContainer} *:is(:focus, :focus-visible, :focus-within)`, | |
| { | |
| outline: "none", | |
| } | |
| ); | |
| globalStyle( | |
| `${chartContainer}:is(:focus, :focus-visible, :focus-within), ${chartContainer} *:is(:focus, :focus-visible, :focus-within)`, | |
| { | |
| outline: `2px solid ${vars.color.text}`, | |
| outlineOffset: "2px", | |
| } | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts`
around lines 300 - 305, The globalStyle call that targets
`${chartContainer}:is(:focus, :focus-visible, :focus-within), ${chartContainer}
*:is(:focus, :focus-visible, :focus-within)` removes all focus outlines and
breaks keyboard accessibility; update this by removing the blanket outline:
"none" rule and instead either (a) delete this globalStyle so default focus
remains, or (b) replace it with explicit :focus-visible styling for interactive
elements inside chartContainer (e.g. tooltips, data points, range buttons) to
add a clear, visible custom outline, or (c) if the chart lib already provides
focus visuals, narrow the selector to avoid overriding them; locate the
globalStyle usage and chartContainer constant to apply the chosen fix.
| const response = await apiClient | ||
| .withOptions({ signal }) | ||
| .yield.YieldsControllerGetYieldRewardRateHistory(yieldId, { | ||
| params: { | ||
| period: periodToApiPeriod[period], | ||
| interval: getYieldHistoryInterval(period), | ||
| }, | ||
| }); | ||
|
|
||
| return (response as RewardRateHistoryResponse).items ?? []; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find the generated type for YieldsControllerGetYieldRewardRateHistory
rg -n "YieldsControllerGetYieldRewardRateHistory" packages/widget/src/generated/api/ -A 5 -B 2Repository: stakekit/widget
Length of output: 7848
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/pages-dashboard/overview/earn-details/use-yield-reward-rate-history.ts"
echo "=== Hook file context (imports + RewardRateHistoryResponse usage) ==="
sed -n '1,140p' "$FILE" | nl -ba | sed -n '1,140p'
echo
echo "=== Find definition/import of RewardRateHistoryResponse ==="
rg -n "RewardRateHistoryResponse" packages/widget/src -S
echo
echo "=== Find definition/import of RewardRateHistoryResponseDto ==="
rg -n "RewardRateHistoryResponseDto" packages/widget/src packages/widget/src/generated -SRepository: stakekit/widget
Length of output: 190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/pages-dashboard/overview/earn-details/use-yield-reward-rate-history.ts"
echo "=== Check available line-number tool ==="
command -v cat >/dev/null && echo "cat found"
command -v awk >/dev/null && echo "awk found"
command -v cat >/dev/null && cat --version 2>/dev/null | head -n 1 || true
command -v nl >/dev/null && echo "nl found" || echo "nl not found"
echo
echo "=== Hook file context (imports + RewardRateHistoryResponse usage) ==="
# Use cat -n (fallback) since nl not available
cat -n "$FILE" | sed -n '1,200p'
echo
echo "=== Find definition/import of RewardRateHistoryResponse ==="
rg -n "RewardRateHistoryResponse" packages/widget/src -S
echo
echo "=== Find definition/import of RewardRateHistoryResponseDto ==="
rg -n "RewardRateHistoryResponseDto" packages/widget/src packages/widget/src/generated -SRepository: stakekit/widget
Length of output: 3929
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/generated/api/yield.ts"
# Print around RewardRateHistoryResponseDto definition
rg -n "export type RewardRateHistoryResponseDto" "$FILE"
# Use sed with line numbers around the match (approx 1807 per earlier output)
sed -n '1760,1860p' "$FILE"Repository: stakekit/widget
Length of output: 3168
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/generated/api/yield.ts"
echo "=== Locate RewardRateSnapshotDto ==="
rg -n "export type RewardRateSnapshotDto" "$FILE"
echo
echo "=== Show RewardRateSnapshotDto definition ==="
# Extract around the match (line unknown); use rg to get line number and print a window
LINE="$(rg -n "export type RewardRateSnapshotDto" "$FILE" | head -n1 | cut -d: -f1)"
START=$((LINE-40))
END=$((LINE+80))
sed -n "${START},${END}p" "$FILE"Repository: stakekit/widget
Length of output: 3369
Reduce the unnecessary cast; it doesn’t change items’ shape but weakens type-safety.
YieldsControllerGetYieldRewardRateHistory’s 2xx payload is RewardRateHistoryResponseDto, whose items are RewardRateSnapshotDto ({ timestamp: string; rewardRate: string }). The hook’s local RewardRateHistoryResponse matches those fields, so the items usage is consistent; however the cast to a locally-defined, items?-optional type makes the code less type-safe than using the generated RewardRateHistoryResponseDto (or RewardRateHistoryResponseDto["items"]) directly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/widget/src/pages-dashboard/overview/earn-details/use-yield-reward-rate-history.ts`
around lines 48 - 57, The cast to the local RewardRateHistoryResponse weakens
type-safety—stop forcing (response as RewardRateHistoryResponse) and use the
generated API DTO types instead: treat the call to
YieldsControllerGetYieldRewardRateHistory as returning
RewardRateHistoryResponseDto (or use RewardRateHistoryResponseDto["items"]) and
return response.items ?? [] so the items retain their generated type
(RewardRateSnapshotDto). Update the return typing of the hook to use that
DTO/items type and remove the unnecessary cast.
| const direction = useMemo( | ||
| () => getDirection(action.actionData.type), | ||
| [action.actionData.type] | ||
| ); | ||
|
|
||
| const actionOlderThan7Days = useMemo( | ||
| () => | ||
| Maybe.of(action.actionData.createdAt) | ||
| .map(dateOlderThen7Days) | ||
| .orDefault(false), | ||
| [action] | ||
| ); | ||
| const iconType = ICON_TYPE_MAP[direction]; | ||
|
|
||
| const badgeLabel = useMemo( | ||
| () => | ||
| Maybe.fromNullable(action.actionData.status) | ||
| .map((status) => { | ||
| if (status === ActionStatus.WAITING_FOR_NEXT) { | ||
| return actionOlderThan7Days | ||
| ? ActionStatus.FAILED | ||
| : t("activity.review.in_progress"); | ||
| } | ||
| return status; | ||
| }) | ||
| .map((status) => status.replaceAll("_", " ")) | ||
| .map(capitalizeFirstLetters) | ||
| .extract(), | ||
| [action, t, actionOlderThan7Days] | ||
| ); | ||
| /** Deposits/rewards show the underlying input token, withdrawals the vault token. */ | ||
| const tokenSymbol = useMemo(() => { | ||
| const yieldToken = action.yieldData.token; | ||
|
|
||
| const badgeColor = useMemo( | ||
| () => | ||
| Maybe.fromNullable(action.actionData.status) | ||
| .map((status) => { | ||
| if ( | ||
| status === ActionStatus.WAITING_FOR_NEXT && | ||
| actionOlderThan7Days | ||
| ) { | ||
| return ActionStatus.FAILED; | ||
| } | ||
| return status; | ||
| }) | ||
| .map((status) => BADGE_BG_MAP[status]) | ||
| .extract(), | ||
| [action, actionOlderThan7Days] | ||
| ); | ||
| if (direction === "withdraw" || direction === "other") { | ||
| return yieldToken.symbol; | ||
| } | ||
|
|
||
| const inputToken = getActionInputToken({ | ||
| actionDto: action.actionData, | ||
| yieldDto: action.yieldData, | ||
| }); | ||
|
|
||
| return inputToken?.symbol ?? yieldToken.symbol; | ||
| }, [action.actionData, action.yieldData, direction]); | ||
|
|
||
| const amount = useMemo( | ||
| () => | ||
| Maybe.fromNullable(action.actionData.amount).map(defaultFormattedNumber), | ||
| [action] | ||
| [action.actionData.amount] | ||
| ); | ||
|
|
||
| const isPositive = direction === "deposit" || direction === "rewards"; | ||
|
|
||
| const amountSign = useMemo(() => { | ||
| if (amount.isNothing()) return ""; | ||
| if (direction === "withdraw") return "-"; | ||
| if (direction === "deposit" || direction === "rewards") return "+"; | ||
| return ""; | ||
| }, [amount, direction]); | ||
|
|
||
| const title = useMemo(() => { | ||
| switch (direction) { | ||
| case "deposit": | ||
| return t("activity.item.deposited", { token: tokenSymbol }); | ||
| case "withdraw": | ||
| return t("activity.item.withdrew", { token: tokenSymbol }); | ||
| case "rewards": | ||
| return t("activity.item.rewards"); | ||
| default: | ||
| return t("activity.item.generic", { | ||
| action: capitalizeFirstLetters( | ||
| action.actionData.type.replaceAll("_", " ") | ||
| ), | ||
| token: tokenSymbol, | ||
| }); | ||
| } | ||
| }, [direction, t, tokenSymbol, action.actionData.type]); | ||
|
|
||
| const { timestampAbsolute, timestampRelative } = useMemo(() => { | ||
| const createdAt = action.actionData.createdAt; | ||
|
|
||
| if (!createdAt) { | ||
| return { timestampAbsolute: "", timestampRelative: "" }; | ||
| } | ||
|
|
||
| const date = new Date(createdAt); | ||
| const dayKind = getActivityDayKind(date); | ||
|
|
||
| const time = date.toLocaleTimeString(locale, { | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: false, | ||
| }); | ||
|
|
||
| const absolute = | ||
| dayKind === "today" | ||
| ? `${t("activity.date_group_labels.today")} · ${time}` | ||
| : dayKind === "yesterday" | ||
| ? t("activity.date_group_labels.yesterday") | ||
| : date.toLocaleDateString(locale, { | ||
| day: "2-digit", | ||
| month: "short", | ||
| year: "numeric", | ||
| }); | ||
|
|
||
| const relativeParts = getActivityRelativeTime(date); | ||
| const relative = | ||
| relativeParts.unit === "now" | ||
| ? t("activity.time.now") | ||
| : relativeParts.unit === "minutes" | ||
| ? t("activity.time.minutes_ago", { count: relativeParts.value }) | ||
| : relativeParts.unit === "hours" | ||
| ? t("activity.time.hours_ago", { count: relativeParts.value }) | ||
| : t("activity.time.days_ago", { count: relativeParts.value }); | ||
|
|
||
| return { timestampAbsolute: absolute, timestampRelative: relative }; | ||
| }, [action.actionData.createdAt, locale, t]); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Remove unnecessary memoization for pure computations.
As per coding guidelines, avoid useMemo for render-performance optimization in React components; reserve it for semantic stability needs (e.g., external API dependency or context value identity). Lines 73-167 wrap pure computations (direction, tokenSymbol, amount, amountSign, title, timestamps) that would be fast to recompute on each render and don't require identity preservation.
♻️ Refactor to plain const declarations
- const direction = useMemo(
- () => getDirection(action.actionData.type),
- [action.actionData.type]
- );
+ const direction = getDirection(action.actionData.type);
- const iconType = ICON_TYPE_MAP[direction];
+ const iconType = ICON_TYPE_MAP[direction];
- const tokenSymbol = useMemo(() => {
+ const tokenSymbol = (() => {
const yieldToken = action.yieldData.token;
if (direction === "withdraw" || direction === "other") {
return yieldToken.symbol;
}
const inputToken = getActionInputToken({
actionDto: action.actionData,
yieldDto: action.yieldData,
});
return inputToken?.symbol ?? yieldToken.symbol;
- }, [action.actionData, action.yieldData, direction]);
+ })();
- const amount = useMemo(
- () =>
- Maybe.fromNullable(action.actionData.amount).map(defaultFormattedNumber),
- [action.actionData.amount]
- );
+ const amount = Maybe.fromNullable(action.actionData.amount).map(defaultFormattedNumber);
- const isPositive = direction === "deposit" || direction === "rewards";
+ const isPositive = direction === "deposit" || direction === "rewards";
- const amountSign = useMemo(() => {
+ const amountSign = (() => {
if (amount.isNothing()) return "";
if (direction === "withdraw") return "-";
if (direction === "deposit" || direction === "rewards") return "+";
return "";
- }, [amount, direction]);
+ })();
- const title = useMemo(() => {
+ const title = (() => {
switch (direction) {
case "deposit":
return t("activity.item.deposited", { token: tokenSymbol });
case "withdraw":
return t("activity.item.withdrew", { token: tokenSymbol });
case "rewards":
return t("activity.item.rewards");
default:
return t("activity.item.generic", {
action: capitalizeFirstLetters(
action.actionData.type.replaceAll("_", " ")
),
token: tokenSymbol,
});
}
- }, [direction, t, tokenSymbol, action.actionData.type]);
+ })();
- const { timestampAbsolute, timestampRelative } = useMemo(() => {
+ const { timestampAbsolute, timestampRelative } = (() => {
const createdAt = action.actionData.createdAt;
if (!createdAt) {
return { timestampAbsolute: "", timestampRelative: "" };
}
const date = new Date(createdAt);
const dayKind = getActivityDayKind(date);
const time = date.toLocaleTimeString(locale, {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
const absolute =
dayKind === "today"
? `${t("activity.date_group_labels.today")} · ${time}`
: dayKind === "yesterday"
? t("activity.date_group_labels.yesterday")
: date.toLocaleDateString(locale, {
day: "2-digit",
month: "short",
year: "numeric",
});
const relativeParts = getActivityRelativeTime(date);
const relative =
relativeParts.unit === "now"
? t("activity.time.now")
: relativeParts.unit === "minutes"
? t("activity.time.minutes_ago", { count: relativeParts.value })
: relativeParts.unit === "hours"
? t("activity.time.hours_ago", { count: relativeParts.value })
: t("activity.time.days_ago", { count: relativeParts.value });
return { timestampAbsolute: absolute, timestampRelative: relative };
- }, [action.actionData.createdAt, locale, t]);
+ })();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const direction = useMemo( | |
| () => getDirection(action.actionData.type), | |
| [action.actionData.type] | |
| ); | |
| const actionOlderThan7Days = useMemo( | |
| () => | |
| Maybe.of(action.actionData.createdAt) | |
| .map(dateOlderThen7Days) | |
| .orDefault(false), | |
| [action] | |
| ); | |
| const iconType = ICON_TYPE_MAP[direction]; | |
| const badgeLabel = useMemo( | |
| () => | |
| Maybe.fromNullable(action.actionData.status) | |
| .map((status) => { | |
| if (status === ActionStatus.WAITING_FOR_NEXT) { | |
| return actionOlderThan7Days | |
| ? ActionStatus.FAILED | |
| : t("activity.review.in_progress"); | |
| } | |
| return status; | |
| }) | |
| .map((status) => status.replaceAll("_", " ")) | |
| .map(capitalizeFirstLetters) | |
| .extract(), | |
| [action, t, actionOlderThan7Days] | |
| ); | |
| /** Deposits/rewards show the underlying input token, withdrawals the vault token. */ | |
| const tokenSymbol = useMemo(() => { | |
| const yieldToken = action.yieldData.token; | |
| const badgeColor = useMemo( | |
| () => | |
| Maybe.fromNullable(action.actionData.status) | |
| .map((status) => { | |
| if ( | |
| status === ActionStatus.WAITING_FOR_NEXT && | |
| actionOlderThan7Days | |
| ) { | |
| return ActionStatus.FAILED; | |
| } | |
| return status; | |
| }) | |
| .map((status) => BADGE_BG_MAP[status]) | |
| .extract(), | |
| [action, actionOlderThan7Days] | |
| ); | |
| if (direction === "withdraw" || direction === "other") { | |
| return yieldToken.symbol; | |
| } | |
| const inputToken = getActionInputToken({ | |
| actionDto: action.actionData, | |
| yieldDto: action.yieldData, | |
| }); | |
| return inputToken?.symbol ?? yieldToken.symbol; | |
| }, [action.actionData, action.yieldData, direction]); | |
| const amount = useMemo( | |
| () => | |
| Maybe.fromNullable(action.actionData.amount).map(defaultFormattedNumber), | |
| [action] | |
| [action.actionData.amount] | |
| ); | |
| const isPositive = direction === "deposit" || direction === "rewards"; | |
| const amountSign = useMemo(() => { | |
| if (amount.isNothing()) return ""; | |
| if (direction === "withdraw") return "-"; | |
| if (direction === "deposit" || direction === "rewards") return "+"; | |
| return ""; | |
| }, [amount, direction]); | |
| const title = useMemo(() => { | |
| switch (direction) { | |
| case "deposit": | |
| return t("activity.item.deposited", { token: tokenSymbol }); | |
| case "withdraw": | |
| return t("activity.item.withdrew", { token: tokenSymbol }); | |
| case "rewards": | |
| return t("activity.item.rewards"); | |
| default: | |
| return t("activity.item.generic", { | |
| action: capitalizeFirstLetters( | |
| action.actionData.type.replaceAll("_", " ") | |
| ), | |
| token: tokenSymbol, | |
| }); | |
| } | |
| }, [direction, t, tokenSymbol, action.actionData.type]); | |
| const { timestampAbsolute, timestampRelative } = useMemo(() => { | |
| const createdAt = action.actionData.createdAt; | |
| if (!createdAt) { | |
| return { timestampAbsolute: "", timestampRelative: "" }; | |
| } | |
| const date = new Date(createdAt); | |
| const dayKind = getActivityDayKind(date); | |
| const time = date.toLocaleTimeString(locale, { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| hour12: false, | |
| }); | |
| const absolute = | |
| dayKind === "today" | |
| ? `${t("activity.date_group_labels.today")} · ${time}` | |
| : dayKind === "yesterday" | |
| ? t("activity.date_group_labels.yesterday") | |
| : date.toLocaleDateString(locale, { | |
| day: "2-digit", | |
| month: "short", | |
| year: "numeric", | |
| }); | |
| const relativeParts = getActivityRelativeTime(date); | |
| const relative = | |
| relativeParts.unit === "now" | |
| ? t("activity.time.now") | |
| : relativeParts.unit === "minutes" | |
| ? t("activity.time.minutes_ago", { count: relativeParts.value }) | |
| : relativeParts.unit === "hours" | |
| ? t("activity.time.hours_ago", { count: relativeParts.value }) | |
| : t("activity.time.days_ago", { count: relativeParts.value }); | |
| return { timestampAbsolute: absolute, timestampRelative: relative }; | |
| }, [action.actionData.createdAt, locale, t]); | |
| const direction = getDirection(action.actionData.type); | |
| const iconType = ICON_TYPE_MAP[direction]; | |
| /** Deposits/rewards show the underlying input token, withdrawals the vault token. */ | |
| const tokenSymbol = (() => { | |
| const yieldToken = action.yieldData.token; | |
| if (direction === "withdraw" || direction === "other") { | |
| return yieldToken.symbol; | |
| } | |
| const inputToken = getActionInputToken({ | |
| actionDto: action.actionData, | |
| yieldDto: action.yieldData, | |
| }); | |
| return inputToken?.symbol ?? yieldToken.symbol; | |
| })(); | |
| const amount = Maybe.fromNullable(action.actionData.amount).map(defaultFormattedNumber); | |
| const isPositive = direction === "deposit" || direction === "rewards"; | |
| const amountSign = (() => { | |
| if (amount.isNothing()) return ""; | |
| if (direction === "withdraw") return "-"; | |
| if (direction === "deposit" || direction === "rewards") return "+"; | |
| return ""; | |
| })(); | |
| const title = (() => { | |
| switch (direction) { | |
| case "deposit": | |
| return t("activity.item.deposited", { token: tokenSymbol }); | |
| case "withdraw": | |
| return t("activity.item.withdrew", { token: tokenSymbol }); | |
| case "rewards": | |
| return t("activity.item.rewards"); | |
| default: | |
| return t("activity.item.generic", { | |
| action: capitalizeFirstLetters( | |
| action.actionData.type.replaceAll("_", " ") | |
| ), | |
| token: tokenSymbol, | |
| }); | |
| } | |
| })(); | |
| const { timestampAbsolute, timestampRelative } = (() => { | |
| const createdAt = action.actionData.createdAt; | |
| if (!createdAt) { | |
| return { timestampAbsolute: "", timestampRelative: "" }; | |
| } | |
| const date = new Date(createdAt); | |
| const dayKind = getActivityDayKind(date); | |
| const time = date.toLocaleTimeString(locale, { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| hour12: false, | |
| }); | |
| const absolute = | |
| dayKind === "today" | |
| ? `${t("activity.date_group_labels.today")} · ${time}` | |
| : dayKind === "yesterday" | |
| ? t("activity.date_group_labels.yesterday") | |
| : date.toLocaleDateString(locale, { | |
| day: "2-digit", | |
| month: "short", | |
| year: "numeric", | |
| }); | |
| const relativeParts = getActivityRelativeTime(date); | |
| const relative = | |
| relativeParts.unit === "now" | |
| ? t("activity.time.now") | |
| : relativeParts.unit === "minutes" | |
| ? t("activity.time.minutes_ago", { count: relativeParts.value }) | |
| : relativeParts.unit === "hours" | |
| ? t("activity.time.hours_ago", { count: relativeParts.value }) | |
| : t("activity.time.days_ago", { count: relativeParts.value }); | |
| return { timestampAbsolute: absolute, timestampRelative: relative }; | |
| })(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/widget/src/pages/details/activity-page/hooks/use-action-list-item.ts`
around lines 73 - 167, Remove unnecessary useMemo wrappers around pure
computations: replace useMemo calls for direction (getDirection), tokenSymbol
(logic using action.yieldData and getActionInputToken), amount
(Maybe.fromNullable(...).map(...)), amountSign, title (switch using t and
action.actionData.type), and the timestamp block producing
timestampAbsolute/timestampRelative with plain const declarations so these
values are recomputed each render; keep the same computation logic and
referenced helpers (getDirection, ICON_TYPE_MAP, getActionInputToken, Maybe,
defaultFormattedNumber, t, getActivityDayKind, getActivityRelativeTime,
capitalizeFirstLetters) and preserve existing dependency usage (action, locale,
t) but remove useMemo imports/usages accordingly.
Source: Coding guidelines
| const { maxYieldRatesByToken } = useTokenListYields( | ||
| data?.tokenBalances ?? [] | ||
| ); |
There was a problem hiding this comment.
Skip yield-query work when token selection is disabled.
When canSelect is false, the component returns the static token display, but useTokenListYields still computes query inputs and may trigger fetching. Pass an empty list when selection is disabled to avoid unnecessary API/query churn.
Suggested diff
- const { maxYieldRatesByToken } = useTokenListYields(
- data?.tokenBalances ?? []
- );
+ const { maxYieldRatesByToken } = useTokenListYields(
+ canSelect ? data?.tokenBalances ?? [] : []
+ );Also applies to: 61-81
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx`
around lines 55 - 57, When selection is disabled (canSelect === false) we should
avoid calling useTokenListYields with the real tokenBalances because it computes
query inputs and may trigger network work; update the calls to
useTokenListYields so they receive an empty array when canSelect is false (e.g.
replace data?.tokenBalances ?? [] with canSelect ? (data?.tokenBalances ?? []) :
[]) and make the same change for the second occurrence around the block
referenced by lines 61-81 so the hook only runs queries when selection is
enabled.
| export const selectorSummaryBadge = style({ | ||
| alignItems: "center", | ||
| background: "#EAF7EE", | ||
| borderRadius: vars.borderRadius.baseContract.full, | ||
| color: "#137333", | ||
| display: "inline-flex", | ||
| fontSize: vars.fontSize.sm, | ||
| fontWeight: vars.fontWeight.bold, | ||
| lineHeight: 1, | ||
| padding: "3px 8px", | ||
| whiteSpace: "nowrap", | ||
| }); | ||
|
|
||
| export const selectorSummaryActive = style({ | ||
| color: "#137333", | ||
| whiteSpace: "nowrap", |
There was a problem hiding this comment.
Avoid hardcoded badge colors in themed styles.
#EAF7EE and #137333 bypass the theme token system and variant overrides, so this badge can drift across theme packs and dark/light modes. Use semantic theme tokens (or add dedicated tokens) instead of fixed hex values.
Suggested diff
export const selectorSummaryBadge = style({
alignItems: "center",
- background: "`#EAF7EE`",
+ background: vars.color.backgroundMuted,
borderRadius: vars.borderRadius.baseContract.full,
- color: "`#137333`",
+ color: vars.color.text,
display: "inline-flex",
fontSize: vars.fontSize.sm,
fontWeight: vars.fontWeight.bold,
lineHeight: 1,
padding: "3px 8px",
whiteSpace: "nowrap",
});
export const selectorSummaryActive = style({
- color: "`#137333`",
+ color: vars.color.text,
whiteSpace: "nowrap",
});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/widget/src/pages/details/earn-page/styles.css.ts` around lines 125 -
140, The styles selectorSummaryBadge and selectorSummaryActive currently
hardcode colors (`#EAF7EE` and `#137333`); replace those hex values with semantic
theme tokens (e.g., use vars.colors.success.surface / vars.colors.success.fg or
the appropriate surface/foreground tokens from your design system) and, if
necessary, add dedicated tokens for badge background and text so theme packs and
dark mode variants pick them up; update both selectorSummaryBadge and
selectorSummaryActive to reference those vars tokens (and keep existing tokens
for borderRadius, fontSize, fontWeight), and ensure the chosen tokens provide
sufficient contrast in dark mode or add a dark-mode variant token.
| const finalTheme = useMemo(() => { | ||
| const baseTheme = merge(structuredClone(lightTheme), theme); | ||
|
|
||
| if (v === "finery") { | ||
| return fineryThemeOverrides; | ||
| } | ||
| const overrides = Just(variant) | ||
| .map((v) => { | ||
| if (v === "utila") { | ||
| return utilaThemeOverrides; | ||
| } | ||
|
|
||
| if (v === "porto") { | ||
| return portoThemeOverrides; | ||
| } | ||
| if (v === "finery") { | ||
| return getFineryThemeOverrides(baseTheme); | ||
| } | ||
|
|
||
| return {}; | ||
| }) | ||
| .map((overrides) => { | ||
| if ("lightMode" in theme) { | ||
| return merge( | ||
| structuredClone(lightTheme), | ||
| theme.lightMode, | ||
| overrides | ||
| ); | ||
| } | ||
| if (v === "porto") { | ||
| return portoThemeOverrides; | ||
| } | ||
|
|
||
| if (theme) { | ||
| return merge(structuredClone(lightTheme), theme, overrides); | ||
| } | ||
| return {}; | ||
| }) | ||
| .unsafeCoerce(); | ||
|
|
||
| return lightTheme; | ||
| }) | ||
| .unsafeCoerce(), | ||
| [theme, variant] | ||
| ); | ||
|
|
||
| const finalDarkTheme = useMemo( | ||
| () => | ||
| "darkMode" in theme | ||
| ? merge(structuredClone(darkTheme), theme.darkMode) | ||
| : null, | ||
| [theme] | ||
| ); | ||
| return merge(structuredClone(lightTheme), theme, overrides); | ||
| }, [theme, variant]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify dark theme removal is documented and check for remaining references
# Check for dark theme references in the codebase
rg -n 'darkTheme|dark.*theme|prefers-color-scheme' --type=ts --type=tsx -g '!node_modules'
# Look for theme documentation
fd -e md -x rg -l 'theme|dark.*mode' {}
# Check if PR description or changelog mentions this change
rg -i 'dark.*mode|theme.*breaking' README.md CHANGELOG.md || echo "No documentation found"Repository: stakekit/widget
Length of output: 236
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect theme-wrapper.tsx for any remaining light/dark or prefers-color-scheme logic
FILE="packages/widget/src/providers/theme-wrapper.tsx"
echo "=== $FILE (lines 1-220) ==="
nl -ba "$FILE" | sed -n '1,220p'
# 2) Search for dark-mode related strings across TS/TSX with correct globs
echo
echo "=== Search: dark mode / prefers-color-scheme across ts + tsx ==="
rg -n 'darkTheme|dark.*theme|dark.*mode|prefers-color-scheme' \
-g'*.ts' -g'*.tsx' -g'!**/node_modules/**' .
# 3) Search for documentation mentions in changelog/readme (include package changelog)
echo
echo "=== Search docs/changelog for dark mode / breaking changes ==="
rg -i 'dark.*mode|theme.*breaking|prefers-color-scheme' \
-g'!**/node_modules/**' \
README.md packages/widget/CHANGELOG.md 2>/dev/null || echo "No matching docs found"
# 4) If matches exist, show surrounding context lines
echo
echo "=== Context for any doc matches (if any) ==="
for f in README.md packages/widget/CHANGELOG.md; do
[ -f "$f" ] || continue
rg -n -i 'dark.*mode|theme.*breaking|prefers-color-scheme' "$f" || true
doneRepository: stakekit/widget
Length of output: 190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/widget/src/providers/theme-wrapper.tsx"
echo "=== Theme wrapper snippet ($FILE) ==="
if [ -f "$FILE" ]; then
# show only the relevant range to avoid large output
sed -n '1,220p' "$FILE" | cat -n | sed -n '1,160p'
else
echo "Missing: $FILE" >&2
fi
echo
echo "=== Search: dark-mode / prefers-color-scheme references in ts/tsx ==="
rg -n 'darkTheme|dark.*theme|dark.*mode|prefers-color-scheme' \
-g'*.ts' -g'*.tsx' -g'!**/node_modules/**' .
echo
echo "=== Docs/changelog mentions (only existing files) ==="
for f in README.md packages/widget/CHANGELOG.md; do
if [ -f "$f" ]; then
echo "--- $f ---"
rg -n -i 'dark.*mode|theme.*breaking|prefers-color-scheme' "$f" || true
fi
doneRepository: stakekit/widget
Length of output: 4101
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect settings hook and theme entrypoints
for f in \
packages/widget/src/providers/settings.ts \
packages/widget/src/providers/theme-wrapper.tsx \
packages/widget/src/styles/theme/themes.ts \
packages/widget/src/main.tsx \
packages/widget/src/styles/tokens/connect-kit.ts; do
echo
echo "=== File: $f ==="
if [ -f "$f" ]; then
sed -n '1,200p' "$f" | cat -n | sed -n '1,200p'
else
echo "Missing $f" >&2
fi
done
echo
echo "=== Search: prefers-color-scheme in repo (broader) ==="
rg -n 'prefers-color-scheme' . || true
echo
echo "=== Search: dark mode selection logic (themeVariant, media query, etc.) ==="
rg -n 'themeVariant|prefers|dark\s*mode|useMediaQuery|matchMedia' \
-g'*.ts' -g'*.tsx' . || trueRepository: stakekit/widget
Length of output: 7387
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Locate useSettings implementation (packages/widget/src/providers) ==="
fd -t f -e ts -e tsx -e js -e jsx -e mjs -e cjs "useSettings" packages/widget/src/providers || true
rg -n "export const useSettings|function useSettings|useSettings\s*\(" packages/widget/src/providers packages/widget/src -g'*.ts' -g'*.tsx' || true
echo
echo "=== List files matching settings module under providers ==="
fd -t f -e ts -e tsx -e js -e jsx -e mjs -e cjs "settings" packages/widget/src/providers || true
echo
echo "=== Search for matchMedia/prefers elsewhere ==="
rg -n '(matchMedia|prefers-color-scheme|prefersDark|prefers-dark)' packages/widget/src -g'*.ts' -g'*.tsx' || trueRepository: stakekit/widget
Length of output: 8769
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect useSettings implementation
SETTINGS_FILE="packages/widget/src/providers/settings/index.tsx"
echo "=== $SETTINGS_FILE ==="
sed -n '1,260p' "$SETTINGS_FILE" | cat -n
echo
echo "=== Repo-wide: dark mode/media-query related tokens ==="
rg -n '(prefers-color-scheme|matchMedia|prefersDark|prefers-dark|darkMode|dark theme|dark mode)' \
-g'*.ts' -g'*.tsx' -g'*.md' . || true
echo
echo "=== Docs: any 'dark' mentions (broader) ==="
for f in README.md packages/widget/CHANGELOG.md; do
if [ -f "$f" ]; then
echo "--- $f ---"
rg -n -i '\bdark\b' "$f" || true
fi
doneRepository: stakekit/widget
Length of output: 3463
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect App and where theme prop defaulting/usage happens
for f in \
packages/widget/src/App.tsx \
packages/widget/src/App/index.tsx \
packages/widget/src/App/ * 2>/dev/null; do
true
done
if [ -f "packages/widget/src/App.tsx" ]; then
sed -n '1,240p' packages/widget/src/App.tsx | cat -n
fi
echo
echo "=== Search for `theme:` defaults / prefers-dark in App area ==="
rg -n 'theme\s*[:=]|prefers|matchMedia|dark\s*mode|darkTheme' \
packages/widget/src -g'*.ts' -g'*.tsx' || trueRepository: stakekit/widget
Length of output: 121
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect App and any index/entrypoints
if [ -f "packages/widget/src/App.tsx" ]; then
echo "=== packages/widget/src/App.tsx ==="
sed -n '1,260p' packages/widget/src/App.tsx | cat -n
else
echo "Missing: packages/widget/src/App.tsx" >&2
fi
echo
echo "=== Search in packages/widget/src/App* and related for theme defaults/auto switching ==="
rg -n 'theme\s*[:=]|\bthemeVariant\b|prefers-color-scheme|matchMedia|dark\s*mode|darkTheme|rootClassName|rootSelector' \
packages/widget/src/App packages/widget/src -g'*.ts' -g'*.tsx' || trueRepository: stakekit/widget
Length of output: 7922
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect rainbow-kit provider usage of finalTheme
FILE="packages/widget/src/providers/rainbow-kit.tsx"
echo "=== $FILE (relevant range) ==="
if [ -f "$FILE" ]; then
sed -n '1,200p' "$FILE" | cat -n | sed -n '60,140p'
else
echo "Missing $FILE" >&2
fi
# Inspect theme global css generation for any media queries
for f in \
packages/widget/src/styles/theme/global.css.ts \
packages/widget/src/styles/theme/*css.ts \
packages/widget/src/styles/theme/*css.js 2>/dev/null; do
if [ -f "$f" ]; then
echo
echo "=== $f: search for media queries ==="
rg -n '`@media`|prefers-color-scheme' "$f" || true
fi
done
# Check global css output files (if any) for prefers-color-scheme
rg -n 'prefers-color-scheme' packages/widget/src/styles -g'*.css*' -g'*.ts' -g'*.tsx' || trueRepository: stakekit/widget
Length of output: 1860
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for '`@media`' and 'prefers-color-scheme' under packages/widget/src/styles/theme ==="
rg -n '`@media`|prefers-color-scheme' packages/widget/src/styles/theme \
-g'*.ts' -g'*.tsx' -g'*.css' -g'*.css.ts' -g'*.css.js' || true
echo
echo "=== Search for 'prefers-color-scheme' under packages/widget/src/styles (broader) ==="
rg -n 'prefers-color-scheme' packages/widget/src/styles \
-g'*.ts' -g'*.tsx' -g'*.css*' || trueRepository: stakekit/widget
Length of output: 443
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for theme prop definition/default in widget settings types ==="
rg -n 'theme\??\s*:|theme:\s*lightTheme|darkTheme|lightTheme' \
packages/widget/src/providers/settings -g'*.ts' -g'*.tsx'
echo
echo "=== Inspect settings props types ==="
for f in packages/widget/src/providers/settings/types.ts packages/widget/src/providers/settings/types.tsx packages/widget/src/providers/settings/*.ts packages/widget/src/providers/settings/*.tsx; do
[ -f "$f" ] || continue
echo "--- $f ---"
sed -n '1,240p' "$f" | cat -n
done
echo
echo "=== Search where SettingsContextProvider is given theme (from props) ==="
rg -n 'SettingsContextProvider|theme\s*=' packages/widget/src -g'*.ts' -g'*.tsx'Repository: stakekit/widget
Length of output: 11551
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate ThemeWrapper usage
echo "=== Search for ThemeWrapper usage ==="
rg -n 'ThemeWrapper' packages/widget/src -g'*.ts' -g'*.tsx'
echo
echo "=== Inspect ThemeWrapper theme type ==="
FILE="packages/widget/src/providers/theme-wrapper-types.ts"
if [ -f "$FILE" ]; then
sed -n '1,200p' "$FILE" | cat -n
else
echo "Missing $FILE" >&2
fi
echo
echo "=== Inspect providers wiring (where ThemeWrapper is mounted) ==="
FILE2="packages/widget/src/providers/index.tsx"
if [ -f "$FILE2" ]; then
sed -n '1,220p' "$FILE2" | cat -n
else
echo "Missing $FILE2" >&2
fi
echo
echo "=== Inspect ThemeWrapper component itself (full file if small) ==="
FILE3="packages/widget/src/providers/theme-wrapper.tsx"
if [ -f "$FILE3" ]; then
wc -l "$FILE3"
sed -n '1,220p' "$FILE3" | cat -n
fi
echo
echo "=== Search for default theme assignment in providers/App ==="
rg -n 'theme:\s*|theme\s*=' packages/widget/src/providers packages/widget/src -g'*.ts' -g'*.tsx' || trueRepository: stakekit/widget
Length of output: 10270
Dark mode isn’t removed—system auto-switching (e.g. prefers-color-scheme) appears to be the part that’s gone; document if intentional.
In packages/widget/src/providers/theme-wrapper.tsx (lines 17-39), finalTheme is always built from lightTheme merged with the theme coming from useSettings(); darkTheme is still exported and can be passed via SettingsProps["theme"] (type + usage wiring exists). There’s no prefers-color-scheme/matchMedia logic in the repo, so if the PR intentionally removed automatic dark-mode selection based on the user’s system theme, document that breaking behavior in the README/widget CHANGELOG so consumers know they must pass darkTheme explicitly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/widget/src/providers/theme-wrapper.tsx` around lines 17 - 39,
finalTheme is always derived from lightTheme (via
merge(structuredClone(lightTheme), theme)) and automatic system dark-mode
selection (prefers-color-scheme / matchMedia) appears removed; update the README
and widget CHANGELOG to state this breaking change and that consumers must now
pass a dark theme explicitly via SettingsProps["theme"] (hint: show an example
using useSettings() or passing darkTheme), and clearly document that finalTheme
no longer auto-switches and how to re-enable system-based switching if desired
(e.g., reintroduce matchMedia logic around finalTheme).
| "no_previous_activity": "Aucune activité précédente", | ||
| "item": { | ||
| "deposited": "Dépôt de {{token}}", | ||
| "withdrew": "Retrait de {{token}}", | ||
| "rewards": "Récompenses", | ||
| "generic": "{{action}} {{token}}" | ||
| }, | ||
| "time": { | ||
| "now": "à l'instant", | ||
| "minutes_ago": "il y a {{count}} min", | ||
| "hours_ago": "il y a {{count}} h", | ||
| "days_ago": "il y a {{count}} j" | ||
| }, | ||
| "failed": "Échec", | ||
| "showing_count": "Affichage de {{showing}} sur {{total}}", | ||
| "filters": { | ||
| "all": "Tout", | ||
| "stake": "Stake", | ||
| "defi": "DeFi", | ||
| "rwa": "RWA", | ||
| "borrow": "Emprunt" | ||
| } | ||
| }, | ||
| "details": { | ||
| "earn": "Earn", | ||
| "estimated_reward": "Récompenses estimées", | ||
| "earn_with": "Earn avec", | ||
| "restake_with": "Restake avec", | ||
| "yield_strategy": "Stratégie de rendement", | ||
| "via": "via {{providerName}}", | ||
| "additional_info": "Infos supplémentaires", | ||
| "kyc_gate": { | ||
| "check_status": "Vérifier le statut", | ||
| "modal": { | ||
| "title": "Terminer la vérification", | ||
| "close": "Fermer la vérification", | ||
| "iframe_title": "Vérification KYC" | ||
| }, | ||
| "checking": { | ||
| "title": "Confirmation de votre statut de vérification...", | ||
| "body": "Presque terminé. Nous revérifions votre statut {{providerName}}. Cela prend généralement quelques secondes.", | ||
| "body_fallback": "Presque terminé. Nous revérifions votre statut de vérification. Cela prend généralement quelques secondes." | ||
| }, | ||
| "start_kyc": { | ||
| "title": "Vérification d'identité requise", | ||
| "body": "{{providerName}} exige une vérification d'identité unique avant que vous puissiez détenir ce RWA. La vérification est hébergée par {{providerName}}. Yield.xyz ne voit jamais vos données.", | ||
| "body_fallback": "Ce RWA exige une vérification d'identité unique avant que vous puissiez le détenir. La vérification est hébergée par le fournisseur. Yield.xyz ne voit jamais vos données.", | ||
| "cta": "Vérifier votre identité" | ||
| }, | ||
| "pending": { | ||
| "title": "Votre vérification est en cours d'examen", | ||
| "body": "{{providerName}} examine votre soumission. Cela prend généralement quelques minutes, mais peut prendre jusqu'à 24 heures.", | ||
| "body_fallback": "Votre vérification est en cours d'examen. Cela prend généralement quelques minutes, mais peut prendre jusqu'à 24 heures." | ||
| }, | ||
| "rejected": { | ||
| "title": "Vérification non approuvée", | ||
| "body": "{{providerName}} n'a pas pu approuver ce portefeuille pour ce rendement. Contactez le support {{providerName}} pour les prochaines étapes.", | ||
| "body_fallback": "Ce portefeuille n'est pas approuvé pour ce rendement. Contactez le fournisseur pour les prochaines étapes.", | ||
| "cta": "Ouvrir le fournisseur" | ||
| }, | ||
| "unknown": { | ||
| "title": "Statut de vérification indisponible", | ||
| "body": "Nous n'avons pas pu confirmer votre statut de vérification {{providerName}}. Vérifiez à nouveau avant de continuer.", | ||
| "body_fallback": "Nous n'avons pas pu confirmer votre statut de vérification. Vérifiez à nouveau avant de continuer." | ||
| } | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that English translations contain the same new keys as French
# Extract sample of new keys from French file
french_keys=$(cat packages/widget/src/translation/French/translations.json | jq -r '
[
.activity.no_previous_activity,
.activity.filters.all,
.details.kyc_gate.check_status,
.details.opportunity_item_subtitle_no_network,
.yield_types.fixed_yield.title,
.yield_types.real_world_asset.title,
.dashboard.details.earn_details.apy,
.dashboard.position_details.action_tabs.stake,
.select_token.up_to_rate_type
] | `@json`
')
echo "Sample French keys found:"
echo "$french_keys" | jq .
# Check if these keys exist in English
echo -e "\nChecking English translations..."
english_keys=$(cat packages/widget/src/translation/English/translations.json | jq -r '
[
.activity.no_previous_activity,
.activity.filters.all,
.details.kyc_gate.check_status,
.details.opportunity_item_subtitle_no_network,
.yield_types.fixed_yield.title,
.yield_types.real_world_asset.title,
.dashboard.details.earn_details.apy,
.dashboard.position_details.action_tabs.stake,
.select_token.up_to_rate_type
] | `@json`
')
echo "Sample English keys found:"
echo "$english_keys" | jq .
# Count total keys in each file
echo -e "\nKey counts:"
echo "French total keys: $(cat packages/widget/src/translation/French/translations.json | jq -r 'paths(scalars) | length')"
echo "English total keys: $(cat packages/widget/src/translation/English/translations.json | jq -r 'paths(scalars) | length')"Repository: stakekit/widget
Length of output: 3256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
if [ ! -f "$FR" ] || [ ! -f "$EN" ]; then
echo "Missing translation file(s)."
exit 1
fi
# Flatten JSON into dot-paths for string values only
# Example: activity.no_previous_activity
flatten_jq='
def paths_strings:
paths(scalars)
| select(getpath(.) | type == "string")
| join(".") ;
paths_strings
'
echo "Computing missing/extra keys (string translation paths)..."
# Missing keys in EN that exist in FR
missing_in_en=$(jq -r "$flatten_jq" "$FR" | sort -u | comm -23 - <(jq -r "$flatten_jq" "$EN" | sort -u))
missing_count=$(printf "%s\n" "$missing_in_en" | sed '/^$/d' | wc -l)
# Extra keys in EN not present in FR
extra_in_en=$(jq -r "$flatten_jq" "$EN" | sort -u | comm -23 - <(jq -r "$flatten_jq" "$FR" | sort -u))
extra_count=$(printf "%s\n" "$extra_in_en" | sed '/^$/d' | wc -l)
# A small sample for context
missing_sample=$(printf "%s\n" "$missing_in_en" | sed '/^$/d' | head -n 30)
extra_sample=$(printf "%s\n" "$extra_in_en" | sed '/^$/d' | head -n 30)
echo "FR string-key count: $(jq -r "$flatten_jq" "$FR" | sort -u | wc -l | tr -d ' ')"
echo "EN string-key count: $(jq -r "$flatten_jq" "$EN" | sort -u | wc -l | tr -d ' ')"
echo
echo "Missing in EN: $missing_count"
if [ "$missing_count" -gt 0 ]; then
echo "Missing sample:"
echo "$missing_sample"
fi
echo
echo "Extra in EN: $extra_count"
if [ "$extra_count" -gt 0 ]; then
echo "Extra sample:"
echo "$extra_sample"
fi
# Focused checks for the areas mentioned in the review
for prefix in \
"activity." \
"details.kyc_gate." \
"details.opportunity_item_subtitle_no_network" \
"yield_types." \
"dashboard.details." \
"dashboard.position_details." \
"select_token."
do
fr_pref=$(jq -r "$flatten_jq" "$FR" | sort -u | rg "^${prefix//./\\.}" || true)
en_pref=$(jq -r "$flatten_jq" "$EN" | sort -u | rg "^${prefix//./\\.}" || true)
missing_pref=$(printf "%s\n" "$fr_pref" | sort -u | comm -23 - <(printf "%s\n" "$en_pref" | sort -u))
missing_pref_count=$(printf "%s\n" "$missing_pref" | sed '/^$/d' | wc -l)
echo
echo "Missing in EN under prefix '$prefix': $missing_pref_count"
if [ "$missing_pref_count" -gt 0 ]; then
printf "%s\n" "$missing_pref" | sed '/^$/d' | head -n 20
fi
doneRepository: stakekit/widget
Length of output: 415
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
echo "=== French around line 892 ==="
nl -ba "$FR" | sed -n '880,910p'
echo
echo "=== English around line 893 ==="
nl -ba "$EN" | sed -n '880,910p'Repository: stakekit/widget
Length of output: 151
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
echo "=== French around line 892 (raw) ==="
sed -n '885,905p' "$FR"
echo
echo "=== English around line 893 (raw) ==="
sed -n '885,905p' "$EN"
echo
echo "=== Testing robust flattener (string value paths) ==="
# New approach: emit paths to values whose jq type is "string" without re-indexing via getpath.
# Then normalize path segments to a stable dot form.
flatten_strings='
paths(type=="string")
| map(tostring)
| join(".")
'
fr_total=$(jq -r "$flatten_strings" "$FR" | sort -u | wc -l)
en_total=$(jq -r "$flatten_strings" "$EN" | sort -u | wc -l)
echo "FR unique string-key paths: $fr_total"
echo "EN unique string-key paths: $en_total"
missing_in_en=$(jq -r "$flatten_strings" "$FR" | sort -u | comm -23 - <(jq -r "$flatten_strings" "$EN" | sort -u))
missing_count=$(printf "%s\n" "$missing_in_en" | sed '/^$/d' | wc -l)
extra_in_en=$(jq -r "$flatten_strings" "$EN" | sort -u | comm -23 - <(jq -r "$flatten_strings" "$FR" | sort -u))
extra_count=$(printf "%s\n" "$extra_in_en" | sed '/^$/d' | wc -l)
echo "Missing in EN: $missing_count"
echo "Extra in EN: $extra_count"
echo
echo "Missing sample (first 30):"
printf "%s\n" "$missing_in_en" | head -n 30
echo
echo "Extra sample (first 30):"
printf "%s\n" "$extra_in_en" | head -n 30
# Focused areas from the original review comment
echo
echo "=== Focused prefix check ==="
for prefix in \
"activity." \
"details.kyc_gate." \
"details.opportunity_item_subtitle_no_network" \
"yield_types." \
"dashboard.details." \
"dashboard.position_details." \
"select_token."
do
fr_pref=$(jq -r "$flatten_strings" "$FR" | rg "^${prefix}" || true)
en_pref=$(jq -r "$flatten_strings" "$EN" | rg "^${prefix}" || true)
missing_pref=$(printf "%s\n" "$fr_pref" | sort -u | comm -23 - <(printf "%s\n" "$en_pref" | sort -u))
missing_pref_count=$(printf "%s\n" "$missing_pref" | sed '/^$/d' | wc -l)
extra_pref=$(printf "%s\n" "$en_pref" | sort -u | comm -23 - <(printf "%s\n" "$fr_pref" | sort -u))
extra_pref_count=$(printf "%s\n" "$extra_pref" | sed '/^$/d' | wc -l)
echo "- $prefix missing_in_en=$missing_pref_count extra_in_en=$extra_pref_count"
if [ "$missing_pref_count" -gt 0 ]; then
echo " missing sample:"
printf "%s\n" "$missing_pref" | head -n 10
fi
if [ "$extra_pref_count" -gt 0 ]; then
echo " extra sample:"
printf "%s\n" "$extra_pref" | head -n 10
fi
doneRepository: stakekit/widget
Length of output: 1398
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
python3 - <<'PY'
import json, subprocess, shlex, sys, os
FR=os.environ["FR"]; EN=os.environ["EN"]
flatten_strings = 'paths(type=="string") | map(tostring) | join(".")'
def jq_paths(path):
cmd = ["jq","-r", flatten_strings, path]
out = subprocess.check_output(cmd, text=True)
s=set([line.strip() for line in out.splitlines() if line.strip()])
return s
fr_paths = jq_paths(FR)
en_paths = jq_paths(EN)
print(f"FR unique string-key paths: {len(fr_paths)}")
print(f"EN unique string-key paths: {len(en_paths)}")
missing_in_en = sorted(fr_paths - en_paths)
extra_in_en = sorted(en_paths - fr_paths)
print(f"Missing in EN (present in FR, absent in EN): {len(missing_in_en)}")
print(f"Extra in EN (present in EN, absent in FR): {len(extra_in_en)}")
print("\nMissing sample (up to 40):")
for k in missing_in_en[:40]:
print(k)
print("\nExtra sample (up to 40):")
for k in extra_in_en[:40]:
print(k)
prefixes = [
"activity.",
"details.kyc_gate.",
"details.opportunity_item_subtitle_no_network",
"yield_types.",
"dashboard.details.",
"dashboard.position_details.",
"select_token."
]
print("\n=== Prefix-focused diffs (FR → EN) ===")
for p in prefixes:
fr_pref = {k for k in fr_paths if k.startswith(p)}
en_pref = {k for k in en_paths if k.startswith(p)}
miss = sorted(fr_pref - en_pref)
extra = sorted(en_pref - fr_pref)
print(f"{p}\n missing_in_en: {len(miss)} extra_in_en: {len(extra)}")
if miss:
for k in miss[:10]:
print(f" missing: {k}")
if extra:
for k in extra[:10]:
print(f" extra: {k}")
print()
PYRepository: stakekit/widget
Length of output: 194
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
python3 - <<'PY'
import os, subprocess
FR=os.environ.get("FR_PATH")
EN=os.environ.get("EN_PATH")
if not FR or not EN:
raise SystemExit("Missing env vars FR_PATH/EN_PATH")
flatten_strings = 'paths(type=="string") | map(tostring) | join(".")'
def jq_paths(path):
out = subprocess.check_output(["jq","-r", flatten_strings, path], text=True)
return {line.strip() for line in out.splitlines() if line.strip()}
fr_paths = jq_paths(FR)
en_paths = jq_paths(EN)
print(f"FR unique string-key paths: {len(fr_paths)}")
print(f"EN unique string-key paths: {len(en_paths)}")
missing_in_en = sorted(fr_paths - en_paths)
extra_in_en = sorted(en_paths - fr_paths)
print(f"Missing in EN (in FR, absent in EN): {len(missing_in_en)}")
print(f"Extra in EN (in EN, absent in FR): {len(extra_in_en)}")
print("\nMissing sample (first 40):")
for k in missing_in_en[:40]:
print(k)
print("\nExtra sample (first 40):")
for k in extra_in_en[:40]:
print(k)
prefixes = [
"activity.",
"details.kyc_gate.",
"details.opportunity_item_subtitle_no_network",
"yield_types.",
"dashboard.details.",
"dashboard.position_details.",
"select_token."
]
print("\n=== Prefix-focused diffs (FR → EN) ===")
for p in prefixes:
fr_pref = {k for k in fr_paths if k.startswith(p)}
en_pref = {k for k in en_paths if k.startswith(p)}
miss = sorted(fr_pref - en_pref)
extra = sorted(en_pref - fr_pref)
print(f"{p}\n missing_in_en: {len(miss)} extra_in_en: {len(extra)}")
if miss:
for k in miss[:10]:
print(f" missing: {k}")
if extra:
for k in extra[:10]:
print(f" extra: {k}")
print()
PYRepository: stakekit/widget
Length of output: 92
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
python3 - <<PY
import subprocess
FR_PATH = "$FR"
EN_PATH = "$EN"
flatten_strings = 'paths(type=="string") | map(tostring) | join(".")'
def jq_paths(path):
out = subprocess.check_output(["jq","-r", flatten_strings, path], text=True)
return {line.strip() for line in out.splitlines() if line.strip()}
fr_paths = jq_paths(FR_PATH)
en_paths = jq_paths(EN_PATH)
print(f"FR unique string-key paths: {len(fr_paths)}")
print(f"EN unique string-key paths: {len(en_paths)}")
missing_in_en = sorted(fr_paths - en_paths)
extra_in_en = sorted(en_paths - fr_paths)
print(f"Missing in EN (in FR, absent in EN): {len(missing_in_en)}")
print(f"Extra in EN (in EN, absent in FR): {len(extra_in_en)}")
print("\\nMissing sample (first 40):")
for k in missing_in_en[:40]:
print(k)
print("\\nExtra sample (first 40):")
for k in extra_in_en[:40]:
print(k)
prefixes = [
"activity.",
"details.kyc_gate.",
"details.opportunity_item_subtitle_no_network",
"yield_types.",
"dashboard.details.",
"dashboard.position_details.",
"select_token."
]
print("\\n=== Prefix-focused diffs (FR → EN) ===")
for p in prefixes:
fr_pref = {k for k in fr_paths if k.startswith(p)}
en_pref = {k for k in en_paths if k.startswith(p)}
miss = sorted(fr_pref - en_pref)
extra = sorted(en_pref - fr_pref)
print(f"{p}\\n missing_in_en: {len(miss)} extra_in_en: {len(extra)}")
if miss:
for k in miss[:10]:
print(f" missing: {k}")
if extra:
for k in extra[:10]:
print(f" extra: {k}")
print()
PYRepository: stakekit/widget
Length of output: 973
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
echo "Line numbers (French) for EN-missing keys:"
rg -n --fixed-strings \
"dashboard.summary_item.rewards_summary_current_enabled_tooltip" \
"$FR" || true
rg -n --fixed-strings \
"dashboard.summary_item.rewards_summary_zero_tooltip" \
"$FR" || true
echo
echo "Check whether those keys exist in English:"
rg -n --fixed-strings \
"dashboard.summary_item.rewards_summary_current_enabled_tooltip" \
"$EN" || true
rg -n --fixed-strings \
"dashboard.summary_item.rewards_summary_zero_tooltip" \
"$EN" || true
echo
echo "Line numbers (French) for EN-allowed extra keys (to see if they’re part of different sections):"
for k in \
"dashboard.overview.summary.average_apy" \
"details.lend.description_with_fee_configuration" \
"details.vault.description_with_fee_configuration" \
"review.staking_fee" \
"shared.fee"
do
rg -n --fixed-strings "$k" "$FR" || true
doneRepository: stakekit/widget
Length of output: 243
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FR="packages/widget/src/translation/French/translations.json"
EN="packages/widget/src/translation/English/translations.json"
# 1) Find occurrences of the leaf key names
leaf_keys=(
"rewards_summary_current_enabled_tooltip"
"rewards_summary_zero_tooltip"
)
echo "=== Leaf key locations in French ==="
for k in "${leaf_keys[@]}"; do
echo "--- $k ---"
rg -n --fixed-strings "$k" "$FR" || true
done
echo
echo "=== Leaf key locations in English ==="
for k in "${leaf_keys[@]}"; do
echo "--- $k ---"
rg -n --fixed-strings "$k" "$EN" || true
done
echo
echo "=== French line ranges from original review comment ==="
echo "--- French 440-469 ---"
sed -n '440,469p' "$FR"
echo
echo "--- French 515-539 ---"
sed -n '515,539p' "$FR"
echo
echo "--- French 665-751 ---"
sed -n '665,751p' "$FR"Repository: stakekit/widget
Length of output: 5089
Fix English/French translation mismatch (missing dashboard summary tooltips)
packages/widget/src/translation/English/translations.json is missing these string keys that exist in packages/widget/src/translation/French/translations.json:
dashboard.summary_item.rewards_summary_current_enabled_tooltipdashboard.summary_item.rewards_summary_zero_tooltip
Other keys under the originally referenced prefixes (activity, details.kyc_gate, details.opportunity_item_subtitle_no_network, yield_types, dashboard.details, dashboard.position_details, select_token) match between EN and FR.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/widget/src/translation/French/translations.json` around lines 48 -
113, The English translations file is missing two keys present in French: add
"dashboard.summary_item.rewards_summary_current_enabled_tooltip" and
"dashboard.summary_item.rewards_summary_zero_tooltip" to
packages/widget/src/translation/English/translations.json with appropriate
English copy matching the intent of the French entries; ensure the keys are
placed under the dashboard.summary_item object (matching the structure used in
French) so lookup for rewards_summary_current_enabled_tooltip and
rewards_summary_zero_tooltip succeeds at runtime.
Source: Coding guidelines
Summary by CodeRabbit
New Features
Improvements
Documentation
Adds the next dashboard v2 iteration for earn, manage, activity, and position details flows.
This introduces a richer dashboard earn experience with yield category tabs, provider/validator selection updates, yield detail panels, reward-rate and TVL history charts, and max yield-rate metadata on token selection. It also adds a dedicated dashboard position details model and UI, including stake/unstake/pending action routing from position details.
The branch also adds RWA KYC gating for eligible yields, including wallet-specific KYC status checks, start/pending/rejected/unknown states, iframe or external verification handling, and status refresh behavior.
Notable Changes
useYieldKycGateAPI integration.