Add shared WebUI layout primitives
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
"test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs",
|
||||
"test:explorer-tree": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/explorer-tree.test.js",
|
||||
"test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js",
|
||||
"test:layout-primitives": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/layout-primitives.test.js",
|
||||
"test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js && node .module-test-build/tests/help-context.test.js && node .module-test-build/tests/definition-graph.test.js",
|
||||
"test:module-permutations": "node scripts/test-module-permutations.mjs",
|
||||
"test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js",
|
||||
|
||||
@@ -18,6 +18,10 @@ const helpMenu = read("src/layout/HelpMenu.tsx");
|
||||
const helpContext = read("src/utils/helpContext.ts");
|
||||
const pageLayout = read("src/components/PageLayout.tsx");
|
||||
const workspaceLayout = read("src/components/WorkspaceLayout.tsx");
|
||||
const actionToolbar = read("src/components/ActionToolbar.tsx");
|
||||
const contentGrid = read("src/components/ContentGrid.tsx");
|
||||
const formSection = read("src/components/FormSection.tsx");
|
||||
const dialogAnatomy = read("src/components/DialogAnatomy.tsx");
|
||||
const adminPageLayout = read("src/components/admin/AdminPageLayout.tsx");
|
||||
const layoutStyles = read("src/styles/layout.css");
|
||||
const authGateStyles = read("src/styles/auth-gate.css");
|
||||
@@ -52,6 +56,11 @@ assert.match(pageLayout, /data-help-scope="page"/, "shared pages preserve contex
|
||||
assert.match(adminPageLayout, /<PageLayout/, "administration composes the central page layout instead of redefining it");
|
||||
assert.match(workspaceLayout, /workspace-layout-\$\{variant\}/, "shared workspaces expose central navigation and split-pane variants");
|
||||
assert.match(workspaceLayout, /data-help-scope="workspace"/, "shared workspaces preserve contextual-help identity");
|
||||
assert.match(actionToolbar, /action-toolbar-wrap-\$\{wrap\}/, "shared toolbars own responsive wrapping");
|
||||
assert.match(actionToolbar, /data-help-scope="toolbar"/, "shared toolbars preserve contextual-help identity");
|
||||
assert.match(contentGrid, /content-grid-collapse-\$\{collapseAt\}/, "shared grids make their collapse point explicit");
|
||||
assert.match(formSection, /form-section-header/, "shared form sections own heading and action placement");
|
||||
assert.match(dialogAnatomy, /dialog-actions-\$\{align\}/, "shared dialog anatomy owns footer action placement");
|
||||
|
||||
assert.match(iconRail, /<div className="icon-rail-scroll">\s*<nav className="icon-nav">/, "the module navigation has a dedicated scroll viewport");
|
||||
assert.match(layoutStyles, /\.icon-rail-scroll \{[^}]*min-height: 0;[^}]*flex: 1 1 auto;[^}]*overflow-y: auto;/, "only the middle rail region scrolls");
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||
import type { PlatformInterfaceIdentityProps } from "../types";
|
||||
|
||||
export type ActionToolbarDensity = "compact" | "default";
|
||||
export type ActionToolbarSurface = "plain" | "subtle";
|
||||
export type ActionToolbarWrap = "responsive" | "always" | "never";
|
||||
|
||||
export type ActionToolbarProps = PlatformInterfaceIdentityProps & Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
|
||||
children: ReactNode;
|
||||
label?: string;
|
||||
density?: ActionToolbarDensity;
|
||||
surface?: ActionToolbarSurface;
|
||||
wrap?: ActionToolbarWrap;
|
||||
};
|
||||
|
||||
export default function ActionToolbar({
|
||||
children,
|
||||
label,
|
||||
density = "default",
|
||||
surface = "plain",
|
||||
wrap = "responsive",
|
||||
className = "",
|
||||
role,
|
||||
"aria-label": ariaLabel,
|
||||
interfaceId,
|
||||
helpContextId,
|
||||
helpModuleId,
|
||||
helpTopicId,
|
||||
...props
|
||||
}: ActionToolbarProps) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const translatedLabel = label || ariaLabel ? translateText(label ?? ariaLabel ?? "") : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={[
|
||||
"action-toolbar",
|
||||
`action-toolbar-density-${density}`,
|
||||
`action-toolbar-surface-${surface}`,
|
||||
`action-toolbar-wrap-${wrap}`,
|
||||
className
|
||||
].filter(Boolean).join(" ")}
|
||||
role={role ?? (translatedLabel ? "toolbar" : undefined)}
|
||||
aria-label={translatedLabel}
|
||||
data-help-scope="toolbar"
|
||||
data-interface-id={interfaceId}
|
||||
data-help-context-id={helpContextId}
|
||||
data-help-module-id={helpModuleId}
|
||||
data-help-topic-id={helpTopicId}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type ToolbarGroupProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
align?: "start" | "center" | "end";
|
||||
grow?: boolean;
|
||||
};
|
||||
|
||||
export function ToolbarGroup({
|
||||
children,
|
||||
align = "start",
|
||||
grow = false,
|
||||
className = "",
|
||||
...props
|
||||
}: ToolbarGroupProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={[
|
||||
"action-toolbar-group",
|
||||
`action-toolbar-group-${align}`,
|
||||
grow ? "action-toolbar-group-grow" : "",
|
||||
className
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarSpacer({ className = "", ...props }: HTMLAttributes<HTMLSpanElement>) {
|
||||
return <span {...props} className={["action-toolbar-spacer", className].filter(Boolean).join(" ")} aria-hidden="true" />;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { FormHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export type ContentGridColumns = 1 | 2 | 3 | 4;
|
||||
export type ContentGridGap = "compact" | "small" | "default" | "loose";
|
||||
export type ContentGridCollapseAt = "narrow" | "workspace" | "standard" | "wide" | "never";
|
||||
|
||||
export type ContentGridProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
columns?: ContentGridColumns;
|
||||
gap?: ContentGridGap;
|
||||
collapseAt?: ContentGridCollapseAt;
|
||||
align?: "start" | "stretch";
|
||||
};
|
||||
|
||||
export default function ContentGrid({
|
||||
children,
|
||||
columns = 2,
|
||||
gap = "default",
|
||||
collapseAt = "workspace",
|
||||
align = "start",
|
||||
className = "",
|
||||
...props
|
||||
}: ContentGridProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={[
|
||||
"content-grid-layout",
|
||||
`content-grid-columns-${columns}`,
|
||||
`content-grid-gap-${gap}`,
|
||||
`content-grid-collapse-${collapseAt}`,
|
||||
`content-grid-align-${align}`,
|
||||
className
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type FormGridProps = Omit<ContentGridProps, "align">;
|
||||
|
||||
export function FormGrid({
|
||||
children,
|
||||
columns = 1,
|
||||
gap = "default",
|
||||
collapseAt = "standard",
|
||||
className = "",
|
||||
...props
|
||||
}: FormGridProps) {
|
||||
return (
|
||||
<ContentGrid
|
||||
{...props}
|
||||
columns={columns}
|
||||
gap={gap}
|
||||
collapseAt={collapseAt}
|
||||
align="start"
|
||||
className={["form-grid-layout", className].filter(Boolean).join(" ")}
|
||||
>
|
||||
{children}
|
||||
</ContentGrid>
|
||||
);
|
||||
}
|
||||
|
||||
export type FormLayoutProps = FormHTMLAttributes<HTMLFormElement> & {
|
||||
children: ReactNode;
|
||||
columns?: ContentGridColumns;
|
||||
gap?: ContentGridGap;
|
||||
collapseAt?: ContentGridCollapseAt;
|
||||
};
|
||||
|
||||
export function FormLayout({
|
||||
children,
|
||||
columns = 1,
|
||||
gap = "default",
|
||||
collapseAt = "standard",
|
||||
className = "",
|
||||
...props
|
||||
}: FormLayoutProps) {
|
||||
return (
|
||||
<form
|
||||
{...props}
|
||||
className={[
|
||||
"content-grid-layout",
|
||||
"form-grid-layout",
|
||||
`content-grid-columns-${columns}`,
|
||||
`content-grid-gap-${gap}`,
|
||||
`content-grid-collapse-${collapseAt}`,
|
||||
"content-grid-align-start",
|
||||
className
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type GridItemProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
span?: "one" | "two" | "full";
|
||||
};
|
||||
|
||||
export function GridItem({ children, span = "one", className = "", ...props }: GridItemProps) {
|
||||
return (
|
||||
<div {...props} className={["content-grid-item", `content-grid-item-${span}`, className].filter(Boolean).join(" ")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FormGrid } from "./ContentGrid";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { KeyRound, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -504,14 +505,14 @@ export default function CredentialEnvelopeManager({
|
||||
</LoadingFrame>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
<Dialog variant="administration" size="wide"
|
||||
open={Boolean(editing)}
|
||||
title={editing === "new" ? "Add reusable credential" : "Edit reusable credential"}
|
||||
helpContextId="access.credentials.editor"
|
||||
helpModuleId="access"
|
||||
onClose={closeEditor}
|
||||
closeDisabled={saving}
|
||||
className="admin-dialog admin-dialog-wide adaptive-config-dialog"
|
||||
className="adaptive-config-dialog"
|
||||
footerClassName="button-row compact-actions"
|
||||
footer={
|
||||
<>
|
||||
@@ -528,7 +529,7 @@ export default function CredentialEnvelopeManager({
|
||||
<h3>Identity</h3>
|
||||
<p>The name and type are visible; secret values are never returned by the API.</p>
|
||||
</header>
|
||||
<div className="form-grid two">
|
||||
<FormGrid columns={2} collapseAt="standard" className="">
|
||||
<FormField label="Name" helpContextId="access.credentials.field.name" helpModuleId="access">
|
||||
<input data-help-context-id="access.credentials.field.name" data-help-module-id="access" value={draft.name} disabled={saving} onChange={(event) => setDraft({ ...draft, name: event.target.value })} autoFocus />
|
||||
</FormField>
|
||||
@@ -563,14 +564,14 @@ export default function CredentialEnvelopeManager({
|
||||
label="Remove configured secret"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</section>
|
||||
<section className="adaptive-config-section">
|
||||
<header>
|
||||
<h3>Availability</h3>
|
||||
<p>Empty module or server lists mean every module or server allowed by scope.</p>
|
||||
</header>
|
||||
<div className="form-grid two">
|
||||
<FormGrid columns={2} collapseAt="standard" className="">
|
||||
<FormField label="Modules" help="Choose installed modules that may use this credential. Empty means every module allowed by scope." helpContextId="access.credentials.field.allowed-modules" helpModuleId="access">
|
||||
<ReferenceMultiSelect
|
||||
values={splitValues(draft.allowedModules)}
|
||||
@@ -594,7 +595,7 @@ export default function CredentialEnvelopeManager({
|
||||
</FormField>
|
||||
<ToggleSwitch checked={draft.inheritToLowerScopes} disabled={saving} helpContextId="access.credentials.field.inherit-to-lower-scopes" helpModuleId="access" onChange={(inheritToLowerScopes) => setDraft({ ...draft, inheritToLowerScopes })} label="Visible to lower scopes" />
|
||||
<ToggleSwitch checked={draft.isActive} disabled={saving} helpContextId="access.credentials.field.active" helpModuleId="access" onChange={(isActive) => setDraft({ ...draft, isActive })} label="Active" />
|
||||
</div>
|
||||
</FormGrid>
|
||||
</section>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,12 +9,23 @@ import {
|
||||
type DialogStackId
|
||||
} from "./dialogStack";
|
||||
import type { PlatformInterfaceIdentityProps } from "../types";
|
||||
import { DialogActions, type DialogActionAlignment } from "./DialogAnatomy";
|
||||
|
||||
export type DialogSize = "small" | "default" | "large" | "wide" | "full";
|
||||
export type DialogVariant = "default" | "administration";
|
||||
export type DialogBodyPadding = "none" | "compact" | "default";
|
||||
|
||||
export type DialogProps = PlatformInterfaceIdentityProps & {
|
||||
open: boolean;
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
description?: ReactNode;
|
||||
notices?: ReactNode;
|
||||
size?: DialogSize;
|
||||
variant?: DialogVariant;
|
||||
bodyPadding?: DialogBodyPadding;
|
||||
footerAlignment?: DialogActionAlignment;
|
||||
role?: "dialog" | "alertdialog";
|
||||
ariaDescribedBy?: string;
|
||||
closeLabel?: string;
|
||||
@@ -42,6 +53,12 @@ export default function Dialog({
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
description,
|
||||
notices,
|
||||
size = "default",
|
||||
variant = "default",
|
||||
bodyPadding = "default",
|
||||
footerAlignment = "end",
|
||||
role = "dialog",
|
||||
ariaDescribedBy,
|
||||
closeLabel = "i18n:govoplan-core.close.bbfa773e",
|
||||
@@ -83,6 +100,8 @@ export default function Dialog({
|
||||
const renderedTitle = translateReactNode(title, translateText);
|
||||
const renderedChildren = translateReactNode(children, translateText);
|
||||
const renderedFooter = translateReactNode(footer, translateText);
|
||||
const renderedDescription = translateReactNode(description, translateText);
|
||||
const renderedNotices = translateReactNode(notices, translateText);
|
||||
const translatedCloseLabel = translateText(closeLabel);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -134,7 +153,7 @@ export default function Dialog({
|
||||
<section
|
||||
ref={panelRef}
|
||||
tabIndex={-1}
|
||||
className={joinClasses("dialog-panel", className)}
|
||||
className={joinClasses("dialog-panel", `dialog-panel-size-${size}`, `dialog-panel-${variant}`, className)}
|
||||
style={panelStyle}
|
||||
role={role}
|
||||
aria-modal="true"
|
||||
@@ -162,8 +181,12 @@ export default function Dialog({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={joinClasses("dialog-body", bodyClassName)}>{renderedChildren}</div>
|
||||
{footer && <div className={joinClasses("dialog-footer", footerClassName)}>{renderedFooter}</div>}
|
||||
<div className={joinClasses("dialog-body", `dialog-body-padding-${bodyPadding}`, bodyClassName)}>
|
||||
{description && <div className="dialog-description">{renderedDescription}</div>}
|
||||
{notices && <div className="dialog-notices">{renderedNotices}</div>}
|
||||
{renderedChildren}
|
||||
</div>
|
||||
{footer && <div className={joinClasses("dialog-footer", footerClassName)}><DialogActions align={footerAlignment}>{renderedFooter}</DialogActions></div>}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { FormHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export type DialogActionAlignment = "start" | "between" | "end";
|
||||
|
||||
export type DialogActionsProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
align?: DialogActionAlignment;
|
||||
};
|
||||
|
||||
export function DialogActions({ children, align = "end", className = "", ...props }: DialogActionsProps) {
|
||||
return (
|
||||
<div {...props} className={["dialog-actions", `dialog-actions-${align}`, className].filter(Boolean).join(" ")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DialogFormProps = FormHTMLAttributes<HTMLFormElement> & {
|
||||
children: ReactNode;
|
||||
spacing?: "compact" | "default" | "loose";
|
||||
};
|
||||
|
||||
export function DialogForm({ children, spacing = "default", className = "", ...props }: DialogFormProps) {
|
||||
return (
|
||||
<form {...props} className={["dialog-form-layout", `dialog-form-spacing-${spacing}`, className].filter(Boolean).join(" ")}>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type DialogSectionProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
variant?: "plain" | "separated" | "inset";
|
||||
};
|
||||
|
||||
export function DialogSection({ children, variant = "plain", className = "", ...props }: DialogSectionProps) {
|
||||
return (
|
||||
<div {...props} className={["dialog-section-layout", `dialog-section-${variant}`, className].filter(Boolean).join(" ")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { translateReactNode, usePlatformLanguage } from "../i18n/LanguageContext";
|
||||
|
||||
export type FormSectionVariant = "plain" | "separated" | "panel";
|
||||
|
||||
export type FormSectionProps = HTMLAttributes<HTMLElement> & {
|
||||
children: ReactNode;
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
variant?: FormSectionVariant;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
export default function FormSection({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
variant = "plain",
|
||||
className = "",
|
||||
contentClassName = "",
|
||||
...props
|
||||
}: FormSectionProps) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
|
||||
return (
|
||||
<section {...props} className={["form-section-layout", `form-section-${variant}`, className].filter(Boolean).join(" ")}>
|
||||
{(title || description || actions) && (
|
||||
<header className="form-section-header">
|
||||
<div className="form-section-heading-copy">
|
||||
{title && <h3>{translateReactNode(title, translateText)}</h3>}
|
||||
{description && <div className="form-section-description">{translateReactNode(description, translateText)}</div>}
|
||||
</div>
|
||||
{actions && <div className="form-section-actions">{translateReactNode(actions, translateText)}</div>}
|
||||
</header>
|
||||
)}
|
||||
<div className={["form-section-content", contentClassName].filter(Boolean).join(" ")}>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FormGrid } from "./ContentGrid";
|
||||
import type { ResourceAccessExplanationResponse } from "../api/resourceAccessContracts";
|
||||
|
||||
export type ResourceAccessExplanationProps = {
|
||||
@@ -23,12 +24,12 @@ export default function ResourceAccessExplanation({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-grid compact responsive-form-grid">
|
||||
<FormGrid columns={2} collapseAt="wide" className="">
|
||||
<div><span className="form-label">i18n:govoplan-core.user.9f8a2389</span><p>{userLabel}</p></div>
|
||||
<div><span className="form-label">i18n:govoplan-core.resource.d1c626a9</span><p>{resourceLabel}</p></div>
|
||||
<div><span className="form-label">i18n:govoplan-core.action.97c89a4d</span><p><code>{explanation.action}</code></p></div>
|
||||
<div><span className="form-label">i18n:govoplan-core.evidence.8487d192</span><p>{explanation.provenance.length}</p></div>
|
||||
</div>
|
||||
</FormGrid>
|
||||
{explanation.provenance.length === 0 ? (
|
||||
<p className="muted small-note">i18n:govoplan-core.no_access_evidence_was_returned.84a21e4e</p>
|
||||
) : (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FormGrid } from "./ContentGrid";
|
||||
import {
|
||||
mergeAttributes,
|
||||
Node,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
type MouseEvent
|
||||
} from "react";
|
||||
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||
import ActionToolbar, { ToolbarGroup } from "./ActionToolbar";
|
||||
import Button from "./Button";
|
||||
import Dialog from "./Dialog";
|
||||
import FormField from "./FormField";
|
||||
@@ -478,8 +480,8 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
<div className={rootClassName} style={editorStyle} role="group" aria-label={translatedAriaLabel}>
|
||||
<div className="wysiwyg-editor-header">
|
||||
{mode === "visual" && (
|
||||
<div className="wysiwyg-editor-toolbar" role="toolbar" aria-label={translatedLabels.editor}>
|
||||
<div className="wysiwyg-toolbar-group">
|
||||
<ActionToolbar className="wysiwyg-editor-toolbar" label={translatedLabels.editor}>
|
||||
<ToolbarGroup className="wysiwyg-toolbar-group">
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
className="wysiwyg-toolbar-button"
|
||||
@@ -498,7 +500,7 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
onMouseDown={preserveSelection}
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
/>
|
||||
</div>
|
||||
</ToolbarGroup>
|
||||
<label className="wysiwyg-block-style">
|
||||
<span className="sr-only">{translatedLabels.blockStyle}</span>
|
||||
<select
|
||||
@@ -513,25 +515,25 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
<option value="heading-3">{translatedLabels.heading3}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="wysiwyg-toolbar-group">
|
||||
<ToolbarGroup className="wysiwyg-toolbar-group">
|
||||
<ToolbarButton active={toolbarState.bold} disabled={disabled} label={translatedLabels.bold} icon={<Bold />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBold().run()} />
|
||||
<ToolbarButton active={toolbarState.italic} disabled={disabled} label={translatedLabels.italic} icon={<Italic />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleItalic().run()} />
|
||||
<ToolbarButton active={toolbarState.underline} disabled={disabled} label={translatedLabels.underline} icon={<Underline />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleUnderline().run()} />
|
||||
<ToolbarButton active={toolbarState.strike} disabled={disabled} label={translatedLabels.strike} icon={<Strikethrough />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleStrike().run()} />
|
||||
<ToolbarButton active={toolbarState.code} disabled={disabled} label={translatedLabels.inlineCodeLabel} icon={<Code2 />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleCode().run()} />
|
||||
</div>
|
||||
<div className="wysiwyg-toolbar-group">
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup className="wysiwyg-toolbar-group">
|
||||
<ToolbarButton active={toolbarState.bulletList} disabled={disabled} label={translatedLabels.bulletList} icon={<List />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBulletList().run()} />
|
||||
<ToolbarButton active={toolbarState.numberedList} disabled={disabled} label={translatedLabels.numberedList} icon={<ListOrdered />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleOrderedList().run()} />
|
||||
<ToolbarButton active={toolbarState.quote} disabled={disabled} label={translatedLabels.quote} icon={<Quote />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBlockquote().run()} />
|
||||
</div>
|
||||
<div className="wysiwyg-toolbar-group">
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup className="wysiwyg-toolbar-group">
|
||||
<IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.insertLink} icon={<Link2 />} disabled={disabled} onMouseDown={preserveSelection} onClick={openLinkDialog} />
|
||||
<IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.removeLink} icon={<Link2Off />} disabled={disabled || !toolbarState.link} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().unsetLink().run()} />
|
||||
<IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.insertImage} icon={<ImagePlus />} disabled={disabled} onMouseDown={preserveSelection} onClick={openImageDialog} />
|
||||
<IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.clearFormatting} icon={<RemoveFormatting />} disabled={disabled} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().unsetAllMarks().clearNodes().run()} />
|
||||
</div>
|
||||
</div>
|
||||
</ToolbarGroup>
|
||||
</ActionToolbar>
|
||||
)}
|
||||
{allowSourceMode && (
|
||||
<SegmentedControl
|
||||
@@ -579,7 +581,7 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
{linkSelectionRef.current.from === linkSelectionRef.current.to && (
|
||||
<FormField label={translatedLabels.linkText}>
|
||||
<input value={linkText} onChange={(event) => setLinkText(event.target.value)} />
|
||||
@@ -589,7 +591,7 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
<input value={linkUrl} inputMode="url" onChange={(event) => setLinkUrl(event.target.value)} />
|
||||
</FormField>
|
||||
{linkError && <p className="wysiwyg-editor-dialog-error" role="alert">{linkError}</p>}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
@@ -604,7 +606,7 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
<FormField label={translatedLabels.imageUrlLabel}>
|
||||
<input value={imageUrl} inputMode="url" onChange={(event) => setImageUrl(event.target.value)} />
|
||||
</FormField>
|
||||
@@ -612,7 +614,7 @@ const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(functi
|
||||
<input value={imageAlt} onChange={(event) => setImageAlt(event.target.value)} />
|
||||
</FormField>
|
||||
{imageError && <p className="wysiwyg-editor-dialog-error" role="alert">{imageError}</p>}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FormGrid } from "../ContentGrid";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import Button from "../Button";
|
||||
import { CredentialFields } from "../CredentialPanel";
|
||||
@@ -307,7 +308,7 @@ export default function MailServerSettingsPanel({
|
||||
<div className="mail-server-settings-view">
|
||||
{activeSection === "smtp" &&
|
||||
<section className="mail-server-subsection" role="tabpanel" aria-label="i18n:govoplan-core.smtp_settings.f103e570">
|
||||
<div className="form-grid compact responsive-form-grid mail-server-form-grid">
|
||||
<FormGrid columns={2} collapseAt="wide" className="mail-server-form-grid">
|
||||
{showServerFields &&
|
||||
<>
|
||||
<FormField label="i18n:govoplan-core.server.cb0cb170" help="i18n:govoplan-core.mail_server_hostname_or_ip_address_for_the_selec.596c2d69"><input value={stringValue(smtp.host)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ host: event.target.value })} /></FormField>
|
||||
@@ -324,7 +325,7 @@ export default function MailServerSettingsPanel({
|
||||
savedPassword={smtpPasswordSaved}
|
||||
savedPasswordPlaceholder={smtpSavedPasswordPlaceholder} />
|
||||
}
|
||||
</div>
|
||||
</FormGrid>
|
||||
{onTestSmtp &&
|
||||
<div className="button-row compact-actions mail-server-actions">
|
||||
<Button type="button" variant="primary" onClick={onTestSmtp} disabled={smtpActionsDisabled || busyAction === "smtp"} disabledReason={busyAction === "smtp" ? "SMTP connection testing is already running." : smtpActionsDisabled ? smtpActionDisabledReason : undefined}>{busyAction === "smtp" ? "i18n:govoplan-core.testing.15ccc832" : smtpTestLabel}</Button>
|
||||
@@ -336,7 +337,7 @@ export default function MailServerSettingsPanel({
|
||||
|
||||
{activeSection === "imap" &&
|
||||
<section className="mail-server-subsection" role="tabpanel" aria-label="i18n:govoplan-core.imap_settings.ab8d8247">
|
||||
<div className="form-grid compact responsive-form-grid mail-server-form-grid">
|
||||
<FormGrid columns={2} collapseAt="wide" className="mail-server-form-grid">
|
||||
{showServerFields &&
|
||||
<>
|
||||
<FormField label="i18n:govoplan-core.server.cb0cb170" help="i18n:govoplan-core.mail_server_hostname_or_ip_address_for_the_selec.596c2d69"><input value={stringValue(imap.host)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ host: event.target.value })} /></FormField>
|
||||
@@ -353,7 +354,7 @@ export default function MailServerSettingsPanel({
|
||||
savedPassword={imapPasswordSaved}
|
||||
savedPasswordPlaceholder={imapSavedPasswordPlaceholder} />
|
||||
}
|
||||
</div>
|
||||
</FormGrid>
|
||||
{onTestImap &&
|
||||
<div className="button-row compact-actions mail-server-actions">
|
||||
<Button type="button" variant="primary" onClick={onTestImap} disabled={imapActionsDisabled || busyAction === "imap"} disabledReason={busyAction === "imap" ? "IMAP connection testing is already running." : imapActionsDisabled ? imapActionDisabledReason : undefined}>{busyAction === "imap" ? "i18n:govoplan-core.testing.15ccc832" : imapTestLabel}</Button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FormLayout } from "../../components/ContentGrid";
|
||||
import { useId, useState } from "react";
|
||||
import type { ApiSettings, LoginResponse } from "../../types";
|
||||
import { login } from "../../api/auth";
|
||||
@@ -53,7 +54,7 @@ export default function LoginModal({
|
||||
</>
|
||||
}>
|
||||
|
||||
<form id={formId} className="form-grid" onSubmit={submit}>
|
||||
<FormLayout columns={1} collapseAt="standard" id={formId} className="" onSubmit={submit}>
|
||||
{message && <DismissibleAlert tone="info" dismissible={false}>{message}</DismissibleAlert>}
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
<FormField label="i18n:govoplan-core.email.84add5b2">
|
||||
@@ -62,7 +63,7 @@ export default function LoginModal({
|
||||
<FormField label="i18n:govoplan-core.password.8be3c943">
|
||||
<PasswordField value={password} autoComplete="current-password" onValueChange={setPassword} />
|
||||
</FormField>
|
||||
</form>
|
||||
</FormLayout>
|
||||
</Dialog>);
|
||||
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import ContentGrid from "../../components/ContentGrid";
|
||||
import Card from "../../components/Card";
|
||||
import MetricCard from "../../components/MetricCard";
|
||||
import PageLayout from "../../components/PageLayout";
|
||||
@@ -17,7 +18,7 @@ export default function DashboardPage() {
|
||||
<MetricCard label="Dashboard module" value="not installed" tone="neutral" detail="Core fallback is active." />
|
||||
</div>
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<ContentGrid columns={2} collapseAt="workspace" className="">
|
||||
<Card title="i18n:govoplan-core.installed_modules.32b3e799">
|
||||
{modules.length === 0 ? <p className="muted">i18n:govoplan-core.no_feature_modules_are_active.e9c2d936</p> :
|
||||
<dl className="detail-list">
|
||||
@@ -33,7 +34,7 @@ export default function DashboardPage() {
|
||||
<Card title="Home">
|
||||
<p className="muted">This minimal core home is only shown while no dashboard WebUI module is available. Feature modules own their pages and can expose dashboard widgets once the dashboard module is installed.</p>
|
||||
</Card>
|
||||
</div>
|
||||
</ContentGrid>
|
||||
</PageLayout>);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ContentGrid, { FormGrid } from "../../components/ContentGrid";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import type { ApiSettings, AuthInfo, AuthUpdate, FilesConnectorsUiCapability, MailProfilesUiCapability, SettingsSectionContribution, SettingsSectionsUiCapability, UserUiPreferences, UserUiTheme } from "../../types";
|
||||
@@ -317,9 +318,9 @@ export default function SettingsPage({
|
||||
</div>
|
||||
|
||||
{active === "profile" &&
|
||||
<div className="dashboard-grid settings-dashboard-grid">
|
||||
<ContentGrid columns={2} collapseAt="workspace" className="">
|
||||
<Card title="i18n:govoplan-core.my_profile.2f3df0a9">
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
<FormField label="i18n:govoplan-core.account_display_name.03ed8fc5" help="i18n:govoplan-core.this_global_name_is_shown_in_the_title_bar_and_f.1d46240e">
|
||||
<input value={profileName} onChange={(event) => setProfileName(event.target.value)} placeholder={auth.user.email} />
|
||||
</FormField>
|
||||
@@ -340,7 +341,7 @@ export default function SettingsPage({
|
||||
</Button>
|
||||
</div>
|
||||
{profileResult && <DismissibleAlert tone={profileResultTone} resetKey={profileResult} floating>{profileResult}</DismissibleAlert>}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-core.account_context.5bb0a918">
|
||||
<dl className="detail-list compact-detail-list">
|
||||
@@ -351,7 +352,7 @@ export default function SettingsPage({
|
||||
</dl>
|
||||
<p className="muted small-note">i18n:govoplan-core.email_changes_and_password_management_require_de.f7443b69</p>
|
||||
</Card>
|
||||
</div>
|
||||
</ContentGrid>
|
||||
}
|
||||
|
||||
{active === "mail-profiles" && MailProfileScopeManager &&
|
||||
@@ -393,9 +394,9 @@ export default function SettingsPage({
|
||||
}
|
||||
|
||||
{active === "interface" &&
|
||||
<div className="dashboard-grid settings-dashboard-grid">
|
||||
<ContentGrid columns={2} collapseAt="workspace" className="">
|
||||
<Card title="i18n:govoplan-core.interface_preferences.b82b39a7">
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
<ToggleSwitch
|
||||
label="i18n:govoplan-core.compact_tables.c755d9ee"
|
||||
help="i18n:govoplan-core.prepared_ui_preference_for_denser_tables_the_cur.45698d83"
|
||||
@@ -425,10 +426,10 @@ export default function SettingsPage({
|
||||
</Button>
|
||||
</div>
|
||||
{uiResult && <DismissibleAlert tone={uiResultTone} resetKey={uiResult} floating>{uiResult}</DismissibleAlert>}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-core.theme_and_language.60a8cf59">
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
<FormField label="i18n:govoplan-core.language.89b86ab0">
|
||||
<select value={language} onChange={(event) => setLanguage(event.target.value)} disabled={selectableLanguages.length <= 1}>
|
||||
{selectableLanguages.map((item) =>
|
||||
@@ -455,15 +456,15 @@ export default function SettingsPage({
|
||||
<div><dt>i18n:govoplan-core.available.7c62a142</dt><dd>{availableLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
|
||||
<div><dt>i18n:govoplan-core.density.f9160c22</dt><dd>{compactTables ? "i18n:govoplan-core.compact_preview.3e06901d" : "i18n:govoplan-core.comfortable.2313707a"}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Card>
|
||||
</div>
|
||||
</ContentGrid>
|
||||
}
|
||||
|
||||
{active === "workspace" &&
|
||||
<div className="dashboard-grid settings-dashboard-grid">
|
||||
<ContentGrid columns={2} collapseAt="workspace" className="">
|
||||
<Card title="i18n:govoplan-core.campaign_workspace.c345580f">
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
<ToggleSwitch
|
||||
label="i18n:govoplan-core.sticky_section_sidebars.399c92d4"
|
||||
help="i18n:govoplan-core.keeps_campaign_and_admin_section_navigation_in_v.f3602938"
|
||||
@@ -488,7 +489,7 @@ export default function SettingsPage({
|
||||
</Button>
|
||||
</div>
|
||||
{uiResult && <DismissibleAlert tone={uiResultTone} resetKey={uiResult} floating>{uiResult}</DismissibleAlert>}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-core.editor_behavior.99581bc1">
|
||||
<div className="placeholder-stack">
|
||||
@@ -498,13 +499,13 @@ export default function SettingsPage({
|
||||
<span>i18n:govoplan-core.template_placeholder_chips_and_preview_overlays.11634d55</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</ContentGrid>
|
||||
}
|
||||
|
||||
{active === "local-connection" &&
|
||||
<div className="dashboard-grid settings-dashboard-grid">
|
||||
<ContentGrid columns={2} collapseAt="workspace" className="">
|
||||
<Card title="i18n:govoplan-core.local_api_connection.bfb02921">
|
||||
<div className="form-grid">
|
||||
<FormGrid columns={1} collapseAt="standard" className="">
|
||||
<FormField label="i18n:govoplan-core.api_base_url.1358fba4" help="i18n:govoplan-core.leave_empty_to_use_the_same_origin_in_vite_dev_a.9a1c25d7">
|
||||
<input value={settings.apiBaseUrl} onChange={(e) => onSettingsChange({ ...settings, apiBaseUrl: e.target.value })} placeholder="https://example.org or empty" />
|
||||
</FormField>
|
||||
@@ -526,7 +527,7 @@ export default function SettingsPage({
|
||||
</Button>
|
||||
</div>
|
||||
{testResult && <DismissibleAlert tone={testResultTone} resetKey={testResult} floating>{testResult}</DismissibleAlert>}
|
||||
</div>
|
||||
</FormGrid>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-core.session_state.b7a3d0f4">
|
||||
<dl className="detail-list compact-detail-list">
|
||||
@@ -536,7 +537,7 @@ export default function SettingsPage({
|
||||
</dl>
|
||||
<p className="muted small-note">i18n:govoplan-core.tenant_user_mail_server_and_policy_administratio.2f551271</p>
|
||||
</Card>
|
||||
</div>
|
||||
</ContentGrid>
|
||||
}
|
||||
|
||||
{activeContributedSection &&
|
||||
|
||||
@@ -49,6 +49,8 @@ export * from "./i18n/LanguageContext";
|
||||
export { PermissionBoundary, ResourceAccessBoundary } from "./components/AccessBoundary";
|
||||
export { default as ActionBlockerHint } from "./components/ActionBlockerHint";
|
||||
export type { ActionBlockerLabels, ActionBlockerReason } from "./components/ActionBlockerHint";
|
||||
export { default as ActionToolbar, ToolbarGroup, ToolbarSpacer } from "./components/ActionToolbar";
|
||||
export type { ActionToolbarDensity, ActionToolbarProps, ActionToolbarSurface, ActionToolbarWrap, ToolbarGroupProps } from "./components/ActionToolbar";
|
||||
export { default as AdvancedOptionsPanel } from "./components/AdvancedOptionsPanel";
|
||||
export { default as AdminIconButton } from "./components/admin/AdminIconButton";
|
||||
export type { AdminIconButtonProps } from "./components/admin/AdminIconButton";
|
||||
@@ -85,6 +87,9 @@ export {
|
||||
export type { CredentialEnvelopePayload, CredentialEnvelopeUpdatePayload } from "./api/credentials";
|
||||
export { default as DateField, TimeField, DateTimeField } from "./components/DateTimeField";
|
||||
export { default as Dialog } from "./components/Dialog";
|
||||
export type { DialogBodyPadding, DialogProps, DialogSize, DialogVariant } from "./components/Dialog";
|
||||
export { DialogActions, DialogForm, DialogSection } from "./components/DialogAnatomy";
|
||||
export type { DialogActionAlignment, DialogActionsProps, DialogFormProps, DialogSectionProps } from "./components/DialogAnatomy";
|
||||
export { default as DisabledActionTooltip } from "./components/DisabledActionTooltip";
|
||||
export type { DisabledActionTooltipProps } from "./components/DisabledActionTooltip";
|
||||
export { default as DismissibleAlert } from "./components/DismissibleAlert";
|
||||
@@ -96,6 +101,10 @@ export type { EffectivePolicyBlockProps } from "./components/EffectivePolicyBloc
|
||||
export { default as FileDropZone } from "./components/FileDropZone";
|
||||
export type { FileDropZoneProps } from "./components/FileDropZone";
|
||||
export { default as FormField } from "./components/FormField";
|
||||
export { default as ContentGrid, FormGrid, FormLayout, GridItem } from "./components/ContentGrid";
|
||||
export type { ContentGridCollapseAt, ContentGridColumns, ContentGridGap, ContentGridProps, FormGridProps, FormLayoutProps, GridItemProps } from "./components/ContentGrid";
|
||||
export { default as FormSection } from "./components/FormSection";
|
||||
export type { FormSectionProps, FormSectionVariant } from "./components/FormSection";
|
||||
export { default as GuidedConfigDialog } from "./components/GuidedConfigDialog";
|
||||
export type { GuidedConfigDialogProps } from "./components/GuidedConfigDialog";
|
||||
export { default as GuidedReviewList } from "./components/GuidedReviewList";
|
||||
|
||||
@@ -1050,24 +1050,10 @@
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.admin-form-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.admin-form-grid > .wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.admin-form-grid.two-columns,
|
||||
.admin-assignment-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.admin-form-grid.three-columns {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.admin-assignment-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
@@ -1111,15 +1097,13 @@
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.admin-form-grid.two-columns,
|
||||
.admin-form-grid.three-columns,
|
||||
.admin-assignment-grid,
|
||||
.admin-permission-groups {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.guided-config-dialog.admin-dialog-wide {
|
||||
.guided-config-dialog {
|
||||
width: min(1120px, calc(100vw - 40px));
|
||||
height: min(760px, calc(100vh - 40px));
|
||||
}
|
||||
@@ -1195,7 +1179,7 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.adaptive-config-dialog.admin-dialog-wide {
|
||||
.adaptive-config-dialog {
|
||||
width: min(1040px, calc(100vw - 40px));
|
||||
}
|
||||
|
||||
@@ -3401,9 +3385,6 @@
|
||||
font-size: 14px;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
.form-grid.compact.mail-server-form-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.mail-server-form-grid {
|
||||
align-items: start;
|
||||
}
|
||||
@@ -3464,7 +3445,6 @@
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.credential-panel-grid { grid-template-columns: 1fr; }
|
||||
.form-grid.compact.mail-server-form-grid { grid-template-columns: 1fr; }
|
||||
.mail-server-section-heading.split { align-items: flex-start; flex-direction: column; }
|
||||
.mail-server-segmented-control { width: 100%; }
|
||||
}
|
||||
@@ -3562,9 +3542,6 @@
|
||||
.compact-detail-list {
|
||||
gap: 8px;
|
||||
}
|
||||
.settings-dashboard-grid {
|
||||
align-items: start;
|
||||
}
|
||||
.settings-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
@@ -86,6 +86,15 @@
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.dialog-panel-size-small { width: min(460px, calc(100vw - 40px)); }
|
||||
.dialog-panel-size-default { width: min(560px, calc(100vw - 40px)); }
|
||||
.dialog-panel-size-large { width: min(680px, calc(100vw - 40px)); }
|
||||
.dialog-panel-size-wide { width: min(1040px, calc(100vw - 40px)); }
|
||||
.dialog-panel-size-full { width: min(1440px, calc(100vw - 40px)); height: min(920px, calc(100vh - 40px)); }
|
||||
.dialog-panel-administration > .dialog-header,
|
||||
.dialog-panel-administration > .dialog-footer { background: var(--panel); }
|
||||
.dialog-panel-administration > .dialog-footer { box-shadow: var(--shadow-footer); }
|
||||
|
||||
.dialog-header {
|
||||
flex: 0 0 auto;
|
||||
min-height: 58px;
|
||||
@@ -135,10 +144,17 @@
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dialog-body-padding-none { padding: 0; }
|
||||
.dialog-body-padding-compact { padding: 12px; }
|
||||
.dialog-body-padding-default { padding: 20px; }
|
||||
.dialog-description { margin-bottom: 16px; color: var(--muted); line-height: 1.5; }
|
||||
.dialog-description > :first-child { margin-top: 0; }
|
||||
.dialog-description > :last-child { margin-bottom: 0; }
|
||||
.dialog-notices { display: grid; gap: 10px; margin-bottom: 16px; }
|
||||
|
||||
.dialog-footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
@@ -149,6 +165,18 @@
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.dialog-actions { width: 100%; min-width: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem; }
|
||||
.dialog-actions-start { justify-content: flex-start; }
|
||||
.dialog-actions-between { justify-content: space-between; }
|
||||
.dialog-actions-end { justify-content: flex-end; }
|
||||
.dialog-form-layout { min-width: 0; display: grid; }
|
||||
.dialog-form-spacing-compact { gap: 10px; }
|
||||
.dialog-form-spacing-default { gap: 16px; }
|
||||
.dialog-form-spacing-loose { gap: 24px; }
|
||||
.dialog-section-layout { min-width: 0; display: grid; gap: 12px; }
|
||||
.dialog-section-separated { padding-top: 16px; border-top: var(--border-line); }
|
||||
.dialog-section-inset { padding: 14px; border: var(--border-line); border-radius: var(--radius); background: var(--panel-soft); }
|
||||
|
||||
.confirm-dialog {
|
||||
width: min(460px, calc(100vw - 40px));
|
||||
}
|
||||
@@ -164,24 +192,14 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.admin-dialog {
|
||||
width: min(680px, calc(100vw - 40px));
|
||||
}
|
||||
|
||||
.admin-dialog-wide {
|
||||
width: min(1040px, calc(100vw - 40px));
|
||||
}
|
||||
|
||||
.admin-dialog > .dialog-header {
|
||||
padding: 16px 18px;
|
||||
border-bottom: var(--border-line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.admin-dialog > .dialog-footer {
|
||||
flex: 0 0 auto;
|
||||
padding: 12px 18px;
|
||||
border-top: var(--border-line);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow-footer);
|
||||
@media (max-width: 600px) {
|
||||
.dialog-backdrop { padding: 10px; }
|
||||
.dialog-panel-size-small,
|
||||
.dialog-panel-size-default,
|
||||
.dialog-panel-size-large,
|
||||
.dialog-panel-size-wide,
|
||||
.dialog-panel-size-full { width: calc(100vw - 20px); max-height: calc(100vh - 20px); }
|
||||
.dialog-header { padding-inline: 14px; }
|
||||
.dialog-body-padding-default { padding: 16px 14px; }
|
||||
.dialog-footer { padding-inline: 14px; }
|
||||
}
|
||||
|
||||
+42
-11
@@ -1,10 +1,29 @@
|
||||
.form-grid { display: grid; gap: 18px; }
|
||||
.form-grid.compact,
|
||||
.form-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.form-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.form-grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.form-grid > .wide { grid-column: 1 / -1; }
|
||||
.responsive-form-grid { align-items: start; }
|
||||
.content-grid-layout { min-width: 0; display: grid; }
|
||||
.content-grid-columns-1 { grid-template-columns: minmax(0, 1fr); }
|
||||
.content-grid-columns-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.content-grid-columns-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.content-grid-columns-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.content-grid-gap-compact { gap: 10px; }
|
||||
.content-grid-gap-small { gap: 14px; }
|
||||
.content-grid-gap-default { gap: 18px; }
|
||||
.content-grid-gap-loose { gap: 26px; }
|
||||
.content-grid-align-start { align-items: start; }
|
||||
.content-grid-align-stretch { align-items: stretch; }
|
||||
.content-grid-item { min-width: 0; }
|
||||
.content-grid-item-two { grid-column: span 2; }
|
||||
.content-grid-item-full { grid-column: 1 / -1; }
|
||||
.form-grid-layout > .wide { grid-column: 1 / -1; }
|
||||
.form-section-layout { min-width: 0; display: grid; gap: 14px; }
|
||||
.form-section-separated { padding-top: 18px; border-top: var(--border-line); }
|
||||
.form-section-panel { padding: 18px; border: var(--border-line); border-radius: var(--radius); background: var(--panel); }
|
||||
.form-section-header { min-width: 0; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.form-section-heading-copy { min-width: 0; display: grid; gap: 5px; }
|
||||
.form-section-heading-copy h3 { margin: 0; color: var(--text-strong); font-size: 1rem; }
|
||||
.form-section-description { color: var(--muted); line-height: 1.45; }
|
||||
.form-section-description > :first-child { margin-top: 0; }
|
||||
.form-section-description > :last-child { margin-bottom: 0; }
|
||||
.form-section-actions { flex: 0 0 auto; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.form-section-content { min-width: 0; display: grid; gap: 14px; }
|
||||
.form-field { display: grid; gap: 7px; }
|
||||
.form-label { font-weight: 700; font-size: 13px; color: var(--text-label); }
|
||||
.form-help { font-size: 12px; color: var(--muted); }
|
||||
@@ -25,13 +44,25 @@ input[type="radio"]:focus-visible {
|
||||
textarea { resize: vertical; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.form-grid.compact.responsive-form-grid { grid-template-columns: 1fr; }
|
||||
.content-grid-collapse-wide { grid-template-columns: minmax(0, 1fr); }
|
||||
.content-grid-collapse-wide > .content-grid-item-two { grid-column: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.form-grid.two,
|
||||
.form-grid.three,
|
||||
.form-grid.four { grid-template-columns: 1fr; }
|
||||
.content-grid-collapse-standard { grid-template-columns: minmax(0, 1fr); }
|
||||
.content-grid-collapse-standard > .content-grid-item-two { grid-column: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.content-grid-collapse-workspace { grid-template-columns: minmax(0, 1fr); }
|
||||
.content-grid-collapse-workspace > .content-grid-item-two { grid-column: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.content-grid-collapse-narrow { grid-template-columns: minmax(0, 1fr); }
|
||||
.content-grid-collapse-narrow > .content-grid-item-two { grid-column: auto; }
|
||||
.form-section-header { flex-direction: column; }
|
||||
.form-section-actions { width: 100%; }
|
||||
}
|
||||
.form-field:has(input:disabled, select:disabled, textarea:disabled, input[readonly], textarea[readonly]) .form-label { color: var(--control-disabled-label); }
|
||||
input:disabled:not([type="checkbox"]):not([type="radio"]),
|
||||
|
||||
@@ -80,6 +80,16 @@
|
||||
.crumb { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.breadcrumb-actions { margin-left: auto; display: flex; gap: 10px; }
|
||||
.ghost-button { border: 0; background: transparent; color: var(--muted); font-weight: 700; }
|
||||
.action-toolbar { min-width: 0; display: flex; align-items: center; gap: 10px; }
|
||||
.action-toolbar-density-compact { gap: 7px; }
|
||||
.action-toolbar-surface-subtle { padding: 10px 12px; border: var(--border-line); border-radius: var(--radius); background: var(--panel-soft); }
|
||||
.action-toolbar-wrap-always { flex-wrap: wrap; }
|
||||
.action-toolbar-wrap-never { flex-wrap: nowrap; overflow-x: auto; }
|
||||
.action-toolbar-group { min-width: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.action-toolbar-group-center { justify-content: center; }
|
||||
.action-toolbar-group-end { justify-content: flex-end; margin-inline-start: auto; }
|
||||
.action-toolbar-group-grow { flex: 1 1 auto; }
|
||||
.action-toolbar-spacer { flex: 1 1 auto; min-width: 8px; }
|
||||
.app-content { min-height: 0; overflow: hidden; }
|
||||
.workspace { height: 100%; min-height: 0; display: grid; grid-template-columns: 198px minmax(0, 1fr); }
|
||||
.workspace-layout-primary,
|
||||
@@ -214,7 +224,6 @@
|
||||
.metric-label { color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 800; letter-spacing: .05em; }
|
||||
.metric-value { margin-top: 7px; font-size: 30px; color: var(--text-strong); font-weight: 700; }
|
||||
.metric-detail { margin-top: 4px; color: var(--muted); font-size: 13px; }
|
||||
.dashboard-grid, .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start; }
|
||||
.wizard-page { min-height: calc(100vh - 112px); display: grid; place-items: start center; padding: 42px; }
|
||||
.wizard-card { width: min(980px, 100%); background: var(--panel); border: var(--border-line); box-shadow: var(--shadow); border-radius: var(--radius); display: grid; grid-template-columns: 290px 1fr; overflow: hidden; }
|
||||
.wizard-body { background: var(--panel-soft); padding: 28px; min-height: 620px; }
|
||||
@@ -271,10 +280,16 @@
|
||||
.workspace-layout-split { grid-template-columns: 1fr; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
|
||||
.section-sidebar { display: none; }
|
||||
.wizard-card { grid-template-columns: 1fr; }
|
||||
.metric-grid, .dashboard-grid, .settings-grid { grid-template-columns: 1fr; }
|
||||
.metric-grid { grid-template-columns: 1fr; }
|
||||
.concurrency-conflict-values { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.action-toolbar-wrap-responsive { align-items: stretch; flex-wrap: wrap; }
|
||||
.action-toolbar-wrap-responsive > .action-toolbar-group { flex: 1 1 100%; }
|
||||
.action-toolbar-wrap-responsive > .action-toolbar-group-end { margin-inline-start: 0; justify-content: flex-start; }
|
||||
}
|
||||
|
||||
|
||||
/* Layout additions: login and help dropdown */
|
||||
.tenant-selector.disabled { opacity: .86; cursor: default; }
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
function assert(condition: unknown, message = "assertion failed"): void {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import ActionToolbar, { ToolbarGroup, ToolbarSpacer } from "../src/components/ActionToolbar";
|
||||
import ContentGrid, { FormGrid, FormLayout, GridItem } from "../src/components/ContentGrid";
|
||||
import Dialog from "../src/components/Dialog";
|
||||
import { DialogForm, DialogSection } from "../src/components/DialogAnatomy";
|
||||
import FormSection from "../src/components/FormSection";
|
||||
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
||||
|
||||
const toolbarMarkup = renderToStaticMarkup(
|
||||
<PlatformLanguageProvider>
|
||||
<ActionToolbar label="Object actions" density="compact" surface="subtle" interfaceId="object.toolbar">
|
||||
<ToolbarGroup grow>Filters</ToolbarGroup>
|
||||
<ToolbarSpacer />
|
||||
<ToolbarGroup align="end">Actions</ToolbarGroup>
|
||||
</ActionToolbar>
|
||||
</PlatformLanguageProvider>
|
||||
);
|
||||
|
||||
assert(toolbarMarkup.includes("action-toolbar-wrap-responsive"), "toolbars use the responsive central wrapping policy by default");
|
||||
assert(toolbarMarkup.includes('role="toolbar"'), "named toolbars expose toolbar semantics");
|
||||
assert(toolbarMarkup.includes('aria-label="Object actions"'), "named toolbars expose their accessible name");
|
||||
assert(toolbarMarkup.includes('data-help-scope="toolbar"'), "toolbar help scope is centralized");
|
||||
assert(toolbarMarkup.includes("action-toolbar-group-grow"), "toolbar groups can own flexible space");
|
||||
assert(toolbarMarkup.includes("action-toolbar-spacer"), "the standard action spacer is available");
|
||||
|
||||
const gridMarkup = renderToStaticMarkup(
|
||||
<ContentGrid columns={3} gap="compact" collapseAt="wide">
|
||||
<GridItem>A</GridItem>
|
||||
<GridItem span="two">B</GridItem>
|
||||
<GridItem span="full">C</GridItem>
|
||||
</ContentGrid>
|
||||
);
|
||||
|
||||
assert(gridMarkup.includes("content-grid-columns-3"), "content grids expose explicit column geometry");
|
||||
assert(gridMarkup.includes("content-grid-collapse-wide"), "content grids expose explicit collapse ownership");
|
||||
assert(gridMarkup.includes("content-grid-item-full"), "grid items can span the complete layout");
|
||||
|
||||
const formMarkup = renderToStaticMarkup(
|
||||
<PlatformLanguageProvider>
|
||||
<FormSection title="Identity" description="Stable identifying values" actions={<button type="button">Reset</button>}>
|
||||
<FormGrid columns={2}><label>Name<input /></label><label>Code<input /></label></FormGrid>
|
||||
</FormSection>
|
||||
</PlatformLanguageProvider>
|
||||
);
|
||||
|
||||
assert(formMarkup.includes("form-section-header"), "form sections centralize heading and action placement");
|
||||
assert(formMarkup.includes("form-grid-layout"), "forms compose the central content grid");
|
||||
assert(formMarkup.includes("content-grid-columns-2"), "form columns remain explicit");
|
||||
|
||||
const formElementMarkup = renderToStaticMarkup(<FormLayout columns={2} id="object-form"><label>Value<input /></label></FormLayout>);
|
||||
assert(formElementMarkup.startsWith('<form id="object-form"'), "form layouts preserve native form semantics");
|
||||
assert(formElementMarkup.includes("form-grid-layout"), "native forms share the central grid geometry");
|
||||
|
||||
const dialogMarkup = renderToStaticMarkup(
|
||||
<PlatformLanguageProvider>
|
||||
<Dialog
|
||||
open
|
||||
title="Edit object"
|
||||
description="Review the values before saving."
|
||||
notices={<div role="status">Draft</div>}
|
||||
size="wide"
|
||||
variant="administration"
|
||||
footer={<><button type="button">Cancel</button><button type="submit">Save</button></>}
|
||||
>
|
||||
<DialogForm><DialogSection variant="inset">Fields</DialogSection></DialogForm>
|
||||
</Dialog>
|
||||
</PlatformLanguageProvider>
|
||||
);
|
||||
|
||||
assert(dialogMarkup.includes("dialog-panel-size-wide"), "dialog size is owned by the central anatomy");
|
||||
assert(dialogMarkup.includes("dialog-panel-administration"), "dialog variants are explicit");
|
||||
assert(dialogMarkup.includes("dialog-description"), "dialog introductions use the central region");
|
||||
assert(dialogMarkup.includes("dialog-notices"), "dialog notices use the central region");
|
||||
assert(dialogMarkup.includes("dialog-actions dialog-actions-end"), "every dialog footer composes the central action layout");
|
||||
assert(dialogMarkup.includes("dialog-form-layout"), "dialog forms use the central vertical flow");
|
||||
assert(dialogMarkup.includes("dialog-section-inset"), "dialog sections use central grouping variants");
|
||||
@@ -25,6 +25,7 @@
|
||||
"tests/documentation-help-link.test.tsx",
|
||||
"tests/explorer-tree.test.tsx",
|
||||
"tests/icon-button.test.tsx",
|
||||
"tests/layout-primitives.test.tsx",
|
||||
"tests/mail-components.test.tsx",
|
||||
"tests/metric-card.test.tsx",
|
||||
"tests/page-layout.test.tsx",
|
||||
@@ -52,6 +53,10 @@
|
||||
"src/components/WorkspaceLayout.tsx",
|
||||
"src/components/LoadingFrame.tsx",
|
||||
"src/components/LoadingIndicator.tsx",
|
||||
"src/components/ActionToolbar.tsx",
|
||||
"src/components/ContentGrid.tsx",
|
||||
"src/components/DialogAnatomy.tsx",
|
||||
"src/components/FormSection.tsx",
|
||||
"src/components/people/PeoplePicker.tsx",
|
||||
"src/components/people/peoplePickerTypes.ts",
|
||||
"src/components/ResourceAccessExplanation.tsx",
|
||||
|
||||
Reference in New Issue
Block a user