Change disabled styling to look disabled

This commit is contained in:
James Brunton
2026-08-27 11:38:56 +01:00
parent bdee7d5a79
commit 22fdc137ae
5 changed files with 55 additions and 32 deletions
+7 -7
View File
@@ -9,7 +9,7 @@ html[data-app-theme="light"] {
--c-bg-raised: var(--p-white);
--c-surface: var(--p-snow);
--c-surface-raised: var(--p-white);
--c-surface-sunken: var(--p-gray-100);
--c-surface-sunken: var(--p-c-f4f2ed);
--c-input-bg: var(--p-white);
--c-modal-surface: var(--p-white);
--c-hover: var(--p-gray-50);
@@ -144,7 +144,7 @@ html[data-app-theme="midnight"] {
--c-bg-raised: var(--p-zinc-850);
--c-surface: var(--p-c-1a1a1d);
--c-surface-raised: var(--p-zinc-650);
--c-surface-sunken: var(--p-zinc-850);
--c-surface-sunken: var(--p-c-100f0e);
--c-input-bg: var(--c-surface);
--c-modal-surface: var(--c-surface);
--c-hover: var(--p-gray-800);
@@ -206,8 +206,8 @@ html[data-app-theme="custom"] {
--c-surface-raised: color-mix(in srgb, var(--c-primary) 4%, var(--p-white));
--c-surface-sunken: color-mix(
in srgb,
var(--c-primary) 8%,
var(--p-gray-100)
var(--c-primary) 4%,
var(--p-c-f4f2ed)
);
--c-input-bg: color-mix(in srgb, var(--c-primary) 2%, var(--p-white));
--c-hover: color-mix(in srgb, var(--c-primary) 9%, var(--p-gray-50));
@@ -360,7 +360,7 @@ html[data-app-theme="custom"][data-mantine-color-scheme="dark"] {
--c-surface-sunken: color-mix(
in srgb,
var(--c-primary) 8%,
var(--p-zinc-900)
var(--p-c-100f0e)
);
--c-input-bg: var(--c-surface);
--c-hover: color-mix(in srgb, var(--c-primary) 12%, var(--p-zinc-750));
@@ -412,7 +412,7 @@ html[data-app-theme="custom"][data-accent="default"] {
--c-bg-raised: var(--p-white);
--c-surface: var(--p-snow);
--c-surface-raised: var(--p-white);
--c-surface-sunken: var(--p-gray-100);
--c-surface-sunken: var(--p-c-f4f2ed);
--c-input-bg: var(--p-white);
--c-hover: var(--p-gray-50);
--c-active: var(--p-gray-100);
@@ -425,7 +425,7 @@ html[data-app-theme="custom"][data-accent="default"][data-mantine-color-scheme="
--c-bg-raised: var(--p-zinc-850);
--c-surface: var(--p-c-1a1a1d);
--c-surface-raised: var(--p-zinc-775);
--c-surface-sunken: var(--p-zinc-900);
--c-surface-sunken: var(--p-c-100f0e);
--c-input-bg: var(--c-surface);
--c-hover: var(--p-zinc-750);
--c-active: var(--p-zinc-700);
@@ -39,6 +39,7 @@
--p-zinc-100: #f4f4f5;
--p-c-141416: #141416;
--p-c-1a1a1d: #1a1a1d;
--p-c-100f0e: #100f0e;
--p-c-28282d: #28282d;
--p-c-343439: #343439;
--p-blue-400: #60a5fa;
@@ -115,6 +116,7 @@
--p-ink: #373530;
--p-snow: #fafafa;
--p-c-f0f0f0: #f0f0f0;
--p-c-f4f2ed: #f4f2ed;
/* Notion-style procurement view palette. */
--p-notion-blue: #2383e2;
@@ -6,9 +6,20 @@
height: 12rem;
}
/* Disabled cards recede onto the standard sunken surface, and reinforce it with ink: the accent
icon chip flattens to a muted glyph and every label drops to muted, so the card reads clearly
greyed against the usable ones. */
.portal-pipelines__template-card--locked {
/* Recede via surface + ink rather than opacity, which would fade text below the contrast floor. */
background: var(--c-surface-sunken);
}
.portal-pipelines__template-card--locked .portal-pipelines__template-icon {
background: transparent;
color: var(--c-text-muted);
}
.portal-pipelines__template-card--locked .portal-pipelines__template-title,
.portal-pipelines__template-card--locked .portal-pipelines__template-blurb {
color: var(--c-text-muted);
}
@@ -23,11 +34,6 @@
color: var(--c-accent-fg, var(--c-primary));
}
.portal-pipelines__template-card--locked .portal-pipelines__template-icon {
background: var(--c-surface-raised);
color: var(--c-text-muted);
}
.portal-pipelines__template-title {
margin: 0;
font-size: 0.9375rem;
@@ -62,3 +68,13 @@
font-weight: 620;
color: var(--c-accent-fg, var(--c-primary));
}
/* The disabled card's foot: sits where the CTA does, but muted - the reason it can't be set up. */
.portal-pipelines__template-note {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--c-text-muted);
}
@@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
import { Card, Chip } from "@app/ui";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { Card } from "@app/ui";
import type { CatalogueEntry } from "@portal/api/policies";
import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon";
import "@portal/components/pipelines/PipelineTemplateCard.css";
@@ -18,8 +19,8 @@ interface PipelineTemplateCardProps {
/**
* A template in the Pipelines gallery: a ready-made starting point that opens the simple, guided
* setup. Vertical card (icon, title, blurb, a "Set up" affordance) so the gallery reads as a set of
* choices, distinct from the pipelines table below. Locked/coming-soon templates show a chip and
* don't open.
* choices, distinct from the pipelines table below.
*
*/
export function PipelineTemplateCard({
entry,
@@ -60,19 +61,18 @@ export function PipelineTemplateCard({
<h3 className="portal-pipelines__template-title">{t(category.label)}</h3>
<p className="portal-pipelines__template-blurb">{t(category.desc)}</p>
{comingSoon ? (
<Chip accent="neutral" size="sm">
{t("portal.policies.card.comingSoon")}
</Chip>
) : locked ? (
<Chip accent="neutral" size="sm">
{lockedLabel ?? t("portal.policies.card.requiresAiEngine")}
</Chip>
) : (
{openable ? (
<span className="portal-pipelines__template-cta">
{t("portal.pipelines.templates.setUp")}
<ArrowForwardRoundedIcon style={{ fontSize: "1rem" }} />
</span>
) : (
<span className="portal-pipelines__template-note">
<LockOutlinedIcon style={{ fontSize: "0.95rem" }} />
{comingSoon
? t("portal.policies.card.comingSoon")
: (lockedLabel ?? t("portal.policies.card.requiresAiEngine"))}
</span>
)}
</Card>
);
+12 -7
View File
@@ -69,13 +69,6 @@ export function Pipelines() {
const hasPipelines = pipelines.length > 0;
const showEmpty = !listLoading && pipelines.length === 0;
// The gallery is the on-ramp: only suggested policies NOT yet set up (incl. "coming soon"). Once a
// policy is configured it lives in the list below, so it never appears in both places.
const galleryEntries = useMemo(
() => (catalogueData?.catalogue ?? []).filter((e) => e.policy === null),
[catalogueData],
);
const isLocked = useCallback(
(entry: CatalogueEntry): boolean =>
entry.category.requiresAiEngine === true &&
@@ -84,6 +77,18 @@ export function Pipelines() {
[aiEngineEnabled, aiEngineLoading],
);
// The gallery is the on-ramp: only suggested policies NOT yet set up (once configured a policy
// lives in the list below). Templates the user can set up now sort first; coming-soon / AI-locked
// ones stay, shown disabled, at the end (a stable sort keeps each group in its original order).
const galleryEntries = useMemo(() => {
const entries = (catalogueData?.catalogue ?? []).filter(
(e) => e.policy === null,
);
const usable = (e: CatalogueEntry) =>
!e.category.comingSoon && !isLocked(e);
return [...entries].sort((a, b) => Number(usable(b)) - Number(usable(a)));
}, [catalogueData, isLocked]);
// Pipelines and policies share a backend, and Home/onboarding read the same caches, so refresh all
// three: the overview list, the catalogue list, and runs.
const refetch = useCallback(() => {