diff --git a/frontend/src/components/IconStepStrip.jsx b/frontend/src/components/IconStepStrip.jsx
index b4c333ad..2a9bd481 100644
--- a/frontend/src/components/IconStepStrip.jsx
+++ b/frontend/src/components/IconStepStrip.jsx
@@ -1,22 +1,36 @@
-import { Fragment } from 'react';
-import { ChevronRight } from 'lucide-react';
+import { Info, ChevronRight } from 'lucide-react';
import InfoTip from './InfoTip';
-// 2-4 steps: icon tile, a short label and an optional (i) with the detail.
+// 2-4 steps as equal-width pills (icon + short label) joined by chevrons.
+// A step with `info` makes its whole pill the tap/hover target for the detail.
// steps = [{ icon: ReactNode, label: string, info?: ReactNode }]
export default function IconStepStrip({ steps, className = '' }) {
return (
-
- ))}
+ );
+ })}
);
}
diff --git a/frontend/src/components/InfoTip.jsx b/frontend/src/components/InfoTip.jsx
index d26eda73..b91cff26 100644
--- a/frontend/src/components/InfoTip.jsx
+++ b/frontend/src/components/InfoTip.jsx
@@ -1,18 +1,19 @@
import { useRef, useState } from 'react';
import { Info } from 'lucide-react';
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
+import { Sheet, SheetTrigger, SheetContent, SheetTitle } from '@/components/ui/sheet';
import useMediaQuery from '../hooks/useMediaQuery';
import { t } from '../i18n/t';
// An (i) button that holds the explanation a page used to print inline.
// Mouse: hover/focus previews it. Touch, click, Enter and Space: toggles a
-// pinned popover that closes on outside tap or Escape. The panel is portalled
-// (no button-in-button) and above Modal/Sheet (.info-tip__panel z-index).
-export default function InfoTip({ children, label, side = 'top', size = 'sm', className = '' }) {
+// pinned popover that closes on outside tap or Escape. Narrow screens get a
+// bottom sheet instead, so the text never sits on top of the content it
+// explains. Both are portalled (no button-in-button) and above Modal/Sheet.
+// `trigger` replaces the (i) glyph (e.g. a whole step pill); `title` heads the sheet.
+export default function InfoTip({ children, label, title, trigger, side = 'top', size = 'sm', className = '' }) {
const [open, setOpen] = useState(false);
- // Narrow screens: keep the panel clear of the sticky app header.
const narrow = useMediaQuery('(max-width: 640px)');
- const collisionPadding = narrow ? { top: 72, right: 12, bottom: 12, left: 12 } : 8; // geometry: header 56px + safe area
const pinned = useRef(false);
const hoverable = (e) => e.pointerType === 'mouse';
@@ -21,31 +22,54 @@ export default function InfoTip({ children, label, side = 'top', size = 'sm', cl
setOpen(next);
};
const stop = (e) => e.stopPropagation();
+ const noFocus = (e) => e.preventDefault();
+
+ const button = (
+ // eslint-disable-next-line no-restricted-syntax -- bare 18px glyph button; Button/IconButton add control-sized chrome and a nested Tip
+
+ );
+
+ if (narrow) {
+ return (
+
+ {button}
+
+ {title || label || t('common.moreInfo', 'More info')}
+
{children}
+
+
+ );
+ }
return (
-
- {/* eslint-disable-next-line no-restricted-syntax -- bare 18px glyph button; Button/IconButton add control-sized chrome and a nested Tip */}
-
-
+ {button} e.preventDefault()}
- onCloseAutoFocus={(e) => e.preventDefault()}
+ onOpenAutoFocus={noFocus}
+ onCloseAutoFocus={noFocus}
onClick={stop}
>
{children}
diff --git a/frontend/src/components/SectionIntro.jsx b/frontend/src/components/SectionIntro.jsx
index d4d75041..156359ee 100644
--- a/frontend/src/components/SectionIntro.jsx
+++ b/frontend/src/components/SectionIntro.jsx
@@ -1,11 +1,26 @@
import InfoTip from './InfoTip';
+import IconStepStrip from './IconStepStrip';
-// One short line with the longer explanation tucked in an (i).
-export default function SectionIntro({ children, info, className = '' }) {
+// One short line with the longer explanation tucked in an (i). With `icon`
+// and/or `steps` it becomes a compact card banner: icon tile + title + (i),
+// then the IconStepStrip (one row on desktop, two on mobile).
+export default function SectionIntro({ children, info, icon, steps, className = '' }) {
+ if (!icon && !steps) {
+ return (
+
);
}
diff --git a/frontend/src/components/illustrations/FileIntegrityIllustration.jsx b/frontend/src/components/illustrations/FileIntegrityIllustration.jsx
deleted file mode 100644
index 8a98d5fe..00000000
--- a/frontend/src/components/illustrations/FileIntegrityIllustration.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-// Decorative; the surrounding text carries the meaning.
-export default function FileIntegrityIllustration() {
- return (
-
- );
-}
diff --git a/frontend/src/components/security/IntegrityTab.jsx b/frontend/src/components/security/IntegrityTab.jsx
index e8b3a9c2..a087c8e5 100644
--- a/frontend/src/components/security/IntegrityTab.jsx
+++ b/frontend/src/components/security/IntegrityTab.jsx
@@ -11,11 +11,9 @@ import {
import { useTableSort } from '@/hooks/useTableSort';
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import { useTranslation } from 'react-i18next';
-import { Camera, Eye, GitCompare, ShieldOff } from 'lucide-react';
+import { Camera, Eye, FileCheck, GitCompare, ShieldOff } from 'lucide-react';
import InfoTip from '@/components/InfoTip';
-import IconStepStrip from '@/components/IconStepStrip';
import SectionIntro from '@/components/SectionIntro';
-import FileIntegrityIllustration from '@/components/illustrations/FileIntegrityIllustration';
import { Card as SharedCard, CardHeader as SharedCardHeader, CardContent as SharedCardContent } from '@/components/ui/card';
// Scoped file-integrity monitoring: baseline-and-diff over the paths
@@ -441,23 +439,17 @@ const IntegrityTab = () => {
)}
-
-
-
-
- {t('app.integrityTab.headerLine', 'Detect changed config files.')}
-
- , label: t('app.integrityTab.stepBaseline', 'Baseline'), info: t('app.integrityTab.stepBaselineInfo', 'Snapshot the current files of a scope.') },
- { icon: , label: t('app.integrityTab.stepWatch', 'Watch'), info: t('app.integrityTab.stepWatchInfo', 'Check manually or let the scheduled sweep run.') },
- { icon: , label: t('app.integrityTab.stepDiff', 'Diff'), info: t('app.integrityTab.stepDiffInfo', 'Added, removed or modified files are flagged and admins are notified. Accepting re-baselines.') },
- ]}
- />
-
-
+ }
+ info={t('app.integrityTab.baselineAndDiffMonitoringOverThe', 'Baseline-and-diff monitoring over the paths ServerKit manages. Baseline a scope, then check it (or let the scheduled sweep do it). Any added, removed or modified files are flagged and admins are notified. Accepting changes re-baselines the scope.')}
+ steps={[
+ { icon: , label: t('app.integrityTab.stepBaseline', 'Baseline'), info: t('app.integrityTab.stepBaselineInfo', 'Snapshot the current files of a scope.') },
+ { icon: , label: t('app.integrityTab.stepWatch', 'Watch'), info: t('app.integrityTab.stepWatchInfo', 'Check manually or let the scheduled sweep run.') },
+ { icon: , label: t('app.integrityTab.stepDiff', 'Diff'), info: t('app.integrityTab.stepDiffInfo', 'Added, removed or modified files are flagged and admins are notified. Accepting re-baselines.') },
+ ]}
+ >
+ {t('app.integrityTab.headerLine', 'Detect changed config files')}
+
{managedScopes.map(renderScopeCard)}
{appScopes.map(renderScopeCard)}
diff --git a/frontend/src/components/ui/sheet.jsx b/frontend/src/components/ui/sheet.jsx
index 650445fb..e28efc27 100644
--- a/frontend/src/components/ui/sheet.jsx
+++ b/frontend/src/components/ui/sheet.jsx
@@ -18,11 +18,11 @@ const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
-const SheetContent = React.forwardRef(({ side = 'right', className, children, ...props }, ref) => {
+const SheetContent = React.forwardRef(({ side = 'right', className, overlayClassName, children, ...props }, ref) => {
const { t } = useTranslation();
return (
-
+ {s.format}
- {s.description}
+ {s.description}
))}
@@ -454,7 +454,7 @@ function ImportWizard() {
{t('app.importWizard.liveSite', 'Live site')}
- {t('app.importWizard.pullALiveWordpressSiteStraight', 'Pull a live WordPress site straight from its current server. Opens the WordPress import surface.')}
+ {t('app.importWizard.pullALiveWordpressSiteStraight', 'Pull a live WordPress site straight from its current server. Opens the WordPress import surface.')}