You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/08/06 15:51:53 UTC
(airavata-custos) branch ui-fixes created (now e2c978f91)
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a change to branch ui-fixes
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
at e2c978f91 Fix dropdown, breadcrumb, and theme UX issues across the portal
This branch includes the following new commits:
new e2c978f91 Fix dropdown, breadcrumb, and theme UX issues across the portal
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(airavata-custos) 01/01: Fix dropdown, breadcrumb, and theme UX issues across the portal
Posted by la...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch ui-fixes
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit e2c978f911632908bd050705c9e545eb50f55ba7
Author: lahiruj <la...@gmail.com>
AuthorDate: Tue Aug 4 22:46:17 2026 -0400
Fix dropdown, breadcrumb, and theme UX issues across the portal
---
web/design-tokens/colors.css | 12 ++--
.../(portal)/admin/users/management/UsersTable.tsx | 65 +++++++++++-------
.../users/management/__tests__/UsersTable.test.tsx | 4 +-
.../(portal)/allocations/[allocationId]/layout.tsx | 34 ----------
.../change-requests/[changeRequestId]/layout.tsx | 37 ----------
web/src/app/(portal)/page.tsx | 11 +--
.../app/(portal)/projects/[projectId]/layout.tsx | 32 ---------
.../allocations/components/AllocationDetail.tsx | 2 +
.../components/AllocationDetailHeader.tsx | 30 ++++++--
.../components/AllocationOverviewTab.tsx | 18 ++---
.../allocations/components/AllocationsList.tsx | 44 ++++++++----
.../components/ChangeRequestSubmitDrawer.tsx | 8 ++-
.../core/identity/components/AccessCard.tsx | 6 +-
.../core/projects/components/ProjectDetail.tsx | 4 +-
.../projects/components/ProjectDetailHeader.tsx | 14 ++--
.../projects/components/ProjectOverviewTab.tsx | 18 ++---
.../core/projects/components/ProjectsList.tsx | 35 +++++++---
web/src/shared/layout/BreadcrumbLabelsProvider.tsx | 79 ++++++++++++++++++++++
web/src/shared/layout/Breadcrumbs.tsx | 4 +-
web/src/shared/layout/PortalLayout.tsx | 31 +++++----
web/src/shared/layout/Sidebar.tsx | 10 +--
web/src/shared/ui/Breadcrumbs.tsx | 74 --------------------
web/src/shared/ui/Loading.tsx | 2 +-
web/src/shared/ui/MetaRow.tsx | 2 +-
web/src/shared/ui/StatusBadge.tsx | 12 ++--
web/src/shared/ui/select.tsx | 10 +--
26 files changed, 289 insertions(+), 309 deletions(-)
diff --git a/web/design-tokens/colors.css b/web/design-tokens/colors.css
index d27d276cd..8cb7bd505 100644
--- a/web/design-tokens/colors.css
+++ b/web/design-tokens/colors.css
@@ -131,8 +131,8 @@
--muted: var(--custos-gray-100);
/* Pinned to gray-600 — gray-500 on bg-muted is 4.28:1 and fails WCAG AA. */
--muted-foreground: var(--custos-gray-600);
- --accent: var(--custos-blue-50);
- --accent-foreground: var(--custos-blue-700);
+ --accent: var(--custos-gray-100);
+ --accent-foreground: var(--custos-gray-900);
--destructive: var(--custos-red-600);
--destructive-foreground: var(--custos-white);
--border: var(--custos-gray-200);
@@ -172,14 +172,14 @@
--card-foreground: var(--custos-gray-50);
--popover: var(--custos-gray-800);
--popover-foreground: var(--custos-gray-50);
- --primary: var(--custos-blue-400);
- --primary-foreground: var(--custos-gray-900);
+ --primary: var(--custos-gray-700);
+ --primary-foreground: var(--custos-gray-50);
--secondary: var(--custos-gray-800);
--secondary-foreground: var(--custos-gray-50);
--muted: var(--custos-gray-800);
--muted-foreground: var(--custos-gray-300);
- --accent: var(--custos-blue-800);
- --accent-foreground: var(--custos-blue-100);
+ --accent: var(--custos-gray-700);
+ --accent-foreground: var(--custos-gray-50);
--destructive: var(--custos-red-500);
--destructive-foreground: var(--custos-white);
--border: var(--custos-gray-700);
diff --git a/web/src/app/(portal)/admin/users/management/UsersTable.tsx b/web/src/app/(portal)/admin/users/management/UsersTable.tsx
index 4b29648b6..b9778ca4a 100644
--- a/web/src/app/(portal)/admin/users/management/UsersTable.tsx
+++ b/web/src/app/(portal)/admin/users/management/UsersTable.tsx
@@ -25,12 +25,13 @@ import {
} from "@/shared/hooks/useShallowSearchParams";
import { DataTable, type DataTableColumn } from "@/shared/ui/DataTable";
import { Input } from "@/shared/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/ui/select";
import { ChevronRight } from "lucide-react";
import * as React from "react";
import { IdentitiesCell } from "./IdentitiesCell";
import { PermissionsDrawer } from "./PermissionsDrawer";
import { RolesCell } from "./RolesCell";
-import { IDENTITY_SOURCE_LABELS } from "./identities";
+import { IDENTITY_SOURCE_LABELS, identitySourceLabel } from "./identities";
function fullNameFor(user: UserManagementRow): string {
const name = [user.first_name, user.last_name].filter(Boolean).join(" ");
@@ -79,6 +80,15 @@ export function UsersTable({
Boolean(search.trim()) || (canManageRoles && roleFilter !== "all") || identityFilter !== "all";
const selectedUser = users.find((user) => user.id === selectedId) ?? null;
+ function roleLabelFor(value: string): string {
+ if (value === "all") return "All roles";
+ return rolesCatalog.find((role) => role.id === value)?.name ?? value;
+ }
+
+ function identityLabelFor(value: string): string {
+ return value === "all" ? "All external identities" : identitySourceLabel(value);
+ }
+
function resetSelection() {
setSelectedId(null);
expandedRow.clear();
@@ -202,33 +212,36 @@ export function UsersTable({
className="sm:w-72"
/>
{canManageRoles ? (
- <select
- value={roleFilter}
- onChange={(event) => updateFilterParam("role", event.target.value)}
- aria-label="Filter this page by role"
- className="h-9 rounded-md border bg-background px-3 text-sm"
- >
- <option value="all">All roles</option>
- {rolesCatalog.map((role) => (
- <option key={role.id} value={role.id}>
- {role.name}
- </option>
- ))}
- </select>
+ <Select value={roleFilter} onValueChange={(value) => updateFilterParam("role", value)}>
+ <SelectTrigger aria-label="Filter this page by role" className="h-9 w-36 px-3">
+ <SelectValue>{(value: string) => roleLabelFor(value)}</SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">All roles</SelectItem>
+ {rolesCatalog.map((role) => (
+ <SelectItem key={role.id} value={role.id}>
+ {role.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
) : null}
- <select
+ <Select
value={identityFilter}
- onChange={(event) => updateFilterParam("identity", event.target.value)}
- aria-label="Filter this page by external identity"
- className="h-9 rounded-md border bg-background px-3 text-sm"
+ onValueChange={(value) => updateFilterParam("identity", value)}
>
- <option value="all">All external identities</option>
- {Object.entries(IDENTITY_SOURCE_LABELS).map(([source, label]) => (
- <option key={source} value={source}>
- {label}
- </option>
- ))}
- </select>
+ <SelectTrigger aria-label="Filter this page by external identity" className="h-9 w-56 px-3">
+ <SelectValue>{(value: string) => identityLabelFor(value)}</SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">All external identities</SelectItem>
+ {Object.entries(IDENTITY_SOURCE_LABELS).map(([source, label]) => (
+ <SelectItem key={source} value={source}>
+ {label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
</div>
{filtersActive ? (
@@ -248,7 +261,7 @@ export function UsersTable({
}}
rowClassName={(row) =>
isCurrentUser(row)
- ? "bg-[color:var(--custos-blue-50)]/40 hover:bg-[color:var(--custos-blue-50)]/60"
+ ? "bg-[color:var(--brand-tint)]/40 hover:bg-[color:var(--brand-tint)]/60"
: undefined
}
empty={
diff --git a/web/src/app/(portal)/admin/users/management/__tests__/UsersTable.test.tsx b/web/src/app/(portal)/admin/users/management/__tests__/UsersTable.test.tsx
index f09c578ae..77e6052bf 100644
--- a/web/src/app/(portal)/admin/users/management/__tests__/UsersTable.test.tsx
+++ b/web/src/app/(portal)/admin/users/management/__tests__/UsersTable.test.tsx
@@ -75,8 +75,8 @@ describe("UsersTable", () => {
renderTable(true);
expect(screen.getByText("Example User")).toBeInTheDocument();
expect(screen.getByText("(You)")).toBeInTheDocument();
- expect(screen.getAllByText("Administrator")).toHaveLength(2);
- expect(screen.getAllByText("CILogon")).toHaveLength(2);
+ expect(screen.getAllByText("Administrator")).toHaveLength(1);
+ expect(screen.getAllByText("CILogon")).toHaveLength(1);
});
it("hides all role UI without roles:manage", () => {
diff --git a/web/src/app/(portal)/allocations/[allocationId]/layout.tsx b/web/src/app/(portal)/allocations/[allocationId]/layout.tsx
deleted file mode 100644
index 8b9b6b8f7..000000000
--- a/web/src/app/(portal)/allocations/[allocationId]/layout.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-import type { ReactNode } from "react";
-import { Breadcrumbs } from "@/shared/ui/Breadcrumbs";
-
-export default async function AllocationDetailLayout(props: {
- children: ReactNode;
- params: Promise<{ allocationId: string }>;
-}) {
- const { allocationId } = await props.params;
- return (
- <div className="space-y-6">
- <Breadcrumbs
- items={[{ label: "Allocations", href: "/allocations" }, { label: allocationId }]}
- />
- {props.children}
- </div>
- );
-}
diff --git a/web/src/app/(portal)/change-requests/[changeRequestId]/layout.tsx b/web/src/app/(portal)/change-requests/[changeRequestId]/layout.tsx
deleted file mode 100644
index 48e1a3f32..000000000
--- a/web/src/app/(portal)/change-requests/[changeRequestId]/layout.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-import type { ReactNode } from "react";
-import { Breadcrumbs } from "@/shared/ui/Breadcrumbs";
-
-export default async function ChangeRequestDetailLayout(props: {
- children: ReactNode;
- params: Promise<{ changeRequestId: string }>;
-}) {
- const { changeRequestId } = await props.params;
- return (
- <div className="space-y-6">
- <Breadcrumbs
- items={[
- { label: "Change requests", href: "/change-requests" },
- { label: changeRequestId },
- ]}
- />
- {props.children}
- </div>
- );
-}
diff --git a/web/src/app/(portal)/page.tsx b/web/src/app/(portal)/page.tsx
index 200858565..18adf34e4 100644
--- a/web/src/app/(portal)/page.tsx
+++ b/web/src/app/(portal)/page.tsx
@@ -15,13 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+import { redirect } from "next/navigation";
+
export default function PortalLandingPage() {
- return (
- <div className="flex flex-col gap-2">
- <h1 className="text-2xl font-bold tracking-tight">Welcome to Custos Portal</h1>
- <p className="text-sm text-muted-foreground">
- Features will land in subsequent phases.
- </p>
- </div>
- );
+ redirect("/allocations");
}
diff --git a/web/src/app/(portal)/projects/[projectId]/layout.tsx b/web/src/app/(portal)/projects/[projectId]/layout.tsx
deleted file mode 100644
index f9717c517..000000000
--- a/web/src/app/(portal)/projects/[projectId]/layout.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-import type { ReactNode } from "react";
-import { Breadcrumbs } from "@/shared/ui/Breadcrumbs";
-
-export default async function ProjectDetailLayout(props: {
- children: ReactNode;
- params: Promise<{ projectId: string }>;
-}) {
- const { projectId } = await props.params;
- return (
- <div className="space-y-6">
- <Breadcrumbs items={[{ label: "Projects", href: "/projects" }, { label: projectId }]} />
- {props.children}
- </div>
- );
-}
diff --git a/web/src/features/core/allocations/components/AllocationDetail.tsx b/web/src/features/core/allocations/components/AllocationDetail.tsx
index 124199f1a..b0d71c293 100644
--- a/web/src/features/core/allocations/components/AllocationDetail.tsx
+++ b/web/src/features/core/allocations/components/AllocationDetail.tsx
@@ -19,6 +19,7 @@
import { useSession } from "next-auth/react";
import { useAbility } from "@/shared/casl/AbilityProvider";
+import { useBreadcrumbLabel } from "@/shared/layout/BreadcrumbLabelsProvider";
import { ErrorState } from "@/shared/ui/ErrorState";
import { CardSkeleton } from "@/shared/ui/Loading";
import { TabsRouter } from "@/shared/ui/TabsRouter";
@@ -39,6 +40,7 @@ export function AllocationDetail({ allocationId }: AllocationDetailProps) {
const { data: session } = useSession();
const allocationQuery = useAllocation(allocationId);
const membersQuery = useAllocationMembers(allocationId);
+ useBreadcrumbLabel(allocationId, allocationQuery.data?.name);
if (allocationQuery.isLoading) return <CardSkeleton />;
if (allocationQuery.error) {
diff --git a/web/src/features/core/allocations/components/AllocationDetailHeader.tsx b/web/src/features/core/allocations/components/AllocationDetailHeader.tsx
index 366f4c460..87d8ff47e 100644
--- a/web/src/features/core/allocations/components/AllocationDetailHeader.tsx
+++ b/web/src/features/core/allocations/components/AllocationDetailHeader.tsx
@@ -17,7 +17,7 @@
"use client";
-import { Calendar, Server, UserSquare } from "lucide-react";
+import { Calendar, Gauge, Server, UserSquare } from "lucide-react";
import { MetaItem, MetaRow } from "@/shared/ui/MetaRow";
import type { ComputeAllocation } from "../schemas";
@@ -38,6 +38,10 @@ function formatDate(iso: string): string {
}
}
+function formatNumber(n: number): string {
+ return new Intl.NumberFormat().format(n);
+}
+
function isExpired(endTime: string, now = new Date()): boolean {
const t = new Date(endTime).getTime();
return Number.isFinite(t) && t < now.getTime();
@@ -58,12 +62,24 @@ export function AllocationDetailHeader({ allocation, memberCount }: AllocationDe
<h1 className="font-display text-[28px] font-bold leading-tight text-foreground">
{allocation.name}
</h1>
- <MetaRow>
- <MetaItem variant="status" tone={tone} value={label} />
- <MetaItem icon={Server} label="Cluster" value={allocation.compute_cluster_id} />
- <MetaItem icon={Calendar} label="End date" value={formatDate(allocation.end_time)} />
- <MetaItem icon={UserSquare} label="Members" value={memberCount} />
- </MetaRow>
+ <MetaItem
+ className="hidden"
+ icon={Server}
+ label="Cluster"
+ value={allocation.compute_cluster_id}
+ />
+ <div className="flex flex-wrap items-center gap-3">
+ <MetaItem variant="status" tone={tone} value={label} className="py-1.5" />
+ <MetaRow>
+ <MetaItem
+ icon={Gauge}
+ label="Initial SUs"
+ value={formatNumber(allocation.initial_su_amount)}
+ />
+ <MetaItem icon={Calendar} label="End date" value={formatDate(allocation.end_time)} />
+ <MetaItem icon={UserSquare} label="Members" value={memberCount} />
+ </MetaRow>
+ </div>
</header>
);
}
diff --git a/web/src/features/core/allocations/components/AllocationOverviewTab.tsx b/web/src/features/core/allocations/components/AllocationOverviewTab.tsx
index d88d936c1..337f83f61 100644
--- a/web/src/features/core/allocations/components/AllocationOverviewTab.tsx
+++ b/web/src/features/core/allocations/components/AllocationOverviewTab.tsx
@@ -56,30 +56,30 @@ export function AllocationOverviewTab({ allocation }: AllocationOverviewTabProps
return (
<div className="space-y-6">
- <dl className="grid gap-x-8 gap-y-3 sm:grid-cols-[max-content_1fr] text-sm">
+ <dl className="grid gap-x-8 gap-y-3 rounded-lg border border-border bg-muted/40 p-4 text-sm sm:grid-cols-[max-content_1fr]">
<dt className="text-muted-foreground">Allocation ID</dt>
- <dd className="font-mono text-foreground">{allocation.id}</dd>
+ <dd className="font-mono text-foreground before:font-sans before:content-[':_']">{allocation.id}</dd>
<dt className="text-muted-foreground">Project</dt>
- <dd className="font-mono text-foreground">{allocation.project_id}</dd>
+ <dd className="font-mono text-foreground before:font-sans before:content-[':_']">{allocation.project_id}</dd>
<dt className="text-muted-foreground">Name</dt>
- <dd className="text-foreground">{allocation.name}</dd>
+ <dd className="text-foreground before:content-[':_']">{allocation.name}</dd>
<dt className="text-muted-foreground">Status</dt>
- <dd className="text-foreground">{allocation.status}</dd>
+ <dd className="text-foreground before:content-[':_']">{allocation.status}</dd>
<dt className="text-muted-foreground">Cluster</dt>
- <dd className="font-mono text-foreground">{allocation.compute_cluster_id}</dd>
+ <dd className="font-mono text-foreground before:font-sans before:content-[':_']">{allocation.compute_cluster_id}</dd>
<dt className="text-muted-foreground">Initial SUs</dt>
- <dd className="tabular-nums text-foreground">{formatNumber(allocation.initial_su_amount)}</dd>
+ <dd className="tabular-nums text-foreground before:content-[':_']">{formatNumber(allocation.initial_su_amount)}</dd>
<dt className="text-muted-foreground">Start</dt>
- <dd className="text-foreground">{allocation.start_time}</dd>
+ <dd className="text-foreground before:content-[':_']">{allocation.start_time}</dd>
<dt className="text-muted-foreground">End</dt>
- <dd className="text-foreground">{allocation.end_time}</dd>
+ <dd className="text-foreground before:content-[':_']">{allocation.end_time}</dd>
</dl>
<section className="space-y-2">
diff --git a/web/src/features/core/allocations/components/AllocationsList.tsx b/web/src/features/core/allocations/components/AllocationsList.tsx
index 35df38d9c..3cecbcf29 100644
--- a/web/src/features/core/allocations/components/AllocationsList.tsx
+++ b/web/src/features/core/allocations/components/AllocationsList.tsx
@@ -24,6 +24,7 @@ import { EmptyState } from "@/shared/ui/EmptyState";
import { ErrorState } from "@/shared/ui/ErrorState";
import { Input } from "@/shared/ui/input";
import { TableSkeleton } from "@/shared/ui/Loading";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/ui/select";
import {
StatusBadge,
statusBadgeVariantFromAllocationStatus,
@@ -42,6 +43,19 @@ function formatDate(iso: string): string {
}
}
+function statusLabelFor(value: string): string {
+ switch (value) {
+ case "ACTIVE":
+ return "Active";
+ case "INACTIVE":
+ return "Inactive";
+ case "DELETED":
+ return "Deleted";
+ default:
+ return "All statuses";
+ }
+}
+
function formatSU(n: number): string {
return new Intl.NumberFormat().format(n);
}
@@ -85,6 +99,7 @@ export function AllocationsList({
header: "Allocation",
sortable: true,
sortValue: (row) => row.name,
+ width: "14rem",
cell: (row) => (
<div className="flex flex-col gap-0.5">
<Link
@@ -93,7 +108,7 @@ export function AllocationsList({
>
{row.name}
</Link>
- <span className="font-mono text-xs text-muted-foreground">{row.id}</span>
+ <span className="hidden font-mono text-xs text-muted-foreground">{row.id}</span>
</div>
),
},
@@ -102,10 +117,11 @@ export function AllocationsList({
header: "Project",
sortable: true,
sortValue: (row) => row.project_id,
+ width: "12rem",
cell: (row) => (
<Link
href={`/projects/${row.project_id}`}
- className="text-sm text-muted-foreground hover:underline"
+ className="break-all text-sm text-muted-foreground hover:underline"
>
{row.project_id}
</Link>
@@ -116,7 +132,8 @@ export function AllocationsList({
header: "Cluster",
sortable: true,
sortValue: (row) => row.compute_cluster_id,
- cell: (row) => <span className="text-sm">{row.compute_cluster_id}</span>,
+ width: "12rem",
+ cell: (row) => <span className="break-all text-sm">{row.compute_cluster_id}</span>,
},
{
key: "initial",
@@ -166,17 +183,20 @@ export function AllocationsList({
aria-label="Search allocations"
className="sm:w-72"
/>
- <select
+ <Select
value={statusFilter}
- onChange={(e) => onStatusFilterChange(e.target.value as AllocationStatus | "all")}
- aria-label="Filter by status"
- className="h-9 rounded-md border bg-background px-3 text-sm"
+ onValueChange={(value) => onStatusFilterChange(value as AllocationStatus | "all")}
>
- <option value="all">All statuses</option>
- <option value="ACTIVE">Active</option>
- <option value="INACTIVE">Inactive</option>
- <option value="DELETED">Deleted</option>
- </select>
+ <SelectTrigger aria-label="Filter by status" className="h-9 w-36 px-3">
+ <SelectValue>{(value: string) => statusLabelFor(value)}</SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">All statuses</SelectItem>
+ <SelectItem value="ACTIVE">Active</SelectItem>
+ <SelectItem value="INACTIVE">Inactive</SelectItem>
+ <SelectItem value="DELETED">Deleted</SelectItem>
+ </SelectContent>
+ </Select>
</div>
{isLoading ? (
diff --git a/web/src/features/core/allocations/components/ChangeRequestSubmitDrawer.tsx b/web/src/features/core/allocations/components/ChangeRequestSubmitDrawer.tsx
index 3c87fd928..50434a515 100644
--- a/web/src/features/core/allocations/components/ChangeRequestSubmitDrawer.tsx
+++ b/web/src/features/core/allocations/components/ChangeRequestSubmitDrawer.tsx
@@ -30,6 +30,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { SideDrawer } from "@/shared/ui/SideDrawer";
import { useSubmitChangeRequest } from "../queries";
+const CHANGE_TYPE_LABELS: Record<string, string> = {
+ INCREASE_CREDITS: "Increase credits (SUs)",
+ EXTEND_END_DATE: "Extend end date",
+ OTHER: "Other",
+};
+
const REASON_MIN = 20;
const reasonField = z.string().min(REASON_MIN, `Reason must be at least ${REASON_MIN} characters`);
@@ -142,7 +148,7 @@ export function ChangeRequestSubmitDrawer({
<Label htmlFor="cr-type">Change type</Label>
<Select value={changeType} onValueChange={(v) => onChangeTypeChange(v as ChangeType)}>
<SelectTrigger id="cr-type" aria-label="Change type">
- <SelectValue />
+ <SelectValue>{(value: string) => CHANGE_TYPE_LABELS[value] ?? value}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="INCREASE_CREDITS">Increase credits (SUs)</SelectItem>
diff --git a/web/src/features/core/identity/components/AccessCard.tsx b/web/src/features/core/identity/components/AccessCard.tsx
index 87cd1ed37..0a28bb4f1 100644
--- a/web/src/features/core/identity/components/AccessCard.tsx
+++ b/web/src/features/core/identity/components/AccessCard.tsx
@@ -113,8 +113,8 @@ export function AccessCard({ access }: { access: MyAccess }) {
className={cn(
"rounded-full px-2.5 py-0.5 text-[11px] whitespace-nowrap",
row.provenance === "direct"
- ? "border border-[color:var(--brand)] bg-[color:var(--brand-tint)] font-semibold text-[color:var(--accent-foreground)]"
- : "bg-[color:var(--brand-tint)] text-[color:var(--accent-foreground)]",
+ ? "border border-[color:var(--brand)] bg-[color:var(--brand-tint)] font-semibold text-[color:var(--brand)]"
+ : "bg-[color:var(--brand-tint)] text-[color:var(--brand)]",
)}
>
{row.provenanceLabel}
@@ -164,7 +164,7 @@ function RolesColumn({
<div className="mb-0.5 flex items-center gap-2">
<span className="font-semibold">{rwp.role.name}</span>
{rwp.role.is_system ? (
- <Badge className="bg-[color:var(--brand-tint)] text-[color:var(--accent-foreground)]">
+ <Badge className="bg-[color:var(--brand-tint)] text-[color:var(--brand)]">
SYSTEM
</Badge>
) : null}
diff --git a/web/src/features/core/projects/components/ProjectDetail.tsx b/web/src/features/core/projects/components/ProjectDetail.tsx
index b9c496089..15e46a44c 100644
--- a/web/src/features/core/projects/components/ProjectDetail.tsx
+++ b/web/src/features/core/projects/components/ProjectDetail.tsx
@@ -17,10 +17,11 @@
"use client";
+import { useAbility } from "@/shared/casl/AbilityProvider";
+import { useBreadcrumbLabel } from "@/shared/layout/BreadcrumbLabelsProvider";
import { ErrorState } from "@/shared/ui/ErrorState";
import { CardSkeleton } from "@/shared/ui/Loading";
import { TabsRouter } from "@/shared/ui/TabsRouter";
-import { useAbility } from "@/shared/casl/AbilityProvider";
import { useProject } from "../queries";
import { ProjectAllocationsTab } from "./ProjectAllocationsTab";
import { ProjectDetailHeader } from "./ProjectDetailHeader";
@@ -34,6 +35,7 @@ export type ProjectDetailProps = {
export function ProjectDetail({ projectId }: ProjectDetailProps) {
const ability = useAbility();
const projectQuery = useProject(projectId);
+ useBreadcrumbLabel(projectId, projectQuery.data?.title);
if (projectQuery.isLoading) return <CardSkeleton />;
if (projectQuery.error) {
diff --git a/web/src/features/core/projects/components/ProjectDetailHeader.tsx b/web/src/features/core/projects/components/ProjectDetailHeader.tsx
index 328e651f0..8fefcf864 100644
--- a/web/src/features/core/projects/components/ProjectDetailHeader.tsx
+++ b/web/src/features/core/projects/components/ProjectDetailHeader.tsx
@@ -45,12 +45,14 @@ export function ProjectDetailHeader({ project }: ProjectDetailHeaderProps) {
<h1 className="font-display text-[28px] font-bold leading-tight text-foreground">
{project.title}
</h1>
- <MetaRow>
- <MetaItem variant="status" tone={tone} value={project.status} />
- <MetaItem icon={UserSquare} label="PI" value={project.project_pi_display_name ?? project.project_pi_id} />
- <MetaItem icon={Building2} label="Origination" value={project.origination} />
- <MetaItem icon={Calendar} label="Created" value={formatDate(project.created_time)} />
- </MetaRow>
+ <div className="flex flex-wrap items-center gap-3">
+ <MetaItem variant="status" tone={tone} value={project.status} className="py-1.5" />
+ <MetaRow>
+ <MetaItem icon={UserSquare} label="PI" value={project.project_pi_display_name ?? project.project_pi_id} />
+ <MetaItem icon={Building2} label="Origination" value={project.origination} />
+ <MetaItem icon={Calendar} label="Created" value={formatDate(project.created_time)} />
+ </MetaRow>
+ </div>
</header>
);
}
diff --git a/web/src/features/core/projects/components/ProjectOverviewTab.tsx b/web/src/features/core/projects/components/ProjectOverviewTab.tsx
index c994d54ba..122ed808a 100644
--- a/web/src/features/core/projects/components/ProjectOverviewTab.tsx
+++ b/web/src/features/core/projects/components/ProjectOverviewTab.tsx
@@ -25,26 +25,26 @@ export type ProjectOverviewTabProps = {
export function ProjectOverviewTab({ project }: ProjectOverviewTabProps) {
return (
- <dl className="grid gap-x-8 gap-y-3 sm:grid-cols-[max-content_1fr] text-sm">
+ <dl className="grid gap-x-8 gap-y-3 rounded-lg border border-border bg-muted/40 p-4 text-sm sm:grid-cols-[max-content_1fr]">
<dt className="text-muted-foreground">Project ID</dt>
- <dd className="font-mono text-foreground">{project.id}</dd>
+ <dd className="font-mono text-foreground before:font-sans before:content-[':_']">{project.id}</dd>
<dt className="text-muted-foreground">Originated ID</dt>
- <dd className="font-mono text-foreground">{project.originated_id || "—"}</dd>
+ <dd className="font-mono text-foreground before:font-sans before:content-[':_']">{project.originated_id || "—"}</dd>
<dt className="text-muted-foreground">Title</dt>
- <dd className="text-foreground">{project.title}</dd>
+ <dd className="text-foreground before:content-[':_']">{project.title}</dd>
<dt className="text-muted-foreground">Origination</dt>
- <dd className="text-foreground">{project.origination}</dd>
+ <dd className="text-foreground before:content-[':_']">{project.origination}</dd>
<dt className="text-muted-foreground">PI</dt>
- <dd className="text-foreground">
+ <dd className="text-foreground before:content-[':_']">
{project.project_pi_display_name ? (
<>
{project.project_pi_display_name}
{project.project_pi_email ? (
- <span className="ml-2 text-xs text-muted-foreground">{project.project_pi_email}</span>
+ <span className="ml-2 text-xs text-muted-foreground">({project.project_pi_email})</span>
) : null}
</>
) : (
@@ -53,10 +53,10 @@ export function ProjectOverviewTab({ project }: ProjectOverviewTabProps) {
</dd>
<dt className="text-muted-foreground">Status</dt>
- <dd className="text-foreground">{project.status}</dd>
+ <dd className="text-foreground before:content-[':_']">{project.status}</dd>
<dt className="text-muted-foreground">Created</dt>
- <dd className="text-foreground">{project.created_time}</dd>
+ <dd className="text-foreground before:content-[':_']">{project.created_time}</dd>
</dl>
);
}
diff --git a/web/src/features/core/projects/components/ProjectsList.tsx b/web/src/features/core/projects/components/ProjectsList.tsx
index 06a16bc5a..8b53222fa 100644
--- a/web/src/features/core/projects/components/ProjectsList.tsx
+++ b/web/src/features/core/projects/components/ProjectsList.tsx
@@ -25,12 +25,26 @@ import { EmptyState } from "@/shared/ui/EmptyState";
import { ErrorState } from "@/shared/ui/ErrorState";
import { Input } from "@/shared/ui/input";
import { TableSkeleton } from "@/shared/ui/Loading";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/ui/select";
import {
StatusBadge,
statusBadgeVariantFromAllocationStatus,
} from "@/shared/ui/StatusBadge";
import type { Project, ProjectStatus } from "../schemas";
+function statusLabelFor(value: string): string {
+ switch (value) {
+ case "ACTIVE":
+ return "Active";
+ case "INACTIVE":
+ return "Inactive";
+ case "DELETED":
+ return "Deleted";
+ default:
+ return "All statuses";
+ }
+}
+
export type ProjectsListProps = {
rows: Project[];
isLoading: boolean;
@@ -137,17 +151,20 @@ export function ProjectsList({
aria-label="Search projects"
className="sm:w-64"
/>
- <select
+ <Select
value={statusFilter}
- onChange={(e) => onStatusFilterChange(e.target.value as ProjectStatus | "all")}
- aria-label="Filter by status"
- className="h-9 rounded-md border bg-background px-3 text-sm"
+ onValueChange={(value) => onStatusFilterChange(value as ProjectStatus | "all")}
>
- <option value="all">All statuses</option>
- <option value="ACTIVE">Active</option>
- <option value="INACTIVE">Inactive</option>
- <option value="DELETED">Deleted</option>
- </select>
+ <SelectTrigger aria-label="Filter by status" className="h-9 w-36 px-3">
+ <SelectValue>{(value: string) => statusLabelFor(value)}</SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">All statuses</SelectItem>
+ <SelectItem value="ACTIVE">Active</SelectItem>
+ <SelectItem value="INACTIVE">Inactive</SelectItem>
+ <SelectItem value="DELETED">Deleted</SelectItem>
+ </SelectContent>
+ </Select>
</div>
{isLoading ? (
diff --git a/web/src/shared/layout/BreadcrumbLabelsProvider.tsx b/web/src/shared/layout/BreadcrumbLabelsProvider.tsx
new file mode 100644
index 000000000..d7812713f
--- /dev/null
+++ b/web/src/shared/layout/BreadcrumbLabelsProvider.tsx
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+"use client";
+
+import * as React from "react";
+
+type BreadcrumbLabelActions = {
+ setLabel: (segment: string, label: string) => void;
+ clearLabel: (segment: string) => void;
+};
+
+// Split into two contexts on purpose: the actions object is created once and
+// never changes identity, so components that only register a label (via
+// useBreadcrumbLabel) never re-render when someone else's label changes.
+// Only useBreadcrumbLabels (read by Breadcrumbs) subscribes to the data
+// itself. Merging these into one context would make the actions' identity
+// change whenever the labels record changes, which — combined with an effect
+// that both depends on and calls those actions — is a self-sustaining
+// render loop.
+const BreadcrumbLabelsValueContext = React.createContext<Record<string, string>>({});
+const BreadcrumbLabelActionsContext = React.createContext<BreadcrumbLabelActions | null>(null);
+
+export function BreadcrumbLabelsProvider({ children }: { children: React.ReactNode }) {
+ const [labels, setLabels] = React.useState<Record<string, string>>({});
+
+ const actions = React.useRef<BreadcrumbLabelActions>({
+ setLabel: (segment, label) => {
+ setLabels((prev) => (prev[segment] === label ? prev : { ...prev, [segment]: label }));
+ },
+ clearLabel: (segment) => {
+ setLabels((prev) => {
+ if (!(segment in prev)) return prev;
+ const next = { ...prev };
+ delete next[segment];
+ return next;
+ });
+ },
+ }).current;
+
+ return (
+ <BreadcrumbLabelActionsContext.Provider value={actions}>
+ <BreadcrumbLabelsValueContext.Provider value={labels}>
+ {children}
+ </BreadcrumbLabelsValueContext.Provider>
+ </BreadcrumbLabelActionsContext.Provider>
+ );
+}
+
+export function useBreadcrumbLabels(): Record<string, string> {
+ return React.useContext(BreadcrumbLabelsValueContext);
+}
+
+// Registers a human-readable breadcrumb label for a dynamic route segment
+// (e.g. a resource's name in place of its raw ID) for as long as the calling
+// component is mounted with that segment/label pair.
+export function useBreadcrumbLabel(segment: string | undefined, label: string | undefined) {
+ const actions = React.useContext(BreadcrumbLabelActionsContext);
+
+ React.useEffect(() => {
+ if (!actions || !segment || !label) return;
+ actions.setLabel(segment, label);
+ return () => actions.clearLabel(segment);
+ }, [actions, segment, label]);
+}
diff --git a/web/src/shared/layout/Breadcrumbs.tsx b/web/src/shared/layout/Breadcrumbs.tsx
index 059826ea7..414d5f49e 100644
--- a/web/src/shared/layout/Breadcrumbs.tsx
+++ b/web/src/shared/layout/Breadcrumbs.tsx
@@ -19,6 +19,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
+import { useBreadcrumbLabels } from "./BreadcrumbLabelsProvider";
type Segment = { label: string; href?: string };
@@ -38,13 +39,14 @@ function collapseSegments(segments: Segment[]): Array<Segment | { ellipsis: true
export function Breadcrumbs() {
const pathname = usePathname() ?? "/";
+ const labels = useBreadcrumbLabels();
const parts = pathname.split("/").filter(Boolean);
if (parts.length === 0) return null;
const segments: Segment[] = parts.map((part, i) => {
const href = `/${parts.slice(0, i + 1).join("/")}`;
const isLast = i === parts.length - 1;
- return { label: titleCase(part), href: isLast ? undefined : href };
+ return { label: labels[part] ?? titleCase(part), href: isLast ? undefined : href };
});
const visible = collapseSegments(segments);
diff --git a/web/src/shared/layout/PortalLayout.tsx b/web/src/shared/layout/PortalLayout.tsx
index 91860d85f..39abcf3da 100644
--- a/web/src/shared/layout/PortalLayout.tsx
+++ b/web/src/shared/layout/PortalLayout.tsx
@@ -16,25 +16,28 @@
// under the License.
import type { ReactNode } from "react";
+import { BreadcrumbLabelsProvider } from "./BreadcrumbLabelsProvider";
import { Sidebar } from "./Sidebar";
import { Topbar } from "./Topbar";
export function PortalLayout({ children }: { children: ReactNode }) {
return (
- <div className="flex h-screen overflow-hidden bg-background text-foreground">
- <a
- href="#main-content"
- className="-translate-y-16 focus:translate-y-0 absolute z-50 m-2 rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
- >
- Skip to main content
- </a>
- <Sidebar />
- <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
- <Topbar />
- <main id="main-content" className="flex-1 overflow-y-auto px-10 py-8" tabIndex={-1}>
- {children}
- </main>
+ <BreadcrumbLabelsProvider>
+ <div className="flex h-screen overflow-hidden bg-background text-foreground">
+ <a
+ href="#main-content"
+ className="-translate-y-16 focus:translate-y-0 absolute z-50 m-2 rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ >
+ Skip to main content
+ </a>
+ <Sidebar />
+ <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
+ <Topbar />
+ <main id="main-content" className="flex-1 overflow-y-auto px-10 py-8" tabIndex={-1}>
+ {children}
+ </main>
+ </div>
</div>
- </div>
+ </BreadcrumbLabelsProvider>
);
}
diff --git a/web/src/shared/layout/Sidebar.tsx b/web/src/shared/layout/Sidebar.tsx
index c1293c3aa..90772f268 100644
--- a/web/src/shared/layout/Sidebar.tsx
+++ b/web/src/shared/layout/Sidebar.tsx
@@ -42,18 +42,18 @@ export function Sidebar() {
return (
<aside className="flex w-[240px] shrink-0 flex-col border-r border-border bg-sidebar text-sidebar-foreground">
- <div className="px-6 pt-8 pb-6">
+ <div className="px-6 pt-4 pb-6">
<Link href="/" aria-label={brand.name} className="inline-flex items-center">
{/* Deployment brand logo; set NEXT_PUBLIC_PORTAL_LOGO to override. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
- <img src={brand.logo} alt={brand.name} className="h-6 w-auto" />
+ <img src={brand.logo} alt={brand.name} className="h-8 w-auto" />
</Link>
</div>
<nav className="flex flex-col">
{groups.map(({ group, items }, idx) => (
<div key={group} className={cn("flex flex-col", idx > 0 && "mt-4")}>
- <div className="px-6 pt-2 pb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
+ <div className="px-6 pt-2 pb-1 text-[11px] font-semibold uppercase tracking-wider text-custos-gray-400">
{NAV_GROUP_LABELS[group]}
</div>
{items.map((item) => (
@@ -79,11 +79,11 @@ function SidebarLink({ item, active }: { item: NavItem; active: boolean }) {
className={cn(
"relative flex h-11 items-center gap-3 px-6 text-sm font-medium transition",
active
- ? "bg-[var(--sidebar-active)] font-semibold text-brand"
+ ? "bg-[var(--sidebar-active)] font-semibold text-foreground"
: "text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground",
)}
>
- <Icon className="h-5 w-5 stroke-[1.75]" />
+ <Icon className={cn("h-5 w-5 stroke-[1.75]", active && "text-brand")} />
<span className="truncate">{item.label}</span>
{active && <span className="absolute top-2 right-0 bottom-2 w-1 rounded-l-full bg-brand" />}
</Link>
diff --git a/web/src/shared/ui/Breadcrumbs.tsx b/web/src/shared/ui/Breadcrumbs.tsx
deleted file mode 100644
index b096a5001..000000000
--- a/web/src/shared/ui/Breadcrumbs.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-"use client";
-
-import { cn } from "@/lib/utils";
-import { ChevronRightIcon } from "lucide-react";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import * as React from "react";
-
-export type BreadcrumbItem = {
- label: string;
- href?: string;
-};
-
-export type BreadcrumbsProps = {
- items?: BreadcrumbItem[];
- className?: string;
-};
-
-function fromPathname(pathname: string): BreadcrumbItem[] {
- const segments = pathname.split("/").filter(Boolean);
- return segments.map((segment, i) => {
- const href = `/${segments.slice(0, i + 1).join("/")}`;
- const label = segment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
- return { label, href: i === segments.length - 1 ? undefined : href };
- });
-}
-
-export function Breadcrumbs({ items, className }: BreadcrumbsProps) {
- const pathname = usePathname();
- const resolved = items ?? fromPathname(pathname ?? "/");
- if (resolved.length === 0) return null;
- return (
- <nav aria-label="Breadcrumb" className={cn("flex text-sm text-muted-foreground", className)}>
- <ol className="flex items-center gap-1">
- {resolved.map((item, i) => (
- <li key={`${item.label}-${i}`} className="flex items-center gap-1">
- {item.href ? (
- <Link
- href={item.href}
- className="rounded transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
- >
- {item.label}
- </Link>
- ) : (
- <span aria-current="page" className="text-foreground">
- {item.label}
- </span>
- )}
- {i < resolved.length - 1 ? (
- <ChevronRightIcon className="size-3" aria-hidden="true" />
- ) : null}
- </li>
- ))}
- </ol>
- </nav>
- );
-}
diff --git a/web/src/shared/ui/Loading.tsx b/web/src/shared/ui/Loading.tsx
index 0222c24ff..929792ecb 100644
--- a/web/src/shared/ui/Loading.tsx
+++ b/web/src/shared/ui/Loading.tsx
@@ -24,7 +24,7 @@ export function Spinner({ className }: { className?: string }) {
role="status"
aria-label="Loading"
className={cn(
- "inline-block size-5 animate-spin rounded-full border-2 border-muted border-t-[color:var(--custos-blue-500)]",
+ "inline-block size-5 animate-spin rounded-full border-2 border-muted border-t-[color:var(--brand)]",
className,
)}
/>
diff --git a/web/src/shared/ui/MetaRow.tsx b/web/src/shared/ui/MetaRow.tsx
index 51309f070..5bcbf6be1 100644
--- a/web/src/shared/ui/MetaRow.tsx
+++ b/web/src/shared/ui/MetaRow.tsx
@@ -37,7 +37,7 @@ export function MetaRow({
role="group"
aria-label="Metadata"
className={cn(
- "flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-foreground",
+ "inline-flex flex-wrap items-center gap-y-2 rounded-full bg-brand-tint/60 px-4 py-1 text-sm text-foreground [&>*:not(:first-child)]:ml-3 [&>*:not(:first-child)]:border-l [&>*:not(:first-child)]:border-border [&>*:not(:first-child)]:pl-3",
className,
)}
>
diff --git a/web/src/shared/ui/StatusBadge.tsx b/web/src/shared/ui/StatusBadge.tsx
index 07e4cfcb4..98fbf0e00 100644
--- a/web/src/shared/ui/StatusBadge.tsx
+++ b/web/src/shared/ui/StatusBadge.tsx
@@ -30,14 +30,14 @@ export type StatusBadgeVariant =
// Flat fill + tinted text; bg-muted for neutral pills so they follow the theme.
const variantStyles: Record<StatusBadgeVariant, string> = {
- active: "bg-[color:var(--custos-green-50)] text-[color:var(--custos-green-700)]",
+ active: "bg-[color:var(--tone-ok-bg)] text-[color:var(--tone-ok-fg)]",
inactive: "bg-muted text-muted-foreground",
- deleted: "bg-[color:var(--custos-red-50)] text-[color:var(--custos-red-700)]",
- pending: "bg-[color:var(--custos-blue-50)] text-[color:var(--custos-blue-700)]",
- approved: "bg-[color:var(--custos-green-50)] text-[color:var(--custos-green-700)]",
- rejected: "bg-[color:var(--custos-red-50)] text-[color:var(--custos-red-700)]",
+ deleted: "bg-[color:var(--tone-error-bg)] text-[color:var(--tone-error-fg)]",
+ pending: "bg-[color:var(--tone-info-bg)] text-[color:var(--tone-info-fg)]",
+ approved: "bg-[color:var(--tone-ok-bg)] text-[color:var(--tone-ok-fg)]",
+ rejected: "bg-[color:var(--tone-error-bg)] text-[color:var(--tone-error-fg)]",
expired: "bg-muted text-muted-foreground",
- warning: "bg-[color:var(--custos-amber-50)] text-[color:var(--custos-amber-700)]",
+ warning: "bg-[color:var(--tone-warn-bg)] text-[color:var(--tone-warn-fg)]",
};
const labels: Record<StatusBadgeVariant, string> = {
diff --git a/web/src/shared/ui/select.tsx b/web/src/shared/ui/select.tsx
index 5cd664a3c..09992dd94 100644
--- a/web/src/shared/ui/select.tsx
+++ b/web/src/shared/ui/select.tsx
@@ -58,7 +58,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
- "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm] [...]
+ "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:border-ring data-popup-open:ring-3 data-popup-open:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder [...]
className,
)}
{...props}
@@ -78,7 +78,7 @@ function SelectContent({
sideOffset = 4,
align = "center",
alignOffset = 0,
- alignItemWithTrigger = true,
+ alignItemWithTrigger = false,
...props
}: SelectPrimitive.Popup.Props &
Pick<
@@ -99,7 +99,7 @@ function SelectContent({
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn(
- "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left- [...]
+ "relative isolate z-50 max-h-(--available-height) min-w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 [...]
className,
)}
{...props}
@@ -128,7 +128,7 @@ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Prop
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
- "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+ "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[selected]:bg-[color:var(--custos-blue-50)]! dark:data-[selected]:bg-[color:var(--custos-blue-800)]! data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span] [...]
className,
)}
{...props}
@@ -141,7 +141,7 @@ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Prop
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
- <CheckIcon className="pointer-events-none" />
+ <CheckIcon className="pointer-events-none text-[color:var(--custos-blue-600)]!" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
);