refactor: redesign sidebar + layout to match Sendkit pattern
- Workspace switcher in sidebar header (dropdown with team list) - NavMain component with label and activePattern support - User menu in sidebar footer via NavUser component - UserMenuContent with theme switcher submenu (light/dark/system) - UserInfo component with avatar fallback and customizable classes - AppSidebarLayout using SidebarProvider/SidebarInset directly - AppSidebarHeader with right slot for header actions - AppLayout with fullWidth and header-right slot support - Remove workspace/user/theme/language from header dropdown - 702 tests passing, build passing
This commit is contained in:
parent
f5a8ea72f1
commit
614cb99435
11 changed files with 403 additions and 456 deletions
1
lang/php_en.json
Normal file
1
lang/php_en.json
Normal file
File diff suppressed because one or more lines are too long
1
lang/php_pt-br.json
Normal file
1
lang/php_pt-br.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,129 +1,146 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@import 'vue-sonner/style.css';
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@layer utilities {
|
||||
|
||||
body,
|
||||
html {
|
||||
--font-sans:
|
||||
'Instrument Sans', ui-sans-serif, system-ui, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #f7f9f3;
|
||||
--foreground: #000000;
|
||||
--background: #fafafa;
|
||||
--foreground: #0a0a0a;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #000000;
|
||||
--card-foreground: #0a0a0a;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #000000;
|
||||
--primary: #4f46e5;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #14b8a6;
|
||||
--secondary-foreground: #ffffff;
|
||||
--muted: #f0f0f0;
|
||||
--muted-foreground: #333333;
|
||||
--accent: #f59e0b;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #000000;
|
||||
--input: #737373;
|
||||
--ring: #a5b4fc;
|
||||
--chart-1: #4f46e5;
|
||||
--chart-2: #14b8a6;
|
||||
--chart-3: #f59e0b;
|
||||
--chart-4: #ec4899;
|
||||
--chart-5: #22c55e;
|
||||
--sidebar: #f7f9f3;
|
||||
--sidebar-foreground: #000000;
|
||||
--sidebar-primary: #4f46e5;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #f59e0b;
|
||||
--sidebar-accent-foreground: #000000;
|
||||
--sidebar-border: #000000;
|
||||
--sidebar-ring: #a5b4fc;
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: DM Sans, sans-serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 1rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 0px;
|
||||
--shadow-blur: 0px;
|
||||
--popover-foreground: #0a0a0a;
|
||||
--primary: #171717;
|
||||
--primary-foreground: #fafafa;
|
||||
--secondary: #e5e5e5;
|
||||
--secondary-foreground: #171717;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
--accent: #e5e5e5;
|
||||
--accent-foreground: #171717;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #fafafa;
|
||||
--success: #16a34a;
|
||||
--error: #dc2626;
|
||||
--warning: #d97706;
|
||||
--info: #2563eb;
|
||||
--border: #e5e5e5;
|
||||
--input: #d4d4d4;
|
||||
--ring: #525252;
|
||||
--chart-1: #1f3fad;
|
||||
--chart-2: #1a4eda;
|
||||
--chart-3: #2563ef;
|
||||
--chart-4: #3a81f6;
|
||||
--chart-5: #91c5ff;
|
||||
--sidebar: #f5f5f5;
|
||||
--sidebar-foreground: #0a0a0a;
|
||||
--sidebar-primary: #1447e6;
|
||||
--sidebar-primary-foreground: #fafafa;
|
||||
--sidebar-accent: #e5e5e5;
|
||||
--sidebar-accent-foreground: #171717;
|
||||
--sidebar-border: #e5e5e5;
|
||||
--sidebar-ring: #a3a3a3;
|
||||
--font-sans:
|
||||
'Space Grotesk', ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
--radius: 0.725rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 2.5px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.05;
|
||||
--shadow-color: #1a1a1a;
|
||||
--shadow-2xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||
--shadow-xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||
--shadow-sm: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-md: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-lg: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-2xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.13);
|
||||
--tracking-normal: normal;
|
||||
--shadow-opacity: 0.04;
|
||||
--shadow-color: oklch(0.3065 0.0243 243.82);
|
||||
--shadow-2xs: 0 1px 2.5px 0px hsl(0 0% 0% / 0.02);
|
||||
--shadow-xs: 0 1px 2.5px 0px hsl(0 0% 0% / 0.02);
|
||||
--shadow-sm:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 1px 2px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 1px 2px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-md:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 2px 4px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-lg:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 4px 6px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-xl:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 8px 10px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-2xl: 0 1px 2.5px 0px hsl(0 0% 0% / 0.1);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #000000;
|
||||
--foreground: #ffffff;
|
||||
--card: #1a212b;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a212b;
|
||||
--popover-foreground: #ffffff;
|
||||
--primary: #818cf8;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #2dd4bf;
|
||||
--secondary-foreground: #000000;
|
||||
--muted: #333333;
|
||||
--muted-foreground: #cccccc;
|
||||
--accent: #fcd34d;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #f87171;
|
||||
--destructive-foreground: #000000;
|
||||
--border: #545454;
|
||||
--input: #ffffff;
|
||||
--ring: #818cf8;
|
||||
--chart-1: #818cf8;
|
||||
--chart-2: #2dd4bf;
|
||||
--chart-3: #fcd34d;
|
||||
--chart-4: #f472b6;
|
||||
--chart-5: #4ade80;
|
||||
--sidebar: #000000;
|
||||
--sidebar-foreground: #ffffff;
|
||||
--sidebar-primary: #818cf8;
|
||||
--sidebar-primary-foreground: #000000;
|
||||
--sidebar-accent: #fcd34d;
|
||||
--sidebar-accent-foreground: #000000;
|
||||
--sidebar-border: #ffffff;
|
||||
--sidebar-ring: #818cf8;
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: DM Sans, sans-serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 1rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 0px;
|
||||
--shadow-blur: 0px;
|
||||
--background: #0a0a0a;
|
||||
--foreground: #fafafa;
|
||||
--card: #171717;
|
||||
--card-foreground: #fafafa;
|
||||
--popover: #262626;
|
||||
--popover-foreground: #fafafa;
|
||||
--primary: #e5e5e5;
|
||||
--primary-foreground: #171717;
|
||||
--secondary: #262626;
|
||||
--secondary-foreground: #fafafa;
|
||||
--muted: #262626;
|
||||
--muted-foreground: #a1a1a1;
|
||||
--accent: #404040;
|
||||
--accent-foreground: #fafafa;
|
||||
--destructive: #ff6467;
|
||||
--destructive-foreground: #fafafa;
|
||||
--success: #16a34a;
|
||||
--error: #dc2626;
|
||||
--warning: #d97706;
|
||||
--info: #2563eb;
|
||||
--border: #333333;
|
||||
--input: #343434;
|
||||
--ring: #737373;
|
||||
--chart-1: #91c5ff;
|
||||
--chart-2: #3a81f6;
|
||||
--chart-3: #2563ef;
|
||||
--chart-4: #1a4eda;
|
||||
--chart-5: #1f3fad;
|
||||
--sidebar: #171717;
|
||||
--sidebar-foreground: #fafafa;
|
||||
--sidebar-primary: #1447e6;
|
||||
--sidebar-primary-foreground: #fafafa;
|
||||
--sidebar-accent: #262626;
|
||||
--sidebar-accent-foreground: #fafafa;
|
||||
--sidebar-border: #282828;
|
||||
--sidebar-ring: #525252;
|
||||
--font-sans:
|
||||
'Space Grotesk', ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
--radius: 0.725rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 2.5px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.05;
|
||||
--shadow-color: #1a1a1a;
|
||||
--shadow-2xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||
--shadow-xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||
--shadow-sm: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-md: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-lg: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.05);
|
||||
--shadow-2xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.13);
|
||||
--shadow-opacity: 0.04;
|
||||
--shadow-color: oklch(0 0 0);
|
||||
--shadow-2xs: 0 1px 2.5px 0px hsl(0 0% 0% / 0.02);
|
||||
--shadow-xs: 0 1px 2.5px 0px hsl(0 0% 0% / 0.02);
|
||||
--shadow-sm:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 1px 2px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 1px 2px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-md:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 2px 4px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-lg:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 4px 6px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-xl:
|
||||
0 1px 2.5px 0px hsl(0 0% 0% / 0.04), 0 8px 10px -1px hsl(0 0% 0% / 0.04);
|
||||
--shadow-2xl: 0 1px 2.5px 0px hsl(0 0% 0% / 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
|
@ -177,17 +194,6 @@ @theme inline {
|
|||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
}
|
||||
|
||||
body {
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -198,4 +204,49 @@ @layer base {
|
|||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-dots {
|
||||
background-image: radial-gradient(circle,
|
||||
var(--border) 1px,
|
||||
transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.workflow-dots:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Animated dashed connector lines */
|
||||
.workflow-line-v {
|
||||
width: 2px;
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--border) 0,
|
||||
var(--border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px);
|
||||
animation: march-v 0.5s linear infinite;
|
||||
}
|
||||
|
||||
.workflow-line-h {
|
||||
height: 2px;
|
||||
background: repeating-linear-gradient(to right,
|
||||
var(--border) 0,
|
||||
var(--border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px);
|
||||
animation: march-h 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes march-v {
|
||||
to {
|
||||
background-position-y: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes march-h {
|
||||
to {
|
||||
background-position-x: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import { Link, router, usePage } from '@inertiajs/vue3';
|
||||
import { IconBrandDiscord, IconCalendar, IconSelector, IconFileText, IconHash, IconLogout, IconPlus, IconSettings, IconAffiliate, IconPencil, IconFileCheck, IconTag, IconUser, IconClock, IconMessageCircle, IconBell, IconBook, IconSun, IconMoon, IconDeviceDesktop, IconLanguage } from '@tabler/icons-vue';
|
||||
import { loadLanguageAsync, trans } from 'laravel-vue-i18n';
|
||||
import {
|
||||
IconAffiliate,
|
||||
IconBook,
|
||||
IconBrandDiscord,
|
||||
IconCalendar,
|
||||
IconChevronRight,
|
||||
IconClock,
|
||||
IconFileCheck,
|
||||
IconFileText,
|
||||
IconHash,
|
||||
IconKey,
|
||||
IconLifebuoy,
|
||||
IconMessageCircle,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconSettings,
|
||||
IconTag,
|
||||
} from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { store as storePost } from '@/actions/App/Http/Controllers/App/PostController';
|
||||
import { index as postsIndex } from '@/actions/App/Http/Controllers/App/PostController';
|
||||
import { updateLanguage } from '@/actions/App/Http/Controllers/App/Settings/ProfileController';
|
||||
import NavMain from '@/components/NavMain.vue';
|
||||
import NavUser from '@/components/NavUser.vue';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { useAppearance } from '@/composables/useAppearance';
|
||||
import { useInitials } from '@/composables/useInitials';
|
||||
import dayjs from '@/dayjs';
|
||||
import { logout } from '@/routes';
|
||||
import { accounts, calendar } from '@/routes/app';
|
||||
import { index as apiKeysIndex } from '@/routes/app/api-keys';
|
||||
import { index as hashtags } from '@/routes/app/hashtags';
|
||||
import { index as labels } from '@/routes/app/labels';
|
||||
import { edit as editProfile } from '@/routes/app/profile';
|
||||
|
|
@ -52,35 +58,16 @@ import type { NavItem } from '@/types';
|
|||
interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: {
|
||||
url: string;
|
||||
media_id: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface Language {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const page = usePage();
|
||||
const auth = computed(() => page.props.auth);
|
||||
const currentWorkspace = computed<Workspace | null>(() => page.props.auth.currentWorkspace as Workspace | null);
|
||||
const workspaces = computed<Workspace[]>(() => page.props.auth.workspaces as Workspace[]);
|
||||
const languages = computed<Language[]>(() => page.props.languages as Language[]);
|
||||
const currentLanguage = computed(() => languages.value.find(l => l.id === auth.value.user.language_id));
|
||||
|
||||
const { getInitials } = useInitials();
|
||||
const { appearance, updateAppearance } = useAppearance();
|
||||
const { state: sidebarState } = useSidebar();
|
||||
|
||||
const themeLabels = computed(() => ({
|
||||
light: trans('sidebar.theme_light'),
|
||||
dark: trans('sidebar.theme_dark'),
|
||||
system: trans('sidebar.theme_system'),
|
||||
}));
|
||||
|
||||
const postsNavItems = computed<NavItem[]>(() => [
|
||||
{
|
||||
title: trans('sidebar.posts.calendar'),
|
||||
|
|
@ -128,232 +115,86 @@ const configNavItems = computed(() => {
|
|||
href: labels.url(),
|
||||
icon: IconTag,
|
||||
},
|
||||
{
|
||||
title: 'API Keys',
|
||||
href: apiKeysIndex.url(),
|
||||
icon: IconKey,
|
||||
},
|
||||
];
|
||||
|
||||
if (canManageWorkspace.value) {
|
||||
items.push({
|
||||
title: trans('sidebar.config.settings'),
|
||||
href: workspaceSettings.url(),
|
||||
href: editProfile.url(),
|
||||
icon: IconSettings,
|
||||
activePattern: '/settings',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
const supportNavItems = computed<NavItem[]>(() => [
|
||||
{
|
||||
title: trans('sidebar.support.discord'),
|
||||
href: 'https://trypost.it/discord',
|
||||
icon: IconBrandDiscord,
|
||||
},
|
||||
{
|
||||
title: trans('sidebar.support.share_feedback'),
|
||||
href: 'https://github.com/trypost-it/trypost/discussions',
|
||||
icon: IconMessageCircle,
|
||||
},
|
||||
{
|
||||
title: trans('sidebar.support.last_updates'),
|
||||
href: 'https://github.com/trypost-it/trypost/releases',
|
||||
icon: IconBell,
|
||||
},
|
||||
{
|
||||
title: trans('sidebar.support.docs'),
|
||||
href: 'https://docs.trypost.it',
|
||||
icon: IconBook,
|
||||
},
|
||||
]);
|
||||
|
||||
function switchWorkspace(workspace: Workspace) {
|
||||
router.post(switchMethod.url(workspace.id), {}, {
|
||||
const switchWorkspace = (workspaceId: string) => {
|
||||
router.post(switchMethod.url(workspaceId), {}, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
function switchLanguage(languageId: string) {
|
||||
const language = languages.value.find(l => l.id === languageId);
|
||||
const languageCode = language?.code || 'en';
|
||||
const previousLanguageCode = currentLanguage.value?.code || 'en';
|
||||
|
||||
// Set locales immediately before the request
|
||||
loadLanguageAsync(languageCode);
|
||||
dayjs.locale(languageCode.toLowerCase());
|
||||
|
||||
router.patch(updateLanguage.url(), { language_id: languageId }, {
|
||||
preserveScroll: true,
|
||||
preserveState: false,
|
||||
onError: () => {
|
||||
// Revert to previous locale if request fails
|
||||
loadLanguageAsync(previousLanguageCode);
|
||||
dayjs.locale(previousLanguageCode.toLowerCase());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createWorkspace() {
|
||||
router.visit(createWorkspaceRoute.url());
|
||||
}
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
// For /posts, also match /posts/{id}/edit
|
||||
if (href === '/posts') {
|
||||
return page.url === '/posts' || page.url.startsWith('/posts/') && page.url.includes('/edit');
|
||||
}
|
||||
// Exact match or match with query string
|
||||
return page.url === href || page.url.startsWith(href + '?');
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
router.flushAll();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sidebar collapsible="icon">
|
||||
<Sidebar collapsible="icon" variant="inset">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar class="h-9 w-9 rounded-full shrink-0">
|
||||
<AvatarImage v-if="auth.user.avatar?.url" :src="auth.user.avatar?.url"
|
||||
:alt="auth.user.name" />
|
||||
<AvatarFallback class="rounded-full text-xs">
|
||||
{{ getInitials(auth.user.name) }}
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar class="h-8 w-8 shrink-0 rounded-lg">
|
||||
<AvatarFallback class="rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
|
||||
{{ getInitials(currentWorkspace?.name ?? '?') }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth.user.name }}</span>
|
||||
<span class="truncate text-xs">
|
||||
{{ currentWorkspace?.name || $t('sidebar.select_workspace') }}
|
||||
<span class="truncate font-semibold">
|
||||
{{ currentWorkspace?.name ?? $t('sidebar.select_workspace') }}
|
||||
</span>
|
||||
</div>
|
||||
<IconSelector class="ml-auto size-4" />
|
||||
<IconChevronRight class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start" side="bottom" :side-offset="4">
|
||||
<!-- User Info -->
|
||||
<DropdownMenuLabel class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-2 py-2 text-left text-sm">
|
||||
<Avatar class="h-8 w-8 rounded-full">
|
||||
<AvatarImage v-if="auth.user.avatar?.url" :src="auth.user.avatar?.url"
|
||||
:alt="auth.user.name" />
|
||||
<AvatarFallback class="rounded-full">
|
||||
{{ getInitials(auth.user.name) }}
|
||||
<DropdownMenuContent
|
||||
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side="right"
|
||||
:side-offset="4"
|
||||
>
|
||||
<DropdownMenuLabel class="text-xs text-muted-foreground">
|
||||
{{ $t('sidebar.workspaces') }}
|
||||
</DropdownMenuLabel>
|
||||
<div class="space-y-0.5">
|
||||
<DropdownMenuItem
|
||||
v-for="workspace in workspaces"
|
||||
:key="workspace.id"
|
||||
class="gap-2"
|
||||
:class="workspace.id === currentWorkspace?.id ? 'bg-accent' : ''"
|
||||
@click="switchWorkspace(workspace.id)"
|
||||
>
|
||||
<Avatar class="h-5 w-5 shrink-0 rounded-md">
|
||||
<AvatarFallback class="rounded-md text-[10px]">
|
||||
{{ getInitials(workspace.name) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth.user.name }}</span>
|
||||
<span class="truncate text-xs text-muted-foreground">{{ auth.user.email
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<!-- Workspaces -->
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<img v-if="currentWorkspace?.logo.url" :src="currentWorkspace.logo.url"
|
||||
:alt="currentWorkspace.name" class="mr-2 size-4 rounded-full object-cover" />
|
||||
<span>{{ currentWorkspace ? $t('sidebar.workspace', { name: currentWorkspace.name })
|
||||
: $t('sidebar.workspace_select') }}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup :model-value="currentWorkspace?.id"
|
||||
@update:model-value="(id: string) => switchWorkspace(workspaces.find(w => w.id === id)!)">
|
||||
<DropdownMenuRadioItem v-for="workspace in workspaces" :key="workspace.id"
|
||||
:value="workspace.id">
|
||||
<img :src="workspace.logo.url" :alt="workspace.name"
|
||||
class="mr-2 size-4 rounded-full object-cover" />
|
||||
<span>{{ workspace.name }}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="cursor-pointer gap-2" @click="createWorkspace">
|
||||
<IconPlus class="size-4" />
|
||||
<span>{{ $t('sidebar.create_workspace') }}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<!-- Theme -->
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<IconSun v-if="appearance === 'light'" class="mr-2 size-4" />
|
||||
<IconMoon v-else-if="appearance === 'dark'" class="mr-2 size-4" />
|
||||
<IconDeviceDesktop v-else class="mr-2 size-4" />
|
||||
<span>{{ $t('sidebar.theme', { name: themeLabels[appearance] }) }}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup :model-value="appearance"
|
||||
@update:model-value="updateAppearance">
|
||||
<DropdownMenuRadioItem value="light">
|
||||
<IconSun class="mr-2 size-4" />
|
||||
<span>{{ $t('sidebar.theme_light') }}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="dark">
|
||||
<IconMoon class="mr-2 size-4" />
|
||||
<span>{{ $t('sidebar.theme_dark') }}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="system">
|
||||
<IconDeviceDesktop class="mr-2 size-4" />
|
||||
<span>{{ $t('sidebar.theme_system') }}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<!-- Language -->
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<IconLanguage class="mr-2 size-4" />
|
||||
<span>{{ currentLanguage ? $t('sidebar.language', { name: currentLanguage.name }) :
|
||||
$t('sidebar.language_select') }}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup :model-value="currentLanguage?.id"
|
||||
@update:model-value="switchLanguage">
|
||||
<DropdownMenuRadioItem v-for="language in languages" :key="language.id"
|
||||
:value="language.id">
|
||||
<span>{{ language.name }}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<!-- User Actions -->
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem as-child>
|
||||
<Link class="cursor-pointer" :href="editProfile()">
|
||||
<IconUser class="mr-2 size-4" />
|
||||
{{ $t('sidebar.profile') }}
|
||||
</Link>
|
||||
{{ workspace.name }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
<DropdownMenuItem as-child>
|
||||
<Link class="w-full cursor-pointer" :href="logout()" @click="handleLogout" as="button">
|
||||
<IconLogout class="mr-2 size-4" />
|
||||
{{ $t('sidebar.log_out') }}
|
||||
<Link :href="createWorkspaceRoute.url()">
|
||||
<IconPlus class="size-4" />
|
||||
{{ $t('sidebar.create_workspace') }}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
@ -373,58 +214,38 @@ function handleLogout() {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
<SidebarGroup v-if="currentWorkspace">
|
||||
<SidebarGroupLabel>{{ $t('sidebar.groups.posts') }}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in postsNavItems" :key="item.title">
|
||||
<SidebarMenuButton as-child :tooltip="item.title"
|
||||
:is-active="isActive(item.href as string)">
|
||||
<Link :href="item.href">
|
||||
<component v-if="item.icon" :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup v-if="currentWorkspace">
|
||||
<SidebarGroupLabel>{{ $t('sidebar.groups.configuration') }}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in configNavItems" :key="item.title">
|
||||
<SidebarMenuButton as-child :tooltip="item.title"
|
||||
:is-active="isActive(item.href as string)">
|
||||
<Link :href="item.href">
|
||||
<component v-if="item.icon" :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup v-if="currentWorkspace">
|
||||
<SidebarGroupLabel>{{ $t('sidebar.groups.support') }}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in supportNavItems" :key="item.title">
|
||||
<SidebarMenuButton as-child :tooltip="item.title">
|
||||
<a :href="item.href"
|
||||
:target="(item.href as string).startsWith('http') ? '_blank' : undefined">
|
||||
<component v-if="item.icon" :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<NavMain v-if="currentWorkspace" :items="postsNavItems" :label="$t('sidebar.groups.posts')" />
|
||||
<NavMain v-if="currentWorkspace" :items="configNavItems" :label="$t('sidebar.groups.configuration')" />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarRail />
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<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" rel="noopener noreferrer">
|
||||
<IconMessageCircle />
|
||||
<span>{{ $t('sidebar.support.share_feedback') }}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton as-child tooltip="Docs">
|
||||
<a href="https://docs.trypost.it" target="_blank" rel="noopener noreferrer">
|
||||
<IconLifebuoy />
|
||||
<span>{{ $t('sidebar.support.docs') }}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<NavUser />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import type { BreadcrumbItemType } from '@/types';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
breadcrumbs?: BreadcrumbItemType[];
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
}>(),
|
||||
{
|
||||
breadcrumbs: () => [],
|
||||
|
|
@ -15,11 +15,14 @@ withDefaults(
|
|||
|
||||
<template>
|
||||
<header
|
||||
v-if="breadcrumbs && breadcrumbs.length > 0"
|
||||
class="flex h-16 shrink-0 items-center gap-2 border-b border-border px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4">
|
||||
class="flex h-16 shrink-0 items-center justify-between gap-2 border-b border-border px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SidebarTrigger class="-ml-1" />
|
||||
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||
<template v-if="breadcrumbs && breadcrumbs.length > 0">
|
||||
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||
</template>
|
||||
</div>
|
||||
<slot name="right" />
|
||||
</header>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { type NavItem } from '@/types';
|
|||
|
||||
defineProps<{
|
||||
items: NavItem[];
|
||||
label?: string;
|
||||
}>();
|
||||
|
||||
const { urlIsActive } = useActiveUrl();
|
||||
|
|
@ -20,12 +21,14 @@ const { urlIsActive } = useActiveUrl();
|
|||
|
||||
<template>
|
||||
<SidebarGroup class="px-2 py-0">
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarGroupLabel v-if="label">
|
||||
{{ label }}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in items" :key="item.title">
|
||||
<SidebarMenuButton
|
||||
as-child
|
||||
:is-active="urlIsActive(item.href)"
|
||||
:is-active="urlIsActive(item.activePattern ?? item.href)"
|
||||
:tooltip="item.title"
|
||||
>
|
||||
<Link :href="item.href">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { IconSelector } from '@tabler/icons-vue';
|
||||
import { IconChevronRight } from '@tabler/icons-vue';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -33,18 +33,12 @@ const { isMobile, state } = useSidebar();
|
|||
data-test="sidebar-menu-button"
|
||||
>
|
||||
<UserInfo :user="user" />
|
||||
<IconSelector class="ml-auto size-4" />
|
||||
<IconChevronRight class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
:side="
|
||||
isMobile
|
||||
? 'bottom'
|
||||
: state === 'collapsed'
|
||||
? 'left'
|
||||
: 'bottom'
|
||||
"
|
||||
:side="isMobile ? 'bottom' : 'right'"
|
||||
align="end"
|
||||
:side-offset="4"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { useInitials } from '@/composables/useInitials';
|
||||
import type { User } from '@/types';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
user: User;
|
||||
showEmail?: boolean;
|
||||
}
|
||||
avatarClass?: string;
|
||||
fallbackClass?: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
showEmail: false,
|
||||
});
|
||||
|
||||
const { getInitials } = useInitials();
|
||||
|
||||
// Compute whether we should show the avatar image
|
||||
const showAvatar = computed(
|
||||
() => props.user.avatar && props.user.avatar !== '',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Avatar class="h-8 w-8 overflow-hidden rounded-lg">
|
||||
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
|
||||
<AvatarFallback class="rounded-lg text-black dark:text-white">
|
||||
<Avatar :class="['h-8 w-8 rounded-lg', avatarClass]">
|
||||
<AvatarImage v-if="user.avatar" :src="user.avatar" :alt="user.name" />
|
||||
<AvatarFallback
|
||||
:class="fallbackClass ?? 'bg-sidebar-accent text-sidebar-accent-foreground'"
|
||||
>
|
||||
{{ getInitials(user.name) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { Link, router } from '@inertiajs/vue3';
|
||||
import { IconLogout, IconSettings } from '@tabler/icons-vue';
|
||||
import {
|
||||
IconBrightnessUp,
|
||||
IconDeviceDesktop,
|
||||
IconLogout,
|
||||
IconMoon,
|
||||
IconUser,
|
||||
} from '@tabler/icons-vue';
|
||||
|
||||
import {
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import UserInfo from '@/components/UserInfo.vue';
|
||||
import { useAppearance } from '@/composables/useAppearance';
|
||||
import { logout } from '@/routes';
|
||||
import { edit } from '@/routes/app/profile';
|
||||
import type { User } from '@/types';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
user: User;
|
||||
}
|
||||
};
|
||||
|
||||
const { appearance, updateAppearance } = useAppearance();
|
||||
|
||||
const handleLogout = () => {
|
||||
router.flushAll();
|
||||
|
|
@ -27,19 +40,59 @@ defineProps<Props>();
|
|||
<template>
|
||||
<DropdownMenuLabel class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<UserInfo :user="user" :show-email="true" />
|
||||
<UserInfo
|
||||
:user="user"
|
||||
:show-email="true"
|
||||
fallback-class="bg-neutral-200 text-neutral-600 dark:bg-neutral-700 dark:text-neutral-200"
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem :as-child="true">
|
||||
<Link class="block w-full cursor-pointer" :href="edit()" prefetch>
|
||||
<IconSettings class="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
<IconUser class="size-4" />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger class="gap-2">
|
||||
<IconBrightnessUp
|
||||
v-if="appearance === 'light'"
|
||||
class="size-4 text-muted-foreground"
|
||||
/>
|
||||
<IconMoon
|
||||
v-else-if="appearance === 'dark'"
|
||||
class="size-4 text-muted-foreground"
|
||||
/>
|
||||
<IconDeviceDesktop
|
||||
v-else
|
||||
class="size-4 text-muted-foreground"
|
||||
/>
|
||||
Theme: <span class="capitalize">{{ appearance }}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem @click="updateAppearance('light')">
|
||||
<IconBrightnessUp class="size-4" />
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="updateAppearance('dark')">
|
||||
<IconMoon class="size-4" />
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="updateAppearance('system')">
|
||||
<IconDeviceDesktop class="size-4" />
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem :as-child="true">
|
||||
<Link
|
||||
class="block w-full cursor-pointer"
|
||||
|
|
@ -48,7 +101,7 @@ defineProps<Props>();
|
|||
as="button"
|
||||
data-test="logout-button"
|
||||
>
|
||||
<IconLogout class="mr-2 h-4 w-4" />
|
||||
<IconLogout class="size-4" />
|
||||
Log out
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
||||
import type { BreadcrumbItemType } from '@/types';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
|
||||
interface Props {
|
||||
breadcrumbs?: BreadcrumbItemType[];
|
||||
}
|
||||
type Props = {
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
breadcrumbs: () => [],
|
||||
fullWidth: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<AppLayout :breadcrumbs="breadcrumbs" :full-width="fullWidth">
|
||||
<template v-if="$slots['header-right']" #header-right>
|
||||
<slot name="header-right" />
|
||||
</template>
|
||||
<slot />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import AppContent from '@/components/AppContent.vue';
|
||||
import AppShell from '@/components/AppShell.vue';
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
|
||||
import AppSidebar from '@/components/AppSidebar.vue';
|
||||
import AppSidebarHeader from '@/components/AppSidebarHeader.vue';
|
||||
import Toast from '@/components/Toast.vue';
|
||||
import type { BreadcrumbItemType } from '@/types';
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
|
||||
interface Props {
|
||||
breadcrumbs?: BreadcrumbItemType[];
|
||||
}
|
||||
const page = usePage();
|
||||
const isOpen = page.props.sidebarOpen;
|
||||
|
||||
type Props = {
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
breadcrumbs: () => [],
|
||||
fullWidth: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppShell variant="sidebar">
|
||||
<SidebarProvider :default-open="isOpen">
|
||||
<AppSidebar />
|
||||
<AppContent variant="sidebar" class="overflow-x-hidden">
|
||||
<AppSidebarHeader :breadcrumbs="breadcrumbs" />
|
||||
<slot />
|
||||
</AppContent>
|
||||
</AppShell>
|
||||
<SidebarInset class="overflow-x-hidden">
|
||||
<AppSidebarHeader :breadcrumbs="breadcrumbs">
|
||||
<template v-if="$slots['header-right']" #right>
|
||||
<slot name="header-right" />
|
||||
</template>
|
||||
</AppSidebarHeader>
|
||||
<div
|
||||
:class="
|
||||
fullWidth
|
||||
? 'flex min-h-0 flex-1 flex-col'
|
||||
: 'mx-auto w-full max-w-7xl'
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<Toast />
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in a new issue