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
1 change: 0 additions & 1 deletion app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ import { SpeedInsights } from '@vercel/speed-insights/nuxt'
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UToaster />
</UApp>
</template>
47 changes: 38 additions & 9 deletions app/components/issue/IssueRepoSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ interface SearchRepo {
visibility: string
openIssues: number
stars: number
fork: boolean
}

const { t } = useI18n()
const repoStore = useRepositoryStore()
const issueStore = useIssueStore()
const { settings, update } = useUserSettings()
const { isPinned, toggle: togglePin } = usePinnedRepos()
const apiFetch = useRequestFetch()

const open = ref(false)
Expand Down Expand Up @@ -159,7 +160,7 @@ onMounted(async () => {
<span
v-else
class="text-muted"
>{{ t('issues.selectRepo') }}</span>
>{{ $t('issues.selectRepo') }}</span>
</UButton>

<template #content>
Expand All @@ -172,7 +173,7 @@ onMounted(async () => {
/>
<input
v-model="query"
:placeholder="t('issues.searchRepos')"
:placeholder="$t('issues.searchRepos')"
class="flex-1 min-w-0 text-sm bg-transparent outline-none placeholder:text-muted"
>
<UIcon
Expand All @@ -187,7 +188,7 @@ onMounted(async () => {
v-if="!query"
class="px-3 pb-1.5 text-xs text-dimmed"
>
{{ t('issues.searchReposHint') }}
{{ $t('issues.searchReposHint') }}
</p>

<USeparator />
Expand All @@ -200,7 +201,7 @@ onMounted(async () => {
v-if="query"
class="px-3 pt-2 pb-1 text-xs font-semibold text-dimmed uppercase tracking-wide"
>
{{ t('issues.yourRepos') }}
{{ $t('issues.yourRepos') }}
</p>
<button
v-for="repo in filteredOwnRepos"
Expand All @@ -223,7 +224,7 @@ onMounted(async () => {
variant="subtle"
size="xs"
>
{{ t('repos.badge.private') }}
{{ $t('repos.badge.private') }}
</UBadge>
</div>
<p
Expand All @@ -241,7 +242,7 @@ onMounted(async () => {
name="i-lucide-circle-dot"
class="size-3.5"
/>
{{ t('issues.openCount', { count: repo.openIssues }) }}
{{ $t('issues.openCount', { count: repo.openIssues }) }}
</span>
<span
v-if="repo.openPrs"
Expand All @@ -261,6 +262,20 @@ onMounted(async () => {
</span>
</div>
</div>
<UTooltip
:text="isPinned(repo.fullName) ?$t('pinnedRepos.unpin') :$t('pinnedRepos.pin')"
>
<UButton
:icon="isPinned(repo.fullName) ? 'i-lucide-pin-off' : 'i-lucide-pin'"
size="xs"
color="neutral"
variant="ghost"
square
:aria-label="isPinned(repo.fullName) ?$t('pinnedRepos.unpin') :$t('pinnedRepos.pin')"
class="shrink-0 self-center cursor-pointer"
@click.stop="togglePin(repo.fullName, repo.fork ? 'fork' : 'repo')"
/>
</UTooltip>
</button>
</div>

Expand Down Expand Up @@ -317,6 +332,20 @@ onMounted(async () => {
</span>
</div>
</div>
<UTooltip
:text="isPinned(repo.fullName) ?$t('pinnedRepos.unpin') :$t('pinnedRepos.pin')"
>
<UButton
:icon="isPinned(repo.fullName) ? 'i-lucide-pin-off' : 'i-lucide-pin'"
size="xs"
color="neutral"
variant="ghost"
square
:aria-label="isPinned(repo.fullName) ?$t('pinnedRepos.unpin') :$t('pinnedRepos.pin')"
class="shrink-0 self-center cursor-pointer"
@click.stop="togglePin(repo.fullName, repo.fork ? 'fork' : 'repo')"
/>
</UTooltip>
</button>
</div>

Expand All @@ -325,15 +354,15 @@ onMounted(async () => {
v-if="!filteredOwnRepos.length && !filteredSearchResults.length && !searching"
class="px-3 py-4 text-sm text-muted text-center"
>
{{ t('repos.noResults') }}
{{ $t('repos.noResults') }}
</p>

<!-- Searching indicator -->
<p
v-if="searching && !filteredSearchResults.length"
class="px-3 py-4 text-sm text-muted text-center"
>
{{ t('common.loading') }}
{{ $t('common.loading') }}
</p>
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions app/components/repo/RepoCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const activityTooltip = computed(() => {
return t('repos.activity.inactive')
})

const { isPinned, toggle: togglePin } = usePinnedRepos()

const expandedSection = ref<'issues' | 'prs' | 'notifications' | null>(null)

function toggleSection(section: 'issues' | 'prs' | 'notifications') {
Expand Down Expand Up @@ -137,6 +139,17 @@ function toggleSection(section: 'issues' | 'prs' | 'notifications') {
<span class="text-xs text-dimmed">
{{ timeAgo }}
</span>
<UTooltip :text="isPinned(repo.fullName) ? $t('pinnedRepos.unpin') : $t('pinnedRepos.pin')">
<UButton
:icon="isPinned(repo.fullName) ? 'i-lucide-pin-off' : 'i-lucide-pin'"
size="xs"
color="neutral"
variant="ghost"
square
:aria-label="isPinned(repo.fullName) ? $t('pinnedRepos.unpin') : $t('pinnedRepos.pin')"
@click.stop="togglePin(repo.fullName, repo.fork ? 'fork' : 'repo')"
/>
</UTooltip>
</div>
</div>

Expand Down
74 changes: 69 additions & 5 deletions app/components/ui/SideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ const isDark = computed({
// Later use a store e.g.
const notificationCount = ref(3)

const { pinnedRepos, unpin } = usePinnedRepos()

const { update: updateSettings } = useUserSettings()

const issueStore = useIssueStore()

function selectPinnedRepo(repo: string) {
issueStore.selectRepo(repo)
updateSettings({ selectedRepo: repo })
}

const mainItems = computed<NavigationMenuItem[]>(() => [
{
label: t('nav.dashboard'),
Expand Down Expand Up @@ -96,7 +107,7 @@ const mainItems = computed<NavigationMenuItem[]>(() => [
<span
class="font-semibold text-sm whitespace-nowrap"
aria-hidden="true"
>{{ t('common.title') }}</span>
>{{ $t('common.title') }}</span>
</div>
<UDashboardSidebarCollapse />
</div>
Expand All @@ -106,7 +117,7 @@ const mainItems = computed<NavigationMenuItem[]>(() => [
<template #default="{ collapsed }">
<UButton
:label="collapsed ? undefined : t('nav.search')"
:aria-label="t('nav.search')"
:aria-label="$t('nav.search')"
icon="i-lucide-search"
color="neutral"
variant="outline"
Expand Down Expand Up @@ -144,12 +155,65 @@ const mainItems = computed<NavigationMenuItem[]>(() => [
inset
>
<UIcon
:name="item.icon!"
:name="
item.icon!"
class="size-5 shrink-0"
/>
</UChip>
</template>
</UNavigationMenu>

<!-- Pinned repos -->
<ClientOnly>
<nav
v-if="pinnedRepos.length && !collapsed"
:aria-label="$t('pinnedRepos.pinned')"
class="mt-2 border-t border-default pt-2 px-1"
Comment on lines +169 to +171
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 14, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Collapsed sidebar hides pinned section entirely (spec mismatch).

Issue #63 calls for an icon-only pinned section when collapsed. The current v-if="pinnedRepos.length && !collapsed" removes it completely, so pinned repos are unreachable while collapsed. Consider rendering a collapsed icon/tooltip state instead.

🤖 Prompt for AI Agents
In `@app/components/ui/SideBar.vue` around lines 170 - 172, The pinned section is
fully removed by v-if="pinnedRepos.length && !collapsed", which hides pinned
repos when the sidebar is collapsed; change the template to render the pinned
section whenever pinnedRepos.length is truthy and use the collapsed flag to
toggle between full list and an icon-only/tooltip compact view instead of
removing it entirely (e.g., keep the container with
:aria-label="$t('pinnedRepos.pinned')" and class="mt-2 border-t border-default
pt-2 px-1", render the detailed list when !collapsed and render an
icon-only/tooltip button when collapsed so pinnedRepos remains reachable).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

please create a new issue and assign it to me. this is a nice idea (I doesn't forgot it, but I though this is a separate feature)

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@Flo0806, I'll create that issue for you right away!


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue for the collapsed sidebar pinned repos feature

gh issue create \
  --title "Pinned repos: show icon-only view when sidebar is collapsed" \
  --body "## Description

When the sidebar is collapsed, the pinned repos section is currently hidden entirely. According to the original specification in `#63`, the pinned section should show an icon-only view when collapsed, allowing users to still access their pinned repositories.

## Current behavior
- The pinned repos section uses \`v-if=\"pinnedRepos.length && !collapsed\"\` which completely removes it when collapsed
- Pinned repos become unreachable while the sidebar is collapsed

## Expected behavior
- Render the pinned section whenever there are pinned repos
- Show full list when expanded
- Show icon-only/tooltip compact view when collapsed
- Pinned repos remain accessible in both states

## Related
- Original feature request: `#63`
- Implemented in: `#68`
- Discussion: https://github.com/flumen-dev/flumen.dev/pull/68#discussion_r2808070109
- Requested by: `@Flo0806`" \
  --assignee "Flo0806" \
  --label "enhancement"

Length of output: 50


@Flo0806, done! I've created issue #69 and assigned it to you: #69

The issue captures the idea of showing an icon-only view for pinned repos when the sidebar is collapsed, so they remain accessible in both states. 👍

>
<p class="px-2 pb-1 text-xs font-semibold text-muted uppercase tracking-wide">
{{ $t('pinnedRepos.pinned') }}
</p>
<div class="space-y-0.5 max-h-50 overflow-y-auto">
<div
v-for="item in pinnedRepos"
:key="item.repo"
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-elevated/50 transition-colors group"
>
<NuxtLink
:to="localePath('/issues')"
class="flex items-center gap-2 flex-1 min-w-0"
@click="selectPinnedRepo(item.repo)"
>
<UIcon
:name="item.type === 'fork' ? 'i-lucide-git-fork' : 'i-lucide-book-marked'"
class="size-4 shrink-0 text-muted"
/>
<span class="truncate">{{ item.repo.split('/')[1] }}</span>
<UBadge
Comment on lines +191 to +192
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 14, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against unexpected repo string formats.

item.repo.split('/')[1] renders blank if the value lacks a slash. Safer extraction avoids empty labels.

✅ Suggested change
-                <span class="truncate">{{ item.repo.split('/')[1] }}</span>
+                <span class="truncate">{{ item.repo.split('/').slice(-1)[0] || item.repo }}</span>
🤖 Prompt for AI Agents
In `@app/components/ui/SideBar.vue` around lines 192 - 193, Guard against
malformed repo strings by replacing direct use of item.repo.split('/')[1] with a
safe extractor: add a small computed prop or helper (e.g., repoLabel or
getRepoName) that checks item.repo is a string and contains '/' and returns the
segment after the slash, otherwise returns item.repo (or an empty
string/fallback). Update the template where item.repo.split('/')[1] is used to
render this safe repoLabel so blank labels are avoided.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! But repo comes always from our pin function - Only way to malform it is when manually edit in storage file (and then our server would be hacked for :-) ) We let this for now

Copy link
Contributor

Choose a reason for hiding this comment

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

@Flo0806, understood! That makes sense—if the repo names are always controlled by your pin function, the risk is indeed minimal. Thanks for the context! 🐰

v-if="item.type === 'fork'"
color="info"
variant="subtle"
size="xs"
>
{{ $t('repos.badge.fork') }}
</UBadge>
</NuxtLink>
<UTooltip :text="$t('pinnedRepos.unpin')">
<UButton
icon="i-lucide-pin-off"
size="xs"
color="neutral"
variant="ghost"
square
:aria-label="$t('pinnedRepos.unpin')"
class="opacity-0 group-hover:opacity-100 shrink-0"
@click="unpin(item.repo)"
/>
</UTooltip>
</div>
</div>
</nav>
</ClientOnly>
</template>

<template #footer="{ collapsed }">
Expand All @@ -160,7 +224,7 @@ const mainItems = computed<NavigationMenuItem[]>(() => [
<UButton
v-if="!loggedIn"
icon="i-lucide-github"
:label="collapsed ? undefined : t('auth.login')"
:label="collapsed ? undefined : $t('auth.login')"
color="neutral"
variant="ghost"
:square="collapsed"
Expand All @@ -186,7 +250,7 @@ const mainItems = computed<NavigationMenuItem[]>(() => [
<ClientOnly>
<UButton
:icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
:aria-label="isDark ? t('theme.light') : t('theme.dark')"
:aria-label="isDark ? $t('theme.light') : $t('theme.dark')"
color="neutral"
variant="ghost"
square
Expand Down
31 changes: 31 additions & 0 deletions app/composables/usePinnedRepos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { PinnedItem, PinnedItemType } from '~~/shared/types/settings'

export function usePinnedRepos() {
const { settings, update } = useUserSettings()

const pinnedRepos = computed(() => settings.value?.pinnedRepos ?? [])

function isPinned(repo: string) {
return pinnedRepos.value.some(p => p.repo === repo)
}

function getItem(repo: string): PinnedItem | undefined {
return pinnedRepos.value.find(p => p.repo === repo)
}

async function pin(repo: string, type: PinnedItemType = 'repo') {
if (isPinned(repo)) return
await update({ pinnedRepos: [...pinnedRepos.value, { repo, type }] })
}

async function unpin(repo: string) {
await update({ pinnedRepos: pinnedRepos.value.filter(p => p.repo !== repo) })
}

async function toggle(repo: string, type: PinnedItemType = 'repo') {
if (isPinned(repo)) await unpin(repo)
else await pin(repo, type)
}

return { pinnedRepos, isPinned, getItem, pin, unpin, toggle }
}
5 changes: 5 additions & 0 deletions i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@
"contributorDescription": "Hat bereits zu diesem Repository beigetragen"
}
},
"pinnedRepos": {
"pin": "Repository anheften",
"unpin": "Repository loslösen",
"pinned": "Angeheftet"
},
"settings": {
"appearance": {
"title": "Erscheinungsbild",
Expand Down
5 changes: 5 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@
"contributorDescription": "Has previously committed to this repository"
}
},
"pinnedRepos": {
"pin": "Pin repository",
"unpin": "Unpin repository",
"pinned": "Pinned"
},
"settings": {
"appearance": {
"title": "Appearance",
Expand Down
15 changes: 15 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,21 @@
},
"additionalProperties": false
},
"pinnedRepos": {
"type": "object",
"properties": {
"pin": {
"type": "string"
},
"unpin": {
"type": "string"
},
"pinned": {
"type": "string"
}
},
"additionalProperties": false
},
"settings": {
"type": "object",
"properties": {
Expand Down
2 changes: 2 additions & 0 deletions server/api/repository/search.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface GitHubSearchResult {
visibility: string
open_issues_count: number
stargazers_count: number
fork: boolean
}>
}

Expand Down Expand Up @@ -37,5 +38,6 @@ export default defineEventHandler(async (event) => {
visibility: r.visibility,
openIssues: r.open_issues_count,
stars: r.stargazers_count,
fork: r.fork,
}))
})
Loading