Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
52bdfb7
fix: Add a confirm step when revoking a creator (#397)
kolawoleoluwaferanmi014-a11y Aug 31, 2026
21b9e32
fix: Add a confirm step when revoking a creator (#397)
kolawoleoluwaferanmi014-a11y Aug 31, 2026
ab1afd7
fix: Add a confirm step when revoking a creator (#397)
kolawoleoluwaferanmi014-a11y Aug 31, 2026
97c6048
fix: Add a confirm step when revoking a creator (#397)
kolawoleoluwaferanmi014-a11y Aug 31, 2026
03c9c1f
fix: Add a confirm step when revoking a creator (#397)
kolawoleoluwaferanmi014-a11y Aug 31, 2026
899002e
fix: Add a confirm step when revoking a creator (#397)
kolawoleoluwaferanmi014-a11y Aug 31, 2026
0bf121e
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
60cedec
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
ee4126b
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
c0488bd
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
c764858
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
6468331
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
97b1fd0
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
671de73
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
cd0f1ea
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
d4aa35b
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
c7ad4c5
fix(ci): resolve failing checks for #397
kolawoleoluwaferanmi014-a11y Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@
"rejectionReason": "السبب",
"actionRevoke": "إلغاء الاعتماد",
"actionApprove": "اعتماد",
"revokeConfirmTitle": "إلغاء اعتماد المبدع؟",
"revokeConfirmBody": "سيؤدي إلغاء اعتماد {name} إلى منعه من تسجيل مشاريع جديدة. هل تريد المتابعة؟",
"toastScoresTitle": "الدرجات مكتوبة على السلسلة",
"toastScoresMsg": "{name} أُعيد التحقق: ائتمان {credit}، أخضر {green}.",
"toastFundTitle": "تم نشر رأس المال",
Expand All @@ -213,6 +215,7 @@
"toastApprovedMsg": "يمكن لـ {name} تسجيل المشاريع الآن.",
"toastRevokedTitle": "تم إلغاء اعتماد المبدع",
"toastRevokedMsg": "لم يعد {name} قادراً على تسجيل مشاريع جديدة.",
"toastRevokedUndo": "تراجع",
"panelPushTitle": "إرسال تحديث درجات",
"panelPushHint": "يكتب قراءة Oracle جديدة في ProjectRegistry.",
"panelFundTitle": "تمويل مشروع",
Expand Down
5 changes: 5 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@
"statusRejected": "Rejected",
"rejectionReason": "Reason",
"actionRevoke": "Revoke",
"revokeConfirmTitle": "Revoke creator?",
"revokeConfirm": "Yes, revoke",
"revokeConfirmBody": "Are you sure you want to revoke {name}? You can undo this action.",
"revokeCancel": "Cancel",
"actionApprove": "Approve",
"toastScoresTitle": "Scores written on-chain",
"toastScoresMsg": "{name} re-verified: credit {credit}, green {green}.",
Expand All @@ -237,6 +241,7 @@
"toastApprovedMsg": "{name} can now register projects.",
"toastRevokedTitle": "Creator revoked",
"toastRevokedMsg": "{name} can no longer register new projects.",
"actionUndo": "Undo",
"panelPushTitle": "Push score update",
"panelPushHint": "Writes a new oracle reading to the ProjectRegistry.",
"panelFundTitle": "Fund a project",
Expand Down
4 changes: 4 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@
"rejectionReason": "Motivo",
"actionRevoke": "Revoke",
"actionApprove": "Approve",
"actionConfirmRevoke": "Sí, revocar",
"actionCancel": "Cancelar",
"revokeConfirmBody": "{name} ya no podrá registrar nuevos proyectos. Esta acción puede deshacerse.",
"toastRevokedUndo": "Deshacer",
"toastScoresTitle": "Scores written on-chain",
"toastScoresMsg": "{name} re-verified: credit {credit}, green {green}.",
"toastFundTitle": "Capital deployed",
Expand Down
12 changes: 12 additions & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@
"statusRejected": "Rejeté",
"rejectionReason": "Raison",
"actionRevoke": "Révoquer",
"revokeConfirm": "Oui, révoquer",
"revokeConfirmBody": "Cette action révoque l'accès du créateur au registre. Voulez-vous continuer ?",
"revokeCancel": "Annuler",
"revokeToast": "Créateur révoqué",
"revokeUndo": "Annuler"voquer",
"revokeConfirmBody": "La révocation retirera immédiatement l'accès de ce créateur. Vous pourrez annuler cette action.",
"revokeCancel": "Annuler",
"revokeToast": "Créateur révoqué",
"revokeUndo": "Annuler",
"actionApprove": "Approuver",
"toastScoresTitle": "Scores écrits sur la chaîne",
"toastScoresMsg": "{name} re-vérifié : crédit {credit}, vert {green}.",
Expand All @@ -229,6 +238,9 @@
"toastApprovedMsg": "{name} peut désormais enregistrer des projets.",
"toastRevokedTitle": "Créateur révoqué",
"toastRevokedMsg": "{name} ne peut plus enregistrer de nouveaux projets.",
"revokeConfirmTitle": "Révoquer ce créateur ?",
"revokeConfirmBody": "{name} ne pourra plus enregistrer de nouveaux projets. Vous pourrez annuler.",
"toastRevokedUndo": "Annuler la révocation",
"panelPushTitle": "Envoyer une mise à jour de scores",
"panelPushHint": "Écrit une nouvelle lecture oracle dans le ProjectRegistry.",
"panelFundTitle": "Financer un projet",
Expand Down
114 changes: 62 additions & 52 deletions src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use client'
'use client';

import {
createContext,
Expand All @@ -10,25 +10,20 @@ import {
} 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
undo:? () => void
onDismiss?: () => void
href?: string
style?: CSSProperties
}

export function Toast({ tone = 'neutral', title, message, action, onDismiss, href, style }: ToastProps) {
export function Toast({ tone = 'neutral', title, message, action, undo, onDismiss, href, style }: ToastProps) {
const accents: Record<ToastTone, string> = {
neutral: 'var(--ink)',
success: 'var(--growth)',
Expand All @@ -37,53 +32,80 @@ export function Toast({ tone = 'neutral', title, message, action, onDismiss, hre
}
const accent = accents[tone] || accents.neutral

const inner = (
const hasActions = Boolean(action) || Boolean(undo)

const content = (
<>
<span
style={{
style={
width: 4,
alignSelf: 'stretch',
borderRadius: 'var(--radius-pill)',
background: accent,
flex: '0 0 auto',
}}
}
aria-hidden="true"
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={ flex: 1, minWidth: 0 }>
{title && (
<div
style={{
style={
fontFamily: 'var(--font-body)',
fontWeight: 600,
fontSize: 'var(--type-data)',
color: 'var(--ink)',
marginBottom: message ? 2 : 0,
}}
}
>
{title}
</div>
)}
{message && (
<div
style={{
style={
fontFamily: 'var(--font-body)',
fontSize: 'var(--type-small)',
lineHeight: 1.45,
color: 'var(--ink-60)',
}}
}
>
{message}
</div>
)}
{action && <div style={{ marginTop: 10 }}>{action}</div>}
{undo && (
<div style={ marginTop: 10 }>
<button
type="button"
onClick={() => {
undo()
onDismiss?.()
}}
style={
fontFamily: 'var(--font-body)',
fontWeight: 600,
fontSize: 'var(--type-small)',
color: 'var(--solar)',
background: 'transparent',
border: 'none',
padding: 0,
cursor: 'pointer',
textDecoration: 'underline',
}
>
Undo
</button>
</div>
)}
{action && <div style={ marginTop: 10 }>{action}</div>}
</div>
</>
)

return (
<div
role="status"
style={{
role={hasActions ? 'alertdialog' : 'status'}
aria-label={hasActions ? (title || message) : undefined}
style={
display: 'flex',
alignItems: 'flex-start',
gap: 12,
Expand All @@ -95,32 +117,32 @@ export function Toast({ tone = 'neutral', title, message, action, onDismiss, hre
borderRadius: 'var(--radius-card)',
boxShadow: 'var(--shadow-md)',
...style,
}}
}
>
{href ? (
{href && !hasActions ? (
<a
href={href}
style={{
style={
display: 'flex',
alignItems: 'flex-start',
gap: 12,
flex: 1,
minWidth: 0,
textDecoration: 'none',
color: 'inherit',
}}
}
>
{inner}
{content}
</a>
) : (
inner
content
)}
{onDismiss && (
<button
type="button"
aria-label="Dismiss"
onClick={onDismiss}
style={{
style={
flex: '0 0 auto',
width: 28,
height: 28,
Expand All @@ -132,7 +154,7 @@ export function Toast({ tone = 'neutral', title, message, action, onDismiss, hre
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
}
>
<CloseIcon />
</button>
Expand All @@ -148,7 +170,7 @@ export interface ToastContextType {

const ToastContext = createContext<ToastContextType | null>(null)

const MAX_ACTIVE_TOASTS = 3
const MAX_ACTIVE_TOACTS = 3

export function ToastProvider({ children }: { children: ReactNode }) {
const [activeToasts, setActiveToasts] = useState<
Expand All @@ -158,16 +180,17 @@ export function ToastProvider({ children }: { children: ReactNode }) {
const showToast = useCallback(
(options: Omit<ToastProps, 'onDismiss'> & { duration?: number }) => {
const id = Math.random().toString(36).substring(2, 9)
const hasActions = Boolean(options.action) || Boolean(options.undo)
const duration = options.duration ?? (hasDctions ? 0 : 5000)
setActiveToasts((prev) => {
const next = [...prev, { ...options, id }]
return next.length > MAX_ACTIVE_TOASTS ? next.slice(next.length - MAX_ACTIVE_TOASTS) : next
const next = [...prev, { ...options, id, duration }]
return next.length > MAX_ACTIVE_TOACTS ? next.slice(next.length - MAX_ACTIVE_TOACTS) : next
})

const ms = options.duration ?? 5000
if (ms > 0) {
if (duration > 0) {
setTimeout(() => {
setActiveToasts((prev) => prev.filter((t) => t.id !== id))
}, ms)
}, duration)
}
},
[],
Expand All @@ -182,36 +205,23 @@ export function ToastProvider({ children }: { children: ReactNode }) {
}, [])

return (
<ToastContext.Provider value={{ toast: showToast, dismiss }}>
<ToastContext.Provider value={ toast: showToast, dismiss }>
{children}
{activeToasts.length > 0 && (
<div
role="status"
aria-live="polite"
aria-atomic="true"
style={{
position: 'fixed',
insetInlineEnd: 24,
bottom: 24,
zIndex: 9999,
display: 'flex',
flexDirection: 'column',
gap: 12,
alignItems: 'flex-end',
}}
>
<div role="status" aria-live="polite" aria-atomic="true" style={ position: 'fixed', insetEnd: 24, bottom: 24, zIndex: 9999, display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-end' }>
{activeToasts.map((activeToast) => (
<Toast
<Toas
key={activeToast.id}
tone={activeToast.tone}
title={activeToast.title}
message={activeToast.message}
action={activeToast.action}
undo={activeToast.undo}
href={activeToast.href}
onDismiss={() => dismiss(activeToast.id)}
style={activeToast.style}
/>
))}
)}
</div>
)}
</ToastContext.Provider>
Expand All @@ -224,4 +234,4 @@ export function useToast() {
throw new Error('useToast must be used within a ToastProvider')
}
return context
}
}
12 changes: 12 additions & 0 deletions src/screens/admin/AdminConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export function AdminConsole() {
status: Creator['status'],
rejectionReason?: string,
) => {
// Revoking a creator is consequential: confirm first, then offer undo.
if (status === 'pending' || status === 'rejected') {
const c = whitelist.find((x) => x.address === address)
if (!window.confirm(`${t('actionRevoke')} ${c?.name ?? 'Creator'}?`)) return
}
setWhitelist((list) =>
list.map((c) =>
c.address === address
Expand Down Expand Up @@ -96,6 +101,13 @@ export function AdminConsole() {
tone: toneMap[status],
title: titleMap[status],
message: messageMap[status],
action:
status !== 'approved'
? {
label: t('actionUndo'),
onClick: () => setCreatorStatus(address, 'approved'),
}
: undefined,
duration: 5000,
stackable: true,
})
Expand Down
Loading