Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,43 @@

/* ── Pipeline view premium touches ──────────────────── */

/* Hide scrollbar but keep functionality (for horizontal-scroll navs) */
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none;
}

/* Thin visible scrollbar for scrollable panels */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-300) transparent;
}
.dark .scrollbar-thin {
scrollbar-color: var(--color-surface-600) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: var(--color-surface-300);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--color-surface-400);
}
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
background: var(--color-surface-600);
}
.dark .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--color-surface-500);
}

/* Smooth candidate card highlight on selection */
.pipeline-candidate-card {
position: relative;
Expand Down
16 changes: 8 additions & 8 deletions app/components/AppTopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -400,18 +400,18 @@ onUnmounted(() => {
v-if="activeJobId"
class="relative z-10 border-b border-surface-200/60 dark:border-surface-800/60 bg-surface-50/90 dark:bg-surface-950/90 backdrop-blur-lg"
>
<div class="flex items-center gap-4 px-4 lg:px-6 h-10">
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 lg:px-6 h-10 overflow-x-auto scrollbar-none">
<NuxtLink
:to="$localePath('/dashboard/jobs')"
class="flex items-center gap-1 text-xs font-medium text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors no-underline shrink-0"
class="hidden sm:flex items-center gap-1 text-xs font-medium text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors no-underline shrink-0"
>
<ChevronLeft class="size-3.5" />
All Jobs
</NuxtLink>

<div class="w-px h-4 bg-surface-200 dark:bg-surface-700" />
<div class="hidden sm:block w-px h-4 bg-surface-200 dark:bg-surface-700 shrink-0" />

<div class="flex items-center gap-2 shrink-0 min-w-0">
<div class="hidden md:flex items-center gap-2 shrink-0 min-w-0">
<Briefcase class="size-3.5 text-brand-500 shrink-0" />
<span class="text-sm font-semibold text-surface-900 dark:text-surface-100 truncate max-w-48">
{{ activeJobTitle }}
Expand All @@ -425,22 +425,22 @@ onUnmounted(() => {
</span>
</div>

<nav class="flex items-center gap-0.5 ml-2">
<nav class="flex items-center gap-0.5 md:ml-2">
<NuxtLink
v-for="tab in jobTabs"
:key="tab.to"
:to="$localePath(tab.to)"
class="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-200 no-underline"
class="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-200 no-underline whitespace-nowrap shrink-0"
:class="isActiveRoute(tab.to, tab.exact)
? 'bg-white dark:bg-surface-800 text-surface-900 dark:text-surface-100 shadow-sm'
: 'text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200 hover:bg-white/60 dark:hover:bg-surface-800/60'"
>
<component :is="tab.icon" class="size-3.5" />
{{ tab.label }}
<span class="hidden sm:inline">{{ tab.label }}</span>
</NuxtLink>
Comment on lines +433 to 440
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mobile job tabs lose accessible names when labels are hidden.

On small screens, Line 439 hides the only visible text label. These links become icon-only without an explicit accessible name.

🛠️ Suggested fix
 <NuxtLink
   v-for="tab in jobTabs"
   :key="tab.to"
   :to="$localePath(tab.to)"
+  :aria-label="tab.label"
   class="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-200 no-underline whitespace-nowrap shrink-0"
   :class="isActiveRoute(tab.to, tab.exact)
     ? 'bg-white dark:bg-surface-800 text-surface-900 dark:text-surface-100 shadow-sm'
     : 'text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200 hover:bg-white/60 dark:hover:bg-surface-800/60'"
 >
   <component :is="tab.icon" class="size-3.5" />
-  <span class="hidden sm:inline">{{ tab.label }}</span>
+  <span class="sr-only sm:not-sr-only sm:inline">{{ tab.label }}</span>
 </NuxtLink>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/AppTopBar.vue` around lines 433 - 440, The mobile tabs become
icon-only and lose accessible names because the visible label (tab.label) is
hidden on small screens; update the NuxtLink for the tab (the element using
isActiveRoute and rendering <component :is="tab.icon" /> and <span>{{ tab.label
}}</span>) to include an explicit accessible name such as
:aria-label="tab.label" (and optionally :title="tab.label") so the icon-only
links retain screen-reader text and hover/tooltips when the span is hidden.

</nav>

<div class="ml-auto flex items-center gap-2">
<div class="ml-auto flex items-center gap-2 shrink-0">
<div id="job-sub-nav-actions" />
</div>
</div>
Expand Down
10 changes: 5 additions & 5 deletions app/components/CandidateDetailSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,11 @@ function formatInterviewDate(dateStr: string) {
<Transition name="slide">
<aside
v-if="open"
class="fixed right-0 z-40 w-[640px] max-w-[calc(100vw-4rem)] border-l border-surface-200/80 dark:border-surface-800/60 bg-white dark:bg-surface-900 shadow-xl flex flex-col"
class="fixed right-0 z-40 w-full sm:w-[640px] sm:max-w-[calc(100vw-4rem)] border-l border-surface-200/80 dark:border-surface-800/60 bg-white dark:bg-surface-900 shadow-xl flex flex-col"
:class="hasSubNav ? 'top-24 h-[calc(100vh-6rem)]' : 'top-14 h-[calc(100vh-3.5rem)]'"
>
<!-- Header -->
<div class="flex items-center justify-between border-b border-surface-200/80 dark:border-surface-800/60 px-6 py-4 shrink-0">
<div class="flex items-center justify-between border-b border-surface-200/80 dark:border-surface-800/60 px-4 sm:px-6 py-4 shrink-0">
<div v-if="application" class="min-w-0 flex-1">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center size-10 rounded-full bg-brand-50 dark:bg-brand-950 text-brand-700 dark:text-brand-400 font-semibold text-sm shrink-0">
Expand Down Expand Up @@ -385,8 +385,8 @@ function formatInterviewDate(dateStr: string) {
</div>

<!-- Tabs -->
<div v-if="application" class="border-b border-surface-200/80 dark:border-surface-800/60 px-6 shrink-0">
<div class="flex gap-1">
<div v-if="application" class="border-b border-surface-200/80 dark:border-surface-800/60 px-4 sm:px-6 shrink-0">
<div class="flex gap-1 overflow-x-auto scrollbar-none">
<button
class="cursor-pointer px-3 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px"
:class="activeTab === 'overview'
Expand Down Expand Up @@ -429,7 +429,7 @@ function formatInterviewDate(dateStr: string) {
</div>

<!-- Body -->
<div class="flex-1 overflow-y-auto px-6 py-5">
<div class="flex-1 overflow-y-auto px-4 sm:px-6 py-5">
<!-- Loading -->
<div v-if="fetchStatus === 'pending'" class="text-center py-12 text-surface-400">
Loading details…
Expand Down
2 changes: 1 addition & 1 deletion app/components/DemoUpsellBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function handleUpgrade() {

<template>
<Teleport to="body">
<div class="fixed bottom-5 left-5 z-50 w-[300px]">
<div class="fixed bottom-4 left-4 right-4 z-50 sm:right-auto sm:w-[300px]">
<div
class="relative overflow-hidden rounded-2xl border border-white/[0.08] bg-gradient-to-br from-surface-900 via-surface-900 to-brand-950/80 shadow-2xl backdrop-blur-xl dark:from-surface-950 dark:via-surface-950 dark:to-brand-950/90"
>
Expand Down
10 changes: 5 additions & 5 deletions app/components/InterviewEmailModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const canSend = computed(() => {
<!-- Modal -->
<div class="relative bg-white dark:bg-surface-900 rounded-2xl shadow-2xl shadow-surface-900/10 dark:shadow-black/30 ring-1 ring-surface-200/80 dark:ring-surface-700/60 w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col overflow-hidden">
<!-- Header -->
<div class="shrink-0 border-b border-surface-200/80 dark:border-surface-800/60 px-6 py-4">
<div class="shrink-0 border-b border-surface-200/80 dark:border-surface-800/60 px-4 sm:px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2.5">
<div class="flex size-9 items-center justify-center rounded-lg bg-brand-50 dark:bg-brand-950/40">
Expand Down Expand Up @@ -198,7 +198,7 @@ const canSend = computed(() => {
<!-- Main content -->
<template v-else>
<!-- Tabs -->
<div class="shrink-0 border-b border-surface-200/80 dark:border-surface-800/60 px-6">
<div class="shrink-0 border-b border-surface-200/80 dark:border-surface-800/60 px-4 sm:px-6 overflow-x-auto scrollbar-none">
<div class="flex gap-1">
<button
v-for="tab in ([
Expand All @@ -207,7 +207,7 @@ const canSend = computed(() => {
{ id: 'manage' as Tab, label: 'Manage Templates', icon: Sparkles },
])"
:key="tab.id"
class="flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-all cursor-pointer -mb-px"
class="flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-all cursor-pointer -mb-px whitespace-nowrap shrink-0"
:class="activeTab === tab.id
? 'border-brand-500 text-brand-700 dark:text-brand-300'
: 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
Expand All @@ -220,13 +220,13 @@ const canSend = computed(() => {
</div>

<!-- Error -->
<div v-if="sendError" class="mx-6 mt-4 flex items-start gap-2.5 rounded-xl border border-danger-200/80 bg-danger-50 p-3.5 text-sm text-danger-700 dark:border-danger-800/60 dark:bg-danger-950/40 dark:text-danger-300">
<div v-if="sendError" class="mx-4 sm:mx-6 mt-4 flex items-start gap-2.5 rounded-xl border border-danger-200/80 bg-danger-50 p-3.5 text-sm text-danger-700 dark:border-danger-800/60 dark:bg-danger-950/40 dark:text-danger-300">
<AlertCircle class="size-4 shrink-0 mt-0.5" />
{{ sendError }}
</div>

<!-- Tab content -->
<div class="flex-1 overflow-y-auto px-6 py-5">
<div class="flex-1 overflow-y-auto px-4 sm:px-6 py-5">
<!-- Template Selection Tab -->
<div v-if="activeTab === 'template'" class="space-y-3">
<p class="text-xs text-surface-500 dark:text-surface-400 mb-3">
Expand Down
91 changes: 91 additions & 0 deletions app/components/SettingsMobileNav.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<script setup lang="ts">
import {
Building2, Users, UserCircle, ChevronLeft, Plug, Brain,
} from 'lucide-vue-next'

const route = useRoute()
const localePath = useLocalePath()

const settingsNav = [
{
label: 'General',
to: '/dashboard/settings',
icon: Building2,
exact: true,
},
{
label: 'Members',
to: '/dashboard/settings/members',
icon: Users,
exact: true,
},
{
label: 'Integrations',
to: '/dashboard/settings/integrations',
icon: Plug,
exact: true,
},
{
label: 'AI',
to: '/dashboard/settings/ai',
icon: Brain,
exact: true,
},
{
label: 'Account',
to: '/dashboard/settings/account',
icon: UserCircle,
exact: true,
},
]
Comment on lines +9 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Desktop/mobile settings labels are drifting.

Line 29 uses "AI", while desktop sidebar uses "AI Configuration" in app/components/SettingsSidebar.vue Line 32. Since both components maintain separate nav arrays, this will keep drifting. Consider extracting a shared settingsNav source and reusing it in both components.

Quick consistency patch
-    label: 'AI',
+    label: 'AI Configuration',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const settingsNav = [
{
label: 'General',
to: '/dashboard/settings',
icon: Building2,
exact: true,
},
{
label: 'Members',
to: '/dashboard/settings/members',
icon: Users,
exact: true,
},
{
label: 'Integrations',
to: '/dashboard/settings/integrations',
icon: Plug,
exact: true,
},
{
label: 'AI',
to: '/dashboard/settings/ai',
icon: Brain,
exact: true,
},
{
label: 'Account',
to: '/dashboard/settings/account',
icon: UserCircle,
exact: true,
},
]
const settingsNav = [
{
label: 'General',
to: '/dashboard/settings',
icon: Building2,
exact: true,
},
{
label: 'Members',
to: '/dashboard/settings/members',
icon: Users,
exact: true,
},
{
label: 'Integrations',
to: '/dashboard/settings/integrations',
icon: Plug,
exact: true,
},
{
label: 'AI Configuration',
to: '/dashboard/settings/ai',
icon: Brain,
exact: true,
},
{
label: 'Account',
to: '/dashboard/settings/account',
icon: UserCircle,
exact: true,
},
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/SettingsMobileNav.vue` around lines 9 - 40, The settings nav
arrays are duplicated (settingsNav in SettingsMobileNav.vue vs the array in
SettingsSidebar.vue) causing label drift (e.g., "AI" vs "AI Configuration");
extract a single shared export (e.g., export const settingsNav = [...]) into a
new module and update both SettingsMobileNav.vue and SettingsSidebar.vue to
import and use that shared settingsNav instead of defining their own arrays,
ensuring the shared object contains the label, to, icon, and exact fields (and
export/import any icon references used) so both components render identical
labels and routes.


function isActive(to: string, exact: boolean) {
const localizedTo = localePath(to)
if (exact) return route.path === localizedTo
return route.path === localizedTo || route.path.startsWith(`${localizedTo}/`)
}
</script>

<template>
<div class="border-b border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 shadow-sm">
<!-- Back link + title -->
<div class="flex items-center gap-3 px-4 pt-3 pb-2">
<NuxtLink
:to="$localePath('/dashboard')"
class="inline-flex items-center gap-1 text-xs font-medium text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors no-underline"
>
<ChevronLeft class="size-3.5" />
Back
</NuxtLink>
<h2 class="text-sm font-semibold text-surface-900 dark:text-surface-100">
Settings
</h2>
</div>

<!-- Scrollable tabs -->
<nav class="flex overflow-x-auto px-3 gap-1 pb-2 scrollbar-none">
<NuxtLink
v-for="item in settingsNav"
:key="item.to"
:to="$localePath(item.to)"
class="flex items-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium transition-colors no-underline shrink-0"
:class="isActive(item.to, item.exact)
? 'bg-brand-50 dark:bg-brand-950/40 text-brand-700 dark:text-brand-300'
: 'text-surface-500 dark:text-surface-400 hover:bg-surface-50 dark:hover:bg-surface-800/60 hover:text-surface-900 dark:hover:text-surface-100'"
>
<component :is="item.icon" class="size-3.5" />
{{ item.label }}
</NuxtLink>
</nav>
</div>
</template>

<style scoped>
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none;
}
</style>
2 changes: 1 addition & 1 deletion app/components/SettingsSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function isActive(to: string, exact: boolean) {

<template>
<aside
class="flex h-full w-56 min-w-56 flex-col border-r border-surface-200 dark:border-surface-800 bg-white/80 dark:bg-surface-900/80 backdrop-blur-sm overflow-y-auto"
class="flex h-full w-56 min-w-56 flex-col border-r border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 overflow-y-auto overscroll-contain"
>
<!-- Header -->
<div class="px-4 pt-5 pb-4">
Expand Down
4 changes: 2 additions & 2 deletions app/layouts/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ const isDemoAccount = computed(() => session.value?.user?.email === 'demo@reqcor
</script>

<template>
<div class="flex min-h-screen flex-col bg-surface-50 dark:bg-surface-950">
<div class="flex h-screen flex-col overflow-hidden bg-surface-50 dark:bg-surface-950">
<AppTopBar />
<AppToasts />
<PreviewUpsellModal v-if="isUpsellOpen" @close="closeUpsell" />
<ClientOnly>
<DemoUpsellBanner v-if="isDemoAccount" />
</ClientOnly>
<main class="flex-1 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<main class="relative flex-1 min-h-0 overflow-y-auto px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<!-- Demo mode banner -->
<div
v-if="isDemo"
Expand Down
61 changes: 61 additions & 0 deletions app/layouts/settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { Eye } from 'lucide-vue-next'
import { usePreviewReadOnly } from '~/composables/usePreviewReadOnly'

const { data: session } = await authClient.useSession(useFetch)

const config = useRuntimeConfig()
const { activeOrg } = useCurrentOrg()
const { isUpsellOpen, closeUpsell } = usePreviewReadOnly()

const isDemo = computed(() => {
const slug = config.public.demoOrgSlug
return slug && activeOrg.value?.slug === slug
})

const isDemoAccount = computed(() => session.value?.user?.email === 'demo@reqcore.com')
</script>

<template>
<div class="flex min-h-screen flex-col bg-surface-50 dark:bg-surface-950">
<!-- AppTopBar: desktop only -->
<AppTopBar class="hidden lg:block" />
<AppToasts />
<PreviewUpsellModal v-if="isUpsellOpen" @close="closeUpsell" />
<ClientOnly>
<DemoUpsellBanner v-if="isDemoAccount" />
</ClientOnly>

<!-- Demo mode banner -->
<div
v-if="isDemo"
class="mx-auto mb-6 flex max-w-5xl items-center gap-3 rounded-lg border border-brand-200 dark:border-brand-900 bg-brand-50 dark:bg-brand-950/40 px-4 py-2.5 text-sm text-brand-700 dark:text-brand-300"
>
<Eye class="size-4 shrink-0" />
<span>
<strong>Live demo</strong> — Explore freely with sample data. Editing is disabled here.
<a
href="https://github.com/reqcore-inc/reqcore#quick-start"
target="_blank"
rel="noopener noreferrer"
class="ml-1 font-semibold underline decoration-brand-400/40 underline-offset-2 hover:decoration-brand-400"
>Deploy your own free instance →</a>
</span>
</div>

<div class="flex flex-1 flex-col lg:flex-row min-w-0">
<!-- Desktop sidebar -->
<div class="hidden lg:block sticky top-14 h-[calc(100vh-3.5rem)] shrink-0 z-10">
<SettingsSidebar />
</div>
<!-- Mobile top nav -->
<div class="lg:hidden sticky top-0 z-10">
<SettingsMobileNav />
</div>
<!-- Page content -->
<main class="flex-1 min-w-0 px-4 py-5 sm:px-6 sm:py-6 lg:px-8 lg:py-8">
<slot />
</main>
</div>
</div>
</template>
Loading
Loading