mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-14 21:15:47 -07:00
Several UI Adjustments
This commit is contained in:
@@ -77,13 +77,6 @@ const formatDateTime = (value) => {
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const formatFingerprint = (fp) => {
|
||||
if (!fp) return "—";
|
||||
const normalized = fp.replace(/[^a-f0-9]/gi, "").toLowerCase();
|
||||
if (!normalized) return fp;
|
||||
return normalized.match(/.{1,4}/g)?.join(" ") ?? normalized;
|
||||
};
|
||||
|
||||
const normalizeStatus = (status) => {
|
||||
if (!status) return "pending";
|
||||
if (status === "completed") return "completed";
|
||||
@@ -289,29 +282,24 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
||||
width: 110,
|
||||
},
|
||||
{ headerName: "Hostname", field: "hostname_claimed", minWidth: 180 },
|
||||
{
|
||||
headerName: "Fingerprint",
|
||||
field: "ssl_key_fingerprint_claimed",
|
||||
valueFormatter: (p) => formatFingerprint(p.value),
|
||||
cellStyle: { fontFamily: "monospace", whiteSpace: "nowrap" },
|
||||
minWidth: 150,
|
||||
Width: 150,
|
||||
},
|
||||
{
|
||||
headerName: "Enrollment Code",
|
||||
field: "enrollment_code_id",
|
||||
cellStyle: { fontFamily: "monospace" },
|
||||
minWidth: 100,
|
||||
Width: 100,
|
||||
},
|
||||
{
|
||||
headerName: "Site",
|
||||
field: "site_name",
|
||||
valueGetter: (p) => p.data?.site_name || (p.data?.site_id ? `Site ${p.data.site_id}` : "—"),
|
||||
minWidth: 160,
|
||||
},
|
||||
{ headerName: "Created", field: "created_at", valueFormatter: (p) => formatDateTime(p.value), minWidth: 160 },
|
||||
{ headerName: "Updated", field: "updated_at", valueFormatter: (p) => formatDateTime(p.value), minWidth: 160 },
|
||||
{
|
||||
headerName: "Date of Enrollment Request",
|
||||
field: "created_at",
|
||||
valueFormatter: (p) => formatDateTime(p.value),
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
headerName: "Date of Approval",
|
||||
field: "updated_at",
|
||||
valueFormatter: (p) => formatDateTime(p.value),
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
headerName: "Approved By",
|
||||
valueGetter: (p) => p.data?.approved_by_username || p.data?.approved_by_user_id || "—",
|
||||
@@ -327,48 +315,56 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
||||
const guidValue = params.context.guidInputs[record.id] || "";
|
||||
const { startApprove, handleDeny, handleGuidChange, actioningId } = params.context;
|
||||
if (!showActions) {
|
||||
return <Typography variant="body2" style={{ color: "#9aa0a6" }}>No actions available</Typography>;
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||
<Typography variant="body2" sx={{ color: "#9aa0a6" }}>
|
||||
No actions available
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const isBusy = actioningId === record.id;
|
||||
return (
|
||||
<Stack direction="row" spacing={8} alignItems="center">
|
||||
<TextField
|
||||
size="small"
|
||||
label="Optional GUID"
|
||||
placeholder="Leave empty to auto-generate"
|
||||
value={guidValue}
|
||||
onChange={(e) => handleGuidChange(record.id, e.target.value)}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="Approve enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="success"
|
||||
variant="text"
|
||||
onClick={() => startApprove(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={isBusy ? <CircularProgress size={16} color="success" /> : <ApproveIcon fontSize="small" />}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Deny enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="error"
|
||||
variant="text"
|
||||
onClick={() => handleDeny(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={<DenyIcon fontSize="small" />}
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Box sx={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||
<Stack direction="row" spacing={8} alignItems="center">
|
||||
<TextField
|
||||
size="small"
|
||||
label="Optional GUID"
|
||||
placeholder="Leave empty to auto-generate"
|
||||
value={guidValue}
|
||||
onChange={(e) => handleGuidChange(record.id, e.target.value)}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="Approve enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="success"
|
||||
variant="text"
|
||||
onClick={() => startApprove(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={isBusy ? <CircularProgress size={16} color="success" /> : <ApproveIcon fontSize="small" />}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Deny enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="error"
|
||||
variant="text"
|
||||
onClick={() => handleDeny(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={<DenyIcon fontSize="small" />}
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
minWidth: 480,
|
||||
@@ -427,68 +423,61 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
boxShadow: "none",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
elevation={0}
|
||||
>
|
||||
overflow: "hidden",
|
||||
}}
|
||||
elevation={0}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 },
|
||||
right: { xs: 12, md: 24 },
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
zIndex: 1400,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
spacing={1.25}
|
||||
alignItems="center"
|
||||
sx={{ pointerEvents: "auto" }}
|
||||
>
|
||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="approval-status-filter-label">Status</InputLabel>
|
||||
<Select
|
||||
labelId="approval-status-filter-label"
|
||||
label="Status"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={loadApprovals} disabled={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{!useGlobalHeader && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<SecurityIcon sx={{ color: MAGIC_UI.accentA }} />
|
||||
<Typography variant="h6" sx={{ color: MAGIC_UI.textBright, fontWeight: 700 }}>
|
||||
Device Approval Queue
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ xs: "stretch", sm: "center" }}>
|
||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="approval-status-filter-label">Status</InputLabel>
|
||||
<Select
|
||||
labelId="approval-status-filter-label"
|
||||
label="Status"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={loadApprovals} disabled={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<SecurityIcon sx={{ color: MAGIC_UI.accentA }} />
|
||||
<Typography variant="h6" sx={{ color: MAGIC_UI.textBright, fontWeight: 700 }}>
|
||||
Device Approval Queue
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Filters under shared header */}
|
||||
{useGlobalHeader && (
|
||||
<Box sx={{ px: 3, pt: 2, pb: 1 }}>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ xs: "stretch", sm: "center" }}>
|
||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="approval-status-filter-label">Status</InputLabel>
|
||||
<Select
|
||||
labelId="approval-status-filter-label"
|
||||
label="Status"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={loadApprovals} disabled={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
{useGlobalHeader && null}
|
||||
|
||||
{/* Feedback */}
|
||||
{feedback && (
|
||||
|
||||
@@ -863,15 +863,16 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 24,
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 }, // align with page title padding beneath the menu bar
|
||||
right: { xs: 12, md: 24 },
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
zIndex: 3,
|
||||
zIndex: 1400,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.25}>
|
||||
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
|
||||
<Tooltip title="Cancel and return">
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
@@ -348,6 +348,55 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 },
|
||||
right: { xs: 12, md: 24 },
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
zIndex: 1400,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
|
||||
<Tooltip title="Refresh">
|
||||
<span>
|
||||
<Button
|
||||
startIcon={<CachedIcon fontSize="small" />}
|
||||
variant="outlined"
|
||||
aria-label="Refresh filters"
|
||||
onClick={loadFilters}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
color: "#a5e0ff",
|
||||
borderColor: "rgba(148,163,184,0.4)",
|
||||
backgroundColor: "rgba(5,7,15,0.6)",
|
||||
borderRadius: 999,
|
||||
px: 2.4,
|
||||
minWidth: 126,
|
||||
height: 38,
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(125,183,255,0.16)",
|
||||
borderColor: "rgba(148,163,184,0.6)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="contained"
|
||||
onClick={() => onCreateFilter?.()}
|
||||
sx={gradientButtonSx}
|
||||
>
|
||||
New Filter
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||
<Box
|
||||
className={gridTheme.themeName}
|
||||
@@ -428,42 +477,6 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh
|
||||
style={{ width: "100%", height: "100%", flex: 1, fontFamily: gridFontFamily }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5 }}>
|
||||
<Stack direction="row" gap={1.75}>
|
||||
<Tooltip title="Refresh">
|
||||
<span>
|
||||
<Button
|
||||
startIcon={<CachedIcon fontSize="small" />}
|
||||
variant="outlined"
|
||||
aria-label="Refresh filters"
|
||||
onClick={loadFilters}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
color: "#a5e0ff",
|
||||
borderColor: "rgba(148,163,184,0.4)",
|
||||
backgroundColor: "rgba(5,7,15,0.6)",
|
||||
borderRadius: 999,
|
||||
px: 2.4,
|
||||
minWidth: 126,
|
||||
height: 38,
|
||||
"&:hover": { backgroundColor: "rgba(125,183,255,0.16)", borderColor: "rgba(148,163,184,0.6)" },
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="contained"
|
||||
onClick={() => onCreateFilter?.()}
|
||||
sx={gradientButtonSx}
|
||||
>
|
||||
New Filter
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,8 @@ const themeClassName = myTheme.themeName || "ag-theme-quartz";
|
||||
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
|
||||
const iconFontFamily = '"Quartz Regular"';
|
||||
|
||||
const AUTO_SIZE_COLUMNS = ["__select__", "device_count", "enrollment_code"];
|
||||
|
||||
const MAGIC_UI = {
|
||||
shellBg:
|
||||
"radial-gradient(120% 120% at 0% 0%, rgba(76, 186, 255, 0.16), transparent 55%), " +
|
||||
@@ -78,6 +80,7 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [rotatingId, setRotatingId] = useState(null);
|
||||
const gridRef = useRef(null);
|
||||
const gridApiRef = useRef(null);
|
||||
const sendNotification = useCallback(async (message) => {
|
||||
if (!message) return;
|
||||
try {
|
||||
@@ -118,6 +121,25 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
|
||||
useEffect(() => { fetchSites(); }, [fetchSites]);
|
||||
|
||||
const autoSizeColumns = useCallback(() => {
|
||||
const api = gridApiRef.current || gridRef.current?.api;
|
||||
if (!api || !rows.length) return;
|
||||
const doSize = () => {
|
||||
try {
|
||||
api.autoSizeColumns(AUTO_SIZE_COLUMNS, true);
|
||||
} catch {}
|
||||
};
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
requestAnimationFrame(doSize);
|
||||
} else {
|
||||
setTimeout(doSize, 0);
|
||||
}
|
||||
}, [rows.length]);
|
||||
|
||||
useEffect(() => {
|
||||
autoSizeColumns();
|
||||
}, [rows, autoSizeColumns]);
|
||||
|
||||
const handleCopy = useCallback(async (code) => {
|
||||
const value = (code || "").trim();
|
||||
if (!value) return;
|
||||
@@ -160,13 +182,19 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
field: "__select__",
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
minWidth: 52,
|
||||
width: 52,
|
||||
maxWidth: 52,
|
||||
pinned: "left",
|
||||
filter: false,
|
||||
sortable: false,
|
||||
suppressMenu: true,
|
||||
},
|
||||
{
|
||||
headerName: "Name",
|
||||
field: "name",
|
||||
minWidth: 180,
|
||||
minWidth: 220,
|
||||
flex: 1,
|
||||
cellRenderer: (params) => (
|
||||
<span
|
||||
style={{ color: "#7dd3fc", cursor: "pointer", fontWeight: 500 }}
|
||||
@@ -176,11 +204,23 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
headerName: "Description",
|
||||
field: "description",
|
||||
minWidth: 220,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
headerName: "Devices",
|
||||
field: "device_count",
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
headerName: "Agent Enrollment Code",
|
||||
field: "enrollment_code",
|
||||
minWidth: 320,
|
||||
flex: 1.2,
|
||||
minWidth: 260,
|
||||
filter: false,
|
||||
suppressMenu: true,
|
||||
cellRenderer: (params) => {
|
||||
const code = params.value || "—";
|
||||
const site = params.data || {};
|
||||
@@ -218,15 +258,12 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{ headerName: "Description", field: "description", minWidth: 220 },
|
||||
{ headerName: "Devices", field: "device_count", minWidth: 120 },
|
||||
], [onOpenDevicesForSite, handleRotate, handleCopy, rotatingId]);
|
||||
|
||||
const defaultColDef = useMemo(() => ({
|
||||
sortable: true,
|
||||
filter: "agTextColumnFilter",
|
||||
resizable: true,
|
||||
flex: 1,
|
||||
minWidth: 160,
|
||||
}), []);
|
||||
|
||||
@@ -254,13 +291,32 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
}}
|
||||
elevation={0}
|
||||
>
|
||||
<Box sx={{ p: { xs: 2, md: 3 }, pb: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", flexWrap: "wrap", gap: 1 }}>
|
||||
{heroStats.selected > 0 ? (
|
||||
<Typography sx={{ color: MAGIC_UI.accentA, fontSize: "0.85rem", fontWeight: 600, mr: 1 }}>
|
||||
{heroStats.selected} selected
|
||||
</Typography>
|
||||
) : null}
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap", justifyContent: "flex-end" }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 },
|
||||
right: { xs: 12, md: 24 },
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
zIndex: 1400,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-end",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{heroStats.selected > 0 ? (
|
||||
<Typography sx={{ color: MAGIC_UI.accentA, fontSize: "0.85rem", fontWeight: 600, mr: 1 }}>
|
||||
{heroStats.selected} selected
|
||||
</Typography>
|
||||
) : null}
|
||||
<Button variant="contained" size="small" startIcon={<AddIcon />} sx={RAINBOW_BUTTON_SX} onClick={() => setCreateOpen(true)}>
|
||||
Create Site
|
||||
</Button>
|
||||
@@ -350,8 +406,12 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
paginationPageSize={20}
|
||||
paginationPageSizeSelector={[20, 50, 100]}
|
||||
animateRows
|
||||
onGridReady={(params) => {
|
||||
gridApiRef.current = params.api;
|
||||
autoSizeColumns();
|
||||
}}
|
||||
onSelectionChanged={() => {
|
||||
const api = gridRef.current?.api;
|
||||
const api = gridApiRef.current || gridRef.current?.api;
|
||||
if (!api) return;
|
||||
const selected = api.getSelectedNodes().map((n) => n.data?.id).filter(Boolean);
|
||||
setSelectedIds(new Set(selected));
|
||||
|
||||
@@ -83,12 +83,11 @@ Applies to all Borealis frontends. Use `Data/Engine/web-interface/src/Admin/Page
|
||||
- Interaction rules: tabs should never scroll vertically; rely on horizontal scroll for overflow. Always align the tab rail with the first section header on the page so the aurora indicator lines up with hero metrics.
|
||||
- Accessibility: keep `aria-label`/`aria-controls` pairs when the panes hold complex content, and ensure the gradient backgrounds preserve 4.5:1 contrast for the text (the current cyan on dark meets this).
|
||||
|
||||
## Page-Level Actions with Tab Rails
|
||||
- When a page uses Aurora Tabs, place primary/secondary page actions inline with the tab rail, floating on the top-right of the content layer (under the global nav).
|
||||
- Wrap the tab stack and actions in a `position: relative` container so actions can be absolutely positioned without leaving the shell flow.
|
||||
- Position the action bar as `position: "absolute", top: 12, right: 24, zIndex: 3` (adjust top/right to match your page padding) and keep it `display: "flex"` with a `Stack` for spacing.
|
||||
- Use the same gradient primary pill and outlined secondary styles from the template; preserve the rounded 999 radius and MagicUI colors.
|
||||
- Keep the bar above content but separate from the title/subtitle block—do not nest buttons inside the title grid. This matches the Filter Editor pattern and keeps tabs and actions visually aligned.
|
||||
## Page-Level Action Buttons
|
||||
- Place page-level actions/buttons/hero-badges in a fixed overlay at the top-right, just below the global menu bar. Match the Filter Editor's placement if an example is needed `Data\Engine\web-interface\src\Devices\Filters\Filter_Editor.jsx`: wrapper `position: "fixed"`, `top: { xs: 72, md: 88 }`, `right: { xs: 12, md: 20 }`, `zIndex: 1400`, with `pointerEvents: "none"` on the wrapper and `pointerEvents: "auto"` on the inner `Stack` so underlying content remains clickable.
|
||||
- Use gradient primary pills and outlined secondary pills (rounded 999 radius, MagicUI colors). Keep horizontal spacing via a `Stack` (e.g., `spacing={1.25}`); do not nest these buttons inside the title grid or tab rail.
|
||||
- Tabs stay in normal document flow beneath the title/subtitle; the floating action bar should not shift layout. When operators request moving page actions (or when building new pages), apply this fixed overlay pattern instead of absolute positioning tied to tab rails.
|
||||
- Keep the responsive offsets (xs/md) unless a specific page has a different header height/padding; only adjust the numeric values when explicitly needed to align with a nonstandard shell.
|
||||
|
||||
## AG Grid Column Behavior (All Tables)
|
||||
- Auto-size value columns and let the last column absorb remaining width so views span available space.
|
||||
|
||||
Reference in New Issue
Block a user