mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-10 05:53:05 +09:00
fix(frontend): redesign mobile intro banner, segmented step strip, info sheet and card header chips
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4ea3fccbd
commit
b95c516c76
13 files changed
+300
-174
No files matched your search
@@ -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 (
|
||||
<ol className={`icon-step-strip ${className}`.trim()}>
|
||||
{steps.map((s, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <li className="icon-step-strip__arrow" aria-hidden="true"><ChevronRight /></li>}
|
||||
<li className="icon-step-strip__step">
|
||||
<span className="icon-step-strip__tile" aria-hidden="true">{s.icon}</span>
|
||||
{steps.map((s, i) => {
|
||||
const body = (
|
||||
<>
|
||||
<span className="icon-step-strip__icon" aria-hidden="true">{s.icon}</span>
|
||||
<span className="icon-step-strip__label">{s.label}</span>
|
||||
{s.info && <InfoTip>{s.info}</InfoTip>}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<li className="icon-step-strip__step" key={i}>
|
||||
{i > 0 && <ChevronRight className="icon-step-strip__arrow" aria-hidden="true" />}
|
||||
{s.info ? (
|
||||
<InfoTip
|
||||
className="icon-step-strip__pill icon-step-strip__pill--info"
|
||||
title={s.label}
|
||||
trigger={<>{body}<Info className="icon-step-strip__more" aria-hidden="true" /></>}
|
||||
>
|
||||
{s.info}
|
||||
</InfoTip>
|
||||
) : (
|
||||
<span className="icon-step-strip__pill">{body}</span>
|
||||
)}
|
||||
</li>
|
||||
</Fragment>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
<button
|
||||
type="button"
|
||||
className={(trigger ? className : `info-tip info-tip--${size} ${className}`).trim()}
|
||||
aria-label={trigger ? label : (label || t('common.moreInfo', 'More info'))}
|
||||
onClick={stop}
|
||||
onPointerEnter={(e) => { if (hoverable(e)) setOpen(true); }}
|
||||
onPointerLeave={(e) => { if (hoverable(e) && !pinned.current) setOpen(false); }}
|
||||
onFocus={(e) => { if (e.currentTarget.matches(':focus-visible')) setOpen(true); }}
|
||||
onBlur={() => { if (!pinned.current) setOpen(false); }}
|
||||
>
|
||||
{trigger || <Info aria-hidden="true" />}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (narrow) {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetTrigger asChild>{button}</SheetTrigger>
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
className="info-tip__sheet"
|
||||
overlayClassName="info-tip__overlay"
|
||||
aria-describedby={undefined}
|
||||
onOpenAutoFocus={noFocus}
|
||||
onCloseAutoFocus={noFocus}
|
||||
onClick={stop}
|
||||
>
|
||||
<SheetTitle className="info-tip__sheet-title">{title || label || t('common.moreInfo', 'More info')}</SheetTitle>
|
||||
<div className="info-tip__sheet-body">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
{/* eslint-disable-next-line no-restricted-syntax -- bare 18px glyph button; Button/IconButton add control-sized chrome and a nested Tip */}
|
||||
<button
|
||||
type="button"
|
||||
className={`info-tip info-tip--${size} ${className}`.trim()}
|
||||
aria-label={label || t('common.moreInfo', 'More info')}
|
||||
onClick={stop}
|
||||
onPointerEnter={(e) => { if (hoverable(e)) setOpen(true); }}
|
||||
onPointerLeave={(e) => { if (hoverable(e) && !pinned.current) setOpen(false); }}
|
||||
onFocus={(e) => { if (e.currentTarget.matches(':focus-visible')) setOpen(true); }}
|
||||
onBlur={() => { if (!pinned.current) setOpen(false); }}
|
||||
>
|
||||
<Info aria-hidden="true" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverTrigger asChild>{button}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side={side}
|
||||
avoidCollisions
|
||||
collisionPadding={collisionPadding}
|
||||
collisionPadding={8}
|
||||
className="info-tip__panel"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
onOpenAutoFocus={noFocus}
|
||||
onCloseAutoFocus={noFocus}
|
||||
onClick={stop}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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 (
|
||||
<p className={`section-intro ${className}`.trim()}>
|
||||
<span>{children}</span>
|
||||
{info && <InfoTip>{info}</InfoTip>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className={`section-intro ${className}`.trim()}>
|
||||
<span>{children}</span>
|
||||
{info && <InfoTip>{info}</InfoTip>}
|
||||
</p>
|
||||
<div className={`section-intro section-intro--banner ${className}`.trim()}>
|
||||
<div className="section-intro__head">
|
||||
{icon && <span className="section-intro__icon" aria-hidden="true">{icon}</span>}
|
||||
<span className="section-intro__title">{children}</span>
|
||||
{info && <InfoTip title={children}>{info}</InfoTip>}
|
||||
</div>
|
||||
{steps && <IconStepStrip steps={steps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Decorative; the surrounding text carries the meaning.
|
||||
export default function FileIntegrityIllustration() {
|
||||
return (
|
||||
<svg className="tip-illustration" viewBox="0 0 48 48" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M12 5h17l8 8v28a2 2 0 0 1-2 2H12a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z" fill="currentColor" fillOpacity="0.12" /><path d="M29 5v8h8" /><path d="m17 29 5 5 9-10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="tip-card-head">
|
||||
<FileIntegrityIllustration />
|
||||
<div className="tip-card-head__text">
|
||||
<SectionIntro
|
||||
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.')}
|
||||
>
|
||||
{t('app.integrityTab.headerLine', 'Detect changed config files.')}
|
||||
</SectionIntro>
|
||||
<IconStepStrip
|
||||
steps={[
|
||||
{ icon: <Camera />, label: t('app.integrityTab.stepBaseline', 'Baseline'), info: t('app.integrityTab.stepBaselineInfo', 'Snapshot the current files of a scope.') },
|
||||
{ icon: <Eye />, label: t('app.integrityTab.stepWatch', 'Watch'), info: t('app.integrityTab.stepWatchInfo', 'Check manually or let the scheduled sweep run.') },
|
||||
{ icon: <GitCompare />, 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.') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionIntro
|
||||
icon={<FileCheck />}
|
||||
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: <Camera />, label: t('app.integrityTab.stepBaseline', 'Baseline'), info: t('app.integrityTab.stepBaselineInfo', 'Snapshot the current files of a scope.') },
|
||||
{ icon: <Eye />, label: t('app.integrityTab.stepWatch', 'Watch'), info: t('app.integrityTab.stepWatchInfo', 'Check manually or let the scheduled sweep run.') },
|
||||
{ icon: <GitCompare />, 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')}
|
||||
</SectionIntro>
|
||||
|
||||
{managedScopes.map(renderScopeCard)}
|
||||
{appScopes.map(renderScopeCard)}
|
||||
|
||||
@@ -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 (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetOverlay className={overlayClassName} />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn('ui-sheet-content', `ui-sheet-content--${side}`, className)}
|
||||
|
||||
@@ -3332,7 +3332,7 @@
|
||||
"change": "Change",
|
||||
"checkNow": "Check now",
|
||||
"couldntLoadIntegrity": "Couldn't load file integrity status.",
|
||||
"headerLine": "Detect changed config files.",
|
||||
"headerLine": "Detect changed config files",
|
||||
"hideChanges": "Hide changes",
|
||||
"lastCheck": "Last check {{age}}",
|
||||
"nginxConfiguration": "Nginx configuration",
|
||||
|
||||
@@ -444,7 +444,7 @@ function ImportWizard() {
|
||||
<span className="tip-chip">{s.format}</span>
|
||||
<ChevronRight size={16} className="import-wizard__source-chev" aria-hidden="true" />
|
||||
</Button>
|
||||
<InfoTip className="import-wizard__source-tip">{s.description}</InfoTip>
|
||||
<InfoTip className="import-wizard__source-tip" title={s.name}>{s.description}</InfoTip>
|
||||
</div>
|
||||
))}
|
||||
<div className="import-wizard__source">
|
||||
@@ -454,7 +454,7 @@ function ImportWizard() {
|
||||
<span className="tip-chip">{t('app.importWizard.liveSite', 'Live site')}</span>
|
||||
<ChevronRight size={16} className="import-wizard__source-chev" aria-hidden="true" />
|
||||
</Link>
|
||||
<InfoTip className="import-wizard__source-tip">{t('app.importWizard.pullALiveWordpressSiteStraight', 'Pull a live WordPress site straight from its current server. Opens the WordPress import surface.')}</InfoTip>
|
||||
<InfoTip className="import-wizard__source-tip" title={t('app.importWizard.wordpressOverSsh', 'WordPress over SSH')}>{t('app.importWizard.pullALiveWordpressSiteStraight', 'Pull a live WordPress site straight from its current server. Opens the WordPress import surface.')}</InfoTip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -649,13 +649,11 @@ const PairAgentForm = ({ groups, onClose, onClaimed }) => {
|
||||
return (
|
||||
<form className="server-setup-form" onSubmit={handleClaim}>
|
||||
<div className="server-setup-form__body">
|
||||
<div className="pair-instructions">
|
||||
<IconStepStrip steps={[
|
||||
{ icon: <Terminal />, label: t('app.servers.stepStartAgent', 'Start agent'), info: t('app.servers.onTheTargetMachineStartThe', 'On the target server, start the agent. It will display a 6-character pair code and a passphrase. Enter both below.') },
|
||||
{ icon: <Hash />, label: t('app.servers.stepReadCode', 'Read code'), info: t('app.servers.theCodeIsShownInThe', 'The code is shown in the terminal output or system tray.') },
|
||||
{ icon: <KeyRound />, label: t('app.servers.stepEnterBoth', 'Enter both') },
|
||||
]} />
|
||||
</div>
|
||||
<IconStepStrip steps={[
|
||||
{ icon: <Terminal />, label: t('app.servers.stepStartAgent', 'Start agent'), info: t('app.servers.onTheTargetMachineStartThe', 'On the target server, start the agent. It will display a 6-character pair code and a passphrase. Enter both below.') },
|
||||
{ icon: <Hash />, label: t('app.servers.stepReadCode', 'Read code'), info: t('app.servers.theCodeIsShownInThe', 'The code is shown in the terminal output or system tray.') },
|
||||
{ icon: <KeyRound />, label: t('app.servers.stepEnterBoth', 'Enter both') },
|
||||
]} />
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor={`${fieldId}-code`}>{t('app.servers.pairCode', 'Pair code')}</label>
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
@include flex-between();
|
||||
margin-bottom: $space-4;
|
||||
|
||||
// Narrow: title takes the row, trailing chips/actions wrap below.
|
||||
// Narrow: the title shrinks first so a trailing chip stays on its row;
|
||||
// only a wide chip/action cluster wraps below.
|
||||
@include respond-to($breakpoint-sm) {
|
||||
flex-wrap: wrap;
|
||||
gap: $space-2;
|
||||
|
||||
> h3 { flex: 1 1 auto; min-width: 0; }
|
||||
> h3 { flex: 1 1 10rem; min-width: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,54 @@
|
||||
code { font-family: $font-mono; font-size: $text-xs; }
|
||||
}
|
||||
|
||||
// Narrow screens: the tip is a bottom sheet with a grab handle, above any
|
||||
// open Modal/Sheet, clear of the home indicator.
|
||||
.ui-sheet-overlay.info-tip__overlay {
|
||||
z-index: $z-tooltip + 9;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.ui-sheet-content.info-tip__sheet {
|
||||
z-index: $z-tooltip + 10;
|
||||
gap: $space-2;
|
||||
max-height: 70vh;
|
||||
padding: $space-6 $space-5 calc(#{$space-5} + env(safe-area-inset-bottom));
|
||||
border-radius: $radius-overlay $radius-overlay 0 0;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-block-start: $space-2;
|
||||
inset-inline-start: 50%;
|
||||
width: 36px; // geometry: grab handle
|
||||
height: 4px; // geometry: grab handle
|
||||
border-radius: $radius-full;
|
||||
background: $border-default;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.info-tip__sheet-title {
|
||||
margin: 0;
|
||||
padding-inline-end: $space-8;
|
||||
color: $text-primary;
|
||||
font-size: $text-base;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.info-tip__sheet-body {
|
||||
color: $text-secondary;
|
||||
font-size: $text-base;
|
||||
line-height: 1.55;
|
||||
|
||||
p { margin: 0; }
|
||||
p + p { margin-top: $space-2; }
|
||||
code { font-family: $font-mono; font-size: $text-sm; }
|
||||
}
|
||||
|
||||
.section-intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -52,76 +100,157 @@
|
||||
margin: 0;
|
||||
color: $text-secondary;
|
||||
font-size: $text-sm;
|
||||
}
|
||||
|
||||
.icon-step-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
// Compact card: icon tile + title + (i), steps inline on the right.
|
||||
&--banner {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: $space-3 $space-6;
|
||||
padding: $space-3 $space-4;
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-subtle;
|
||||
border-radius: $radius-card;
|
||||
}
|
||||
|
||||
&__step {
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__tile {
|
||||
&__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
width: $control-lg;
|
||||
height: $control-lg;
|
||||
width: $control-md;
|
||||
height: $control-md;
|
||||
border-radius: $radius-control;
|
||||
background: $bg-secondary;
|
||||
border: 1px solid $border-default;
|
||||
color: $accent-primary;
|
||||
@include status-variant($accent-primary, $accent-bg);
|
||||
|
||||
svg { width: 20px; height: 20px; } // geometry: icon in tile
|
||||
svg { width: 18px; height: 18px; } // geometry: icon in tile
|
||||
}
|
||||
|
||||
&__label {
|
||||
&__title {
|
||||
min-width: 0;
|
||||
color: $text-primary;
|
||||
font-size: $text-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
font-size: $text-base;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
@include respond-to($breakpoint-sm) {
|
||||
&--banner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: $space-4;
|
||||
}
|
||||
|
||||
&__title { flex: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
// Equal-width step pills joined by thin chevrons that sit in the gaps.
|
||||
.icon-step-strip {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr; // equal pills, but never narrower than a label
|
||||
gap: $space-5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
&__step {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: -16px; // geometry: centres 12px chevron in the 20px gap
|
||||
width: 12px; // geometry: connector icon
|
||||
height: 12px; // geometry: connector icon
|
||||
color: $text-muted;
|
||||
svg { width: 16px; height: 16px; } // geometry: connector icon
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
// Narrow: stay one compact row; label sits under a smaller tile.
|
||||
@include respond-to($breakpoint-sm) {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: $space-1;
|
||||
&__pill {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $space-2;
|
||||
min-width: 0;
|
||||
min-height: $control-md;
|
||||
margin: 0;
|
||||
padding: $space-1 $space-2;
|
||||
border: 1px solid $border-default;
|
||||
border-radius: $radius-control;
|
||||
background: $bg-secondary;
|
||||
color: $text-primary;
|
||||
font: inherit;
|
||||
text-align: center;
|
||||
|
||||
&__step {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $space-1;
|
||||
&--info {
|
||||
cursor: pointer;
|
||||
@include transition(border-color);
|
||||
|
||||
&:hover,
|
||||
&[data-state='open'] { border-color: $accent-primary; }
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $accent-primary;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__tile {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: center;
|
||||
width: 36px; // geometry: compact tile
|
||||
height: 36px; // geometry: compact tile
|
||||
svg { width: 18px; height: 18px; } // geometry: icon in compact tile
|
||||
&__icon {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
color: $accent-primary;
|
||||
|
||||
svg { width: 16px; height: 16px; } // geometry: icon in pill
|
||||
}
|
||||
|
||||
&__label {
|
||||
min-width: 0;
|
||||
font-size: $text-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
// Touch hint only; on desktop hover + the accent border say "more here".
|
||||
&__more {
|
||||
display: none;
|
||||
width: 12px; // geometry: info hint glyph
|
||||
height: 12px; // geometry: info hint glyph
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
// Narrow: icon over a wrapping label, the (i) hint tucked in the corner.
|
||||
@include respond-to($breakpoint-sm) {
|
||||
gap: $space-4;
|
||||
|
||||
&__arrow { inset-inline-start: -14px; } // geometry: centres chevron in the 16px gap
|
||||
|
||||
&__pill {
|
||||
flex-direction: column;
|
||||
gap: $space-1;
|
||||
padding: $space-2 $space-1;
|
||||
}
|
||||
|
||||
&__label { font-size: $text-xs; }
|
||||
|
||||
// Centre the 14px chevron on the 36px tile.
|
||||
&__arrow { margin-block-start: 11px; svg { width: 14px; height: 14px; } } // geometry: connector
|
||||
&__more {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset-block-start: $space-1;
|
||||
inset-inline-end: $space-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,29 +323,6 @@
|
||||
color: $accent-primary;
|
||||
}
|
||||
|
||||
.tip-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-3;
|
||||
|
||||
&__text { flex: 1; min-width: 0; }
|
||||
|
||||
@include respond-to($breakpoint-sm) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $space-2;
|
||||
|
||||
.tip-illustration {
|
||||
width: 36px; // geometry: compact illustration
|
||||
height: 36px; // geometry: compact illustration
|
||||
}
|
||||
|
||||
&__text { flex: none; width: 100%; }
|
||||
}
|
||||
&__title { display: flex; align-items: center; gap: $space-1; color: $text-primary; font-weight: $font-weight-medium; }
|
||||
&__line { margin: 0; color: $text-secondary; font-size: $text-sm; }
|
||||
}
|
||||
|
||||
// Label + (i) tip on one row, for plain <label> markup outside FormField.
|
||||
.label-with-tip {
|
||||
display: flex;
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
.security-page {
|
||||
// ── card chrome (all tabs) ──────────────────────────────
|
||||
.tab-content {
|
||||
.card {
|
||||
.card,
|
||||
.section-intro--banner {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
@@ -27,6 +28,17 @@
|
||||
font-weight: $font-weight-semibold;
|
||||
color: var(--text-faint);
|
||||
|
||||
// One line: the subtitle suffix gives way first (ellipsis).
|
||||
white-space: nowrap;
|
||||
|
||||
// Subtitle suffix gives way first: one line, ellipsis.
|
||||
.sec-count {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.sec-listtitle--green { color: var(--green); }
|
||||
&.sec-listtitle--red { color: var(--red); }
|
||||
}
|
||||
@@ -66,8 +78,6 @@
|
||||
font-weight: $font-weight-medium;
|
||||
color: var(--text-faint);
|
||||
letter-spacing: 0;
|
||||
|
||||
@include respond-to($breakpoint-sm) { display: block; }
|
||||
}
|
||||
|
||||
// Severity / state chip (mono uppercase on a semantic wash) —
|
||||
|
||||
@@ -179,32 +179,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Brief instruction callout shown above the pair-code input
|
||||
.pair-instructions {
|
||||
display: flex;
|
||||
gap: $space-3;
|
||||
padding: $space-3 $space-4;
|
||||
background: rgba($accent-primary-raw, 0.07);
|
||||
border: 1px solid rgba($accent-primary-raw, 0.22);
|
||||
border-radius: $radius-control;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: $text-xs;
|
||||
color: $text-secondary;
|
||||
line-height: 1.5;
|
||||
|
||||
code {
|
||||
font-family: $font-mono;
|
||||
font-size: $text-xs;
|
||||
color: $accent-primary;
|
||||
background: rgba($accent-primary-raw, 0.12);
|
||||
padding: 1px $space-tight;
|
||||
border-radius: $radius-badge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-setup-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in new issue
Block a user