The full equirectangular viewBox (-90..+90 lat) leaves a tall empty
ocean band at the bottom of the hero on wide viewports. Antarctica's
coastline only reaches ~lat -75 around its perimeter, so the bottom
~5° of the viewBox is always empty. Trim to -85..+85 so 'slice' fills
the hero with land texture instead of empty space. No distortion —
preserveAspectRatio still scales uniformly.
Cards in a multi-column grid now have a deterministic, predictable
silhouette. The body region is a fixed stack — title (1 line, truncates)
+ summary (1 line, truncates with a non-breaking-space placeholder when
absent) + progress (invisible-bar placeholder absorbs no-goal and
silent-payment cases) + creator footer — so no card has dead space or
ragged bottoms regardless of which optional fields are populated.
Country, deadline, hidden badge, and moderation menu all moved onto the
banner image as glass chips overlaid on a bottom gradient (chips
appear only when there's data to show, so a chip-free banner stays
clean). The donor-count chip became an inline aside on the creator
line. The variable meta row is gone entirely from the body.
CampaignPrivateNotice was rebuilt to mirror CampaignProgress's
vertical footprint (invisible bar + one text row) so silent-payment
cards line up with public-progress cards beside them.
Progress bars on cards and the detail-page DonateColumn use
`bg-foreground/15` for the track instead of the primitive's default
`bg-secondary`, which in dark mode shares its color with the card
surface and made the empty portion of the bar invisible. The new
foreground-tinted track reads cleanly on the card in both themes.
- Wrap composer + comments / composer + feed in a rounded muted panel
with continuous border-primary rails.
- Composer and campaign sidebar use a near-white surface with a
brand-orange border; dark mode swaps to a near-black warm surface.
- Per-article borders and backgrounds retinted via scoped selectors
so NoteCard stays untouched globally.
- Add hideCommentContext, authorBadge to NoteCard and
renderAuthorBadge, leafCardClassName to ThreadedReplyList; move the
Campaigner badge into the author row.
- Light-mode tokens: --card to 0 0% 98%, --muted to 30 12% 94% so
cards and muted regions read as real surface steps.
- Drop the unused campaignsDetail.commentCount locale key everywhere.
DialogContent is a CSS grid container, and grid items default to
`min-width: auto` — i.e. the intrinsic content width. Long
unbreakable strings (e.g. a combined BIP-21 `bitcoin:<bc1>?sp=<sp1>`
URI in the wallet receive dialog) refuse to shrink, blow past
`max-w-lg` / `w-[calc(100%-2rem)]`, and the `flex-1 min-w-0 truncate`
pattern on the inner span never engages because the parent already
accepted the full intrinsic width.
Adding `[&>*]:min-w-0` to the grid container fixes truncation for
every dialog without touching individual call sites.
Strip the redundant in-dialog header, the BIP-86/Taproot explainer, and
both the Reveal and Copy buttons from the seed-phrase backup UI. The
seed-phrase box itself is now the reveal affordance — tap to expose the
words, tap again to hide them. Copy-to-clipboard is gone entirely (users
write the words down rather than paste them).
- WalletBackupMnemonic: drop the inline "Wallet seed phrase" h2 + KeyRound
icon (the DialogTitle already renders the same heading), drop the
Nostr-key-derivation explainer paragraph, drop the two-button row at the
bottom, drop the copied-state and useToast wiring. Wrap the seed-phrase
box in a <button> with aria-pressed and aria-label so screen readers know
it's a toggle.
- en.json:
- Rewrite walletBackup.dialogDescription to the new short copy: "You can
access your Agora wallet in any Bitcoin wallet by importing this 24-word
backup."
- Rewrite walletBackup.hidden to "Tap to display your 24-word seed
phrase." (dropping the "Tap Reveal" wording, since there is no Reveal
button anymore).
- Add walletBackup.revealAria / hideAria for the toggle button's
aria-label.
- Remove walletBackup.explainer, .reveal, .hide, .copy, .copied,
.copyFailedTitle, .copyFailedDescription.
- Mirror the same restructuring across all 14 other locales. Four locales
(hi, id, tr, zh-Hant) didn't previously have a walletBackup block at all
and were falling back to English — they now ship full translations.
The HD wallet seed is now BIP-39-compatible. Pipeline:
entropy = HKDF-SHA256(nsec, info="agora/v1", length=32)
mnemonic = BIP-39 encoding of (entropy || checksum) // 24 words
seed = PBKDF2-HMAC-SHA512(mnemonic, salt="mnemonic", iters=2048)
The 24 words import cleanly into Sparrow, Electrum, Trezor, Ledger,
BlueWallet, Phoenix, etc., at the BIP-86 / BIP-352 paths. HKDF domain
separation means a leaked mnemonic compromises only the wallet, not
the Nostr identity (unlike the raw nsec).
v1 derivation (nsec used directly as BIP-32 master seed) is retained
as migration-only code. A new /wallet/migrate-v1 page detects funds
at the legacy addresses and builds a single sweep PSBT to consolidate
them into the v2 wallet. A persistent banner on /wallet surfaces the
flow when v1 funds exist.
The mnemonic shows up in two places: a "Back up wallet" dialog on
/wallet, and a section in Profile -> Advanced next to the nsec
backup. nsec backup copy updated to explain the relationship.
Locked test vectors pin the entire derivation pipeline (nsec -> 24
words -> first BIP-86 address -> sp1q...) so any future drift fails
loudly. Regenerate via scripts/derive_vectors.mjs.
Other changes:
- Re-key SP storage NIP-78 d-tag to /v2 so v1 and v2 UTXOs do not mix
- Re-key the persisted receive-address cursor to :v2: namespace
- Relax SP spend-key helper to 16-64 byte seeds (BIP-32 range) so the
migration sweep can sign with the legacy 32-byte v1 seed too
- Remove stale NIP-SP references from derivation comments (the draft
was not relevant to our use case)
- Document the wallet derivation scheme in NIP.md
- Translate every new string to all 10 non-English locales
Splits the guide content same way the FAQ was split:
* Structure (block order, kinds, audience, callout variant, option
grid chips/hrefs) stays in helpContent.ts as a typed array of
GuideBlockStructure descriptors with stable IDs.
* Every user-visible string moves to en.json under guides.donor.*,
guides.activist.*, and guides.shared.* (badge labels, tldr eyebrow,
payment comparison table headers + rows).
PaymentComparisonTable, InlinePaymentBadge, and GuideTLDR call
useTranslation() so a language switch triggers re-render. DonorGuidePage
and ActivistGuidePage already do, so getDonorGuideBlocks /
getActivistGuideBlocks re-run on every render and pick up fresh i18n
values automatically.
This commit lands the en strings only; non-en locales follow in the
next commit.
Splits the FAQ definition in `helpContent.ts` into two layers:
* a structural template (category order, item IDs, hidden flag) that
stays in TS, and
* a flat `faq.*` namespace in `locales/*.json` that holds every
user-visible string (category labels, questions, answer paragraph
arrays).
`getFAQCategories` / `getFAQItems` / `getFAQItem` keep the same
`(appName)` signature and `FAQCategory` / `FAQItem` shape — internally
they resolve strings through `i18n.t()`, with `{appName}` literals
rewritten to `{{appName}}` for i18next interpolation. Answer arrays use
`returnObjects: true` so a missing locale entry falls back to English
without breaking the renderer.
`HelpFAQSection` reads `i18n.language` to re-resolve on language switch;
`HelpTip` calls `useTranslation()` for the same reason. The Donor /
Activist guide blocks below are still keyed off the original
single-brace `{appName}` literal — that's a separate i18n pass.
Aggressive cleanup of 359 exports across 153 files identified as
having zero importers outside their declaring module:
- 105 symbols deleted entirely (no internal uses either)
- 254 symbols un-exported (still referenced file-locally; dropped the
`export` keyword to shrink the public surface)
- ~70 cascade cleanups of locals that became dead once their sole
consumer was removed
Notable shrinkage:
- src/hooks/useShakespeare.ts: 626 \u2192 22 lines (unwired AI chat surface;
only the ChatMessage type is consumed)
- src/hooks/useTrending.ts: only useEventStats survives; trending feed
hooks were never wired up
- src/hooks/useTrustedCountryStats.ts: dead type re-exports removed
- src/lib/bitcoin.ts: PSBT helpers \u2014 unused wallet feature scaffolding
- src/lib/communityUtils.ts: unused NIP-72 moderation helpers
- src/lib/extraKinds.ts, src/lib/colorUtils.ts: unused helpers
- src/lib/logger.ts: bare debug/info/warn/error exports dropped;
consumers use the `logger` object
- src/lib/aiChatSystemPrompt.ts: trimmed to the
DEFAULT_SYSTEM_PROMPT_TEMPLATE constant
- src/components/music/MusicTrackRow.tsx: dead row component removed;
only the skeleton is consumed
src/hooks/useNostr.ts (intentional decoy) and src/i18n.ts
(side-effect import) were preserved per their respective contracts.
Extracts the ~110-line JSX prose body into per-language markdown files
and renders it through a new `usePolicyMarkdown` hook + `PolicyMarkdown`
component. Eight locales ship beside `en.md`; missing locales fall back
to English at runtime.
The loader uses dynamic `import()` of `*.md?raw` (Vite + Bun friendly,
no `import.meta.glob`) keyed on a static `{ slug: { lng: loader } }`
table so bundlers can code-split per locale and we keep one shared chunk
for the markdown ecosystem. `{{appName}}` placeholders are interpolated
at render time with backslash-escape of markdown specials, and the
rendered output goes through rehype-sanitize.
This is the reusable loader infrastructure for the long-form policy
pages — CSAEPolicyPage is the next consumer.
Four files with zero importers across the repo:
- src/components/TeamSoapboxCard.tsx
- src/components/discovery/ProfileCard.tsx (superseded by
src/components/ProfileCard.tsx)
- src/components/letter/StickerPicker.tsx (superseded by
src/components/StickerPicker.tsx)
- src/hooks/useDominantColor.ts
src/hooks/useNostr.ts is also orphan but intentional \u2014 it exists as
a re-export decoy and is kept.
Adds two large namespaces:
noteCard.* — the action labels surfaced under reaction / repost / poll
vote actor rows (reacted / reposted / voted), the Read more / Show less
truncation toggle, the live-stream status badges (LIVE / ENDED /
PLANNED / UNKNOWN), the Bot account avatar title, the donation prefix
on synthetic zap cards (Donated X to / Donated to), and the entire
KIND_HEADER_MAP — every 'X did a Y' header for the 24 kinds Agora
renders an action header for (photos, encrypted messages, letters,
treasures + finds, decks, emoji packs, groups, campaigns, badges,
streams, Zapstore app/release/asset, generic apps, git repos / patches
/ PRs, NIPs, nsites, zaps, pledges, follow packs, follow sets).
The KIND_HEADER_MAP refactor pulls the same trick as profileSettings
presets: each entry stores i18n keys instead of English strings, and
EventActionHeader runs them through t() at render time. publishedAtKey
is just publishedAtAction returning keys instead of phrases. The map
itself stays a module-level constant.
noteMoreMenu.* — the entire 'more' overflow menu hanging off every
NoteCard: every menu item (View post details, View Event JSON,
Bookmark, Add to list, Add/Remove from sidebar, Pin/Unpin to profile,
Pin/Unpin to country feed, Mute Conversation / mute @user / Report /
Remove from group / Delete post), the Encrypted content fallback in
the post preview, every toast (success + failure for each mutation),
the Delete confirmation alert dialog, and the Event JSON dialog (title,
Event ID label, Raw JSON label, Broadcast Event button + Broadcasting
state, plus the {{label}} copied toast).
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Adds the compose.* namespace covering ComposeBox: placeholders
(default, poll, country-scoped, content warning), preview / edit toggle,
poll mode (Add option, Option N, Single/Multiple choice, Back to post,
Publish poll, success/failure toasts), voice recording (Cancel, Send,
Sending, mic-denied toast, voice-message tooltip), toolbar
(Attach file, Emoji/GIF, More, Poll, Spoiler, Emoji, Stickers tabs),
submit (Posting / Posted toasts, upload-failed / publish-failed),
and destination dropdown (Post to label, Global / community
explainer popover, Choose another country picker, search placeholder,
empty state, Set as default, default-updated toasts).
Also adds replyModal.* for ReplyComposeModal — the six title fallbacks
(New poll, New comment, Comment on profile, Reply to post, Quote post,
New post), three placeholder fallbacks, and the Bluesky disclaimer.
ComposeBox's hardcoded default placeholder ('What's on your mind?') and
submitLabel ('Post!') props moved from prop defaults to t() fallbacks
inside the component so existing call sites can keep passing translated
strings unchanged. Renamed an inner .map((t) => …) variable in the poll
type/duration row to .map((pt) => …) since t shadowed the i18next t.
Added rtl:rotate-180 to the 'Back to post' chevron since it visually
points left/back.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Extends the feed.* namespace with five new sub-namespaces:
compose.placeholder, tabs (Follows / Following / Global), empty (the
six per-tab + per-mode empty-state messages plus the three CTA labels
that show under them), modeSwitcher (the home feed's mode-picker dropdown
with Agora / All Nostr / Following options, the dropdown trigger's aria
label, and the disabled-Following tooltip), replyContext (Replying to,
the and-joiner, plus a pluralized andOthers_one/andOthers_other), and
actions (Reply / Repost / Undo repost / React / Zap / Share / More + the
Link-copied toast).
PostActionBar's replyLabel prop default went from 'Reply' to undefined;
the component falls back to t('feed.actions.reply') at render time so
existing call sites that pass an already-translated string keep working
unchanged. FeedModeSwitcher's OPTIONS array swapped its hard-coded
label string for an i18nKey suffix, materialized through t() inside the
component.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Adds the profile.* namespace covering ProfilePage (the more menu, follow
toast, image lightbox, follow/follower modals, Bitcoin QR modal, inline
profile fields, NIP-05 not-found states) and every sub-component the page
mounts: ProfileIdentityRail (action bar, stats, campaigns/pledges/groups
sections, RailPledgeCard, RailOrgCell), ProfileActivityTab,
ProfileCampaignsTab, ProfilePledgesTab, and OrganizationsAllDialog.
The desktop and mobile tab arrays (DESKTOP_TAB_LABELS / MOBILE_TAB_LABELS)
moved from English label strings to label keys so the tab maps are
translated through t('profile.tabs.*') at render time. CORE_TAB_IDS now
keys off the same identifier slugs to keep the click-through routing
intact.
Inner sub-components (RailCampaignsSection, RailPledgeCard, RailOrgCell,
StatList, ActionBar, ProfilePledgeCard, ProfileFieldInline,
BitcoinQRModal, FollowingListModal, FollowersListModal,
ProfileImageLightbox, ProfileMoreMenu) each pick up their own
useTranslation() per the established pattern — no t prop drilling.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn, ps,
fa still pending native-speaker review.
Adds a top-level spScan.* namespace covering the BIP-352 silent-
payment scan dialog: header (title + description), the from/to block
inputs (label + 'tip' placeholder), the indexer-tip / last-scanned
footer line (with 'never' fallback), the include-already-spent
toggle + its long explanation, the in-progress block-counter and
matches_one/_other plural, the post-scan summary
(scannedRange + foundOutputs_one/_other / noNewPayments), and the
'Reconcile spent UTXOs' subsection (title, long description,
checking interpolation, checked_one/_other plural with prune count,
Reconciling… / Reconcile now button). Action buttons reuse
common.cancel + common.close.
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Two top-level namespaces:
walletSend.* — the Send Bitcoin dialog at /wallet: dialog title,
amount approx-sats interpolation, recipient label/placeholder, the
three recipient descriptions (silent payment / Nostr / raw address),
fee speed labels (10 min / 30 min / 1 hour / 1 day) and the
{{rate}} sat/vB display, network fee row, available-balance footer,
the four progress strings (building / signing / broadcasting /
sending), the 10-error error catalogue thrown into setError() +
the mutation onError toast title, two-tap arming ('Tap again to
confirm'), and the SuccessScreen (title, sats fallback, view
transaction, done).
bitcoinPublic.* — the BitcoinPublicDisclaimer shared component
used by both the wallet's Send dialog and the campaign DonateDialog.
Lead sentence, 'Learn more' link, the two long body variants
(with/without cash-out advice), and the 'I understand this
transaction is public.' acknowledgement label. The leadText prop
override is preserved; only the default is now translated.
The FEE_SPEED_LABELS module constant was replaced by an in-component
useMemo so the labels stay reactive to language switches.
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Adds a top-level walletConnect.* namespace covering the NWC settings
component used by the Advanced settings page: status section (WebLN
+ NWC cards with Ready / Not Found / None badges), the Add button,
empty state, per-connection rows (active badge, defaultWalletName
fallback, set-active and remove icon-button titles), the help text
shown when no signing method is available, and the Connect-NWC
dialog (title, description, alias placeholder, Connect /
Connecting... button).
Toast titles+descriptions for URI-required validation and active-
wallet-changed feedback also go through t(). The connectedCount_one
/ _other plural lets each locale handle pluralisation natively (zh
collapses to one form, ar has separate one/other, etc.).
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Conflicts in NetworkSettingsPage.tsx and SettingsPage.tsx, both
introduced by upstream's Low-Bandwidth Mode + image proxy feature
landing on top of my i18n refactor of the same settings pages.
Resolution:
- Translated the new Low-Bandwidth Mode and Image Proxy block under
settings.network.{lowBandwidthHeading,reduceDataUsage,
reduceDataUsageDesc,useImageProxy,useImageProxyDesc,proxyUrl,
proxyApiDesc,reset}, using <Trans> for the wsrv.nl link in
proxyApiDesc (<0>...</0> component slot).
- Updated settings.network.subtitle and settings.sections.networkDesc
in en.json to match the new upstream English ("Manage data usage,
relays, and file upload servers." / "Data usage, relays, and file
upload servers.").
- Mirrored the new keys + updated values into all 7 non-English
locales. Native-speaker review still pending for km, sn, ps, fa.
Previously the tap-to-load placeholder only kicked in when the proxy was
also disabled. The proxy still saves bandwidth, but users who flipped on
low-bandwidth mode generally want the explicit consent step on every
image — proxied or not. Make the two settings independent:
- ImageGallery / ProxiedImage: gate on lowBandwidthMode alone.
- LinkPreview: suppress thumbnail whenever lowBandwidthMode is on.
- Settings copy + AppConfig JSDoc updated to match.
Extract user-facing strings on the create-pledge form into the
pledges.create.* namespace and add per-form-section labels via a new
forms.* namespace shared with FormSection (Required/Recommended/
Optional badge).
Translates the login gate, field labels, placeholders, the
mutation's user-facing error messages, success / failure toasts, the
country search box, and the country-hint footnote (with a <0> wrapper
around the iso3166 monospace span). The back-arrow flips with
rtl:rotate-180 for Arabic/Farsi/Pashto.
Native-speaker review still pending for: km, sn, ps, fa.
Two new AppConfig fields cover the data-saving story:
- `imageProxy` (default `https://wsrv.nl`) — wsrv.nl/weserv-compatible
image-resizing proxy. Empty string disables it. `proxyImageUrl(src, width)`
and `useImageProxy()` rewrite URLs to WebP at quality 75; the `default=`
param redirects to the origin if the proxy can't fetch upstream. The
proxy base URL is parsed through `URL` and rejected unless it's
`https:`, so user input from the settings field can't smuggle non-https
schemes into <img src>.
- `lowBandwidthMode` (default `false`) — forces autoplay off everywhere
(VideoPlayer, LiveStreamPlayer), skips `useVideoThumbnail`'s background
frame-grab, and (when the proxy is also disabled) gates feed images and
link-preview thumbnails behind a tap-to-load placeholder.
The two settings are independent. A privacy-conscious user can run with
the proxy off without being forced into tap-to-load, and a metered-data
user can keep the proxy on without ever seeing a placeholder. Tap-to-load
only kicks in when both "I'm low-bandwidth" and "the proxy is off"
are true (or the proxy errors in a gated context).
Three new shared pieces:
- `src/lib/proxyImageUrl.ts` — pure URL rewriter
- `src/hooks/useImageProxy.ts` — memoized `(src, width) => string`
- `src/components/MediaPlaceholder.tsx` — tap-to-load pill with
optional blurhash background; rendered as `<div role="button">`
so it nests cleanly inside InlineImage's outer lightbox button
- `src/components/ProxiedImage.tsx` — `<img>` with proxy + onError
fallback + optional placeholder gating
Wired call sites with per-context widths:
Inline post images (InlineImage, w=600, gated)
Image gallery tiles (GridImage, w=600, gated)
Lightbox (LightboxImage, w=1200)
Profile banner (ProfileBannerImage, w=1200)
Profile hover banner (ProfileHoverCard, w=400)
Link preview thumb (LinkPreview, w=400, suppressed when
low-bandwidth + no proxy)
Sidebar media tile (ProfileRightSidebar, w=300)
Avatars (via shadcn) (AvatarImage, w=96 default,
w=128 hover card,
w=256 profile header)
Custom emojis (CustomEmojiImg, w=48)
Badge thumbnails (BadgeThumbnail, w=max(size*2, 128))
Badge hero + glare mask (BadgeContent, w=256;
BadgeDetailContent, w=320;
EmbeddedNaddr, w=192)
Music / podcast art (AudioKindContent, w=600;
MusicTrackRow, w=96;
MusicDetailContent, w=320 hero / 96 row;
PodcastDetailContent, w=320)
Settings UI lives in NetworkSettingsPage with the Low-Bandwidth Mode
toggle at the top (above relay / Blossom plumbing) and the Image Proxy
section between Blossom Servers and Image Uploads. The low-bandwidth
description text adapts to whether the proxy is configured, so the
proxy↔tap-to-load coupling is visible at the toggle.
Video Tier 1:
- VideoPlayer overrides `autoPlay` to false in low-bandwidth
- LiveStreamPlayer no longer hardcodes `autoPlay`, reads config
- useVideoThumbnail skips its background frame-grab entirely
`lowBandwidthMode` is added to EncryptedSettingsSchema so it crosses
devices alongside `autoplayVideos`. `imageProxy` is local-only (privacy
/ network choice, not a UX preference).
Wire useTranslation into TopNav (nav items, mobile drawer, profile menu,
search button aria-label, brand home aria-label, open/close menu labels,
mobile footer links) and SiteFooter (tagline + footer nav links).
Add nav.* namespace to en.json + all 7 locale files. The mobile drawer
and desktop nav now switch language live the moment a user picks a new
language in /settings/language.
Agora's colors and fonts are now hardcoded in the bundle. There is no
runtime CSS-variable injection, no remote-loaded theme, no font-family
override from event data, no background image, no recolored favicon.
Switching themes only toggles the .dark class on <html>.
Removed:
- src/themes.ts: ThemeConfig, ThemesConfig, CoreThemeColors, ThemeFont,
ThemeBackground, themePresets (22 presets), buildThemeCssFromCore,
deriveTokensFromCore, the 'custom' Theme variant. Now exports only
resolveTheme(theme) -> 'light' | 'dark'.
- src/lib/fontLoader.ts: deleted. Mounted arbitrary @font-face rules with
event-sourced URLs and font-family overrides — the primary CSS-injection
vector. sanitizeCssString moved to src/lib/cssSanitize.ts (still used by
the Letter feature).
- AppProvider hooks useApplyFonts / useApplyBackground / useApplyFavicon.
- useTheme.applyCustomTheme — the entrypoint that wrote external palettes
to global theme state.
- ColorMomentEyeButton + its callers in NoteCard and PostDetailPage. Color
Moments (kind 3367) still render as palette art, but the 'Set as theme'
button is gone.
- LetterAttachment in LetterDetailSheet — the gift-box UI that applied an
attached color moment as the recipient's theme.
- paletteToTheme() from colorMomentUtils — only getColors() remains.
- customTheme / themes fields from AppConfig, AppConfigSchema,
EncryptedSettings, EncryptedSettingsSchema. Existing customTheme values
in localStorage and encrypted NIP-78 settings are now ignored.
- 14 unused @fontsource packages. Only the 10 letter fonts and the 2 base
UI fonts (Inter Variable, Bebas Neue) remain. noto-sans-nushu is kept
for the encrypted-letter obfuscation indicator.
Added:
- Static :root {} and .dark {} blocks in src/index.css with the 19 shadcn
tokens hardcoded. These were previously injected at runtime by
AppProvider; without them the app would lose all colors when the runtime
injector was removed.
- src/lib/cssSanitize.ts holding the sanitizeCssString helper for the
Letter feature's font-family interpolation.
Simplified:
- public/theme.js: no longer reads customTheme or themes from localStorage,
just resolves system/light/dark with hardcoded built-ins. Must stay in
sync with src/index.css colors.
- src/lib/fonts.ts: trimmed to only the 10 letter fonts. findBundledFont
and resolveCssFamily removed (they were only used by fontLoader).
- Rename MySquarePage -> MyDashboardPage (file, component, default export)
- Change route from /my-square to /my-dashboard
- Update user-facing copy: SEO title, logged-out heading, JSDoc
- Add 'My Dashboard' link to AccountSwitcher dropdown
- Add 'My Dashboard' entry to TopNav mobile/profile menu
- Use LayoutDashboard icon to distinguish from the existing Dashboard
- Existing /dashboard route and EventDashboardPage are untouched
Replaces the previous near-uniform dark surfaces (#0e1218 / #11151c)
with genuinely dark, alternating tones plus a subtle world-map
texture on two sections, so the page reads as an editorial document
in dark mode the same way it does in light mode.
Section background mapping (dark mode)
Section Was Now
----------------------- ---------- ------------
Hero #0a0c14 #0a0c14 (unchanged)
Three steps #11151c #0a0c14 + texture
Two ways to get paid #0e1218 #13181f + texture
Frequently asked #11151c #0a0c14
Pick the side #0e1218 #13181f
The two alternating tones (#0a0c14 / #13181f) preserve the
cream/white rhythm from light mode. Sections 2 and 3 gain a
dark-only world-map background image at 5-6% opacity (the same
texture used in the hero) so they don't read as flat slabs. The
texture is gated behind 'hidden dark:block' so it has zero impact
on the light-mode rendering.
Card surface lifted
bg-[#1a1f29] → bg-[#1c2230] across StepCard, RailCard, GuideCard,
the FAQ accordion card-row (reference mode), the FAQ card variant,
and the FAQ tab pill. Hover-state for the tab pill follows from
#222937 to #252b3a. Net effect: cards now sit ~12 lightness above
the deepest section background and ~6 above the lifted one, keeping
clear elevation against both tones.
Every section of the About page now renders correctly in dark mode.
The hero stays dark in both modes (its identity is anchored on the
dark navy backdrop), and the four light sections (Three steps, Two
ways to get paid, Frequently asked, Pick the side) each gain a
dark-mode counterpart so the page reads consistently inside the
app's theme.
Section background mapping
Section Light bg Dark bg
----------------------- ----------- -----------
Hero #0a0c14 #0a0c14 (unchanged)
Three steps #faf8f4 #11151c
Two ways to get paid white #0e1218
Frequently asked #f5f1eb #11151c
Pick the side white #0e1218
Card surfaces (StepCard, RailCard, GuideCard, FAQ accordion item)
map bg-white -> dark:bg-[#1a1f29] and border-gray-200 ->
dark:border-white/10. Hover shadows pick up a darker variant
(dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.4)]) on cards that have
a hover-lift effect.
Text-color mapping (applied throughout)
text-gray-900 -> dark:text-white (headings)
text-gray-800 -> dark:text-gray-100 (inline strongs)
text-gray-700 -> dark:text-gray-300 (bullet body)
text-gray-600 -> dark:text-gray-300 (body prose)
text-amber-600 / 700 -> dark:text-amber-400 (trade-off label
+ icon)
border-gray-100 -> dark:border-white/10 (separators)
No-Custody comparison banner
The light gradient card (white -> primary/5) converts to a glass
tile in dark mode (white/0.04 -> primary/0.08) with primary/30
border. The inner Comparison grid's separator border picks up
dark:border-white/10. ComparisonItem's light theme path now also
sets dark:text-gray-300 on its body copy so it reads on the dark
glass tile.
HelpFAQSection
Both the 'reference' card-row accordion (used by the About page)
and the 'cards' masonry variant gain dark-mode card surfaces and
text-color mapping. The category tab pills get a dark-mode
inactive state. The active pill (bg-primary) is already correct in
both modes.
These three embed components rendered third-party content in
iframes (Twitter's platform.twitter.com widget, Spotify's
embed-iframe, Reddit's embed.reddit.com widget). Removed wholesale:
- SpotifyEmbed, TweetEmbed, RedditEmbed components.
- extractTweetId, extractSpotifyEmbed, extractRedditPost, and the
SpotifyEmbedInfo type from lib/linkEmbed.ts.
- The Twitter/Spotify/Reddit branches in LinkEmbed and
isEmbeddableUrl/embedLabel.
Tweet, Spotify, and Reddit URLs now fall through to the generic
LinkPreview card. useLinkPreview keeps its native Spotify/Reddit
oEmbed shortcuts so those previews still render with rich title +
thumbnail metadata instead of going through the link-preview proxy.
The iframe.diy-based sandbox infrastructure powered two features:
the nsite preview dialog (Run button on NsiteCard and AppHandlerContent)
and the webxdc embed (cartridge launcher + sandboxed iframe runtime
with kind 4932/20932 sync events).
Both are removed wholesale:
- iframe sandbox plumbing: SandboxFrame, src/lib/sandbox/*,
iframeSubdomain, previewInjectedScript.
- nsite preview: NsitePreviewDialog, NsitePermissionManager,
NsitePermissionPrompt, useNsiteSignerRpc, nsitePermissions,
nsiteNostrProvider, NsitePlayerContext.
- webxdc runtime: WebxdcEmbed, Webxdc, GameControls, useWebxdc,
webxdcMeta, public/cartridge.png, NOSTR_WEBXDC.md,
@webxdc/types dependency, kindLabels/signerWithNudge entries
for kinds 4932/20932.
- AppConfig: drop sandboxDomain, showWebxdc, feedIncludeWebxdc.
- extraKinds: drop kind 1063 webxdc entry; sidebar drops 'webxdc'.
- ComposeBox: .xdc uploads now flow through the generic file path
(no UUID injection, no manifest extraction, .xdc removed from
the file picker accept list).
- NoteContent / FileMetadataContent: webxdc branches removed; .xdc
attachments fall through to the generic file card.
- LayoutContext / CenterColumnContext: only consumed by the removed
fullscreen preview panels — deleted along with its provider in
AppRouter.
NsiteCard keeps its rich link-preview card but loses the Run button
and preview dialog. AppHandlerContent keeps a kind 35128 `a`-tag
reference but replaces 'Run' with an external 'Open Site' link to
`<pubkeyB36><dTag>.nsite.lol`. The standard HTML iframe `sandbox`
attribute used by SpotifyEmbed/TweetEmbed/RedditEmbed is unrelated
to iframe.diy and stays.
Squashed re-application of 15 local commits onto the new route-level
layout system (refactor: replace useLayoutOptions store with route-level
layout choice). Drops the obsolete useLayoutOptions({ fullBleed: true })
calls; the About page and the two guide pages instead live under the
wide FundraiserLayout route group in AppRouter.
Routing
- /about, /about/donors, /about/activists are now the canonical paths,
in the wide layout (no max-width cap so sections can span the
viewport with their own backgrounds).
- /help, /help/donors, /help/activists become <Navigate> redirects so
existing bookmarks and links keep working.
About page (new src/pages/AboutPage.tsx)
A landing-style document modeled on https://soapbox.pub/agora,
brought in-app to explain how the platform works. Five sections:
1. Hero (dark navy + world-map texture + orange halos), Bebas Neue
italic headline with an inline orange highlighter behind the
brand name, three trust chips, and Donor / Activist Guide CTAs.
Tilted Venezuelan sample-campaign card on lg+, hidden on mobile.
On mobile the H1 fits on one line via text-4xl + a conditional
<br className="hidden sm:inline" />.
2. Three steps. No middleman. (cream) 3-up white cards with 4:3
step images and corner 01 / 02 / 03 numerals.
3. Two ways to get paid. (white) Bitcoin Public Payments vs.
Bitcoin Silent Payments compare cards with gradient header
strips. Public-Payments trade-off carries the above-ground-
activism warning; Silent-Payments trade-off is five bold-headline
bullets (few wallets, slow, no push notifications, buggy,
no public counts). Below: a primary-tinted No-Custody banner
plus a 3-column comparison grid (Unlike GoFundMe / Unlike
GiveSendGo / Unlike other 'Bitcoin' platforms).
4. Frequently asked. (cream) Three integrated FAQ chapters in
page flow (Getting started / Bitcoin donations / About Nostr),
each with a Bebas Neue numeral + Inter Bold heading + card-row
accordion items with a left orange accent on hover/open.
5. Pick the side you're on. (white) Two large image-led guide
cards (Donor / Activist) using the soapbox.pub photography.
Closes with a quiet 'Still stuck? Follow Team Soapbox' line
linking in-app to the pack via the /:nip19 route.
Typography is Bebas Neue (font-display) italic font-normal with
WebkitTextStroke for the hero H1 and the step numerals only; every
other heading uses Inter Bold (font-sans font-bold tracking-tight).
Bebas Neue is never font-bold (synthetic bold renders as smear at
display sizes).
Em dashes have been removed throughout the page and the
HelpFAQSection component. Box-drawing chars (U+2500) in section
banner comments are not em dashes and stay.
Section backgrounds alternate dark → cream → white → cream → white.
The dark and cream sections keep their literal palette in both light
and dark mode (an editorial choice that gives the page its
landing-page identity rather than being just another themed surface).
Donor + Activist Guides (new block-based design)
Both pages now compose from a typed sequence of GuideBlock variants
defined in helpContent.ts. Each block kind is rendered by a dedicated
component under src/components/guide/:
- GuideTLDR top-of-page summary card with lede + checklist
- GuideSteps numbered vertical flow of short steps
- PaymentComparisonTable Public vs. Silent side-by-side. Three-column
grid on desktop, two stacked tinted cards on
mobile. Row content driven by audience flag.
- CalloutCard tinted info / warning / danger / success blocks
- OptionGrid two-column tile grid for privacy and cash-out
options
- GuideProse plain prose escape hatch
- InlinePaymentBadge small pill that distinguishes the two payment
options
- index.ts barrel
Content is rewritten throughout to reflect current reality: campaigns
can accept Public only, Silent only, or both; the QR code carries
both endpoints when both are accepted; wallets without silent-
payments support fall back to a regular Bitcoin transaction; silent
payments are slow, scan-based, lack push notifications, are
bleeding-edge, and produce no public donation counts.
Activist Guide structure:
TLDR
How receiving works
What everyone can see (intentionally before the table)
Public vs. Silent comparison
A note on silent payments today (calm prose, not an alarm callout)
Move donations promptly
Cashing out privately (silent-payments hop, Lightning
swap, coinjoin, P2P with brokers,
spend it directly)
Avoid centralized tumblers
Donor Guide structure:
TLDR
How a donation flows
Public vs. Silent comparison
Public donations are visible on-chain forever callout
Donating privately option grid
Consumer apps can't make you anonymous callout
A note on silent payments today
Other touchpoints
- Sidebar (sidebarItems.tsx): Help label → About, icon LifeBuoy → Info,
path /help → /about.
- Top nav profile menu (TopNav.tsx): Help → About.
- Site footer (AppRouter.tsx inline): Help → About.
- AccountSwitcher dropdown: Help → About.
- LandingHero FAQ button → /about#faq.
- HelpTip popover footer link → /about#faq.
- GuideHero back link → /about, label 'Back to About', wider
max-w-5xl on lg+ container so it sits well on the now-full-bleed
hero. Inner overlay min-height bumped to 320px on lg+.
- CampaignsPage 'How it works' button → /about.
New assets in /public/about/ pulled from soapbox.pub:
- world-map-bg.png (hero + textures)
- venezuela-libertad-presos-politicos.png (hero sample-card image)
- donor-guide-freedom-libertad.jpeg
- activist-guide-unity.png
Step photos in /public/help/ (step-1-account.jpg, step-2-send.jpg,
step-3-spend.jpg) for the Three Steps section.
HelpFAQSection gains:
- variant: 'list' | 'cards' (default 'list')
- tabs: boolean (only meaningful with variant='cards')
- listTone: 'default' | 'reference' (quieter category labels and more
breathable accordion items for the About page; existing inline
callers keep the default pill style)
In 'reference' mode each accordion item gets a card-row treatment
(rounded white card, subtle border, hover lifts to primary/40 border,
left orange accent rule driven off data-state=open).
helpContent.ts FAQ content (FAQItem / FAQCategory and templates) is
left untouched. Only the donor/activist guide section was rewritten
into GuideBlock[] arrays.