feat(foundry): close the operational map data loop

This commit is contained in:
Codex
2026-07-20 20:45:05 +03:00
parent aac44d057f
commit a02c3ff3dd
44 changed files with 4158 additions and 811 deletions
+4 -3
View File
@@ -52,16 +52,17 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
shape?: "circle" | "rounded";
}
export function IconButton({
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton({
label,
shape = "circle",
className,
children,
type = "button",
...props
}: IconButtonProps) {
}, ref) {
return (
<button
ref={ref}
type={type}
className={cn("nodedc-icon-button", className)}
data-shape={shape === "circle" ? undefined : shape}
@@ -72,4 +73,4 @@ export function IconButton({
{children}
</button>
);
}
});
+9
View File
@@ -43,3 +43,12 @@ export interface GlassMaterialSurfaceProps extends HTMLAttributes<HTMLDivElement
export function GlassMaterialSurface({ children, className, ...props }: GlassMaterialSurfaceProps) {
return <div className={cn("nodedc-glass-material", className)} {...props}>{children}</div>;
}
export interface MapGlassSurfaceProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
/** Light translucent surface used only over Cesium/map imagery. */
export function MapGlassSurface({ children, className, ...props }: MapGlassSurfaceProps) {
return <div className={cn("nodedc-map-glass", className)} {...props}>{children}</div>;
}
+2
View File
@@ -21,6 +21,7 @@ import {
KeyRound,
LayoutDashboard,
ListChecks,
LocateFixed,
LockKeyhole,
MailPlus,
Maximize2,
@@ -71,6 +72,7 @@ const icons = {
inbox: Inbox,
key: KeyRound,
list: ListChecks,
target: LocateFixed,
lock: LockKeyhole,
mail: MailPlus,
minimize: Minimize2,
+69
View File
@@ -0,0 +1,69 @@
import { useEffect, type HTMLAttributes } from "react";
import { createPortal } from "react-dom";
import { cn } from "./cn.js";
import { Icon, type IconName } from "./Icon.js";
export type ToastTone = "success" | "error" | "warning" | "info" | "loading";
export type ToastItem = {
id: string;
tone: ToastTone;
title: string;
description?: string;
durationMs?: number | null;
};
const toastIcons: Record<ToastTone, IconName> = {
success: "check",
error: "alert",
warning: "alert",
info: "activity",
loading: "refresh",
};
export interface ToastCardProps extends HTMLAttributes<HTMLDivElement> {
item: ToastItem;
onDismiss?: (id: string) => void;
}
export function ToastCard({ item, onDismiss, className, ...props }: ToastCardProps) {
return (
<div
className={cn("nodedc-toast nodedc-glass-material nodedc-material-rim", className)}
data-tone={item.tone}
role={item.tone === "error" ? "alert" : "status"}
{...props}
>
<span className="nodedc-toast__icon" aria-hidden="true"><Icon name={toastIcons[item.tone]} /></span>
<span className="nodedc-toast__copy">
<strong>{item.title}</strong>
{item.description ? <small>{item.description}</small> : null}
</span>
{onDismiss ? (
<button type="button" className="nodedc-toast__dismiss" aria-label="Закрыть уведомление" onClick={() => onDismiss(item.id)}>
<Icon name="close" />
</button>
) : null}
</div>
);
}
export function ToastStack({ items, onDismiss }: { items: ToastItem[]; onDismiss: (id: string) => void }) {
useEffect(() => {
const timers = items.flatMap((item) => {
const duration = item.durationMs === undefined ? 4200 : item.durationMs;
return typeof duration === "number" && duration > 0
? [window.setTimeout(() => onDismiss(item.id), duration)]
: [];
});
return () => timers.forEach((timer) => window.clearTimeout(timer));
}, [items, onDismiss]);
if (typeof document === "undefined" || items.length === 0) return null;
return createPortal(
<div className="nodedc-toast-viewport nodedc-ui-root" aria-live="polite" aria-relevant="additions removals">
{items.map((item) => <ToastCard key={item.id} item={item} onDismiss={onDismiss} />)}
</div>,
document.body,
);
}
+1
View File
@@ -20,6 +20,7 @@ export * from "./StatusBadge.js";
export * from "./Settings.js";
export * from "./SharingModals.js";
export * from "./Toolbar.js";
export * from "./Toast.js";
export * from "./UserProfileMenu.js";
export * from "./Window.js";
export * from "./WorkspaceWindow.js";