visual enhancements to sidebar

This commit is contained in:
miloschwartz
2025-12-19 21:57:44 -05:00
parent 5587bd9d59
commit afc19f192b
7 changed files with 78 additions and 69 deletions

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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(

View File

@@ -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}`)}

View File

@@ -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>

View File

@@ -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",