- 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.
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
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;
|
|
}
|