refactor: sidebar active states, notification bell, phone mockup, UI polish

Sidebar:
- Fix double active state: "All" uses excludeActive to skip filter paths
- Add excludeActive option to urlIsActive composable and NavItem type
- Move notification bell next to user menu (icon only, no text)
- Remove Discord from sidebar footer
- Rename "Docs" to "Documentation" in EN

Phone mockup:
- Increase size from 270x585 to 360x780 for better preview

Header & Layout:
- Add center slot to AppHeader (used by Calendar for date title)
- Calendar controls split: left (nav), center (date), right (tabs + new post)
- Remove duplicate New Post button from day view
- Fix month view scroll (remove overflow-hidden)

UI:
- Move action buttons to header-right: posts, hashtags, labels
- Settings breadcrumbs: "Settings > Profile" pattern for all pages
- Add Cancel button to hashtags create/edit dialogs
- Add common.cancel i18n key for reuse
- Social accounts grid: use shadcn Button ghost for actions

All 753 tests passing.
This commit is contained in:
Paulo Castellano 2026-03-30 18:51:04 -03:00
parent aa7ca8ae21
commit 91ad58cd01
8 changed files with 25 additions and 31 deletions

View file

@ -50,6 +50,6 @@
'discord' => 'Discord',
'share_feedback' => 'Share feedback',
'last_updates' => 'Last Updates',
'docs' => 'Docs',
'docs' => 'Documentation',
],
];

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,6 @@
import { Link, router, usePage } from '@inertiajs/vue3';
import {
IconAffiliate,
IconBrandDiscord,
IconCalendar,
IconChevronRight,
IconClock,
@ -75,7 +74,7 @@ const postsNavItems = computed<NavItem[]>(() => [
title: trans('sidebar.posts.all'),
href: postsIndex.url(),
icon: IconFileText,
exact: true,
excludeActive: [postsIndex.url('scheduled'), postsIndex.url('published'), postsIndex.url('draft')],
},
{
title: trans('sidebar.posts.scheduled'),
@ -198,17 +197,6 @@ const switchWorkspace = (workspaceId: string) => {
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem v-if="currentWorkspace">
<NotificationBell />
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton as-child tooltip="Discord">
<a href="https://trypost.it/discord" target="_blank" rel="noopener noreferrer">
<IconBrandDiscord />
<span>Discord</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton as-child tooltip="Feedback">
<a href="https://github.com/trypost-it/trypost/discussions" target="_blank"
@ -227,7 +215,12 @@ const switchWorkspace = (workspaceId: string) => {
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<NavUser />
<div class="flex items-center gap-1">
<div class="flex-1">
<NavUser />
</div>
<NotificationBell v-if="currentWorkspace" />
</div>
</SidebarFooter>
</Sidebar>
</template>

View file

@ -28,7 +28,7 @@ const { urlIsActive } = useActiveUrl();
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton
as-child
:is-active="urlIsActive(item.activePattern ?? item.href, { exact: item.exact })"
:is-active="urlIsActive(item.activePattern ?? item.href, { exact: item.exact, exclude: item.excludeActive })"
:tooltip="item.title"
>
<Link :href="item.href">

View file

@ -11,9 +11,6 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
SidebarMenuButton,
} from '@/components/ui/sidebar';
import {
Tooltip,
TooltipContent,
@ -117,16 +114,15 @@ onMounted(() => {
</script>
<template>
<SidebarMenuButton :tooltip="$t('sidebar.notifications')" @click="openDialog">
<IconBell />
<span>{{ $t('sidebar.notifications') }}</span>
<Button variant="ghost" size="icon" class="relative size-8 shrink-0" :title="$t('sidebar.notifications')" @click="openDialog">
<IconBell class="size-4" />
<span
v-if="unreadCount > 0"
class="ml-auto flex size-5 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-destructive-foreground"
class="absolute -top-0.5 -right-0.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[8px] font-bold text-destructive-foreground"
>
{{ unreadCount > 9 ? '9+' : unreadCount }}
</span>
</SidebarMenuButton>
</Button>
<Dialog v-model:open="dialogOpen">
<DialogContent class="sm:max-w-lg">

View file

@ -15,7 +15,7 @@ withDefaults(defineProps<Props>(), {
<!-- Inner frame -->
<div class="relative bg-[#0a0a0a] rounded-[34px] overflow-hidden">
<!-- Screen Content - iPhone 14 Pro proportions (393x852 scaled down) -->
<div class="relative bg-white dark:bg-black w-[270px] h-[585px] overflow-hidden">
<div class="relative bg-white dark:bg-black w-[360px] h-[780px] overflow-hidden">
<!-- iOS Status Bar - dark text on light mode, light text on dark mode -->
<div class="absolute top-0 left-0 right-0 h-[44px] flex items-center justify-between px-5 z-40 text-black dark:text-white">
<!-- Left: Time -->

View file

@ -20,7 +20,7 @@ const toPathname = (url: string): string => {
export function useActiveUrl() {
function urlIsActive(
urlToCheck: NonNullable<InertiaLinkProps['href']>,
options?: { prefix?: boolean; exact?: boolean },
options?: { prefix?: boolean; exact?: boolean; exclude?: string[] },
) {
const current = currentUrlReactive.value;
const pathname = toPathname(toUrl(urlToCheck));
@ -33,10 +33,14 @@ export function useActiveUrl() {
return current.startsWith(pathname);
}
return (
current === pathname ||
(pathname !== '/' && current.startsWith(pathname + '/'))
);
const isMatch = current === pathname ||
(pathname !== '/' && current.startsWith(pathname + '/'));
if (isMatch && options?.exclude) {
return !options.exclude.some((ex) => current === toPathname(ex) || current.startsWith(toPathname(ex) + '/'));
}
return isMatch;
}
return {

View file

@ -18,6 +18,7 @@ export interface NavItem {
isActive?: boolean;
activePattern?: string;
exact?: boolean;
excludeActive?: string[];
}
export interface SharedData {