From 18a03b12aab7f54d0414f13c0a774c4cb17f18e5 Mon Sep 17 00:00:00 2001 From: olutundee <195462625+olutundee@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:55:00 +0100 Subject: [PATCH] refactor: extract styled-components definitions to theme config (#416) --- messages/ar.json | 16 + messages/es.json | 16 + src/__tests__/catalog-parity.test.ts | 2 + src/components/AmountInput.tsx | 16 +- src/components/EmailAuthModal.tsx | 82 +++-- src/components/Toast.tsx | 204 ++++++++++- src/hooks/useSessionTimeout.ts | 9 +- src/lib/webauthn.ts | 6 +- src/screens/Explore.tsx | 63 +++- src/screens/Portfolio.tsx | 11 +- src/screens/admin/AdminConsole.test.tsx | 2 +- src/screens/admin/AdminConsole.tsx | 139 ++------ src/screens/admin/OracleForms.tsx | 135 ++----- src/screens/admin/RegistryTable.tsx | 68 +--- src/screens/admin/utils.ts | 1 - src/screens/creator/CreatorApplication.tsx | 18 +- src/screens/creator/CreatorDashboard.tsx | 53 +-- src/screens/creator/ProjectBuilder.tsx | 95 +---- src/screens/sharedStyles.ts | 20 +- src/theme/breakpoints.ts | 22 ++ src/theme/colors.ts | 101 ++++++ src/theme/index.ts | 13 + src/theme/shadows.ts | 13 + src/theme/spacing.ts | 84 +++++ src/theme/styles.ts | 396 +++++++++++++++++++++ src/theme/theme.test.ts | 163 +++++++++ src/theme/theme.ts | 68 ++++ src/theme/typography.ts | 54 +++ 28 files changed, 1365 insertions(+), 505 deletions(-) create mode 100644 src/theme/breakpoints.ts create mode 100644 src/theme/colors.ts create mode 100644 src/theme/index.ts create mode 100644 src/theme/shadows.ts create mode 100644 src/theme/spacing.ts create mode 100644 src/theme/styles.ts create mode 100644 src/theme/theme.test.ts create mode 100644 src/theme/theme.ts create mode 100644 src/theme/typography.ts diff --git a/messages/ar.json b/messages/ar.json index 4e277c4..dcbd594 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -13,6 +13,7 @@ "testnet": "شبكة الاختبار (نشطة)", "networkStatus": "الشبكة: شبكة الاختبار نشطة", "portfolio": "محفظتك", + "watchlist": "قائمة المراقبة", "switchToDark": "التبديل إلى الوضع الداكن", "switchToLight": "التبديل إلى الوضع الفاتح", "language": "اللغة", @@ -110,6 +111,21 @@ "loadMore": "عرض {count} المزيد", "showingCount": "عرض {shown} من {total} مشاريع" }, + "Watchlist": { + "title": "قائمة المراقبة الخاصة بك", + "sub": "السندات التي تتابعها. حفظ سند لا يعني الاستثمار — بل يبقيه في متناول اليد ويشير إلى وقت فتحه للتمويل.", + "cardFundedFromPool": "ممول من المجمع", + "emptyTitle": "لا شيء محفوظ حتى الآن", + "emptySub": "افتح سنداً من استكشف وانقر على النجمة لإضافته هنا. سنشير هنا عندما يكون السند المحفوظ مفتوحاً للتمويل.", + "added": "تمت الإضافة إلى قائمة المراقبة", + "removed": "تمت الإزالة من قائمة المراقبة", + "saveLabel": "إضافة {name} إلى قائمة المراقبة", + "removeLabel": "إزالة {name} من قائمة المراقبة", + "availableBanner": "{count, plural, one {سند واحد متابع مفتوح للتمويل الآن.} other {# سندات متابعة مفتوحة للتمويل الآن.}}", + "statusOpen": "مفتوح للتمويل", + "statusUpcoming": "غير متاح بعد", + "notYetAvailable": "سنشير هنا بمجرد فتحه." + }, "Deposit": { "stepAmount": "المبلغ", "stepReview": "مراجعة", diff --git a/messages/es.json b/messages/es.json index 49811fc..bf924c3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -13,6 +13,7 @@ "testnet": "Testnet (active)", "networkStatus": "Network: Testnet active", "portfolio": "Your portfolio", + "watchlist": "Lista de seguimiento", "switchToDark": "Switch to dark", "switchToLight": "Switch to light", "language": "Language", @@ -110,6 +111,21 @@ "loadMore": "Show {count} more", "showingCount": "Showing {shown} of {total} projects" }, + "Watchlist": { + "title": "Tu lista de seguimiento", + "sub": "Los bonos que sigues. Guardar uno no invierte — lo mantiene a mano y te avisa cuando se abre a financiación.", + "cardFundedFromPool": "Financiado por el fondo", + "emptyTitle": "Nada guardado por ahora", + "emptySub": "Abre un bono desde Explorar y toca la estrella para añadirlo aquí. Te avisaremos cuando un bono guardado esté abierto a financiación.", + "added": "Añadido a la lista de seguimiento", + "removed": "Eliminado de la lista de seguimiento", + "saveLabel": "Añadir {name} a la lista de seguimiento", + "removeLabel": "Eliminar {name} de la lista de seguimiento", + "availableBanner": "{count, plural, one {# bono seguido está abierto a financiación ahora mismo.} other {# bonos seguidos están abiertos a financiación ahora mismo.}}", + "statusOpen": "Abierto a financiación", + "statusUpcoming": "Aún no disponible", + "notYetAvailable": "Te avisaremos aquí en cuanto se abra." + }, "Deposit": { "stepAmount": "Amount", "stepReview": "Review", diff --git a/src/__tests__/catalog-parity.test.ts b/src/__tests__/catalog-parity.test.ts index 53c262b..3fd8a23 100644 --- a/src/__tests__/catalog-parity.test.ts +++ b/src/__tests__/catalog-parity.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest' import en from '../../messages/en.json' import fr from '../../messages/fr.json' import es from '../../messages/es.json' +import ar from '../../messages/ar.json' function getLeafKeys(obj: Record, prefix = ''): string[] { const keys: string[] = [] @@ -21,6 +22,7 @@ describe('Message catalog parity', () => { it.each([ ['fr.json', fr], ['es.json', es], + ['ar.json', ar], ] as const)('en.json and %s have identical key sets', (_name, catalog) => { const enKeys = getLeafKeys(en).sort() const catalogKeys = getLeafKeys(catalog).sort() diff --git a/src/components/AmountInput.tsx b/src/components/AmountInput.tsx index d9a83be..c3d99a0 100644 --- a/src/components/AmountInput.tsx +++ b/src/components/AmountInput.tsx @@ -57,7 +57,7 @@ export function AmountInput({ const liveMsg = overCap && !wasOverCap ? (capMessage ?? '') : '' - const set = (v: number) => onChange?(String(v)) + const set = (v: number) => onChange?.(String(v)) return (
@@ -99,7 +99,7 @@ export function AmountInput({ alignItems: 'center', gap: 10, background: 'var(--surface)', - border: `p1px solid ${overCap ? 'var(--solar)' : 'var(--ink-12)'}`, + border: `1px solid ${overCap ? 'var(--solar)' : 'var(--ink-12)'}`, borderRadius: 'var(--radius-input)', padding: '0 16px', height: 64, @@ -108,7 +108,7 @@ export function AmountInput({ > { const v = sanitizeAmount(e.target.value) // If editing existing value, typing should replace not append when field was pre-filled - onChange?(v) + onChange?.(v) }} style={{ flex: 1, @@ -134,7 +134,7 @@ export function AmountInput({ fontWeight: 600, fontSize: 'var(--type-data-display)', color: 'var(--ink)', - fontFeatureSettings: '"tnum' 1', + fontFeatureSettings: '"tnum" 1', }} /> - Min 1 USDC — Max {cap ?? ‘⁊7} USDC + Min 1 USDC — Max {cap ?? '—'} USDC

{chips.map((c) => ( - )} + ))} {cap != null && maxChipLabel && (
{submitted ? ( -
+

{t('linkSentMessage', { email })}

-
) : (
@@ -194,10 +192,10 @@ export function EmailAuthModal({ open, onClose, onSuccess, onSocialLogin }: Emai onChange={(e) => { setEmail(e.target.value) setBypassWarning(false) - } + }} placeholder="you@example.com" required - style={ + style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--type-body)', padding: '12px 14px', @@ -206,7 +204,7 @@ export function EmailAuthModal({ open, onClose, onSuccess, onSocialLogin }: Emai background: 'var(--canvas)', color: 'var(--ink)', outline: 'none', - } + }} /> {/* Social Account Collision Warning */} @@ -224,7 +222,7 @@ export function EmailAuthModal({ open, onClose, onSuccess, onSocialLogin }: Emai variant="primary" size="lg" disabled={showConflict || !email} - style={ marginTop: 8 } + style={{ marginTop: 8 }} > {t('submitCta')} @@ -234,15 +232,29 @@ export function EmailAuthModal({ open, onClose, onSuccess, onSocialLogin }: Emai {!submitted && (
-
- - or +
+ + or -
+
{bioError && ( -

+

{bioError}

)} @@ -254,7 +266,7 @@ export function EmailAuthModal({ open, onClose, onSuccess, onSocialLogin }: Emai size="md" onClick={handleBiometricLogin} disabled={!email || bioLoading} - style={ flex: 1 } + style={{ flex: 1 }} > Face ID / Touch ID Login @@ -264,7 +276,7 @@ export function EmailAuthModal({ open, onClose, onSuccess, onSocialLogin }: Emai size="md" onClick={handleBiometricRegister} disabled={!email || bioLoading} - style={ flex: 1 } + style={{ flex: 1 }} > Register Biometric diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index f389a71..ea60e85 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1 +1,203 @@ -'client'\n\nimport {\n createContext,\n useContext,\n useState,\n useCallback,\n type CSSProperties,\n type ReactNode,\n} from 'react'\nimport { CloseIcon } from './icons'\n\n/**\n * Heliobond Toast — enters and exits from the same edge, swipe-to-dismiss in\n * spirit. Tones use an ink/semantic label + icon, never color alone. State\n * changes that matter (preview, balance) belong in aria-live regions elsewhere;\n * this is for transient confirmations.\n */\nexport type ToastTone = 'neutral' | 'success' | 'error' | 'solar'\n\nexport interface ToastProps {\n tone?: ToastTone\n title?: string\n message?: string\n action?: ReactNode\n onDismiss?: () => void\n style?: CSSProperties\n}\n\nexport function Toast({ tone = 'neutral', title, message, action, onDismiss, style }: ToastProps) {\n const accents: Record = {\n neutral: 'var(--ink)',\n success: 'var(--growth)',\n error: 'var(--ember)',\n solar: 'var(--solar)',\n }\n const accent = accents[tone] || accents.neutral\n\n return (\n \n \n
\n {title && (\n \n {title}\n
\n }\n {message && (\n \n {message}\n
\n }\n {action &&
{action}
}\n
\n {onDismiss && (\n \n \n \n )}\n
\n )\n}\n\nexport interface ToastContextType {\n toast: (options: Omit & { duration?: number }) => void\n dismiss: (id?: string) => void\n}\n\nconst ToastContext = createContext(null)\n\nconst MAX_ACTIVE_TOASTS = 3\n\nexport function ToastProvider({ children }: { children: ReactNode }) {\n const [activeToasts, setActiveToasts] = useState<\n (ToastProps & { id: string; duration?: number })[]\n >([])\n\n const showToast = useCallback(\n (options: Omit & { duration?: number }) => {\n const id = Math.random().toString(36).substring(2, 9)\n setActiveToasts((prev) {\n const next = [...prev, { ...options, id }]\n return next.length > MAX_ACTIVE_TOASTS ? next.slice(next.length - MAX_ACTIVE_TOASTS) : next\n })\n\n const ms = options.duration ?? 5000\n if (ms > 0) {\n setTimeout(() => {\n setActiveToasts((prev) => prev.filter((t) => t.id !== id))\n }, ms)\n }\n },\n [],\n )\n\n const dismiss = useCallback((id?: string) => {\n if (id) {\n setActiveToasts((prev) => prev.filter((t) => t.id !== id))\n } else {\n setActiveToasts([])\n }\n }, [])\n\n return (\n \n {children}\n {activeToasts.length > 0 && (\n \n {activeToasts.map((activeToast) => (\n dismiss(activeToast.id)}\n style={activeToast.style}\n />\n ))}\n
\n )}\n \n )\n}\n\nexport function useToast() {\n const context = useContext(ToastContext)\n if (!context) {\n throw new Error('useToast must be used within a ToastProvider')\n }\n return context\n}\n \ No newline at end of file +'use client' + +import { + createContext, + useContext, + useState, + useCallback, + type CSSProperties, + type ReactNode, +} from 'react' +import { CloseIcon } from './icons' + +/** + * Heliobond Toast — enters and exits from the same edge, swipe-to-dismiss in + * spirit. Tones use an ink/semantic label + icon, never color alone. State + * changes that matter (preview, balance) belong in aria-live regions elsewhere; + * this is for transient confirmations. + */ +export type ToastTone = 'neutral' | 'success' | 'error' | 'solar' + +export interface ToastProps { + tone?: ToastTone + title?: string + message?: string + action?: ReactNode + onDismiss?: () => void + style?: CSSProperties + stackable?: boolean +} + +export function Toast({ tone = 'neutral', title, message, action, onDismiss, style }: ToastProps) { + const accents: Record = { + neutral: 'var(--ink)', + success: 'var(--growth)', + error: 'var(--ember)', + solar: 'var(--solar)', + } + const accent = accents[tone] || accents.neutral + + return ( +
+
+ ) +} + +export interface ToastContextType { + toast: (options: Omit & { duration?: number }) => void + dismiss: (id?: string) => void +} + +const ToastContext = createContext(null) + +const MAX_ACTIVE_TOASTS = 3 + +export function ToastProvider({ children }: { children: ReactNode }) { + const [activeToasts, setActiveToasts] = useState< + (ToastProps & { id: string; duration?: number })[] + >([]) + + const showToast = useCallback( + (options: Omit & { duration?: number }) => { + const id = Math.random().toString(36).substring(2, 9) + setActiveToasts((prev) => { + const next = [...prev, { ...options, id }] + return next.length > MAX_ACTIVE_TOASTS ? next.slice(next.length - MAX_ACTIVE_TOASTS) : next + }) + + const ms = options.duration ?? 5000 + if (ms > 0) { + setTimeout(() => { + setActiveToasts((prev) => prev.filter((t) => t.id !== id)) + }, ms) + } + }, + [], + ) + + const dismiss = useCallback((id?: string) => { + if (id) { + setActiveToasts((prev) => prev.filter((t) => t.id !== id)) + } else { + setActiveToasts([]) + } + }, []) + + return ( + + {children} + {activeToasts.length > 0 && ( +
+ {activeToasts.map((activeToast) => ( + dismiss(activeToast.id)} + style={activeToast.style} + /> + ))} +
+ )} +
+ ) +} + +export function useToast() { + const context = useContext(ToastContext) + if (!context) { + throw new Error('useToast must be used within a ToastProvider') + } + return context +} diff --git a/src/hooks/useSessionTimeout.ts b/src/hooks/useSessionTimeout.ts index b6442a5..d6d79f1 100644 --- a/src/hooks/useSessionTimeout.ts +++ b/src/hooks/useSessionTimeout.ts @@ -130,6 +130,11 @@ export function useSessionTimeout({ scheduleWarning() }, [scheduleWarning]) + const isWarningOpenRef = useRef(isWarningOpen) + useEffect(() => { + isWarningOpenRef.current = isWarningOpen + }, [isWarningOpen]) + // Track user activity with throttled event handler useEffect(() => { if (!enabled) { @@ -143,7 +148,7 @@ export function useSessionTimeout({ const handleUserActivity = () => { // Do not reset activity automatically while the warning modal is actively open // (user must explicitly click Extend Session) - if (isWarningOpen) return + if (isWarningOpenRef.current) return const now = Date.now() if (now - lastThrottleRef.current > throttleMs) { @@ -162,7 +167,7 @@ export function useSessionTimeout({ window.removeEventListener(event, handleUserActivity) }) } - }, [enabled, isWarningOpen, scheduleWarning, clearTimers, throttleMs]) + }, [enabled, scheduleWarning, clearTimers, throttleMs]) // Format MM:SS for countdown display const minutes = Math.floor(remainingSeconds / 60) diff --git a/src/lib/webauthn.ts b/src/lib/webauthn.ts index 59f7bef..d650dbd 100644 --- a/src/lib/webauthn.ts +++ b/src/lib/webauthn.ts @@ -39,7 +39,7 @@ async function postJSON(url: string, body: unknown): Promise { throw new Error(err.error || `HTTP ${res.status}`) } return res.json() -] +} export async function registerBiometric(username: string): Promise { const options = await postJSON('/webauthn/register/begin', { username }) @@ -69,7 +69,7 @@ export async function registerBiometric(username: string): Promise { }, } - return postJSON('/webauthn'/register/complete', { username, credential: credentialJSON }) + return postJSON('/webauthn/register/complete', { username, credential: credentialJSON }) } export async function loginBiometric(username: string): Promise { @@ -103,5 +103,5 @@ export async function loginBiometric(username: string): Promise { }, } - return postJSON('/webauthn'/login/complete', { username, credential: credentialJSON }) + return postJSON('/webauthn/login/complete', { username, credential: credentialJSON }) } diff --git a/src/screens/Explore.tsx b/src/screens/Explore.tsx index e371072..c859da7 100644 --- a/src/screens/Explore.tsx +++ b/src/screens/Explore.tsx @@ -4,7 +4,6 @@ import { useEffect, useState, type CSSProperties } from 'react' import { useTranslations } from 'next-intl' import { useSearchParams, useRouter } from 'next/navigation' import { ProjectCard, Tag, WatchlistButton } from '../components' -import { Pagination } from '../components/Pagination' import { HB_DATA, type Project, type ProjectType } from '../data' import { getProjects } from '../lib/api' @@ -23,7 +22,7 @@ const TYPES: (ProjectType | 'All')[] = ['All', 'Solar', 'Wind', 'Hydro'] * every breakpoint (the grid runs 1–4 columns), so a page boundary never leaves * a ragged half-row. */ -const PAGE_SIZE = 6 +const PAGE_SIZE = 12 export function Explore({ onOpen }: ExploreProps) { const t = useTranslations('Explore') @@ -36,7 +35,6 @@ export function Explore({ onOpen }: ExploreProps) { const [filter, setFilter] = useState( urlType && ['Solar', 'Wind', 'Hydro'].includes(urlType) ? urlType : 'All', ) - const [currentPage, setCurrentPage] = useState(1) useEffect(() => { getProjects() @@ -52,7 +50,7 @@ export function Explore({ onOpen }: ExploreProps) { const setFilterAndUrl = (next: ProjectType | 'All') => { setFilter(next) - setCurrentPage(1) + setVisibleCount(PAGE_SIZE) if (next === 'All') { router.replace('/explore', { scroll: false }) } else { @@ -61,11 +59,13 @@ export function Explore({ onOpen }: ExploreProps) { } const shown = filter === 'All' ? projects : projects.filter((p) => p.type === filter) - const totalPages = Math.max(1, Math.ceil(shown.length / PAGE_SIZE)) - const paged = shown.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE) + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE) + const paged = shown.slice(0, visibleCount) + const remaining = shown.length - visibleCount + const nextChunk = Math.min(PAGE_SIZE, remaining) useEffect(() => { - setCurrentPage(1) + setVisibleCount(PAGE_SIZE) }, [filter, projects.length]) return ( @@ -206,13 +206,48 @@ export function Explore({ onOpen }: ExploreProps) { ))}
{!loading && shown.length > 0 && ( - +
+
+ {t('showingCount', { shown: paged.length, total: shown.length })} +
+ {remaining > 0 && ( + + )} +
)} )} diff --git a/src/screens/Portfolio.tsx b/src/screens/Portfolio.tsx index 6eb6889..6de5c38 100644 --- a/src/screens/Portfolio.tsx +++ b/src/screens/Portfolio.tsx @@ -1,8 +1,9 @@ 'use client' -import { type CSSProperties, type ReactNode } from 'react' +import { type ReactNode } from 'react' import { useTranslations } from 'next-intl' import { Button, StatBlock, LiquidityMeter, Card } from '../components' +import { cardTitleLg as cardTitle } from '@/theme' import { Helio } from '../brand/Helio' import { HB_DATA } from '../data' import { useWallet } from '../wallet/WalletProvider' @@ -234,11 +235,3 @@ export function Portfolio({ onWithdraw, onDeposit }: PortfolioProps) { ) } - -const cardTitle: CSSProperties = { - fontFamily: 'var(--font-display)', - fontWeight: 700, - fontSize: 'var(--type-body-lg)', - margin: '0 0 10px', - color: 'var(--ink)', -} diff --git a/src/screens/admin/AdminConsole.test.tsx b/src/screens/admin/AdminConsole.test.tsx index 5b38087..b611af0 100644 --- a/src/screens/admin/AdminConsole.test.tsx +++ b/src/screens/admin/AdminConsole.test.tsx @@ -42,7 +42,7 @@ describe('AdminConsole money formatting helpers', () => { ) it('parses funded amounts with thousands separators', () => { - expect(parseFundedNum(',234,567')).toBe(1_234_567) + expect(parseFundedNum('1,234,567')).toBe(1_234_567) }) }) diff --git a/src/screens/admin/AdminConsole.tsx b/src/screens/admin/AdminConsole.tsx index 7ba6efc..5d5d32f 100644 --- a/src/screens/admin/AdminConsole.tsx +++ b/src/screens/admin/AdminConsole.tsx @@ -1,8 +1,28 @@ 'use client' -import { useState, type CSSProperties, type ReactNode } from 'react' +import { useState, type ReactNode } from 'react' import { useTranslations } from 'next-intl' import { Badge, Button, AddressChip, useToast } from '@/components' +import { + sectionCard, + statRow, + subtext, + consolePage, + header, + pageTitle, + sectionTitle, + statCell, + statCellLabel, + statValueRow, + statValue, + statUnit, + whitelistRow, + whitelistName, + whitelistNameText, + whitelistMeta, + whitelistData, + whitelistActions, +} from '@/theme' import { VAULT_STATS, REGISTRY, WHITELIST, type RegistryEntry, type Creator } from '@/data/admin' import { RegistryTable } from './RegistryTable' import { OracleForms } from './OracleForms' @@ -258,120 +278,3 @@ export { parseFundedNum } export function formatFunded(n: number): string { return sharedFormatMoney(n, { includeSymbol: true }) } - -const sectionCard: CSSProperties = { - background: 'var(--surface)', - border: '1px solid var(--ink-12)', - borderRadius: 'var(--radius-card)', - padding: 16, - boxShadow: 'var(--shadow-sm)', -} - -const statRow: CSSProperties = { - display: 'flex', - flexWrap: 'wrap', -} - -const subtext: CSSProperties = { - margin: 0, - fontFamily: 'var(--font-body)', - fontSize: 'var(--type-caption)', - lineHeight: 1.5, - color: 'var(--ink-60)', -} - -const consolePage: CSSProperties = { - fontFamily: 'var(--font-body)', - color: 'var(--ink)', -} - -const header: CSSProperties = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'flex-start', - gap: 16, - flexWrap: 'wrap', - marginBottom: 20, -} - -const pageTitle: CSSProperties = { - fontFamily: 'var(--font-display)', - fontWeight: 700, - fontSize: 'var(--type-h2)', - margin: 0, - color: 'var(--ink)', -} - -const sectionTitle: CSSProperties = { - fontFamily: 'var(--font-display)', - fontWeight: 700, - fontSize: 'var(--type-h5)', - margin: 0, - color: 'var(--ink)', -} - -const statCell: CSSProperties = { - flex: '1 1 0', - minWidth: 140, - padding: '14px 16px', -} - -const statCellLabel: CSSProperties = { - marginBottom: 6, -} - -const statValueRow: CSSProperties = { - display: 'flex', - alignItems: 'baseline', - gap: 5, - flexWrap: 'wrap', -} - -const statValue: CSSProperties = { - fontFamily: 'var(--font-data)', - fontWeight: 600, - fontSize: 'var(--type-h4)', - color: 'var(--ink)', - fontFeatureSettings: '"tnum" 1', - lineHeight: 1.1, -} - -const statUnit: CSSProperties = { - fontFamily: 'var(--font-data)', - fontSize: 'var(--type-fine)', - color: 'var(--ink-60)', -} - -const whitelistRow: CSSProperties = { - display: 'flex', - alignItems: 'center', - gap: 14, - flexWrap: 'wrap', - padding: '12px 0', -} - -const whitelistName: CSSProperties = { - minWidth: 180, - flex: '1 1 200px', -} - -const whitelistNameText: CSSProperties = { - fontWeight: 600, - fontSize: 'var(--type-small)', -} - -const whitelistMeta: CSSProperties = { - ...subtext, - fontSize: 'var(--type-eyebrow)', -} - -const whitelistData: CSSProperties = { - fontFamily: 'var(--font-data)', - fontFeatureSettings: '"tnum" 1', -} - -const whitelistActions: CSSProperties = { - display: 'flex', - gap: 8, - marginInlineStart: 'auto', -} diff --git a/src/screens/admin/OracleForms.tsx b/src/screens/admin/OracleForms.tsx index 1d859f7..35fbe63 100644 --- a/src/screens/admin/OracleForms.tsx +++ b/src/screens/admin/OracleForms.tsx @@ -3,6 +3,22 @@ import { useState, type CSSProperties, type ReactNode } from 'react' import { useTranslations } from 'next-intl' import { Button, FormField, FormInput, FormSelect, sanitizeAmount } from '@/components' +import { + panelStyle, + textInput, + helpText, + gridStyle, + formRowStyle, + fieldGrowStyle, + inputGroupStyle, + dataCaptionStyle, + warningBoxStyle, + warningTextStyle, + moneyStyle, + panelTitleStyle, + panelHeaderStyle, + panelBodyStyle, +} from '@/theme' import { type RegistryEntry } from '@/data/admin' import { clampScore, validateScores } from './utils' import { formatMoney, parseAmount } from '@/lib/format' @@ -129,31 +145,17 @@ export function OracleForms({ projects, liquid, onPushScores, onFund }: OracleFo borderColor: overLiquid ? 'var(--ember)' : undefined, }} /> - - USDC - + USDC
{overLiquid && ( -
-

- {t('fundExceeds')} -

+
+

{t('fundExceeds')}

)}

- {t('liquidHint')}{' '} - - ${formatMoney(liquid)} - - . {amountN > liquid ? t('fundExceeds') : t('fundOk')} + {t('liquidHint')} ${formatMoney(liquid)}.{' '} + {amountN > liquid ? t('fundExceeds') : t('fundOk')}

) } - -const cardTitle: CSSProperties = { - fontFamily: 'var(--font-display)', - fontWeight: 700, - fontSize: 'var(--type-body-lg)', - margin: 0, - color: 'var(--ink)', - letterSpacing: '-0.01em', -} -const subtleBlock: CSSProperties = { - fontFamily: 'var(--font-body)', - fontSize: 'var(--type-small)', - lineHeight: 1.5, - color: 'var(--ink-60)', - margin: '0 0 16px', -} - -const cardInner: CSSProperties = { - padding: 22, - height: '100%', - boxSizing: 'border-box', -} - -const scoreColumn: CSSProperties = { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: 8, -} - -const sectionHeaderTop: CSSProperties = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, -} - -const sectionHeaderBottom: CSSProperties = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, -} diff --git a/src/screens/creator/ProjectBuilder.tsx b/src/screens/creator/ProjectBuilder.tsx index 8a49e33..c21aa36 100644 --- a/src/screens/creator/ProjectBuilder.tsx +++ b/src/screens/creator/ProjectBuilder.tsx @@ -1,8 +1,18 @@ 'use client' -import { useId, useState, useEffect, type CSSProperties, type ReactNode } from 'react' +import { useId, useState, useEffect, type ReactNode } from 'react' import { useTranslations } from 'next-intl' import { ProjectCard, Tag, Card, UploadIcon } from '@/components' +import { + cardTitle, + subtle, + inputStyle, + labelText, + fieldLabel, + fieldSpacing, + hintText, + errorText, +} from '@/theme' import { PROJECT_TYPES, DRAFT_PROJECT, type ProjectType } from '@/data/creator' import { formatMoney } from '@/lib/format' @@ -155,11 +165,7 @@ export function ProjectBuilder() { />
{goalError && ( - )} @@ -189,11 +195,7 @@ export function ProjectBuilder() { border: '1px solid var(--ink)', }} /> - - {t('previewLabel')} - + {t('previewLabel')} {t('previewSub')} @@ -278,15 +280,8 @@ function DropZone({ }} /> -
- {label} -
-
+
{label}
+
{selectedFiles || hint}
@@ -312,66 +307,8 @@ function Field({ function Label({ htmlFor, children }: { htmlFor?: string; children: ReactNode }) { return ( -