Add toasts, Settings tabs, Clients page and item presets (Phase E1)
- Global Carbon toast stack replaces scattered inline notices; errors persist until closed. - Settings is split into eight tabs with one Save and a remembered last tab; Numbering embeds the series settings. - New Clients page with a DataTable, shared add/edit modal, structured addresses, GST category (informational), per-client notes and payment terms. The composed address string is kept for back-compat. Deleting a client warns about invoice count; issued invoices keep their own snapshot. - Migration M7 adds the client columns and item_presets; existing clients with a GSTIN are backfilled to registered_regular. - New invoice form: client ComboBox with type-ahead and inline create, client defaults applied on selection, and an Add from preset control. - Rust tests for M7 (fresh and v6 upgrade), client validation and CRUD, presets CRUD; vitest for the toast store and new helpers. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { createContext, useContext, useMemo, useRef, useSyncExternalStore, type ReactNode } from "react";
|
||||
import { ToastNotification } from "@carbon/react";
|
||||
import { createToastStore, type ToastStore } from "../lib/toastStore";
|
||||
|
||||
export interface ToastApi {
|
||||
success: (title: string, subtitle?: string) => number;
|
||||
info: (title: string, subtitle?: string) => number;
|
||||
warning: (title: string, subtitle?: string) => number;
|
||||
/** Errors stay until the user closes them. */
|
||||
error: (title: string, subtitle?: string) => number;
|
||||
dismiss: (id: number) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastApi | null>(null);
|
||||
|
||||
function apiFor(store: ToastStore): ToastApi {
|
||||
return {
|
||||
success: (t, s) => store.add("success", t, s),
|
||||
info: (t, s) => store.add("info", t, s),
|
||||
warning: (t, s) => store.add("warning", t, s),
|
||||
error: (t, s) => store.add("error", t, s),
|
||||
dismiss: store.dismiss,
|
||||
};
|
||||
}
|
||||
|
||||
/** Stacked top-right toasts. Place it inside the Carbon <Theme> so the tokens apply. */
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const storeRef = useRef<ToastStore | null>(null);
|
||||
if (!storeRef.current) storeRef.current = createToastStore();
|
||||
const store = storeRef.current;
|
||||
const toasts = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||
const api = useMemo(() => apiFor(store), [store]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={api}>
|
||||
{children}
|
||||
{/* The live region stays mounted so screen readers announce toasts as they are added. */}
|
||||
<div className="voiced-toasts" aria-live="polite" aria-relevant="additions" role="region" aria-label="Notifications">
|
||||
{toasts.map((t) => (
|
||||
<ToastNotification
|
||||
key={t.id}
|
||||
kind={t.kind}
|
||||
lowContrast
|
||||
title={t.title}
|
||||
subtitle={t.subtitle}
|
||||
role={t.kind === "error" ? "alert" : "status"}
|
||||
onClose={() => {
|
||||
store.dismiss(t.id);
|
||||
return false;
|
||||
}}
|
||||
onCloseButtonClick={() => store.dismiss(t.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast(): ToastApi {
|
||||
const api = useContext(ToastContext);
|
||||
if (!api) throw new Error("useToast must be used inside <ToastProvider>");
|
||||
return api;
|
||||
}
|
||||
Reference in New Issue
Block a user