feat(foundry): add managed data consumers and agent settings

This commit is contained in:
Codex
2026-07-19 15:03:54 +03:00
parent 5f583caa05
commit aac44d057f
35 changed files with 4534 additions and 243 deletions
@@ -0,0 +1,86 @@
import type { ReactNode } from "react";
import { HeaderAvatar } from "./AppHeader.js";
import { Icon, type IconName } from "./Icon.js";
import { Window } from "./Window.js";
export interface FeatureSettingsSection<T extends string = string> {
id: T;
label: ReactNode;
group?: string;
icon?: IconName;
disabled?: boolean;
}
export interface FeatureSettingsWindowProps<T extends string = string> {
open: boolean;
title: ReactNode;
subtitle?: ReactNode;
identity: {
title: ReactNode;
subtitle?: ReactNode;
avatarLabel: string;
avatarUrl?: string;
};
sections: readonly FeatureSettingsSection<T>[];
activeSection: T;
onSectionChange: (section: T) => void;
onClose: () => void;
children: ReactNode;
footer?: ReactNode;
}
export function FeatureSettingsWindow<T extends string>({
open,
title,
subtitle,
identity,
sections,
activeSection,
onSectionChange,
onClose,
children,
footer,
}: FeatureSettingsWindowProps<T>) {
return (
<Window
open={open}
title={title}
subtitle={subtitle}
size="lg"
className="nodedc-feature-settings-window"
footer={footer}
closeOnBackdrop={false}
onClose={onClose}
>
<div className="nodedc-feature-settings">
<aside className="nodedc-feature-settings__navigation" aria-label="Разделы настроек">
<div className="nodedc-feature-settings__identity">
<HeaderAvatar label={identity.avatarLabel} imageUrl={identity.avatarUrl} />
<span><strong>{identity.title}</strong>{identity.subtitle ? <small>{identity.subtitle}</small> : null}</span>
</div>
<nav>
{sections.map((section, index) => {
const showGroup = section.group && section.group !== sections[index - 1]?.group;
return (
<div key={section.id} className="nodedc-feature-settings__navigation-entry">
{showGroup ? <span className="nodedc-feature-settings__group">{section.group}</span> : null}
<button
type="button"
className="nodedc-feature-settings__section"
data-active={section.id === activeSection ? "true" : undefined}
disabled={section.disabled}
onClick={() => onSectionChange(section.id)}
>
{section.icon ? <Icon name={section.icon} size={16} /> : null}
<span>{section.label}</span>
</button>
</div>
);
})}
</nav>
</aside>
<section className="nodedc-feature-settings__content">{children}</section>
</div>
</Window>
);
}
+108
View File
@@ -0,0 +1,108 @@
import type { ReactNode } from "react";
import { Dropdown } from "./Dropdown.js";
import { HeaderAvatar } from "./AppHeader.js";
import { Icon, type IconName } from "./Icon.js";
export interface UserProfileMenuAction {
id: string;
label: ReactNode;
icon?: IconName;
href?: string;
disabled?: boolean;
tone?: "default" | "danger";
onSelect?: () => void;
}
export interface UserProfileMenuProps {
displayName: string;
subtitle?: string;
avatarUrl?: string;
coverImageUrl?: string;
triggerLabel?: ReactNode;
actions: readonly UserProfileMenuAction[];
}
export function UserProfileMenu({
displayName,
subtitle,
avatarUrl,
coverImageUrl,
triggerLabel,
actions,
}: UserProfileMenuProps) {
const label = displayName || "NODE.DC";
return (
<Dropdown
placement="bottom-end"
width={296}
offset={10}
surfaceClassName="nodedc-user-profile-menu"
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
<button
ref={setTriggerRef}
type="button"
className="nodedc-user-profile-menu__trigger"
title={label}
aria-label={`Меню пользователя ${label}`}
aria-expanded={open}
aria-controls={surfaceId}
onClick={toggle}
>
{triggerLabel === null ? null : <span className="nodedc-user-profile-menu__trigger-label">{triggerLabel ?? label}</span>}
<HeaderAvatar label={label} imageUrl={avatarUrl} />
</button>
)}
>
{({ close }) => (
<div className="nodedc-user-profile-card">
<div
className="nodedc-user-profile-card__cover"
style={coverImageUrl ? { backgroundImage: `linear-gradient(180deg, rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.52)), url(${JSON.stringify(coverImageUrl)})` } : undefined}
>
<HeaderAvatar label={label} imageUrl={avatarUrl} />
<strong>{label}</strong>
{subtitle ? <span>{subtitle}</span> : null}
</div>
<div className="nodedc-user-profile-card__actions">
{actions.map((action) => {
const content = (
<>
{action.icon ? <Icon name={action.icon} size={16} /> : <span />}
<span>{action.label}</span>
</>
);
if (action.href && !action.disabled) {
return (
<a
key={action.id}
className="nodedc-user-profile-card__action"
data-tone={action.tone === "danger" ? "danger" : undefined}
href={action.href}
onClick={() => close()}
>
{content}
</a>
);
}
return (
<button
key={action.id}
type="button"
className="nodedc-user-profile-card__action"
data-tone={action.tone === "danger" ? "danger" : undefined}
disabled={action.disabled}
onClick={() => {
action.onSelect?.();
close();
}}
>
{content}
</button>
);
})}
</div>
</div>
)}
</Dropdown>
);
}
+2
View File
@@ -8,6 +8,7 @@ export * from "./ConfirmationModal.js";
export * from "./Dropdown.js";
export * from "./DragDrop.js";
export * from "./Field.js";
export * from "./FeatureSettingsWindow.js";
export * from "./Glass.js";
export * from "./Inspector.js";
export * from "./Icon.js";
@@ -19,5 +20,6 @@ export * from "./StatusBadge.js";
export * from "./Settings.js";
export * from "./SharingModals.js";
export * from "./Toolbar.js";
export * from "./UserProfileMenu.js";
export * from "./Window.js";
export * from "./WorkspaceWindow.js";