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 (
    - {steps.map((s, i) => ( - - {i > 0 && } -
  1. - + {steps.map((s, i) => { + const body = ( + <> + {s.label} - {s.info && {s.info}} + + ); + return ( +
  2. + {i > 0 &&
  3. -
    - ))} + ); + })}
); } 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 ( +

+ {children} + {info && {info}} +

+ ); + } return ( -

- {children} - {info && {info}} -

+
+
+ {icon && } + {children} + {info && {info}} +
+ {steps && } +
); } 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}