mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-21 12:26:40 +00:00
visual enhancements to sidebar
This commit is contained in:
@@ -73,13 +73,14 @@ export function LayoutMobileMenu({
|
|||||||
{t("navbarDescription")}
|
{t("navbarDescription")}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="p-4">
|
<div className="px-3">
|
||||||
<OrgSelector
|
<OrgSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4">
|
<div className="w-full border-b border-border" />
|
||||||
|
<div className="px-3">
|
||||||
{!isAdminPage &&
|
{!isAdminPage &&
|
||||||
user.serverAdmin && (
|
user.serverAdmin && (
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
@@ -113,7 +114,7 @@ export function LayoutMobileMenu({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 space-y-4 border-t shrink-0">
|
<div className="px-3 pt-3 pb-3 space-y-4 border-t shrink-0">
|
||||||
<SupporterStatus />
|
<SupporterStatus />
|
||||||
{env?.app?.version && (
|
{env?.app?.version && (
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
|||||||
@@ -109,14 +109,18 @@ export function LayoutSidebar({
|
|||||||
isSidebarCollapsed ? "w-16" : "w-64"
|
isSidebarCollapsed ? "w-16" : "w-64"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-4 shrink-0">
|
<div className="shrink-0">
|
||||||
<OrgSelector
|
<OrgSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className={cn(
|
||||||
|
"w-full border-b border-border",
|
||||||
|
isSidebarCollapsed && "mb-2"
|
||||||
|
)} />
|
||||||
|
<div className="flex-1 overflow-y-auto relative">
|
||||||
<div className="px-2 pt-1">
|
<div className="px-2 pt-1">
|
||||||
{!isAdminPage && user.serverAdmin && (
|
{!isAdminPage && user.serverAdmin && (
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
@@ -153,8 +157,12 @@ export function LayoutSidebar({
|
|||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Fade gradient at bottom to indicate scrollable content */}
|
||||||
|
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full border-t border-border" />
|
||||||
|
|
||||||
<div className="p-4 pt-1 flex flex-col shrink-0">
|
<div className="p-4 pt-1 flex flex-col shrink-0">
|
||||||
{canShowProductUpdates && (
|
{canShowProductUpdates && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -52,13 +51,14 @@ export function OrgSelector({
|
|||||||
const orgSelectorContent = (
|
const orgSelectorContent = (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<div
|
||||||
variant="secondary"
|
|
||||||
size={isCollapsed ? "icon" : "lg"}
|
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn(
|
className={cn(
|
||||||
isCollapsed ? "w-8 h-8" : "w-full h-12 px-3 py-4"
|
"cursor-pointer transition-colors",
|
||||||
|
isCollapsed
|
||||||
|
? "w-full h-16 flex items-center justify-center hover:bg-muted"
|
||||||
|
: "w-full px-4 py-4 hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
@@ -66,9 +66,8 @@ export function OrgSelector({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between w-full min-w-0">
|
<div className="flex items-center justify-between w-full min-w-0">
|
||||||
<div className="flex items-center min-w-0 flex-1">
|
<div className="flex items-center min-w-0 flex-1">
|
||||||
<Building2 className="h-4 w-4 mr-3 shrink-0" />
|
<div className="flex flex-col items-start min-w-0 flex-1 gap-1">
|
||||||
<div className="flex flex-col items-start min-w-0 flex-1">
|
<span className="font-bold">
|
||||||
<span className="font-bold text-sm">
|
|
||||||
{t("org")}
|
{t("org")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground truncate w-full text-left">
|
<span className="text-sm text-muted-foreground truncate w-full text-left">
|
||||||
@@ -79,7 +78,7 @@ export function OrgSelector({
|
|||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 ml-2" />
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 ml-2" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[320px] p-0" align="start">
|
<PopoverContent className="w-[320px] p-0" align="start">
|
||||||
<Command className="rounded-lg">
|
<Command className="rounded-lg">
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ export default function ProductUpdates({
|
|||||||
(update) => !productUpdatesRead.includes(update.id)
|
(update) => !productUpdatesRead.includes(update.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (filteredUpdates.length === 0 && !showNewVersionPopup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function CollapsibleNavItem({
|
|||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center w-full rounded-md transition-colors",
|
"flex items-center w-full rounded-md transition-colors",
|
||||||
level === 0 ? "px-3 py-2" : "px-3 py-1.5",
|
level === 0 ? "px-3 py-1.5" : "px-3 py-1",
|
||||||
isActive
|
isActive
|
||||||
? "bg-secondary font-medium"
|
? "bg-secondary font-medium"
|
||||||
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
||||||
@@ -124,17 +124,18 @@ function CollapsibleNavItem({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">{item.icon}</span>
|
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
|
||||||
|
{item.icon}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span className="text-left truncate">{t(item.title)}</span>
|
<span className="text-left truncate">
|
||||||
|
{t(item.title)}
|
||||||
|
</span>
|
||||||
{item.isBeta && (
|
{item.isBeta && (
|
||||||
<Badge
|
<span className="uppercase font-mono text-yellow-600 dark:text-yellow-800 font-black text-xs">
|
||||||
variant="outline"
|
|
||||||
className="text-muted-foreground flex-shrink-0"
|
|
||||||
>
|
|
||||||
{t("beta")}
|
{t("beta")}
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
|
<div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
|
||||||
@@ -256,7 +257,11 @@ export function SidebarNav({
|
|||||||
href={isDisabled ? "#" : hydratedHref}
|
href={isDisabled ? "#" : hydratedHref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center rounded-md transition-colors",
|
"flex items-center rounded-md transition-colors",
|
||||||
isCollapsed ? "px-2 py-2 justify-center" : level === 0 ? "px-3 py-2" : "px-3 py-1.5",
|
isCollapsed
|
||||||
|
? "px-2 py-2 justify-center"
|
||||||
|
: level === 0
|
||||||
|
? "px-3 py-1.5"
|
||||||
|
: "px-3 py-1",
|
||||||
isActive
|
isActive
|
||||||
? "bg-secondary font-medium"
|
? "bg-secondary font-medium"
|
||||||
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
||||||
@@ -284,21 +289,21 @@ export function SidebarNav({
|
|||||||
)}
|
)}
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span className="truncate">{t(item.title)}</span>
|
<span className="truncate">{t(item.title)}</span>
|
||||||
{item.isBeta && (
|
{item.isBeta && (
|
||||||
<Badge
|
<span className="uppercase font-mono text-yellow-600 dark:text-yellow-800 font-black text-xs">
|
||||||
variant="outline"
|
|
||||||
className="text-muted-foreground flex-shrink-0"
|
|
||||||
>
|
|
||||||
{t("beta")}
|
{t("beta")}
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{build === "enterprise" &&
|
{build === "enterprise" &&
|
||||||
item.showEE &&
|
item.showEE &&
|
||||||
!isUnlocked() && (
|
!isUnlocked() && (
|
||||||
<Badge variant="outlinePrimary" className="flex-shrink-0">
|
<Badge
|
||||||
|
variant="outlinePrimary"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
{t("licenseBadge")}
|
{t("licenseBadge")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -309,27 +314,31 @@ export function SidebarNav({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center rounded-md transition-colors",
|
"flex items-center rounded-md transition-colors",
|
||||||
level === 0 ? "px-3 py-2" : "px-3 py-1.5",
|
level === 0 ? "px-3 py-1.5" : "px-3 py-1",
|
||||||
"text-muted-foreground",
|
"text-muted-foreground",
|
||||||
isDisabled && "cursor-not-allowed opacity-60"
|
isDisabled && "cursor-not-allowed opacity-60"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">{item.icon}</span>
|
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
|
||||||
|
{item.icon}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span className="truncate">{t(item.title)}</span>
|
<span className="truncate">{t(item.title)}</span>
|
||||||
{item.isBeta && (
|
{item.isBeta && (
|
||||||
<Badge
|
<span className="uppercase font-mono text-yellow-600 dark:text-yellow-800 font-black text-xs">
|
||||||
variant="outline"
|
|
||||||
className="text-muted-foreground flex-shrink-0"
|
|
||||||
>
|
|
||||||
{t("beta")}
|
{t("beta")}
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{build === "enterprise" && item.showEE && !isUnlocked() && (
|
{build === "enterprise" && item.showEE && !isUnlocked() && (
|
||||||
<Badge variant="outlinePrimary" className="flex-shrink-0 ml-2">{t("licenseBadge")}</Badge>
|
<Badge
|
||||||
|
variant="outlinePrimary"
|
||||||
|
className="flex-shrink-0 ml-2"
|
||||||
|
>
|
||||||
|
{t("licenseBadge")}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -422,23 +431,23 @@ export function SidebarNav({
|
|||||||
{childItem.icon}
|
{childItem.icon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{t(childItem.title)}
|
{t(childItem.title)}
|
||||||
</span>
|
</span>
|
||||||
{childItem.isBeta && (
|
{childItem.isBeta && (
|
||||||
<Badge
|
<span className="uppercase font-mono text-yellow-600 dark:text-yellow-800 font-black text-xs">
|
||||||
variant="outline"
|
|
||||||
className="text-muted-foreground flex-shrink-0"
|
|
||||||
>
|
|
||||||
{t("beta")}
|
{t("beta")}
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{build === "enterprise" &&
|
{build === "enterprise" &&
|
||||||
childItem.showEE &&
|
childItem.showEE &&
|
||||||
!isUnlocked() && (
|
!isUnlocked() && (
|
||||||
<Badge variant="outlinePrimary" className="flex-shrink-0 ml-2">
|
<Badge
|
||||||
|
variant="outlinePrimary"
|
||||||
|
className="flex-shrink-0 ml-2"
|
||||||
|
>
|
||||||
{t(
|
{t(
|
||||||
"licenseBadge"
|
"licenseBadge"
|
||||||
)}
|
)}
|
||||||
@@ -481,7 +490,10 @@ export function SidebarNav({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{sections.map((section, sectionIndex) => (
|
{sections.map((section, sectionIndex) => (
|
||||||
<div key={section.heading} className={cn(sectionIndex > 0 && "mt-4")}>
|
<div
|
||||||
|
key={section.heading}
|
||||||
|
className={cn(sectionIndex > 0 && "mt-4")}
|
||||||
|
>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="px-3 py-2 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider">
|
<div className="px-3 py-2 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider">
|
||||||
{t(`${section.heading}`)}
|
{t(`${section.heading}`)}
|
||||||
|
|||||||
@@ -56,11 +56,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
if (props.providerError?.error) {
|
if (props.providerError?.error) {
|
||||||
const providerMessage =
|
const providerMessage =
|
||||||
props.providerError.description ||
|
props.providerError.description ||
|
||||||
t("idpErrorOidcProviderRejected", {
|
"The identity provider returned an error: {error}.";
|
||||||
error: props.providerError.error,
|
|
||||||
defaultValue:
|
|
||||||
"The identity provider returned an error: {error}."
|
|
||||||
});
|
|
||||||
const suffix = props.providerError.uri
|
const suffix = props.providerError.uri
|
||||||
? ` (${props.providerError.uri})`
|
? ` (${props.providerError.uri})`
|
||||||
: "";
|
: "";
|
||||||
@@ -76,10 +72,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
if (!isCancelled) {
|
if (!isCancelled) {
|
||||||
setIsProviderError(false);
|
setIsProviderError(false);
|
||||||
setError(
|
setError(
|
||||||
t("idpErrorOidcMissingCode", {
|
"The identity provider did not return an authorization code."
|
||||||
defaultValue:
|
|
||||||
"The identity provider did not return an authorization code."
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -90,10 +83,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
if (!isCancelled) {
|
if (!isCancelled) {
|
||||||
setIsProviderError(false);
|
setIsProviderError(false);
|
||||||
setError(
|
setError(
|
||||||
t("idpErrorOidcMissingState", {
|
"The login request is missing state information. Please restart the login process."
|
||||||
defaultValue:
|
|
||||||
"The login request is missing state information. Please restart the login process."
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -159,12 +149,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
if (!isCancelled) {
|
if (!isCancelled) {
|
||||||
setIsProviderError(false);
|
setIsProviderError(false);
|
||||||
setError(
|
setError("An unexpected error occurred. Please try again.");
|
||||||
t("idpErrorOidcTokenValidating", {
|
|
||||||
defaultValue:
|
|
||||||
"An unexpected error occurred. Please try again."
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!isCancelled) {
|
if (!isCancelled) {
|
||||||
@@ -181,7 +166,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const alertVariants = cva(
|
|||||||
default: "bg-card border text-foreground",
|
default: "bg-card border text-foreground",
|
||||||
neutral: "bg-card bg-muted border text-foreground",
|
neutral: "bg-card bg-muted border text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
"border-destructive/50 border bg-destructive/8 text-destructive dark:border-destructive/50 [&>svg]:text-destructive",
|
||||||
success:
|
success:
|
||||||
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
||||||
info: "border-blue-500/50 border bg-blue-500/10 text-blue-800 dark:text-blue-400 dark:border-blue-400 [&>svg]:text-blue-500",
|
info: "border-blue-500/50 border bg-blue-500/10 text-blue-800 dark:text-blue-400 dark:border-blue-400 [&>svg]:text-blue-500",
|
||||||
|
|||||||
Reference in New Issue
Block a user