Refine glass material and environment controls
This commit is contained in:
parent
4c74c22a6e
commit
0e030136bb
|
|
@ -47,6 +47,28 @@ const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
|
||||||
{ label: "Magenta", value: [215, 70, 255], hex: "#d746ff" },
|
{ label: "Magenta", value: [215, 70, 255], hex: "#d746ff" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type MaterialDraft = {
|
||||||
|
panelHex: string;
|
||||||
|
panelOpacity: number;
|
||||||
|
fieldHex: string;
|
||||||
|
fieldOpacity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const materialDefaults: Record<NodedcTheme, MaterialDraft> = {
|
||||||
|
dark: {
|
||||||
|
panelHex: "#16161a",
|
||||||
|
panelOpacity: 68,
|
||||||
|
fieldHex: "#101016",
|
||||||
|
fieldOpacity: 78,
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
panelHex: "#ffffff",
|
||||||
|
panelOpacity: 62,
|
||||||
|
fieldHex: "#ffffff",
|
||||||
|
fieldOpacity: 72,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
type ShareRole = "viewer" | "editor" | "admin";
|
type ShareRole = "viewer" | "editor" | "admin";
|
||||||
|
|
||||||
const shareRoleOptions = [
|
const shareRoleOptions = [
|
||||||
|
|
@ -208,6 +230,11 @@ function Preview({ title, note, children, className }: {
|
||||||
export function CatalogApp() {
|
export function CatalogApp() {
|
||||||
const [theme, setTheme] = useState<NodedcTheme>("dark");
|
const [theme, setTheme] = useState<NodedcTheme>("dark");
|
||||||
const [accentHex, setAccentHex] = useState("#ff2f92");
|
const [accentHex, setAccentHex] = useState("#ff2f92");
|
||||||
|
const [materialByTheme, setMaterialByTheme] = useState<Record<NodedcTheme, MaterialDraft>>(() => ({
|
||||||
|
dark: { ...materialDefaults.dark },
|
||||||
|
light: { ...materialDefaults.light },
|
||||||
|
}));
|
||||||
|
const [savedEnvironmentControl, setSavedEnvironmentControl] = useState<string | null>(null);
|
||||||
const workspace = useApplicationWorkspace<CatalogSection>();
|
const workspace = useApplicationWorkspace<CatalogSection>();
|
||||||
const { navigationOpen: guidelineOpen, activeView: activeSection, contentExpanded: panelExpanded } = workspace;
|
const { navigationOpen: guidelineOpen, activeView: activeSection, contentExpanded: panelExpanded } = workspace;
|
||||||
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
|
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
|
||||||
|
|
@ -219,7 +246,9 @@ export function CatalogApp() {
|
||||||
const [lightColor, setLightColor] = useState("#ff2f92");
|
const [lightColor, setLightColor] = useState("#ff2f92");
|
||||||
const [connectionColor, setConnectionColor] = useState("#404040");
|
const [connectionColor, setConnectionColor] = useState("#404040");
|
||||||
const [fillColor, setFillColor] = useState("#ff6caf");
|
const [fillColor, setFillColor] = useState("#ff6caf");
|
||||||
|
const [fillOpacity, setFillOpacity] = useState(100);
|
||||||
const [strokeColor, setStrokeColor] = useState("#2b2b36");
|
const [strokeColor, setStrokeColor] = useState("#2b2b36");
|
||||||
|
const [strokeOpacity, setStrokeOpacity] = useState(100);
|
||||||
const [windowOpen, setWindowOpen] = useState(false);
|
const [windowOpen, setWindowOpen] = useState(false);
|
||||||
const [sidePanelOpen, setSidePanelOpen] = useState(false);
|
const [sidePanelOpen, setSidePanelOpen] = useState(false);
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
|
@ -253,10 +282,22 @@ export function CatalogApp() {
|
||||||
const stageMediaSrc = mediaSource === "url" && externalMediaSrc ? externalMediaSrc : fileMediaSrc;
|
const stageMediaSrc = mediaSource === "url" && externalMediaSrc ? externalMediaSrc : fileMediaSrc;
|
||||||
|
|
||||||
const accent = useMemo(() => rgbFromHex(accentHex) ?? accents[0].value, [accentHex]);
|
const accent = useMemo(() => rgbFromHex(accentHex) ?? accents[0].value, [accentHex]);
|
||||||
|
const material = materialByTheme[theme];
|
||||||
|
const panelMaterial = useMemo(() => rgbFromHex(material.panelHex) ?? rgbFromHex(materialDefaults[theme].panelHex)!, [material.panelHex, theme]);
|
||||||
|
const fieldMaterial = useMemo(() => rgbFromHex(material.fieldHex) ?? rgbFromHex(materialDefaults[theme].fieldHex)!, [material.fieldHex, theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
applyNodedcTheme(document.documentElement, { theme, accent });
|
applyNodedcTheme(document.documentElement, {
|
||||||
}, [accent, theme]);
|
theme,
|
||||||
|
accent,
|
||||||
|
material: {
|
||||||
|
panel: panelMaterial,
|
||||||
|
panelOpacity: material.panelOpacity / 100,
|
||||||
|
field: fieldMaterial,
|
||||||
|
fieldOpacity: material.fieldOpacity / 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [accent, fieldMaterial, material.fieldOpacity, material.panelOpacity, panelMaterial, theme]);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current);
|
if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current);
|
||||||
|
|
@ -296,16 +337,89 @@ export function CatalogApp() {
|
||||||
|
|
||||||
const closeGuideline = workspace.closeNavigation;
|
const closeGuideline = workspace.closeNavigation;
|
||||||
|
|
||||||
|
const updateMaterial = (changes: Partial<MaterialDraft>) => {
|
||||||
|
setMaterialByTheme((current) => ({
|
||||||
|
...current,
|
||||||
|
[theme]: { ...current[theme], ...changes },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEnvironmentLayout = (control: string) => {
|
||||||
|
const draft = {
|
||||||
|
theme,
|
||||||
|
accentHex,
|
||||||
|
material,
|
||||||
|
environment: {
|
||||||
|
lightColor,
|
||||||
|
brightness,
|
||||||
|
glowDistance,
|
||||||
|
connectionType,
|
||||||
|
connectionColor,
|
||||||
|
usePortColors,
|
||||||
|
fillColor,
|
||||||
|
fillOpacity,
|
||||||
|
strokeColor,
|
||||||
|
strokeOpacity,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem("nodedc-design-guideline-draft", JSON.stringify(draft));
|
||||||
|
} catch {
|
||||||
|
// The preview remains useful in restricted browser contexts even without local persistence.
|
||||||
|
}
|
||||||
|
setSavedEnvironmentControl(control);
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedAction = (control: string) => (
|
||||||
|
<IconButton label={`Сохранить «${control}» в локальный layout`} onClick={() => saveEnvironmentLayout(control)}>
|
||||||
|
<Icon name="save" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
const environmentSections = [
|
const environmentSections = [
|
||||||
|
{
|
||||||
|
id: "application-material",
|
||||||
|
label: "Материал приложения",
|
||||||
|
description: "live draft",
|
||||||
|
tone: "accent" as const,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<ControlRow label="Акцент" action={savedAction("Акцент")}>
|
||||||
|
<ColorField label="Акцент приложения" value={accentHex} onChange={setAccentHex} />
|
||||||
|
</ControlRow>
|
||||||
|
<ControlRow label="Плашки" action={savedAction("Плашки")}>
|
||||||
|
<ColorField label="Цвет плашек" value={material.panelHex} onChange={(panelHex) => updateMaterial({ panelHex })} />
|
||||||
|
</ControlRow>
|
||||||
|
<ControlRow label="Прозрачность плашки" action={savedAction("Прозрачность плашки")}>
|
||||||
|
<RangeControl label="Прозрачность плашки" showLabel={false} value={material.panelOpacity} min={18} max={96} formatValue={(value) => `${value}%`} onChange={(panelOpacity) => updateMaterial({ panelOpacity })} />
|
||||||
|
</ControlRow>
|
||||||
|
<ControlRow label="Поля ввода" action={savedAction("Поля ввода")}>
|
||||||
|
<ColorField label="Цвет полей ввода" value={material.fieldHex} onChange={(fieldHex) => updateMaterial({ fieldHex })} />
|
||||||
|
</ControlRow>
|
||||||
|
<ControlRow label="Прозрачность полей" action={savedAction("Прозрачность полей")}>
|
||||||
|
<RangeControl label="Прозрачность полей" showLabel={false} value={material.fieldOpacity} min={18} max={100} formatValue={(value) => `${value}%`} onChange={(fieldOpacity) => updateMaterial({ fieldOpacity })} />
|
||||||
|
</ControlRow>
|
||||||
|
<p className="catalog-environment-draft" aria-live="polite">
|
||||||
|
{savedEnvironmentControl ? `«${savedEnvironmentControl}» сохранён в локальный layout. Сервер и MCP не используются.` : "Дискета сохраняет текущий параметр в локальный layout браузера."}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "illumination",
|
id: "illumination",
|
||||||
label: "Подсветка",
|
label: "Подсветка",
|
||||||
tone: "accent" as const,
|
tone: "accent" as const,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
|
<ControlRow label="Цвет" action={savedAction("Цвет подсветки")}><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
|
||||||
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
|
<div className="catalog-environment-with-action">
|
||||||
<RangeControl label="Дистанция свечения" value={glowDistance} min={0} max={200} formatValue={(value) => `${value}%`} onChange={setGlowDistance} />
|
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
|
||||||
|
{savedAction("Яркость")}
|
||||||
|
</div>
|
||||||
|
<div className="catalog-environment-with-action">
|
||||||
|
<RangeControl label="Дистанция свечения" value={glowDistance} min={0} max={200} formatValue={(value) => `${value}%`} onChange={setGlowDistance} />
|
||||||
|
{savedAction("Дистанция свечения")}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -315,7 +429,7 @@ export function CatalogApp() {
|
||||||
tone: "accent" as const,
|
tone: "accent" as const,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<ControlRow label="Связи по умолчанию" layout="stack">
|
<ControlRow label="Связи по умолчанию" action={savedAction("Связи по умолчанию")}>
|
||||||
<Select
|
<Select
|
||||||
variant="split"
|
variant="split"
|
||||||
label="Тип связи"
|
label="Тип связи"
|
||||||
|
|
@ -328,9 +442,12 @@ export function CatalogApp() {
|
||||||
onChange={setConnectionType}
|
onChange={setConnectionType}
|
||||||
/>
|
/>
|
||||||
</ControlRow>
|
</ControlRow>
|
||||||
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
|
<div className="catalog-environment-with-action">
|
||||||
<ControlRow label="Цвет связей по умолчанию"><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
|
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
|
||||||
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
|
{savedAction("Использовать цвет портов")}
|
||||||
|
</div>
|
||||||
|
<ControlRow label="Цвет связей по умолчанию" action={savedAction("Цвет связей")}><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
|
||||||
|
<ControlRow label="Применить ко всем"><Button variant="primary" shape="pill" width="full">Применить</Button></ControlRow>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -340,10 +457,16 @@ export function CatalogApp() {
|
||||||
tone: "accent" as const,
|
tone: "accent" as const,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<ControlRow label="Заливка"><ColorField value={fillColor} onChange={setFillColor} /></ControlRow>
|
<ControlRow label="Заливка" action={savedAction("Заливка портов")}><ColorField value={fillColor} onChange={setFillColor} /></ControlRow>
|
||||||
<RangeControl label="Прозрачность" value={100} min={0} max={100} formatValue={(value) => `${value}%`} onChange={() => undefined} />
|
<div className="catalog-environment-with-action">
|
||||||
<ControlRow label="Обводка"><ColorField value={strokeColor} onChange={setStrokeColor} /></ControlRow>
|
<RangeControl label="Прозрачность заливки" value={fillOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setFillOpacity} />
|
||||||
<RangeControl label="Прозрачность" value={100} min={0} max={100} formatValue={(value) => `${value}%`} onChange={() => undefined} />
|
{savedAction("Прозрачность заливки")}
|
||||||
|
</div>
|
||||||
|
<ControlRow label="Обводка" action={savedAction("Обводка портов")}><ColorField value={strokeColor} onChange={setStrokeColor} /></ControlRow>
|
||||||
|
<div className="catalog-environment-with-action">
|
||||||
|
<RangeControl label="Прозрачность обводки" value={strokeOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setStrokeOpacity} />
|
||||||
|
{savedAction("Прозрачность обводки")}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -356,25 +479,10 @@ export function CatalogApp() {
|
||||||
<div className="catalog-grid catalog-grid--foundation">
|
<div className="catalog-grid catalog-grid--foundation">
|
||||||
<Preview title="Тема приложения" note={theme === "dark" ? "dark glass" : "light glass"}>
|
<Preview title="Тема приложения" note={theme === "dark" ? "dark glass" : "light glass"}>
|
||||||
<div className="catalog-theme-row">
|
<div className="catalog-theme-row">
|
||||||
<Button variant={theme === "dark" ? "accent" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button>
|
<Button variant={theme === "dark" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button>
|
||||||
<Button variant={theme === "light" ? "accent" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button>
|
<Button variant={theme === "light" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="catalog-swatches">
|
<p className="catalog-preview__explanation">Тема задаёт нейтральный каркас. Акцент, плашки, поля и их прозрачность настраиваются живым Environment control — там же сохраняется локальный draft.</p>
|
||||||
{accents.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.label}
|
|
||||||
type="button"
|
|
||||||
className="catalog-swatch"
|
|
||||||
data-active={item.hex.toLowerCase() === accentHex.toLowerCase() ? "true" : undefined}
|
|
||||||
style={{ "--swatch": item.hex } as CSSProperties}
|
|
||||||
onClick={() => setAccentHex(item.hex)}
|
|
||||||
>
|
|
||||||
<span />{item.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<ColorField label="Акцент приложения" value={accentHex} onChange={setAccentHex} />
|
|
||||||
<p className="catalog-preview__explanation">Акцент сразу перекрашивает environment-controls, активные действия, заливку range, file action и состояния окон. Геометрия и semantic-status при этом не меняются.</p>
|
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Иерархия стекла" note="default / strong / soft">
|
<Preview title="Иерархия стекла" note="default / strong / soft">
|
||||||
<div className="catalog-surface-stack">
|
<div className="catalog-surface-stack">
|
||||||
|
|
@ -398,7 +506,7 @@ export function CatalogApp() {
|
||||||
<div className="catalog-grid">
|
<div className="catalog-grid">
|
||||||
<Preview title="Действия" note="общая геометрия">
|
<Preview title="Действия" note="общая геометрия">
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
<Button variant="accent" shape="pill" icon={<Icon name="save" />}>Сохранить</Button>
|
<Button variant="primary" shape="pill" icon={<Icon name="save" />}>Сохранить</Button>
|
||||||
<Button icon={<Icon name="refresh" />}>Обновить</Button>
|
<Button icon={<Icon name="refresh" />}>Обновить</Button>
|
||||||
<Button variant="ghost" icon={<Icon name="external" />}>Открыть</Button>
|
<Button variant="ghost" icon={<Icon name="external" />}>Открыть</Button>
|
||||||
<Button variant="danger" icon={<Icon name="trash" />}>Удалить</Button>
|
<Button variant="danger" icon={<Icon name="trash" />}>Удалить</Button>
|
||||||
|
|
@ -448,9 +556,9 @@ export function CatalogApp() {
|
||||||
<div className="catalog-environment-demo">
|
<div className="catalog-environment-demo">
|
||||||
<GlassSurface tone="strong" radius="panel" className="catalog-environment-card">
|
<GlassSurface tone="strong" radius="panel" className="catalog-environment-card">
|
||||||
<div className="catalog-environment-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
|
<div className="catalog-environment-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
|
||||||
<Inspector sections={environmentSections} defaultOpen={["illumination", "connections"]} />
|
<Inspector sections={environmentSections} defaultOpen={["application-material"]} />
|
||||||
</GlassSurface>
|
</GlassSurface>
|
||||||
<p className="catalog-preview__explanation">Здесь собраны все контролы, которые нужны новым приложениям: color palette, range, checker, split-select, accent action и раскрывающиеся sections. Отдельной вкладки Inspector в каталоге больше нет.</p>
|
<p className="catalog-preview__explanation">Environment controls — единый референс Engine: palette, range, checker, split-select и раскрывающиеся sections. Верхняя секция меняет настоящий material приложения; отдельной вкладки Inspector больше нет.</p>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide">
|
<Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide">
|
||||||
|
|
@ -505,7 +613,7 @@ export function CatalogApp() {
|
||||||
<Preview title="Управляемые окна" note="center / modeless end">
|
<Preview title="Управляемые окна" note="center / modeless end">
|
||||||
<p className="catalog-preview__explanation">Обычное окно блокирует фон и удерживает фокус. Правый инспектор не затемняет, не блокирует stage и не превращается в modal.</p>
|
<p className="catalog-preview__explanation">Обычное окно блокирует фон и удерживает фокус. Правый инспектор не затемняет, не блокирует stage и не превращается в modal.</p>
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
<Button variant="accent" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button>
|
<Button variant="primary" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button>
|
||||||
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setSidePanelOpen(true)}>Открыть боковую панель</Button>
|
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setSidePanelOpen(true)}>Открыть боковую панель</Button>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
|
|
@ -524,7 +632,7 @@ export function CatalogApp() {
|
||||||
<div className="catalog-grid">
|
<div className="catalog-grid">
|
||||||
<Preview title="Создание" note="форма адаптируется по ширине">
|
<Preview title="Создание" note="форма адаптируется по ширине">
|
||||||
<p className="catalog-preview__explanation">Поля переходят в одну колонку на узком viewport; footer остаётся видимым, прокручивается только тело окна.</p>
|
<p className="catalog-preview__explanation">Поля переходят в одну колонку на узком viewport; footer остаётся видимым, прокручивается только тело окна.</p>
|
||||||
<Button variant="accent" shape="pill" icon={<Icon name="plus" />} onClick={() => setCreateModalOpen(true)}>Создать проект</Button>
|
<Button variant="primary" shape="pill" icon={<Icon name="plus" />} onClick={() => setCreateModalOpen(true)}>Создать проект</Button>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Подтверждение удаления" note="async-safe">
|
<Preview title="Подтверждение удаления" note="async-safe">
|
||||||
<p className="catalog-preview__explanation">Destructive-цвет появляется у подтверждения, а не окрашивает всё окно заранее.</p>
|
<p className="catalog-preview__explanation">Destructive-цвет появляется у подтверждения, а не окрашивает всё окно заранее.</p>
|
||||||
|
|
@ -532,7 +640,7 @@ export function CatalogApp() {
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Workflow sharing" note="ENGINE / access modal">
|
<Preview title="Workflow sharing" note="ENGINE / access modal">
|
||||||
<p className="catalog-preview__explanation">Участники, роли, приглашение и immutable-владелец. API доступа остаётся в Engine.</p>
|
<p className="catalog-preview__explanation">Участники, роли, приглашение и immutable-владелец. API доступа остаётся в Engine.</p>
|
||||||
<Button variant="accent" shape="pill" icon={<Icon name="users" />} onClick={() => setShareAccessOpen(true)}>Поделиться графом</Button>
|
<Button variant="primary" shape="pill" icon={<Icon name="users" />} onClick={() => setShareAccessOpen(true)}>Поделиться графом</Button>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Ссылка на модель" note="BIM / share link">
|
<Preview title="Ссылка на модель" note="BIM / share link">
|
||||||
<p className="catalog-preview__explanation">Read-only ссылка, copy-state и единая механика focus/Escape/backdrop.</p>
|
<p className="catalog-preview__explanation">Read-only ссылка, copy-state и единая механика focus/Escape/backdrop.</p>
|
||||||
|
|
@ -676,7 +784,7 @@ export function CatalogApp() {
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" onClick={() => setWindowOpen(false)}>Отмена</Button>
|
<Button variant="ghost" onClick={() => setWindowOpen(false)}>Отмена</Button>
|
||||||
<WindowFooterActions><Button variant="accent" shape="pill" onClick={() => setWindowOpen(false)}>Сохранить</Button></WindowFooterActions>
|
<WindowFooterActions><Button variant="primary" shape="pill" onClick={() => setWindowOpen(false)}>Сохранить</Button></WindowFooterActions>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -709,7 +817,7 @@ export function CatalogApp() {
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" onClick={() => setCreateModalOpen(false)}>Отмена</Button>
|
<Button variant="ghost" onClick={() => setCreateModalOpen(false)}>Отмена</Button>
|
||||||
<WindowFooterActions><Button variant="accent" shape="pill" icon={<Icon name="plus" />} onClick={() => setCreateModalOpen(false)}>Создать</Button></WindowFooterActions>
|
<WindowFooterActions><Button variant="primary" shape="pill" icon={<Icon name="plus" />} onClick={() => setCreateModalOpen(false)}>Создать</Button></WindowFooterActions>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -805,7 +913,7 @@ export function CatalogApp() {
|
||||||
</Button>
|
</Button>
|
||||||
<WindowFooterActions>
|
<WindowFooterActions>
|
||||||
<Button onClick={() => setDetailOpen(false)}>Отмена</Button>
|
<Button onClick={() => setDetailOpen(false)}>Отмена</Button>
|
||||||
<Button variant="accent" onClick={() => setDetailOpen(false)}>Сохранить</Button>
|
<Button variant="primary" onClick={() => setDetailOpen(false)}>Сохранить</Button>
|
||||||
</WindowFooterActions>
|
</WindowFooterActions>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ textarea {
|
||||||
gap: clamp(1rem, 4vw, 4rem);
|
gap: clamp(1rem, 4vw, 4rem);
|
||||||
border-radius: var(--nodedc-radius-card);
|
border-radius: var(--nodedc-radius-card);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem),
|
radial-gradient(circle at 18% 24%, rgb(var(--nodedc-accent-rgb) / 0.13), transparent 22rem),
|
||||||
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
|
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
|
||||||
padding: clamp(1rem, 3vw, 2rem);
|
padding: clamp(1rem, 3vw, 2rem);
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +336,21 @@ textarea {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-environment-draft {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-environment-with-action {
|
||||||
|
display: grid;
|
||||||
|
width: min(var(--nodedc-inspector-inner-width), 100%);
|
||||||
|
grid-template-columns: minmax(0, 1fr) var(--nodedc-control-height);
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--nodedc-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-icon-catalog {
|
.catalog-icon-catalog {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
|
||||||
|
|
@ -14,25 +14,32 @@
|
||||||
--nodedc-text-muted: rgba(247, 248, 244, 0.48);
|
--nodedc-text-muted: rgba(247, 248, 244, 0.48);
|
||||||
--nodedc-text-disabled: rgba(247, 248, 244, 0.34);
|
--nodedc-text-disabled: rgba(247, 248, 244, 0.34);
|
||||||
|
|
||||||
--nodedc-glass-panel-bg: rgba(28, 28, 30, 0.72);
|
/* Runtime material controls override only these five values. */
|
||||||
--nodedc-glass-panel-bg-strong: rgba(18, 18, 21, 0.88);
|
--nodedc-panel-material-rgb: 22 22 26;
|
||||||
--nodedc-glass-panel-bg-soft: rgba(42, 42, 46, 0.5);
|
--nodedc-panel-material-opacity: 0.68;
|
||||||
|
--nodedc-panel-material-strong-opacity: 0.8;
|
||||||
|
--nodedc-panel-material-soft-opacity: 0.48;
|
||||||
|
--nodedc-field-material-rgb: 16 16 22;
|
||||||
|
--nodedc-field-material-opacity: 0.78;
|
||||||
|
--nodedc-glass-panel-bg: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-opacity));
|
||||||
|
--nodedc-glass-panel-bg-strong: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-strong-opacity));
|
||||||
|
--nodedc-glass-panel-bg-soft: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-soft-opacity));
|
||||||
--nodedc-header-pill-bg: rgba(64, 64, 64, 0.48);
|
--nodedc-header-pill-bg: rgba(64, 64, 64, 0.48);
|
||||||
--nodedc-glass-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.014)), var(--nodedc-glass-panel-bg);
|
--nodedc-glass-panel-surface: radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.075), transparent 38%), linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.012)), var(--nodedc-glass-panel-bg);
|
||||||
--nodedc-glass-panel-surface-strong: linear-gradient(180deg, rgba(255, 255, 255, 0.068), rgba(255, 255, 255, 0.018)), var(--nodedc-glass-panel-bg-strong);
|
--nodedc-glass-panel-surface-strong: radial-gradient(circle at 16% 0%, rgba(255, 255, 255, 0.09), transparent 36%), linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.016)), var(--nodedc-glass-panel-bg-strong);
|
||||||
--nodedc-glass-control-bg: rgba(110, 110, 110, 0.07);
|
--nodedc-glass-control-bg: rgba(110, 110, 110, 0.07);
|
||||||
--nodedc-glass-control-hover: rgba(129, 129, 129, 0.13);
|
--nodedc-glass-control-hover: rgba(129, 129, 129, 0.13);
|
||||||
--nodedc-glass-control-active: rgba(255, 255, 255, 0.92);
|
--nodedc-glass-control-active: rgba(255, 255, 255, 0.92);
|
||||||
--nodedc-glass-control-active-text: rgba(8, 8, 10, 0.96);
|
--nodedc-glass-control-active-text: rgba(8, 8, 10, 0.96);
|
||||||
--nodedc-field-bg: rgba(20, 20, 27, 0.96);
|
--nodedc-field-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.022), transparent), rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity));
|
||||||
--nodedc-overlay-bg: rgba(0, 0, 0, 0.46);
|
--nodedc-overlay-bg: rgba(0, 0, 0, 0.46);
|
||||||
--nodedc-glass-rim: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.026));
|
--nodedc-glass-rim: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.035));
|
||||||
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
|
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
|
||||||
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.075), 0 22px 72px rgba(0, 0, 0, 0.42);
|
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.11), 0 28px 90px rgba(0, 0, 0, 0.38);
|
||||||
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(0, 0, 0, 0.42), 0 6px 18px rgba(0, 0, 0, 0.2);
|
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(0, 0, 0, 0.42), 0 6px 18px rgba(0, 0, 0, 0.2);
|
||||||
--nodedc-focus-surface: rgba(var(--nodedc-accent-rgb), 0.14);
|
--nodedc-focus-surface: rgb(var(--nodedc-accent-rgb) / 0.14);
|
||||||
--nodedc-glass-outline: rgba(255, 255, 255, 0.11);
|
--nodedc-glass-outline: rgba(255, 255, 255, 0.11);
|
||||||
--nodedc-app-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)), rgba(10, 10, 13, 0.9);
|
--nodedc-app-panel-surface: var(--nodedc-glass-panel-surface-strong);
|
||||||
--nodedc-app-panel-shadow: 0 34px 110px rgba(0, 0, 0, 0.42);
|
--nodedc-app-panel-shadow: 0 34px 110px rgba(0, 0, 0, 0.42);
|
||||||
--nodedc-app-panel-shadow-strong: 0 34px 110px rgba(0, 0, 0, 0.52);
|
--nodedc-app-panel-shadow-strong: 0 34px 110px rgba(0, 0, 0, 0.52);
|
||||||
--nodedc-panel-action-border: rgba(255, 255, 255, 0.22);
|
--nodedc-panel-action-border: rgba(255, 255, 255, 0.22);
|
||||||
|
|
@ -44,6 +51,10 @@
|
||||||
--nodedc-panel-active-icon-bg: rgba(247, 248, 244, 0.96);
|
--nodedc-panel-active-icon-bg: rgba(247, 248, 244, 0.96);
|
||||||
--nodedc-panel-active-icon-color: rgb(var(--nodedc-on-accent-rgb));
|
--nodedc-panel-active-icon-color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
--nodedc-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
--nodedc-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||||
|
--nodedc-primary-action-bg: rgb(var(--nodedc-accent-rgb));
|
||||||
|
--nodedc-primary-action-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
|
||||||
|
--nodedc-primary-action-color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
|
--nodedc-primary-action-shadow: 0 12px 28px rgb(var(--nodedc-accent-rgb) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-nodedc-theme="light"] {
|
[data-nodedc-theme="light"] {
|
||||||
|
|
@ -61,25 +72,32 @@
|
||||||
--nodedc-text-muted: rgba(18, 20, 22, 0.48);
|
--nodedc-text-muted: rgba(18, 20, 22, 0.48);
|
||||||
--nodedc-text-disabled: rgba(18, 20, 22, 0.32);
|
--nodedc-text-disabled: rgba(18, 20, 22, 0.32);
|
||||||
|
|
||||||
--nodedc-glass-panel-bg: rgba(255, 255, 255, 0.48);
|
/* The light values follow the SEO launcher material: white glass, soft depth. */
|
||||||
--nodedc-glass-panel-bg-strong: rgba(255, 255, 255, 0.72);
|
--nodedc-panel-material-rgb: 255 255 255;
|
||||||
--nodedc-glass-panel-bg-soft: rgba(255, 255, 255, 0.34);
|
--nodedc-panel-material-opacity: 0.62;
|
||||||
|
--nodedc-panel-material-strong-opacity: 0.8;
|
||||||
|
--nodedc-panel-material-soft-opacity: 0.46;
|
||||||
|
--nodedc-field-material-rgb: 255 255 255;
|
||||||
|
--nodedc-field-material-opacity: 0.72;
|
||||||
|
--nodedc-glass-panel-bg: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-opacity));
|
||||||
|
--nodedc-glass-panel-bg-strong: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-strong-opacity));
|
||||||
|
--nodedc-glass-panel-bg-soft: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-soft-opacity));
|
||||||
--nodedc-header-pill-bg: rgba(255, 255, 255, 0.56);
|
--nodedc-header-pill-bg: rgba(255, 255, 255, 0.56);
|
||||||
--nodedc-glass-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.58), rgba(255, 255, 255, 0.22)), var(--nodedc-glass-panel-bg);
|
--nodedc-glass-panel-surface: radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.92), transparent 45%), linear-gradient(180deg, rgba(255, 255, 255, 0.76), rgba(255, 255, 255, 0.26)), var(--nodedc-glass-panel-bg);
|
||||||
--nodedc-glass-panel-surface-strong: linear-gradient(180deg, rgba(255, 255, 255, 0.76), rgba(255, 255, 255, 0.34)), var(--nodedc-glass-panel-bg-strong);
|
--nodedc-glass-panel-surface-strong: radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.98), transparent 45%), linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.36)), var(--nodedc-glass-panel-bg-strong);
|
||||||
--nodedc-glass-control-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.54), rgba(255, 255, 255, 0.22)), rgba(255, 255, 255, 0.38);
|
--nodedc-glass-control-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.46)), rgba(255, 255, 255, 0.7);
|
||||||
--nodedc-glass-control-hover: linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.34)), rgba(255, 255, 255, 0.54);
|
--nodedc-glass-control-hover: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.58)), rgba(255, 255, 255, 0.84);
|
||||||
--nodedc-glass-control-active: rgba(32, 32, 34, 0.96);
|
--nodedc-glass-control-active: rgba(32, 32, 34, 0.96);
|
||||||
--nodedc-glass-control-active-text: rgba(247, 248, 244, 0.96);
|
--nodedc-glass-control-active-text: rgba(247, 248, 244, 0.96);
|
||||||
--nodedc-field-bg: rgba(255, 255, 255, 0.58);
|
--nodedc-field-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.3)), rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity));
|
||||||
--nodedc-overlay-bg: rgba(24, 32, 29, 0.22);
|
--nodedc-overlay-bg: rgba(24, 32, 29, 0.22);
|
||||||
--nodedc-glass-rim: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(24, 32, 29, 0.1));
|
--nodedc-glass-rim: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(24, 32, 29, 0.1));
|
||||||
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.68);
|
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88), 0 10px 22px rgba(24, 32, 29, 0.08);
|
||||||
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
|
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
|
||||||
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(24, 32, 29, 0.2), 0 6px 18px rgba(24, 32, 29, 0.1);
|
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(24, 32, 29, 0.2), 0 6px 18px rgba(24, 32, 29, 0.1);
|
||||||
--nodedc-focus-surface: rgba(var(--nodedc-accent-rgb), 0.1);
|
--nodedc-focus-surface: rgb(var(--nodedc-accent-rgb) / 0.1);
|
||||||
--nodedc-glass-outline: rgba(24, 32, 29, 0.12);
|
--nodedc-glass-outline: rgba(24, 32, 29, 0.12);
|
||||||
--nodedc-app-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 255, 255, 0.22) 100%), rgba(255, 255, 255, 0.48);
|
--nodedc-app-panel-surface: var(--nodedc-glass-panel-surface-strong);
|
||||||
--nodedc-app-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
|
--nodedc-app-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
|
||||||
--nodedc-app-panel-shadow-strong: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 24px 82px rgba(24, 32, 29, 0.2);
|
--nodedc-app-panel-shadow-strong: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 24px 82px rgba(24, 32, 29, 0.2);
|
||||||
--nodedc-panel-action-border: rgba(24, 32, 29, 0.12);
|
--nodedc-panel-action-border: rgba(24, 32, 29, 0.12);
|
||||||
|
|
@ -91,4 +109,8 @@
|
||||||
--nodedc-panel-active-icon-bg: rgba(32, 32, 34, 0.96);
|
--nodedc-panel-active-icon-bg: rgba(32, 32, 34, 0.96);
|
||||||
--nodedc-panel-active-icon-color: rgba(247, 248, 244, 0.96);
|
--nodedc-panel-active-icon-color: rgba(247, 248, 244, 0.96);
|
||||||
--nodedc-scrollbar-thumb: rgba(24, 32, 29, 0.22);
|
--nodedc-scrollbar-thumb: rgba(24, 32, 29, 0.22);
|
||||||
|
--nodedc-primary-action-bg: rgba(24, 24, 24, 0.94);
|
||||||
|
--nodedc-primary-action-hover: rgba(8, 8, 10, 0.98);
|
||||||
|
--nodedc-primary-action-color: rgba(247, 248, 244, 0.96);
|
||||||
|
--nodedc-primary-action-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,57 @@ import { createAccentVariables, type RgbTuple } from "./accent.js";
|
||||||
|
|
||||||
export type NodedcTheme = "dark" | "light";
|
export type NodedcTheme = "dark" | "light";
|
||||||
|
|
||||||
|
export interface NodedcMaterial {
|
||||||
|
/** Base fill used by glass panels, application windows and modals. */
|
||||||
|
panel?: RgbTuple;
|
||||||
|
/** 0–1 alpha for the base glass panel. */
|
||||||
|
panelOpacity?: number;
|
||||||
|
/** Base fill used by text, range and selection fields. */
|
||||||
|
field?: RgbTuple;
|
||||||
|
/** 0–1 alpha for field fill. */
|
||||||
|
fieldOpacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApplyThemeOptions {
|
export interface ApplyThemeOptions {
|
||||||
theme?: NodedcTheme;
|
theme?: NodedcTheme;
|
||||||
accent?: RgbTuple;
|
accent?: RgbTuple;
|
||||||
|
material?: NodedcMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRgb([red, green, blue]: RgbTuple): string {
|
||||||
|
return `${red} ${green} ${blue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampOpacity(value: number): number {
|
||||||
|
return Math.max(0, Math.min(1, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyNodedcTheme(
|
export function applyNodedcTheme(
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
{ theme = "dark", accent }: ApplyThemeOptions = {},
|
{ theme = "dark", accent, material }: ApplyThemeOptions = {},
|
||||||
): void {
|
): void {
|
||||||
target.dataset.nodedcTheme = theme;
|
target.dataset.nodedcTheme = theme;
|
||||||
target.dataset.nodedcUi = "true";
|
target.dataset.nodedcUi = "true";
|
||||||
|
|
||||||
if (!accent) return;
|
if (accent) {
|
||||||
const variables = createAccentVariables(accent);
|
const variables = createAccentVariables(accent);
|
||||||
Object.entries(variables).forEach(([property, value]) => {
|
Object.entries(variables).forEach(([property, value]) => {
|
||||||
target.style.setProperty(property, value);
|
target.style.setProperty(property, value);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!material) return;
|
||||||
|
if (material.panel) target.style.setProperty("--nodedc-panel-material-rgb", formatRgb(material.panel));
|
||||||
|
if (typeof material.panelOpacity === "number" && Number.isFinite(material.panelOpacity)) {
|
||||||
|
const opacity = clampOpacity(material.panelOpacity);
|
||||||
|
target.style.setProperty("--nodedc-panel-material-opacity", String(opacity));
|
||||||
|
target.style.setProperty("--nodedc-panel-material-strong-opacity", String(clampOpacity(opacity + 0.12)));
|
||||||
|
target.style.setProperty("--nodedc-panel-material-soft-opacity", String(clampOpacity(Math.max(0.12, opacity - 0.2))));
|
||||||
|
}
|
||||||
|
if (material.field) target.style.setProperty("--nodedc-field-material-rgb", formatRgb(material.field));
|
||||||
|
if (typeof material.fieldOpacity === "number" && Number.isFinite(material.fieldOpacity)) {
|
||||||
|
target.style.setProperty("--nodedc-field-material-opacity", String(clampOpacity(material.fieldOpacity)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearNodedcAccent(target: HTMLElement): void {
|
export function clearNodedcAccent(target: HTMLElement): void {
|
||||||
|
|
@ -26,3 +60,13 @@ export function clearNodedcAccent(target: HTMLElement): void {
|
||||||
target.style.removeProperty("--nodedc-on-accent-rgb");
|
target.style.removeProperty("--nodedc-on-accent-rgb");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearNodedcMaterial(target: HTMLElement): void {
|
||||||
|
[
|
||||||
|
"--nodedc-panel-material-rgb",
|
||||||
|
"--nodedc-panel-material-opacity",
|
||||||
|
"--nodedc-panel-material-strong-opacity",
|
||||||
|
"--nodedc-panel-material-soft-opacity",
|
||||||
|
"--nodedc-field-material-rgb",
|
||||||
|
"--nodedc-field-material-opacity",
|
||||||
|
].forEach((property) => target.style.removeProperty(property));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
:where(.nodedc-button, .nodedc-icon-button, .nodedc-checker, .nodedc-select-trigger, .nodedc-select__toggle, .nodedc-dropdown-option, .nodedc-segmented__item, .nodedc-header__nav-item, .nodedc-inspector__section-trigger, .nodedc-window__close):focus-visible {
|
:where(.nodedc-button, .nodedc-icon-button, .nodedc-checker, .nodedc-select-trigger, .nodedc-select__toggle, .nodedc-dropdown-option, .nodedc-segmented__item, .nodedc-header__nav-item, .nodedc-inspector__section-trigger, .nodedc-window__close):focus-visible {
|
||||||
background-color: var(--nodedc-focus-surface);
|
background-color: var(--nodedc-focus-surface);
|
||||||
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22);
|
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-material-rim {
|
.nodedc-material-rim {
|
||||||
|
|
@ -54,8 +54,8 @@
|
||||||
background: var(--nodedc-glass-panel-surface);
|
background: var(--nodedc-glass-panel-surface);
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
box-shadow: var(--nodedc-glass-panel-shadow);
|
box-shadow: var(--nodedc-glass-panel-shadow);
|
||||||
backdrop-filter: blur(var(--nodedc-blur-panel)) saturate(1.24) brightness(1.04);
|
backdrop-filter: blur(var(--nodedc-blur-panel)) saturate(1.28) brightness(1.05);
|
||||||
-webkit-backdrop-filter: blur(var(--nodedc-blur-panel)) saturate(1.24) brightness(1.04);
|
-webkit-backdrop-filter: blur(var(--nodedc-blur-panel)) saturate(1.28) brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-glass[data-tone="strong"] {
|
.nodedc-glass[data-tone="strong"] {
|
||||||
|
|
@ -136,12 +136,24 @@
|
||||||
|
|
||||||
.nodedc-button[data-variant="primary"],
|
.nodedc-button[data-variant="primary"],
|
||||||
.nodedc-button[data-variant="accent"] {
|
.nodedc-button[data-variant="accent"] {
|
||||||
--nodedc-button-bg: rgb(var(--nodedc-accent-rgb));
|
|
||||||
--nodedc-button-color: rgb(var(--nodedc-on-accent-rgb));
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-button[data-variant="primary"]:hover:not(:disabled),
|
.nodedc-button[data-variant="primary"] {
|
||||||
|
--nodedc-button-bg: var(--nodedc-primary-action-bg);
|
||||||
|
--nodedc-button-color: var(--nodedc-primary-action-color);
|
||||||
|
box-shadow: var(--nodedc-primary-action-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-button[data-variant="accent"] {
|
||||||
|
--nodedc-button-bg: rgb(var(--nodedc-accent-rgb));
|
||||||
|
--nodedc-button-color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-button[data-variant="primary"]:hover:not(:disabled) {
|
||||||
|
background: var(--nodedc-primary-action-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-button[data-variant="accent"]:hover:not(:disabled) {
|
.nodedc-button[data-variant="accent"]:hover:not(:disabled) {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
|
background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +171,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-button[data-variant="danger"]:hover:not(:disabled) {
|
.nodedc-button[data-variant="danger"]:hover:not(:disabled) {
|
||||||
background: rgba(var(--nodedc-danger-rgb), 0.12);
|
background: rgb(var(--nodedc-danger-rgb) / 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-button:disabled,
|
.nodedc-button:disabled,
|
||||||
|
|
@ -279,7 +291,7 @@
|
||||||
|
|
||||||
.nodedc-field__control:focus-visible {
|
.nodedc-field__control:focus-visible {
|
||||||
background: var(--nodedc-focus-surface);
|
background: var(--nodedc-focus-surface);
|
||||||
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22);
|
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.nodedc-field__control {
|
textarea.nodedc-field__control {
|
||||||
|
|
@ -366,8 +378,8 @@ textarea.nodedc-field__control {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: var(--nodedc-radius-circle);
|
border-radius: var(--nodedc-radius-circle);
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: var(--nodedc-primary-action-bg);
|
||||||
color: rgba(8, 8, 10, 0.96);
|
color: var(--nodedc-primary-action-color);
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
|
|
@ -376,7 +388,7 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-media-file__button:hover {
|
.nodedc-media-file__button:hover {
|
||||||
background: #fff;
|
background: var(--nodedc-primary-action-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-media-file__name {
|
.nodedc-media-file__name {
|
||||||
|
|
@ -433,9 +445,9 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-media-source-button[data-active="true"] {
|
.nodedc-media-source-button[data-active="true"] {
|
||||||
border-color: rgba(255, 255, 255, 0.92);
|
border-color: var(--nodedc-primary-action-bg);
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: var(--nodedc-primary-action-bg);
|
||||||
color: rgba(8, 8, 10, 0.96);
|
color: var(--nodedc-primary-action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-media-preview {
|
.nodedc-media-preview {
|
||||||
|
|
@ -744,7 +756,7 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-color-field__picker[data-open="true"],
|
.nodedc-color-field__picker[data-open="true"],
|
||||||
.nodedc-color-field__picker:focus-visible {
|
.nodedc-color-field__picker:focus-visible {
|
||||||
box-shadow: 0 0 0 2px rgba(var(--nodedc-accent-rgb), 0.3), 0 0 0 3px var(--nodedc-canvas);
|
box-shadow: 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.3), 0 0 0 3px var(--nodedc-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-color-field__picker:disabled {
|
.nodedc-color-field__picker:disabled {
|
||||||
|
|
@ -780,7 +792,7 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-color-palette__spectrum:focus-visible {
|
.nodedc-color-palette__spectrum:focus-visible {
|
||||||
box-shadow: 0 0 0 2px rgba(var(--nodedc-accent-rgb), 0.28);
|
box-shadow: 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-color-palette__marker {
|
.nodedc-color-palette__marker {
|
||||||
|
|
@ -881,22 +893,22 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-status[data-tone="success"] {
|
.nodedc-status[data-tone="success"] {
|
||||||
background: rgba(var(--nodedc-success-rgb), 0.1);
|
background: rgb(var(--nodedc-success-rgb) / 0.1);
|
||||||
color: color-mix(in srgb, rgb(var(--nodedc-success-rgb)) 74%, var(--nodedc-text-primary));
|
color: color-mix(in srgb, rgb(var(--nodedc-success-rgb)) 74%, var(--nodedc-text-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-status[data-tone="warning"] {
|
.nodedc-status[data-tone="warning"] {
|
||||||
background: rgba(var(--nodedc-warning-rgb), 0.1);
|
background: rgb(var(--nodedc-warning-rgb) / 0.1);
|
||||||
color: color-mix(in srgb, rgb(var(--nodedc-warning-rgb)) 74%, var(--nodedc-text-primary));
|
color: color-mix(in srgb, rgb(var(--nodedc-warning-rgb)) 74%, var(--nodedc-text-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-status[data-tone="danger"] {
|
.nodedc-status[data-tone="danger"] {
|
||||||
background: rgba(var(--nodedc-danger-rgb), 0.1);
|
background: rgb(var(--nodedc-danger-rgb) / 0.1);
|
||||||
color: color-mix(in srgb, rgb(var(--nodedc-danger-rgb)) 74%, var(--nodedc-text-primary));
|
color: color-mix(in srgb, rgb(var(--nodedc-danger-rgb)) 74%, var(--nodedc-text-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-status[data-tone="accent"] {
|
.nodedc-status[data-tone="accent"] {
|
||||||
background: rgba(var(--nodedc-accent-rgb), 0.1);
|
background: rgb(var(--nodedc-accent-rgb) / 0.1);
|
||||||
color: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 76%, var(--nodedc-text-primary));
|
color: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 76%, var(--nodedc-text-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1682,7 +1694,7 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-dropdown-option[data-selected="true"] {
|
.nodedc-dropdown-option[data-selected="true"] {
|
||||||
background: rgba(var(--nodedc-accent-rgb), 0.14);
|
background: rgb(var(--nodedc-accent-rgb) / 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-dropdown-option__icon,
|
.nodedc-dropdown-option__icon,
|
||||||
|
|
@ -1773,7 +1785,7 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-select__toggle:focus-visible {
|
.nodedc-select__toggle:focus-visible {
|
||||||
background: var(--nodedc-focus-surface);
|
background: var(--nodedc-focus-surface);
|
||||||
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22);
|
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-dropdown-surface.nodedc-select__menu {
|
.nodedc-dropdown-surface.nodedc-select__menu {
|
||||||
|
|
@ -1876,8 +1888,8 @@ textarea.nodedc-field__control {
|
||||||
background: var(--nodedc-glass-panel-surface-strong);
|
background: var(--nodedc-glass-panel-surface-strong);
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
box-shadow: var(--nodedc-glass-panel-shadow);
|
box-shadow: var(--nodedc-glass-panel-shadow);
|
||||||
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.35) brightness(1.04);
|
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06);
|
||||||
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.35) brightness(1.04);
|
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06);
|
||||||
animation: nodedc-window-in var(--nodedc-duration-normal) var(--nodedc-ease-standard);
|
animation: nodedc-window-in var(--nodedc-duration-normal) var(--nodedc-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1922,11 +1934,11 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-window__head {
|
.nodedc-window__head {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 4.75rem;
|
min-height: 4.25rem;
|
||||||
grid-template-columns: minmax(0, 1fr) var(--nodedc-icon-button-size);
|
grid-template-columns: minmax(0, 1fr) var(--nodedc-icon-button-size);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--nodedc-space-3);
|
gap: var(--nodedc-space-3);
|
||||||
padding: 1rem 1rem 0.75rem 1.5rem;
|
padding: 0.8rem 0.9rem 0.55rem 1.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-window__titles {
|
.nodedc-window__titles {
|
||||||
|
|
@ -2005,7 +2017,7 @@ textarea.nodedc-field__control {
|
||||||
.nodedc-window__body {
|
.nodedc-window__body {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 0.75rem 1.5rem 1.5rem;
|
padding: 0.5rem 1.15rem 0.9rem;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.14) transparent;
|
scrollbar-color: rgba(255, 255, 255, 0.14) transparent;
|
||||||
}
|
}
|
||||||
|
|
@ -2015,7 +2027,7 @@ textarea.nodedc-field__control {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--nodedc-space-3);
|
gap: var(--nodedc-space-3);
|
||||||
padding: 0.9rem 1.5rem 1.25rem;
|
padding: 0.65rem 1.15rem 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-window__footer-actions {
|
.nodedc-window__footer-actions {
|
||||||
|
|
@ -2026,7 +2038,7 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-window.nodedc-share-access-window {
|
.nodedc-window.nodedc-share-access-window {
|
||||||
width: min(37.5rem, calc(100vw - 2rem));
|
width: min(35rem, calc(100vw - 2rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
@media (min-width: 641px) {
|
||||||
|
|
@ -2038,7 +2050,7 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-share-access {
|
.nodedc-share-access {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-share-access__members,
|
.nodedc-share-access__members,
|
||||||
|
|
@ -2052,7 +2064,7 @@ textarea.nodedc-field__control {
|
||||||
.nodedc-share-access__members {
|
.nodedc-share-access__members {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
gap: 0.62rem;
|
gap: 0.38rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-share-access__section-label {
|
.nodedc-share-access__section-label {
|
||||||
|
|
@ -2203,9 +2215,9 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-share-access__invite {
|
.nodedc-share-access__invite {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 11rem;
|
grid-template-columns: minmax(0, 1fr) 9.5rem;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
gap: 0.75rem;
|
gap: 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-share-access__readonly {
|
.nodedc-share-access__readonly {
|
||||||
|
|
@ -2244,7 +2256,7 @@ textarea.nodedc-field__control {
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: var(--nodedc-radius-control-compact);
|
border-radius: var(--nodedc-radius-control-compact);
|
||||||
background: rgba(var(--nodedc-danger-rgb), 0.1);
|
background: rgb(var(--nodedc-danger-rgb) / 0.1);
|
||||||
color: rgb(var(--nodedc-danger-rgb));
|
color: rgb(var(--nodedc-danger-rgb));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2335,6 +2347,11 @@ textarea.nodedc-field__control {
|
||||||
gap: var(--nodedc-inspector-control-gap);
|
gap: var(--nodedc-inspector-control-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-control-row[data-action="true"] {
|
||||||
|
grid-template-columns: minmax(6.4rem, 1fr) minmax(0, 1fr) var(--nodedc-control-height);
|
||||||
|
gap: var(--nodedc-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-control-row[data-layout="stack"] {
|
.nodedc-control-row[data-layout="stack"] {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -2352,6 +2369,12 @@ textarea.nodedc-field__control {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-control-row__action {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-empty-state {
|
.nodedc-empty-state {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 12rem;
|
min-height: 12rem;
|
||||||
|
|
@ -2494,6 +2517,23 @@ textarea.nodedc-field__control {
|
||||||
gap: var(--nodedc-space-2);
|
gap: var(--nodedc-space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-control-row[data-action="true"] {
|
||||||
|
grid-template-columns: minmax(0, 1fr) var(--nodedc-control-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-control-row[data-action="true"] .nodedc-control-row__label {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-control-row[data-action="true"] .nodedc-control-row__control {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-control-row[data-action="true"] .nodedc-control-row__action {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-media-control {
|
.nodedc-media-control {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
border-radius: var(--nodedc-radius-card);
|
border-radius: var(--nodedc-radius-card);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function ConfirmationModal({
|
||||||
<span />
|
<span />
|
||||||
<WindowFooterActions>
|
<WindowFooterActions>
|
||||||
<Button disabled={pending} onClick={onClose}>{cancelLabel}</Button>
|
<Button disabled={pending} onClick={onClose}>{cancelLabel}</Button>
|
||||||
<Button variant={danger ? "danger" : "accent"} disabled={pending} onClick={handleConfirm}>
|
<Button variant={danger ? "danger" : "primary"} disabled={pending} onClick={handleConfirm}>
|
||||||
{pending ? pendingLabel : confirmLabel}
|
{pending ? pendingLabel : confirmLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</WindowFooterActions>
|
</WindowFooterActions>
|
||||||
|
|
@ -73,4 +73,3 @@ export function ConfirmationModal({
|
||||||
</Window>
|
</Window>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,15 +88,22 @@ export function Inspector({
|
||||||
export interface ControlRowProps {
|
export interface ControlRowProps {
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
/** Optional trailing action, for example a per-control save-to-layout affordance. */
|
||||||
|
action?: ReactNode;
|
||||||
layout?: "inline" | "stack";
|
layout?: "inline" | "stack";
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ControlRow({ label, children, layout = "inline", className }: ControlRowProps) {
|
export function ControlRow({ label, children, action, layout = "inline", className }: ControlRowProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("nodedc-control-row", className)} data-layout={layout === "inline" ? undefined : layout}>
|
<div
|
||||||
|
className={cn("nodedc-control-row", className)}
|
||||||
|
data-layout={layout === "inline" ? undefined : layout}
|
||||||
|
data-action={action ? "true" : undefined}
|
||||||
|
>
|
||||||
<div className="nodedc-control-row__label">{label}</div>
|
<div className="nodedc-control-row__label">{label}</div>
|
||||||
<div className="nodedc-control-row__control">{children}</div>
|
<div className="nodedc-control-row__control">{children}</div>
|
||||||
|
{action ? <div className="nodedc-control-row__action">{action}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { Dropdown } from "./Dropdown.js";
|
||||||
|
|
||||||
export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
|
export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
|
||||||
label: string;
|
label: string;
|
||||||
|
/** Keeps the accessible name while letting a parent ControlRow render the visible label. */
|
||||||
|
showLabel?: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
|
@ -13,6 +15,7 @@ export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputEle
|
||||||
|
|
||||||
export function RangeControl({
|
export function RangeControl({
|
||||||
label,
|
label,
|
||||||
|
showLabel = true,
|
||||||
value,
|
value,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
|
|
@ -26,7 +29,7 @@ export function RangeControl({
|
||||||
const progress = Math.max(0, Math.min(100, ((value - min) / (safeMax - min)) * 100));
|
const progress = Math.max(0, Math.min(100, ((value - min) / (safeMax - min)) * 100));
|
||||||
const style = { "--nodedc-range-progress": `${progress}%` } as CSSProperties;
|
const style = { "--nodedc-range-progress": `${progress}%` } as CSSProperties;
|
||||||
return (
|
return (
|
||||||
<label className={cn("nodedc-range", className)} style={style}>
|
<label className={cn("nodedc-range", className)} data-label-hidden={showLabel ? undefined : "true"} style={style}>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
value={value}
|
value={value}
|
||||||
|
|
@ -37,10 +40,10 @@ export function RangeControl({
|
||||||
onChange={(event) => onChange(Number(event.target.value))}
|
onChange={(event) => onChange(Number(event.target.value))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<span className="nodedc-range__label">{label}</span>
|
{showLabel ? <span className="nodedc-range__label">{label}</span> : null}
|
||||||
<span className="nodedc-range__value">{formatValue(value)}</span>
|
<span className="nodedc-range__value">{formatValue(value)}</span>
|
||||||
<span className="nodedc-range__fill-text" aria-hidden="true">
|
<span className="nodedc-range__fill-text" aria-hidden="true">
|
||||||
<span className="nodedc-range__label">{label}</span>
|
{showLabel ? <span className="nodedc-range__label">{label}</span> : null}
|
||||||
<span className="nodedc-range__value">{formatValue(value)}</span>
|
<span className="nodedc-range__value">{formatValue(value)}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export function ShareAccessModal<Role extends string>({
|
||||||
<WindowFooterActions>
|
<WindowFooterActions>
|
||||||
<Button onClick={onClose} disabled={pending}>Отменить</Button>
|
<Button onClick={onClose} disabled={pending}>Отменить</Button>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Button variant="accent" onClick={() => void onInvite()} disabled={pending || !inviteEmail.trim()}>
|
<Button variant="primary" onClick={() => void onInvite()} disabled={pending || !inviteEmail.trim()}>
|
||||||
{pending ? "Сохранение..." : "Поделиться"}
|
{pending ? "Сохранение..." : "Поделиться"}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -233,7 +233,7 @@ export function ShareLinkModal({
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose}>Закрыть</Button>
|
<Button onClick={onClose}>Закрыть</Button>
|
||||||
<WindowFooterActions>
|
<WindowFooterActions>
|
||||||
<Button variant="accent" icon={<Icon name={copied ? "check" : "copy"} />} onClick={() => void copy()} disabled={!link}>
|
<Button variant="primary" icon={<Icon name={copied ? "check" : "copy"} />} onClick={() => void copy()} disabled={!link}>
|
||||||
{copied ? "Скопировано" : copyLabel}
|
{copied ? "Скопировано" : copyLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</WindowFooterActions>
|
</WindowFooterActions>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue