Simplify settings shell and nav guide

This commit is contained in:
rcourtman 2026-04-23 15:49:00 +01:00
parent b4692ef7f0
commit 42d657caf5
38 changed files with 84 additions and 100 deletions

View file

@ -407,7 +407,7 @@ export function AppLayout(props: AppLayoutProps) {
return (
<div
class={`pulse-shell ${layoutStore.isFullWidth() || kioskMode() ? 'pulse-shell--full-width' : ''} ${!kioskMode() ? 'pb-safe-or-20 md:pb-0' : ''}`}
class={`pulse-shell ${layoutStore.isFullWidth() || kioskMode() ? 'pulse-shell--full-width' : ''} ${!kioskMode() ? 'pb-safe-or-20 lg:pb-0' : ''}`}
>
<Show when={kioskMode()}>
<div
@ -566,7 +566,7 @@ export function AppLayout(props: AppLayoutProps) {
<Show when={!kioskMode()}>
<div
class="tabs mb-2 hidden md:flex items-end gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap border-b border-border scrollbar-hide"
class="tabs mb-2 hidden lg:flex items-end gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap border-b border-border scrollbar-hide"
role="tablist"
aria-label="Primary navigation"
>
@ -760,7 +760,7 @@ export function AppLayout(props: AppLayoutProps) {
<button
type="button"
onClick={() => aiChatStore.toggle()}
class="fixed right-0 top-1/2 -translate-y-1/2 z-40 flex min-h-10 sm:min-h-9 min-w-10 items-center justify-center px-2.5 py-2.5 rounded-l-xl bg-blue-600 text-white shadow-sm hover:bg-blue-700 transition-colors duration-200 group sm:top-1/2 sm:translate-y-[-50%] top-auto bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] translate-y-0"
class="fixed right-0 top-1/2 -translate-y-1/2 z-40 flex min-h-10 sm:min-h-9 min-w-10 items-center justify-center px-2.5 py-2.5 rounded-l-lg border border-r-0 border-border bg-surface text-blue-600 dark:text-blue-400 hover:bg-surface-hover hover:text-blue-700 dark:hover:text-blue-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 transition-colors duration-200 group sm:top-1/2 sm:translate-y-[-50%] top-auto bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] translate-y-0"
title={getAIChatLauncherTitle(aiChatStore.context.context?.name)}
aria-label={AI_CHAT_LAUNCHER_ARIA_LABEL}
>

View file

@ -26,21 +26,6 @@ export const AISettings: Component = () => {
<SettingsPanel
title={AI_SETTINGS_PANEL_TITLE}
description={AI_SETTINGS_PANEL_DESCRIPTION}
icon={
<svg
class="w-5 h-5 text-blue-600 dark:text-blue-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.8"
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611l-2.576.43a18.003 18.003 0 01-5.118 0l-2.576-.43c-1.717-.293-2.299-2.379-1.067-3.611L5 14.5"
/>
</svg>
}
action={(() => {
return (
<Toggle

View file

@ -2,7 +2,6 @@ import { Component } from 'solid-js';
import SettingsPanel from '@/components/shared/SettingsPanel';
import { API_TOKEN_SCOPES_DOC_URL } from '@/utils/docsLinks';
import APITokenManager from './APITokenManager';
import BadgeCheck from 'lucide-solid/icons/badge-check';
interface APIAccessPanelProps {
currentTokenHint?: string;
@ -17,7 +16,6 @@ export const APIAccessPanel: Component<APIAccessPanelProps> = (props) => {
<SettingsPanel
title="API Access"
description="Generate and manage scoped tokens for agents and automation."
icon={<BadgeCheck class="w-5 h-5" strokeWidth={2} />}
noPadding
>
<div class="space-y-3 p-4 sm:p-6 pb-6">

View file

@ -127,7 +127,6 @@ export const AgentProfilesPanel: Component = () => {
<SettingsPanel
title="Configuration Profiles"
description="Reusable agent configurations"
icon={<Settings class="w-5 h-5" strokeWidth={2} />}
action={
<div class="flex items-center gap-2">
<button
@ -236,7 +235,6 @@ export const AgentProfilesPanel: Component = () => {
<SettingsPanel
title="Agent Assignments"
description="Assign profiles to connected agents"
icon={<Users class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -100,7 +100,6 @@ export default function AuditLogPanel() {
<SettingsPanel
title="Audit Log"
description="Persistent, searchable audit events with optional signature verification."
icon={<Shield class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="space-y-6 p-4 sm:p-6"
action={

View file

@ -55,7 +55,6 @@ export const AuditWebhookPanel: Component<AuditWebhookPanelProps> = (props) => {
<SettingsPanel
title="Audit Webhooks"
description="Configure real-time delivery of security audit events to external systems."
icon={<Globe class="w-5 h-5" strokeWidth={2} />}
>
<Show when={!loading()} fallback={<div class="text-sm text-muted">Loading...</div>}>
<Card tone="info" padding="md">
@ -99,7 +98,6 @@ export const AuditWebhookPanel: Component<AuditWebhookPanelProps> = (props) => {
<SettingsPanel
title="Audit Webhooks"
description="Configure real-time delivery of security audit events to external systems."
icon={<Globe class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -4,7 +4,6 @@ import {
ORGANIZATION_SETTINGS_UNAVAILABLE_CLASS as ORGANIZATION_SETTINGS_PANEL_UNAVAILABLE_CLASS,
ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE as ORGANIZATION_SETTINGS_PANEL_UNAVAILABLE_MESSAGE,
} from '@/utils/organizationSettingsPresentation';
import CreditCard from 'lucide-solid/icons/credit-card';
import { BillingAdminOrganizationsTable } from './BillingAdminOrganizationsTable';
import { useBillingAdminPanelState } from './useBillingAdminPanelState';
@ -23,7 +22,6 @@ export const BillingAdminPanel: Component = () => {
<SettingsPanel
title="Billing Admin"
description="View and manage billing state across all tenants (hosted mode only)."
icon={<CreditCard class="w-5 h-5" />}
action={
<button
type="button"

View file

@ -33,7 +33,6 @@ interface CommercialUsageMetersProps {
interface CommercialBillingShellProps {
title: string;
description: string;
icon: JSX.Element;
action?: JSX.Element;
loading?: boolean;
loadingFallback?: JSX.Element;
@ -103,7 +102,6 @@ export const CommercialBillingShell: Component<CommercialBillingShellProps> = (p
<SettingsPanel
title={props.title}
description={props.description}
icon={props.icon}
action={props.action}
bodyClass="space-y-5"
>

View file

@ -1,6 +1,5 @@
import { Component, Show } from 'solid-js';
import OperationsPanel from '@/components/Settings/OperationsPanel';
import Activity from 'lucide-solid/icons/activity';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
import Download from 'lucide-solid/icons/download';
import { DiagnosticsResultsPanel } from '@/components/Settings/DiagnosticsResultsPanel';
@ -17,7 +16,6 @@ export const DiagnosticsPanel: Component = () => {
<OperationsPanel
title={DIAGNOSTICS_PANEL_COPY.title}
description={DIAGNOSTICS_PANEL_COPY.description}
icon={<Activity class="w-5 h-5 sm:w-5 sm:h-5" />}
action={
<div class="flex items-center justify-between sm:justify-end gap-3 flex-wrap">
<Show when={diagnosticsData()}>

View file

@ -5,14 +5,14 @@ import { EnvironmentLockBadge } from '@/components/shared/EnvironmentLockBadge';
import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup';
import type { TelemetryPreviewResponse } from '@/api/settings';
import { DockerRuntimeSettingsCard } from './DockerRuntimeSettingsCard';
import Sliders from 'lucide-solid/icons/sliders-horizontal';
import Activity from 'lucide-solid/icons/activity';
import Sun from 'lucide-solid/icons/sun';
import Moon from 'lucide-solid/icons/moon';
import Thermometer from 'lucide-solid/icons/thermometer';
import Maximize2 from 'lucide-solid/icons/maximize-2';
import Compass from 'lucide-solid/icons/compass';
import { temperatureStore } from '@/utils/temperature';
import { layoutStore } from '@/utils/layout';
import { WHATS_NEW_REOPEN_EVENT } from '@/components/shared/whatsNewModalModel';
import {
PVE_POLLING_MAX_SECONDS,
PVE_POLLING_MIN_SECONDS,
@ -95,7 +95,6 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
<SettingsPanel
title="General"
description="Manage appearance, layout, and default monitoring cadence."
icon={<Sliders class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>
@ -124,7 +123,7 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
</div>
</div>
<FilterButtonGroup
class="shrink-0 self-start sm:self-auto ml-12 sm:ml-0"
class="w-full sm:w-auto sm:shrink-0 max-w-full"
options={THEME_PREFERENCE_OPTIONS}
value={props.themePreference()}
onChange={props.setThemePreference}
@ -133,7 +132,7 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
</div>
{/* Temperature Unit Selector */}
<div class="flex items-center justify-between gap-4 p-4 sm:p-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 sm:p-6">
<div class="flex items-center gap-3 min-w-0">
<div class="shrink-0 p-2.5 rounded-md border border-border bg-surface">
<Thermometer class="w-5 h-5" strokeWidth={2} />
@ -146,7 +145,7 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
</div>
</div>
<FilterButtonGroup
class="shrink-0"
class="w-full sm:w-auto sm:shrink-0 max-w-full"
options={TEMPERATURE_UNIT_OPTIONS}
value={temperatureStore.unit()}
onChange={(value) => temperatureStore.setUnit(value)}
@ -173,13 +172,37 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
onChange={() => layoutStore.toggle()}
/>
</div>
{/* Reopen Navigation Guide */}
<div class="flex items-center justify-between gap-4 p-4 sm:p-6">
<div class="flex items-center gap-3 min-w-0">
<div class="shrink-0 p-2.5 rounded-md border border-border bg-surface">
<Compass class="w-5 h-5 text-slate-500" strokeWidth={2} />
</div>
<div class="text-sm text-muted min-w-0">
<p class="font-medium text-base-content truncate">Navigation guide</p>
<p class="text-xs text-muted line-clamp-2">
Replay the five-stop walkthrough of Dashboard, Infrastructure, Workloads, Storage,
and Recovery.
</p>
</div>
</div>
<button
type="button"
onClick={() => {
window.dispatchEvent(new CustomEvent(WHATS_NEW_REOPEN_EVENT));
}}
class="shrink-0 rounded-md border border-border bg-surface px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Reopen
</button>
</div>
</SettingsPanel>
{/* Usage Data + Privacy Card */}
<SettingsPanel
title="Usage data and privacy"
description="Control local-only usage events and anonymous outbound telemetry."
icon={<Sliders class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>
@ -306,7 +329,6 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
<SettingsPanel
title="Monitoring cadence"
description="Control how frequently Pulse polls Proxmox VE nodes."
icon={<Activity class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -1,6 +1,5 @@
import type { Component } from 'solid-js';
import { For, Show, createSignal } from 'solid-js';
import Server from 'lucide-solid/icons/server';
import SettingsPanel from '@/components/shared/SettingsPanel';
import { copyToClipboard } from '@/utils/clipboard';
import { formatAbsoluteTime, formatRelativeTime } from '@/utils/format';
@ -31,7 +30,6 @@ export const InfrastructureInstallerSection: Component = () => {
? 'Start here to add the first system you want Pulse to monitor, then expand into Docker, Kubernetes, Proxmox, and related infrastructure.'
: 'Primary setup hub for installing Pulse on the first host you want to monitor, then expanding into Docker, Kubernetes, Proxmox, and related infrastructure.'
}
icon={<Server class="h-5 w-5" strokeWidth={2} />}
bodyClass="space-y-5"
>
<Show when={state.setupHandoff()}>

View file

@ -22,7 +22,6 @@ import type {
} from '@/api/license';
import { apiErrorCode, apiErrorDetailField } from '@/api/responseUtils';
import { getSimpleStatusIndicator } from '@/utils/status';
import { PulseLogoIcon } from '@/components/icons/PulseLogoIcon';
import {
formatMonitoredSystemGroupedSourcesLabel,
formatMonitoredSystemLedgerUnavailableMessage,
@ -519,7 +518,6 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp
<SettingsPanel
title={presentation.panelTitle}
description={getMonitoredSystemLedgerDescription()}
icon={<PulseLogoIcon class="w-5 h-5" />}
bodyClass="space-y-4"
>
{content}

View file

@ -1,6 +1,5 @@
import { Component } from 'solid-js';
import SettingsPanel from '@/components/shared/SettingsPanel';
import Network from 'lucide-solid/icons/network';
import { NetworkBoundarySettingsSection } from './NetworkBoundarySettingsSection';
import type { NetworkSettingsPanelProps } from './networkSettingsModel';
@ -9,7 +8,6 @@ export const NetworkSettingsPanel: Component<NetworkSettingsPanelProps> = (props
<SettingsPanel
title="Network"
description="Configure the public URL, CORS, embedding, and webhook network boundaries."
icon={<Network class="w-5 h-5" strokeWidth={2} />}
noPadding
>
<NetworkBoundarySettingsSection {...props} />

View file

@ -4,7 +4,6 @@ import SettingsPanel from '@/components/shared/SettingsPanel';
type OperationsPanelProps = {
title: JSX.Element;
description?: JSX.Element;
icon?: JSX.Element;
action?: JSX.Element;
class?: string;
children: JSX.Element;
@ -15,7 +14,6 @@ export function OperationsPanel(props: OperationsPanelProps) {
<SettingsPanel
title={props.title}
description={props.description}
icon={props.icon}
action={props.action}
class={props.class}
noPadding

View file

@ -6,7 +6,6 @@ import {
ORGANIZATION_SETTINGS_UNAVAILABLE_CLASS,
ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE,
} from '@/utils/organizationSettingsPresentation';
import Users from 'lucide-solid/icons/users';
import { OrganizationAccessLoadingState } from './OrganizationAccessLoadingState';
import { OrganizationAccessInvitationsSection } from './OrganizationAccessInvitationsSection';
import { OrganizationAccessManagementSection } from './OrganizationAccessManagementSection';
@ -35,7 +34,6 @@ export const OrganizationAccessPanel: Component<OrganizationAccessPanelProps> =
<SettingsPanel
title="Organization Access"
description="Manage organization invitations, member roles, and ownership transfers."
icon={<Users class="w-5 h-5" />}
bodyClass="space-y-5"
>
<Show when={!state.loading()} fallback={<OrganizationAccessLoadingState />}>

View file

@ -3,7 +3,6 @@ import {
ORGANIZATION_SETTINGS_UNAVAILABLE_CLASS,
ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE,
} from '@/utils/organizationSettingsPresentation';
import CreditCard from 'lucide-solid/icons/credit-card';
import {
CommercialBillingShell,
CommercialSection,
@ -33,7 +32,6 @@ export const OrganizationBillingPanel: Component<OrganizationBillingPanelProps>
<CommercialBillingShell
title="Billing & Usage"
description="Review your organization plan, usage against limits, and available upgrade paths."
icon={<CreditCard class="w-5 h-5" />}
loading={loading()}
loadingFallback={<OrganizationBillingLoadingState />}
>

View file

@ -6,7 +6,6 @@ import {
ORGANIZATION_SETTINGS_UNAVAILABLE_CLASS,
ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE,
} from '@/utils/organizationSettingsPresentation';
import Building2 from 'lucide-solid/icons/building-2';
import { OrganizationOverviewDetailsSection } from './OrganizationOverviewDetailsSection';
import { OrganizationOverviewLoadingState } from './OrganizationOverviewLoadingState';
import { OrganizationOverviewMembersSection } from './OrganizationOverviewMembersSection';
@ -34,7 +33,6 @@ export const OrganizationOverviewPanel: Component<OrganizationOverviewPanelProps
<SettingsPanel
title="Organization Overview"
description="Review organization metadata, membership footprint, and edit the display name."
icon={<Building2 class="w-5 h-5" />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -6,7 +6,6 @@ import {
ORGANIZATION_SETTINGS_UNAVAILABLE_CLASS,
ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE,
} from '@/utils/organizationSettingsPresentation';
import Share2 from 'lucide-solid/icons/share-2';
import { OrganizationIncomingSharesSection } from './OrganizationIncomingSharesSection';
import { OrganizationOutgoingSharesSection } from './OrganizationOutgoingSharesSection';
import { OrganizationSharingCreateSection } from './OrganizationSharingCreateSection';
@ -35,7 +34,6 @@ export const OrganizationSharingPanel: Component<OrganizationSharingPanelProps>
<SettingsPanel
title="Organization Sharing"
description="Share views and resources across organizations with explicit role-based access and target-organization approval."
icon={<Share2 class="w-5 h-5" />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -1,6 +1,5 @@
import { Show, type Component } from 'solid-js';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
import { PulseLogoIcon } from '@/components/icons/PulseLogoIcon';
import { MonitoredSystemLedgerPanel } from './MonitoredSystemLedgerPanel';
import { CommercialBillingShell, CommercialSection } from './CommercialBillingSections';
import { ProLicensePlanSection } from './ProLicensePlanSection';
@ -23,7 +22,6 @@ const ProLicensePolicyLoadingPanel: Component = () => (
<CommercialBillingShell
title={SELF_HOSTED_PRO_BILLING_PRESENTATION.hiddenShellTitle}
description={SELF_HOSTED_PRO_BILLING_PRESENTATION.hiddenShellDescription}
icon={<PulseLogoIcon class="w-5 h-5" />}
loading={false}
>
<div class="rounded-lg border border-border bg-surface px-4 py-4 text-sm">
@ -39,7 +37,6 @@ const ProLicenseHiddenPanel: Component = () => (
<CommercialBillingShell
title={SELF_HOSTED_PRO_BILLING_PRESENTATION.hiddenShellTitle}
description={SELF_HOSTED_PRO_BILLING_PRESENTATION.hiddenShellDescription}
icon={<PulseLogoIcon class="w-5 h-5" />}
loading={false}
>
<div class="rounded-lg border border-border bg-surface px-4 py-4 text-sm">
@ -59,7 +56,6 @@ const ProLicensePanelContent: Component = () => {
<CommercialBillingShell
title={SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle}
description={SELF_HOSTED_PRO_BILLING_PRESENTATION.shellDescription}
icon={<PulseLogoIcon class="w-5 h-5" />}
action={
<button
class="inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60"

View file

@ -1,7 +1,6 @@
import { Component, Show, For, Accessor, Setter } from 'solid-js';
import SettingsPanel from '@/components/shared/SettingsPanel';
import { SectionHeader } from '@/components/shared/SectionHeader';
import Clock from 'lucide-solid/icons/clock';
import {
BACKUP_INTERVAL_MAX_MINUTES,
BACKUP_INTERVAL_OPTIONS,
@ -35,7 +34,6 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
<SettingsPanel
title="Recovery"
description="Manage backup/snapshot polling and configuration export/import."
icon={<Clock class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -38,7 +38,6 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
<SettingsPanel
title="Remote Access"
description={RELAY_SETTINGS_DESCRIPTION}
icon={<RadioTower size={20} strokeWidth={2} />}
>
<Show when={!state.loading()} fallback={<div class="text-sm ">Loading...</div>}>
<Card tone="info" padding="md">
@ -80,7 +79,6 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
<SettingsPanel
title="Remote Access"
description={RELAY_SETTINGS_DESCRIPTION}
icon={<RadioTower size={20} strokeWidth={2} />}
>
<Show
when={!state.loading()}

View file

@ -102,7 +102,6 @@ export function ReportingPanel() {
<OperationsPanel
title="Reporting"
description="Loading reporting surfaces..."
icon={<BarChart class="w-5 h-5" strokeWidth={2} />}
>
<div class="p-4 sm:p-6">
<p class="text-sm text-muted">Loading reporting surfaces...</p>
@ -114,7 +113,6 @@ export function ReportingPanel() {
<OperationsPanel
title="Reporting"
description="Reporting surfaces are currently unavailable."
icon={<BarChart class="w-5 h-5" strokeWidth={2} />}
>
<div class="space-y-4 p-4 sm:p-6">
<p class="text-sm text-warning">{reportingCatalogError()}</p>
@ -135,7 +133,6 @@ export function ReportingPanel() {
<OperationsPanel
title={reportingCatalog()!.title}
description={reportingCatalog()!.description}
icon={<BarChart class="w-5 h-5" strokeWidth={2} />}
>
<div class="p-4 sm:p-6">
<div class="flex flex-col sm:flex-row items-center gap-4">
@ -177,7 +174,6 @@ export function ReportingPanel() {
<OperationsPanel
title={reportingCatalog()!.title}
description={reportingCatalog()!.description}
icon={<BarChart class="w-5 h-5" strokeWidth={2} />}
>
<div class="space-y-6 p-4 sm:p-6">
<Show when={reportingCatalogLoading()}>

View file

@ -7,7 +7,6 @@ import { getRolesEmptyState } from '@/utils/rbacPresentation';
import Plus from 'lucide-solid/icons/plus';
import Pencil from 'lucide-solid/icons/pencil';
import Trash2 from 'lucide-solid/icons/trash-2';
import Shield from 'lucide-solid/icons/shield';
import BadgeCheck from 'lucide-solid/icons/badge-check';
import { PulseDataGrid } from '@/components/shared/PulseDataGrid';
@ -19,7 +18,6 @@ export const RolesPanel: Component = () => {
<SettingsPanel
title="Roles"
description="Manage built-in and custom roles with granular permissions."
icon={<Shield class="w-5 h-5" />}
action={
<button
type="button"

View file

@ -185,7 +185,6 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
<SettingsPanel
title="Single Sign-On Providers"
description="Configure OIDC and SAML identity providers."
icon={<Shield class="w-5 h-5" strokeWidth={2} />}
action={
<div class="flex flex-wrap justify-end gap-2">
<button

View file

@ -16,7 +16,6 @@ import {
SECURITY_AUTH_SETUP_LABEL,
} from '@/utils/securityAuthPresentation';
import { QuickSecuritySetup } from './QuickSecuritySetup';
import Lock from 'lucide-solid/icons/lock';
import AlertTriangle from 'lucide-solid/icons/alert-triangle';
import type { VersionInfo } from '@/api/updates';
@ -60,7 +59,6 @@ export const SecurityAuthPanel: Component<SecurityAuthPanelProps> = (props) => {
<SettingsPanel
title="Authentication"
description="Manage password-based authentication, login visibility, and credential rotation."
icon={<Lock class="w-5 h-5" strokeWidth={2} />}
noPadding={showAuthenticationControls()}
bodyClass={showAuthenticationControls() ? 'divide-y divide-border' : 'space-y-6'}
>

View file

@ -86,7 +86,6 @@ export const SecurityOverviewPanel: Component<SecurityOverviewPanelProps> = (pro
<SettingsPanel
title="Security Overview"
description="Review your security posture, authentication boundary, and the next hardening steps for this Pulse instance."
icon={<Shield class="w-5 h-5" strokeWidth={2} />}
bodyClass="space-y-6"
>
<Show when={props.securityStatusLoading()}>

View file

@ -24,7 +24,6 @@ export const SystemLogsPanel: Component = () => {
<OperationsPanel
title={SYSTEM_LOGS_PANEL_COPY.title}
description={SYSTEM_LOGS_PANEL_COPY.description}
icon={<Terminal class="w-5 h-5" strokeWidth={2} />}
>
{/* Controls */}
<div class="p-4 sm:p-6">

View file

@ -90,7 +90,6 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
<SettingsPanel
title={UPDATES_PANEL_COPY.title}
description={UPDATES_PANEL_COPY.description}
icon={<RefreshCw class="w-5 h-5" strokeWidth={2} />}
noPadding
bodyClass="divide-y divide-border"
>

View file

@ -21,7 +21,6 @@ export const UserAssignmentsPanel: Component = () => {
<SettingsPanel
title="User Access"
description="Assign roles to users and review effective permissions."
icon={<Users class="w-5 h-5" />}
action={
<SearchField
placeholder="Search users..."

View file

@ -14,11 +14,11 @@ export function MobileNavBar(props: MobileNavBarProps) {
return (
<>
<nav class="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-surface pb-safe md:hidden">
<nav class="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-surface pb-safe lg:hidden">
<div class="relative">
<div
ref={mobileNav.setNavRef}
class="flex items-center gap-1 overflow-x-auto scrollbar-hide px-2 py-1.5"
class="flex items-center gap-1 overflow-x-auto scrollbar-hide px-2 py-1.5 sm:gap-2 sm:overflow-x-visible sm:px-4 sm:justify-between"
role="tablist"
aria-label="Mobile navigation"
>

View file

@ -43,7 +43,9 @@ export function PageHeader(props: PageHeaderProps) {
<Show when={local.titleMeta}>{local.titleMeta}</Show>
</div>
<Show when={local.description}>
<p class={`mt-1 text-sm font-medium text-muted ${local.descriptionClass ?? ''}`.trim()}>
<p
class={`mt-1 hidden text-sm font-medium text-muted sm:block ${local.descriptionClass ?? ''}`.trim()}
>
{local.description}
</p>
</Show>

View file

@ -1,12 +1,10 @@
import { JSX, Show, splitProps } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { SectionHeader } from '@/components/shared/SectionHeader';
type SettingsPanelProps = {
title: JSX.Element;
description?: JSX.Element;
action?: JSX.Element;
icon?: JSX.Element;
bodyClass?: string;
tone?: 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger';
padding?: 'none' | 'sm' | 'md' | 'lg';
@ -18,7 +16,6 @@ export function SettingsPanel(props: SettingsPanelProps) {
'title',
'description',
'action',
'icon',
'bodyClass',
'children',
'class',
@ -37,16 +34,15 @@ export function SettingsPanel(props: SettingsPanelProps) {
>
<div class="px-3 py-3 sm:px-6 sm:py-4 border-b border-border bg-surface-alt">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<div class="flex min-w-0 items-center gap-3 flex-1">
<Show when={local.icon}>
<div class="text-base-content dark:text-slate-100">{local.icon}</div>
<div class="flex min-w-0 flex-col gap-1 flex-1">
<h2 class="text-sm sm:text-base tracking-tight font-semibold text-base-content dark:text-slate-100">
{local.title}
</h2>
<Show when={local.description}>
<p class="text-xs sm:text-sm text-muted dark:text-slate-200">
{local.description}
</p>
</Show>
<SectionHeader
title={local.title}
description={local.description}
size="sm"
class="min-w-0 flex-1"
/>
</div>
<Show when={local.action}>
<div class="w-full sm:w-auto">{local.action}</div>

View file

@ -64,7 +64,7 @@ export function WhatsNewModal() {
<div
data-tour-spotlight=""
data-tour-step={step().target}
class="pointer-events-none absolute rounded-[1.25rem] border border-blue-300/80 bg-white/5 transition-all duration-200 dark:border-blue-300/60"
class="pointer-events-none absolute rounded-[1.25rem] border border-blue-300/80 bg-white/10 transition-all duration-200 dark:border-blue-300/60"
style={style()}
/>
)}

View file

@ -68,10 +68,10 @@ describe('WhatsNewModal', () => {
it('renders when the navigation modal has not been seen yet', async () => {
render(() => <WhatsNewModal />);
const dialog = await screen.findByRole('dialog', { name: 'Welcome to Pulse v6' });
const dialog = await screen.findByRole('dialog', { name: 'Pulse navigation guide' });
expect(dialog).toBeInTheDocument();
expect(within(dialog).getByText('Step 1 of 5')).toBeInTheDocument();
expect(within(dialog).getByText('Quick Tour')).toBeInTheDocument();
expect(within(dialog).getByText('Nav guide')).toBeInTheDocument();
expect(within(dialog).getByText(/Start here for health, alerts, capacity/i)).toBeInTheDocument();
expect(within(dialog).queryByText('Where Things Moved')).not.toBeInTheDocument();
expect(within(dialog).getByRole('link', { name: 'Navigation guide' })).toBeInTheDocument();

View file

@ -4,7 +4,7 @@ import {
presentationPolicyIsDemoMode,
sessionPresentationPolicyResolved,
} from '@/stores/sessionPresentationPolicy';
import { WHATS_NEW_FEATURE_CARDS } from './whatsNewModalModel';
import { WHATS_NEW_FEATURE_CARDS, WHATS_NEW_REOPEN_EVENT } from './whatsNewModalModel';
type SpotlightRect = {
top: number;
@ -111,6 +111,16 @@ export function useWhatsNewModalState() {
setStepIndex(clamp(index, 0, WHATS_NEW_FEATURE_CARDS.length - 1));
};
if (typeof window !== 'undefined') {
const handleReopen = () => {
setHasSeen(false);
setDismissedForSession(false);
setStepIndex(0);
};
window.addEventListener(WHATS_NEW_REOPEN_EVENT, handleReopen);
onCleanup(() => window.removeEventListener(WHATS_NEW_REOPEN_EVENT, handleReopen));
}
createEffect(() => {
if (!isOpen()) return;
@ -208,7 +218,7 @@ export function useWhatsNewModalState() {
width: `${rect.width}px`,
height: `${rect.height}px`,
'box-shadow':
'0 0 0 9999px rgba(15, 23, 42, 0.68), 0 0 0 1px rgba(255, 255, 255, 0.18), 0 0 28px rgba(96, 165, 250, 0.55)',
'0 0 0 9999px rgba(15, 23, 42, 0.78), 0 0 0 2px rgba(255, 255, 255, 0.4), 0 0 40px rgba(96, 165, 250, 0.75)',
};
});

View file

@ -27,7 +27,7 @@ export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [
title: 'Infrastructure',
},
{
accent: 'border-purple-200 bg-purple-50 dark:border-purple-800 dark:bg-purple-900',
accent: 'border-rose-200 bg-rose-50 dark:border-rose-800 dark:bg-rose-900',
description: 'Use this for VMs, containers, and pods.',
icon: 'workloads',
target: 'workloads',
@ -49,8 +49,10 @@ export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [
},
];
export const WHATS_NEW_KICKER_LABEL = 'Quick Tour';
export const WHATS_NEW_TITLE = 'Welcome to Pulse v6';
export const WHATS_NEW_REOPEN_EVENT = 'pulse:reopen-nav-guide';
export const WHATS_NEW_KICKER_LABEL = 'Nav guide';
export const WHATS_NEW_TITLE = 'Pulse navigation guide';
export const WHATS_NEW_PROGRESS_PREFIX = 'Step';
export const WHATS_NEW_BACK_LABEL = 'Back';
export const WHATS_NEW_CLOSE_LABEL = 'Close';

View file

@ -2,6 +2,24 @@
@tailwind components;
@tailwind utilities;
/*
* Neutralize a Tailwind color/font-size collision: the dead `base` alias in
* the color palette was making Tailwind emit `color: var(--color-bg-base)` on
* `.text-base` / `.sm\:text-base`, painting text the same color as the
* background. The alias has been removed from tailwind.config.js; this rule
* keeps things visible until the next dev-server restart picks up that change.
*/
@layer utilities {
.text-base,
.sm\:text-base,
.md\:text-base,
.lg\:text-base,
.xl\:text-base,
.\32xl\:text-base {
color: inherit;
}
}
/* Update banner animation */
@keyframes slideDown {
from {

View file

@ -18,7 +18,6 @@ export default {
extend: {
colors: {
// Semantic design system mapped to index.css
base: 'var(--color-bg-base)',
surface: 'var(--color-bg-surface)',
'surface-hover': 'var(--color-bg-surface-hover)',
'surface-alt': 'var(--color-bg-surface-alt)',