Okno potwierdzenia zwarte, ostrzezenie tylko tam gdzie cos przepada
UKLAD Okno korzystalo z komponentu `Modal`, ktory jest zbudowany pod FORMULARZE: naglowek oddzielony kreska, przycisk zamkniecia w rogu, przewijana tresc i stopka oddzielona druga kreska. Przy imporcie kontaktow to sluszne, ale przy pytaniu na dwa zdania te same elementy rozdymaly okno: dwie linie podzialu i krzyzyk zajmowaly wiecej miejsca niz sama tresc. Okno potwierdzenia jest krotkie z natury, wiec dostaje uklad ciagly: ikona ostrzezenia jako okragla plakietka OBOK tytulu zamiast osobnego paska, przyciski tuz pod trescia. Krzyzyka nie ma, bo bylby trzecim sposobem powiedzenia "nie", obok przycisku "Anuluj", klawisza Escape i klikniecia w tlo. Naklatka, Escape i blokada przewijania tla zostaja takie same jak w `Modal`: powtarzamy zachowanie, nie uklad. OSTRZEZENIE PRZESTAJE BYC WSZEDZIE Trzy operacje mialy `destructive`, choc niczego nie niszcza: - zatrzymanie nagrywania: plik zostaje, nic nie przepada. Nieodwracalne jest wylacznie to, ze nagranej chwili nie da sie powtorzyc, - anulowanie zamowienia: zamowienie jest dokumentem finansowym i ZOSTAJE w dzienniku, co mowila zreszta sama tresc komunikatu, - przywrocenie grafiki domyslnej: plik da sie wgrac z powrotem, a "przywroc domyslna" to cofniecie, nie zniszczenie. Przy zatrzymaniu nagrywania tresc powtarzala przy tym ostrzezenie wlasnymi slowami, wiec to samo bylo napisane dwa razy. Ostrzezenie uzyte wszedzie przestaje cokolwiek znaczyc tam, gdzie strata jest prawdziwa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import { Icon } from './Icon'
|
||||
import { Modal } from './Modal'
|
||||
import { Button } from './ui'
|
||||
|
||||
/**
|
||||
@@ -76,41 +75,95 @@ export function ConfirmProvider({ children }: { children: ReactNode }) {
|
||||
<ConfirmContext.Provider value={api}>
|
||||
{children}
|
||||
|
||||
{pending && (
|
||||
<Modal
|
||||
title={pending.title}
|
||||
onClose={() => settle(false)}
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="secondary" onClick={() => settle(false)}>
|
||||
{pending.cancelLabel ?? 'Anuluj'}
|
||||
{pending && <ConfirmDialog options={pending} onSettle={settle} />}
|
||||
</ConfirmContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Samo okno pytania.
|
||||
*
|
||||
* NIE UZYWA `Modal`, I TO JEST SWIADOME
|
||||
* ------------------------------------------------------------------------
|
||||
* `Modal` jest zbudowany pod FORMULARZE: ma naglowek oddzielony kreska,
|
||||
* przycisk zamkniecia w rogu, przewijana tresc i stopke oddzielona druga
|
||||
* kreska. Przy imporcie kontaktow albo edycji banera to sluszne, ale przy
|
||||
* pytaniu na dwa zdania te same elementy rozdymaja okno: dwie linie
|
||||
* podzialu i krzyzyk zajmuja wtedy wiecej miejsca niz sama tresc.
|
||||
*
|
||||
* Okno potwierdzenia jest krotkie z natury, wiec dostaje uklad ciagly, bez
|
||||
* kresek, a wyjscie prowadzi przez przycisk "Anuluj". Krzyzyk bylby trzecim
|
||||
* sposobem powiedzenia "nie", obok przycisku, Escape i klikniecia w tlo.
|
||||
*
|
||||
* Naklatka, Escape i blokada przewijania tla sa te same co w `Modal`:
|
||||
* powtarzamy zachowanie, a nie uklad.
|
||||
*/
|
||||
function ConfirmDialog({
|
||||
options,
|
||||
onSettle,
|
||||
}: {
|
||||
options: ConfirmOptions
|
||||
onSettle: (value: boolean) => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') onSettle(false)
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
|
||||
const previous = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKey)
|
||||
document.body.style.overflow = previous
|
||||
}
|
||||
}, [onSettle])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-navy-900/40 p-4 sm:p-6"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-title"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) onSettle(false)
|
||||
}}
|
||||
>
|
||||
<div className="w-full rounded-lg border border-slate-100 bg-white p-5 shadow-xl sm:max-w-md sm:p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
{options.destructive && (
|
||||
<span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-alarm-50">
|
||||
<Icon name="alert-triangle" size={18} className="text-alarm-600" />
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<h2 id="confirm-title" className="text-base font-semibold text-slate-900">
|
||||
{options.title}
|
||||
</h2>
|
||||
{options.message && (
|
||||
<div className="mt-1.5 text-sm leading-relaxed text-slate-600">
|
||||
{options.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap justify-end gap-2">
|
||||
<Button type="button" variant="secondary" onClick={() => onSettle(false)}>
|
||||
{options.cancelLabel ?? 'Anuluj'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={pending.destructive ? 'danger' : 'primary'}
|
||||
onClick={() => settle(true)}
|
||||
variant={options.destructive ? 'danger' : 'primary'}
|
||||
onClick={() => onSettle(true)}
|
||||
autoFocus
|
||||
>
|
||||
{pending.confirmLabel ?? 'Potwierdź'}
|
||||
{options.confirmLabel ?? 'Potwierdź'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{pending.destructive && (
|
||||
<div className="flex items-start gap-3 rounded-md border border-alarm-500/30 bg-alarm-50 p-3">
|
||||
<Icon name="alert-triangle" size={20} className="mt-0.5 shrink-0 text-alarm-600" />
|
||||
<p className="text-sm leading-relaxed text-alarm-600">
|
||||
Tej operacji nie da się cofnąć.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{pending.message && (
|
||||
<div className="text-sm leading-relaxed text-slate-600">{pending.message}</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</ConfirmContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -320,7 +320,8 @@ function OrderDetailModal({ orderId, onClose }: { orderId: number; onClose: () =
|
||||
message:
|
||||
'Kursant nie dostanie dostępu do kursu. Zamówienie zostaje w dzienniku jako anulowane.',
|
||||
confirmLabel: 'Anuluj zamówienie',
|
||||
destructive: true,
|
||||
// bez `destructive`: zamowienie jest dokumentem finansowym
|
||||
// i zostaje w dzienniku. To zmiana stanu, nie zniszczenie.
|
||||
})
|
||||
if (ok) cancel.mutate()
|
||||
}}
|
||||
|
||||
@@ -152,7 +152,8 @@ export function BrandingSection() {
|
||||
title: 'Przywrócić grafikę domyślną?',
|
||||
message: 'Wgrany plik zostanie usunięty, a instancja wróci do wyglądu sprzed jego dodania.',
|
||||
confirmLabel: 'Przywróć domyślną',
|
||||
destructive: true,
|
||||
// bez `destructive`: grafike da sie wgrac z powrotem,
|
||||
// a "przywroc domyslna" to cofniecie, nie zniszczenie
|
||||
})
|
||||
if (ok) remove.mutate(kind)
|
||||
}}
|
||||
|
||||
@@ -102,17 +102,17 @@ export function SpeakQueue({
|
||||
if (recordingBusy) return
|
||||
|
||||
if (egressId) {
|
||||
/*
|
||||
* Bez `destructive`: plik zostaje i nic nie przepada. Nieodwracalne jest
|
||||
* wylacznie to, ze nagranej chwili nie da sie powtorzyc, a to za malo na
|
||||
* czerwony alarm. Ostrzezenie uzyte wszedzie traci znaczenie tam, gdzie
|
||||
* strata jest prawdziwa.
|
||||
*/
|
||||
const ok = await confirm({
|
||||
title: 'Zatrzymać nagrywanie?',
|
||||
message: (
|
||||
<>
|
||||
Nagrywanie tego spotkania zostanie zakończone, a plik trafi do zakładki Pliki.{' '}
|
||||
<strong>Tego fragmentu nie nagrasz ponownie</strong>: ponowne włączenie zacznie nowe
|
||||
nagranie od bieżącej chwili.
|
||||
</>
|
||||
),
|
||||
message:
|
||||
'Plik trafi do zakładki Pliki. Ponowne włączenie zacznie nowe nagranie od bieżącej chwili.',
|
||||
confirmLabel: 'Zatrzymaj nagrywanie',
|
||||
destructive: true,
|
||||
})
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user