You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Type: Task Links back to:#127 (parent [Feature] issue — body unchanged)
One reusable GoodWidget (public-page + GoodWallet V2 embed) that presents the Superfluid Ecosystem Rewards campaign: header/description, two reward-pool sections (GoodDollar actions, Ecosystem funding actions), a public leaderboard, and an FAQ accordion. Wallet connection is optional and only reveals the connected user's own rank/points/activity state. This phase is UI + responsive layout only, against mock data — no points logic or live API integration (tracked as follow-up, out of scope here per #127).
Reference files mapped
GoodDollar/GoodWidget (this repo):
packages/ui/src/config.ts:222-246 — Tamagui breakpoint config (media: { sm: maxWidth 480, md: maxWidth 768, gtSm: minWidth 480, gtMd: minWidth 768 }), the mechanism this plan's responsive approach builds on.
packages/ai-credits-widget/src/components/manage/CreditsManagementCard.tsx:28-48 — $gtSm grid-column-count pattern (45% width columns below gtSm, 28% above), direct precedent for the pool action-card grid.
packages/citizen-claim-widget/src/CitizenClaimWidget.tsx:533 — activeTab is currently internal useState, hardcoded to 'claim', no initialTab prop exists today. This plan adds one (see Execution plan) rather than working around its absence.
packages/citizen-claim-widget/src/widgetRuntimeContract.ts — existing prop/type contract shape to extend.
packages/embed/src/createMiniAppElement.tsx — shared web-component bridge (shadow DOM, theme override CSS vars, event bridging) used by every widget's element.ts; the new widget reuses this unchanged.
packages/core/src/hooks.ts (useWallet, useHost, useGoodWidget), packages/core/src/provider.tsx (GoodWidgetProvider) — wallet connection state and host detection, reused as-is; no new wallet logic needed.
packages/ui/src/index.ts — existing primitives to reuse (Card, Heading, Text, Button, Separator, WidgetTabs, Icon, Drawer/Dialog). No Accordion, ProgressBar, Table, or Avatar component exists yet — these are new (see New components below).
1Hive/gardens-v2 (behavior reference only, per #127 — visual design is the approved mockups, not this repo):
apps/web/app/(app)/gardens/campaigns/[campaignId]/client-page.tsx — optional-wallet-connection gating pattern: connected-only "Your Position" block rendered conditionally on useAccount()/points existing, disconnected users see the same table with no personal section.
apps/web/components/SuperfluidLeaderboard.tsx — leaderboard-modal structure: a distinct "Your Position" summary block above the table (not an inline highlight), scrollable table body, search-by-address/ENS filtering, no pagination.
apps/web/types/index.ts (fetchSuperfluidLeaderboard) — single fetch returns the full ranked list pre-sorted by points; connected user's rank is derived client-side via array index rather than a second API call. Since this phase uses mock data only, the plan adopts the same shape (one mock dataset, rank derived from array position) so swapping in the real API later is a drop-in.
Note: gardens-v2 does not highlight the user's row inline within the table — only the separate summary block. The approved mockup does both (summary block and an inline highlighted row). This plan follows the mockup, since [Feature]: Add Superfluid Campaign GoodWidget #127 states gardens is behavior reference only and the mockups are the design source of truth.
@goodwidget/embed — createMiniAppElement for the web-component bridge.
@goodwidget/citizen-claim-widget — embedded directly for the Claim and Invite flows (not rebuilt), via a new initialTab prop (see Execution plan).
New components mapped (package placement)
New widget package: packages/superfluid-campaign-widget (@goodwidget/superfluid-campaign-widget), following the streaming-widget decomposition:
packages/superfluid-campaign-widget/src/
index.ts # public exports (component + types)
element.ts / register.ts # web-component bridge (mirrors streaming-widget)
widgetRuntimeContract.ts # SuperfluidCampaignWidgetProps, mock data shape, activity types
SuperfluidCampaignWidget.tsx # thin contract: provider wiring + tab/view routing (content vs leaderboard)
components/
CampaignHeader.tsx # title, description, SUP-allocated/ends-date pills, background art
RewardPoolSection.tsx # one of the two pools: heading, participant count, progress bar, action cards
ActionCard.tsx # single eligible action row (icon, title, source, copy, points, CTA)
LeaderboardSummary.tsx # collapsed "top contributors" + progress bar shown on the content page
LeaderboardView.tsx # full leaderboard: search, table, your-position block, pagination controls
LeaderboardRow.tsx # one leaderboard row, incl. highlighted-row state for the connected user
ActivityIcons.tsx # renders the six done/not-done activity icons from the exact mapping in #127
FaqAccordion.tsx # FAQ accordion using the new shared Accordion primitive
ConnectWalletPrompt.tsx # disconnected-state CTA
New shared primitives — built in packages/ui (reusable beyond this widget, not campaign-specific):
Accordion (packages/ui/src/components/Accordion.tsx) — expandable item list; FAQ is the first consumer but this is generic UI, belongs in packages/ui per the repo's existing "reusable in packages/ui vs widget-specific" split.
ProgressBar (packages/ui/src/components/ProgressBar.tsx) — labeled linear progress bar (SUP-allocated and per-pool progress all reuse the same primitive); generic enough for any widget with a completion/allocation metric.
Kept inside the widget package (not packages/ui) because they're specific to this campaign's data shape, not generic UI:
ActionCard/RewardPoolSection — tied to the GoodDollar-actions/Ecosystem-funding-actions pool structure specific to this campaign.
citizen-claim-widget change (small, contract-level, not a new component): add initialTab?: 'claim' | 'invite-rewards' to CitizenClaimWidgetProps in widgetRuntimeContract.ts, threaded into the existing activeTabuseState initializer in CitizenClaimWidget.tsx:533. This is the only change to an existing package.
Required states, flows, and behaviors
Disconnected (public) view: campaign header, both reward-pool sections with mock progress/participant counts, a collapsed leaderboard summary + "View Leaderboard" action, FAQ accordion, and a "Connect wallet" prompt. No personal data anywhere.
Connected view: adds, in the leaderboard summary and full leaderboard view, the user's rank + total points, per-activity completion icons (done/not-done, not counts) for the six activity types, and a highlighted row for the user within the full leaderboard table (in addition to the separate "your position" summary block, per the approved mockup).
Leaderboard drill-in: "View Leaderboard" opens the full LeaderboardView (search by address/ENS, ranked table, pagination controls per the mockup — gardens-v2 uses scroll instead of pagination; the mockup explicitly shows page-number controls, so this plan follows the mockup).
CTA routing:
Claim UBI → CitizenClaimWidget with initialTab="claim".
Invite users → CitizenClaimWidget with initialTab="invite-rewards".
Data: all progress/participant/leaderboard values come from a static mock dataset (shape mirrors gardens-v2's pre-sorted-array approach) so a later real-data swap doesn't change component contracts.
Breakpoints reuse the existing packages/ui/src/config.ts media config (sm/md/gtSm/gtMd) via $gtSm/$gtMd props — no new responsive utility introduced, per [Feature]: Add Superfluid Campaign GoodWidget #127's constraint.
Reward-pool action cards: desktop shows each ActionCard as a single-row card (icon, copy, points pill, CTA button in a horizontal XStack). Below gtSm (i.e. on phones, <480px), each card's internal XStack switches to a stacked YStack (icon+title row, then copy, then points pill + CTA row) — same content and order, just relayout, no columns to collapse since cards are already full-width single-column in the mockup at desktop.
Leaderboard table: desktop shows all four columns (Rank, Address/ENS, Points, Activities) inline. Below gtMd (tablet, <768px) the table remains but the six activity icons wrap onto a second line within the Activities cell instead of truncating, since [Feature]: Add Superfluid Campaign GoodWidget #127 requires no content to be dropped. Below gtSm (phone, <480px), each row switches from a table row to a stacked card (YStack) with the same four data groups in the same order (rank+address on top, points below, activities below that) — this preserves hierarchy and avoids introducing horizontal scroll, which the mockup doesn't show and [Feature]: Add Superfluid Campaign GoodWidget #127 doesn't sanction as a mobile pattern.
"Your position" summary block: already a compact horizontal strip on desktop (rank, avatar, points, activities); below gtSm it stacks the same four groups vertically in the same order, matching the leaderboard row behavior for consistency.
Campaign header: the background wave art is decorative; below gtSm it's de-emphasized (reduced opacity/cropped, not removed) so the title/description/stat-pills — all functional content — keep full width and remain first in reading order, satisfying "no content dropped."
Two reward-pool sections: already stacked vertically at desktop per [Feature]: Add Superfluid Campaign GoodWidget #127's explicit requirement ("stacked vertically, not side-by-side") — this is unchanged at every breakpoint, so no responsive behavior is needed here.
This breakdown is a proposal for review, not a final locked design.
Execution plan
Scaffold packages/superfluid-campaign-widget (package.json modeled on streaming-widget's, @goodwidget/superfluid-campaign-widget, registered in the existing packages/* workspace glob — no config change needed).
Add widgetRuntimeContract.ts: SuperfluidCampaignWidgetProps, mock leaderboard/pool data types, the six ActivityType enum values with their exact icon mapping from [Feature]: Add Superfluid Campaign GoodWidget #127.
Build static presentational components against the mock dataset: CampaignHeader, RewardPoolSection + ActionCard, LeaderboardSummary, LeaderboardView + LeaderboardRow + ActivityIcons, FaqAccordion, ConnectWalletPrompt — matching the two approved mockups pixel-for-pixel on desktop.
Build the two new shared packages/ui primitives (Accordion, ProgressBar) needed by step 3, each with its own minimal prop contract (not campaign-specific).
Wire SuperfluidCampaignWidget.tsx (thin contract component): consumes useWallet()/useGoodWidget() from @goodwidget/core to branch connected/disconnected props into the components from step 3; routes CTA clicks to citizen-claim-widget (embedded with initialTab) or external links.
Add initialTab prop to citizen-claim-widget's widgetRuntimeContract.ts + CitizenClaimWidget.tsx:533 (small, isolated change; existing internal default behavior unaffected when the prop is omitted).
Implement the responsive layout per the breakpoint plan above across all components, verified against both mockups at desktop width and manually at sm/gtSm/md/gtMd widths.
Add element.ts/register.ts web-component bridge mirroring streaming-widget's, so the widget works both as a public-page React component and a GoodWallet V2 embed from the same implementation.
Add Storybook/example fixture (following the ai-credits-web app pattern) for manual visual QA against the mockups.
Manual QA pass: disconnected view, connected view, all six CTA destinations, FAQ expand/collapse, leaderboard search, all four breakpoints.
Acceptance criteria
Widget renders as a public-page component and as a GoodWallet V2 embed from the same reusable implementation (packages/superfluid-campaign-widget).
Disconnected state shows full campaign header, both reward-pool sections with mock progress/participant data, leaderboard summary + full leaderboard, and a "Connect wallet" option — no personal data.
Connected state additionally shows the user's rank, total points, per-activity completion icons (six types, done/not-done only), and highlights their row in the full leaderboard.
Claim UBI and Invite users open citizen-claim-widget on the correct tab via the new initialTab prop; Vote/Fund/Donate CTAs link to the exact URLs from [Feature]: Add Superfluid Campaign GoodWidget #127.
Layout, styling, colors, icons, and copy match the two approved mockups on desktop.
Tablet/mobile responsive behavior matches the breakpoint plan above: no content dropped or restructured, only relayout, using $gtSm/$gtMd (no new responsive utility).
No live points/reward-calculation logic or external API integration is introduced — all data is mocked, matching gardens-v2's pre-sorted-array shape for a drop-in-ready future swap.
[PLAN] Add Superfluid Campaign GoodWidget
Type: Task
Links back to: #127 (parent
[Feature]issue — body unchanged)One reusable GoodWidget (public-page + GoodWallet V2 embed) that presents the Superfluid Ecosystem Rewards campaign: header/description, two reward-pool sections (GoodDollar actions, Ecosystem funding actions), a public leaderboard, and an FAQ accordion. Wallet connection is optional and only reveals the connected user's own rank/points/activity state. This phase is UI + responsive layout only, against mock data — no points logic or live API integration (tracked as follow-up, out of scope here per #127).
Reference files mapped
GoodDollar/GoodWidget (this repo):
packages/ui/src/config.ts:222-246— Tamagui breakpoint config (media: { sm: maxWidth 480, md: maxWidth 768, gtSm: minWidth 480, gtMd: minWidth 768 }), the mechanism this plan's responsive approach builds on.packages/ai-credits-widget/src/components/manage/CreditsManagementCard.tsx:28-48—$gtSmgrid-column-count pattern (45% width columns belowgtSm, 28% above), direct precedent for the pool action-card grid.packages/governance-widget/src/OptimisticVotingProposalCard.tsx:67,FundingDistributionChart.tsx:114,163— fixed/contained-width card patterns.packages/streaming-widget/src/— modularity precedent to follow: thinindex.tscontract +adapter.ts/adapter/(data/state hooks) +components/(presentational) +element.ts/register.ts(web-component bridge) +widgetRuntimeContract.ts(types). This repo's newest widget and closest structural analog.packages/citizen-claim-widget/src/CitizenClaimWidget.tsx:533—activeTabis currently internaluseState, hardcoded to'claim', noinitialTabprop exists today. This plan adds one (see Execution plan) rather than working around its absence.packages/citizen-claim-widget/src/widgetRuntimeContract.ts— existing prop/type contract shape to extend.packages/embed/src/createMiniAppElement.tsx— shared web-component bridge (shadow DOM, theme override CSS vars, event bridging) used by every widget'selement.ts; the new widget reuses this unchanged.packages/core/src/hooks.ts(useWallet,useHost,useGoodWidget),packages/core/src/provider.tsx(GoodWidgetProvider) — wallet connection state and host detection, reused as-is; no new wallet logic needed.packages/ui/src/index.ts— existing primitives to reuse (Card,Heading,Text,Button,Separator,WidgetTabs,Icon,Drawer/Dialog). NoAccordion,ProgressBar,Table, orAvatarcomponent exists yet — these are new (see New components below).AGENTS.md,ARCHITECTURE.md,pnpm-workspace.yaml— package naming (@goodwidget/<name>), workspace registration (packages/*), tsup build convention.1Hive/gardens-v2 (behavior reference only, per #127 — visual design is the approved mockups, not this repo):
apps/web/app/(app)/gardens/campaigns/[campaignId]/client-page.tsx— optional-wallet-connection gating pattern: connected-only "Your Position" block rendered conditionally onuseAccount()/points existing, disconnected users see the same table with no personal section.apps/web/components/SuperfluidLeaderboard.tsx— leaderboard-modal structure: a distinct "Your Position" summary block above the table (not an inline highlight), scrollable table body, search-by-address/ENS filtering, no pagination.apps/web/types/index.ts(fetchSuperfluidLeaderboard) — single fetch returns the full ranked list pre-sorted by points; connected user's rank is derived client-side via array index rather than a second API call. Since this phase uses mock data only, the plan adopts the same shape (one mock dataset, rank derived from array position) so swapping in the real API later is a drop-in.Existing @GoodDollar packages imported
@goodwidget/core—GoodWidgetProvider,useWallet,useHost,useGoodWidget.@goodwidget/ui—Card,Heading,Text,Button,Separator,Icon,WidgetTabs,Stack/XStack/YStack,createGoodWidgetConfig/theme tokens.@goodwidget/embed—createMiniAppElementfor the web-component bridge.@goodwidget/citizen-claim-widget— embedded directly for the Claim and Invite flows (not rebuilt), via a newinitialTabprop (see Execution plan).New components mapped (package placement)
New widget package:
packages/superfluid-campaign-widget(@goodwidget/superfluid-campaign-widget), following thestreaming-widgetdecomposition:New shared primitives — built in
packages/ui(reusable beyond this widget, not campaign-specific):Accordion(packages/ui/src/components/Accordion.tsx) — expandable item list; FAQ is the first consumer but this is generic UI, belongs inpackages/uiper the repo's existing "reusable in packages/ui vs widget-specific" split.ProgressBar(packages/ui/src/components/ProgressBar.tsx) — labeled linear progress bar (SUP-allocated and per-pool progress all reuse the same primitive); generic enough for any widget with a completion/allocation metric.Kept inside the widget package (not
packages/ui) because they're specific to this campaign's data shape, not generic UI:LeaderboardRow/LeaderboardView/ActivityIcons— tied to the six-activity, points-based campaign model from [Feature]: Add Superfluid Campaign GoodWidget #127, not a generic table component.ActionCard/RewardPoolSection— tied to the GoodDollar-actions/Ecosystem-funding-actions pool structure specific to this campaign.citizen-claim-widgetchange (small, contract-level, not a new component): addinitialTab?: 'claim' | 'invite-rewards'toCitizenClaimWidgetPropsinwidgetRuntimeContract.ts, threaded into the existingactiveTabuseStateinitializer inCitizenClaimWidget.tsx:533. This is the only change to an existing package.Required states, flows, and behaviors
LeaderboardView(search by address/ENS, ranked table, pagination controls per the mockup — gardens-v2 uses scroll instead of pagination; the mockup explicitly shows page-number controls, so this plan follows the mockup).CitizenClaimWidgetwithinitialTab="claim".CitizenClaimWidgetwithinitialTab="invite-rewards".packages/ui/src/config.tsmedia config (sm/md/gtSm/gtMd) via$gtSm/$gtMdprops — no new responsive utility introduced, per [Feature]: Add Superfluid Campaign GoodWidget #127's constraint.ActionCardas a single-row card (icon, copy, points pill, CTA button in a horizontalXStack). BelowgtSm(i.e. on phones, <480px), each card's internalXStackswitches to a stackedYStack(icon+title row, then copy, then points pill + CTA row) — same content and order, just relayout, no columns to collapse since cards are already full-width single-column in the mockup at desktop.gtMd(tablet, <768px) the table remains but the six activity icons wrap onto a second line within the Activities cell instead of truncating, since [Feature]: Add Superfluid Campaign GoodWidget #127 requires no content to be dropped. BelowgtSm(phone, <480px), each row switches from a table row to a stacked card (YStack) with the same four data groups in the same order (rank+address on top, points below, activities below that) — this preserves hierarchy and avoids introducing horizontal scroll, which the mockup doesn't show and [Feature]: Add Superfluid Campaign GoodWidget #127 doesn't sanction as a mobile pattern.gtSmit stacks the same four groups vertically in the same order, matching the leaderboard row behavior for consistency.gtSmit's de-emphasized (reduced opacity/cropped, not removed) so the title/description/stat-pills — all functional content — keep full width and remain first in reading order, satisfying "no content dropped."Execution plan
packages/superfluid-campaign-widget(package.json modeled onstreaming-widget's,@goodwidget/superfluid-campaign-widget, registered in the existingpackages/*workspace glob — no config change needed).widgetRuntimeContract.ts:SuperfluidCampaignWidgetProps, mock leaderboard/pool data types, the sixActivityTypeenum values with their exact icon mapping from [Feature]: Add Superfluid Campaign GoodWidget #127.CampaignHeader,RewardPoolSection+ActionCard,LeaderboardSummary,LeaderboardView+LeaderboardRow+ActivityIcons,FaqAccordion,ConnectWalletPrompt— matching the two approved mockups pixel-for-pixel on desktop.packages/uiprimitives (Accordion,ProgressBar) needed by step 3, each with its own minimal prop contract (not campaign-specific).SuperfluidCampaignWidget.tsx(thin contract component): consumesuseWallet()/useGoodWidget()from@goodwidget/coreto branch connected/disconnected props into the components from step 3; routes CTA clicks tocitizen-claim-widget(embedded withinitialTab) or external links.initialTabprop tocitizen-claim-widget'swidgetRuntimeContract.ts+CitizenClaimWidget.tsx:533(small, isolated change; existing internal default behavior unaffected when the prop is omitted).sm/gtSm/md/gtMdwidths.element.ts/register.tsweb-component bridge mirroringstreaming-widget's, so the widget works both as a public-page React component and a GoodWallet V2 embed from the same implementation.ai-credits-webapp pattern) for manual visual QA against the mockups.Acceptance criteria
packages/superfluid-campaign-widget).citizen-claim-widgeton the correct tab via the newinitialTabprop; Vote/Fund/Donate CTAs link to the exact URLs from [Feature]: Add Superfluid Campaign GoodWidget #127.packages/uiAccordionprimitive.$gtSm/$gtMd(no new responsive utility).