diff --git a/apps/shelve/app/components/team/Manager.vue b/apps/shelve/app/components/team/Manager.vue
index 6a18b8fb..266a48fe 100644
--- a/apps/shelve/app/components/team/Manager.vue
+++ b/apps/shelve/app/components/team/Manager.vue
@@ -1,5 +1,5 @@
@@ -380,7 +211,8 @@ watch(isSearchActive, (newValue) => {
diff --git a/apps/shelve/app/composables/useAppCommands.ts b/apps/shelve/app/composables/useAppCommands.ts
new file mode 100644
index 00000000..9815ce51
--- /dev/null
+++ b/apps/shelve/app/composables/useAppCommands.ts
@@ -0,0 +1,129 @@
+import type { CommandGroup, CommandItem } from '@types'
+
+export function useAppCommands() {
+ const teams = useTeams()
+ const colorMode = useColorMode()
+ const { version } = useRuntimeConfig().public
+ const defaultTeamSlug = useCookie('defaultTeamSlug')
+ const _currentTeam = useTeam()
+ const { selectTeam, createTeam } = useTeamsService()
+
+ const currentTeam = computed(() =>
+ _currentTeam.value ?? teams.value.find((team) => team.slug === defaultTeamSlug.value)
+ )
+
+ // Theme commands
+ const themeCommands = ref([
+ {
+ id: 'theme-light',
+ label: 'Light Mode',
+ icon: 'lucide:sun',
+ description: 'Switch to light mode',
+ action: () => {
+ colorMode.preference = 'light'
+ },
+ keywords: ['light', 'theme', 'mode', 'day', 'bright'],
+ active: computed(() => colorMode.preference === 'light').value
+ },
+ {
+ id: 'theme-dark',
+ label: 'Dark Mode',
+ icon: 'lucide:moon',
+ description: 'Switch to dark mode',
+ action: () => {
+ colorMode.preference = 'dark'
+ },
+ keywords: ['dark', 'theme', 'mode', 'night', 'black'],
+ active: computed(() => colorMode.preference === 'dark').value
+ }
+ ])
+
+ // Navigation commands
+ const navigationCommands = ref([
+ {
+ id: 'nav-home',
+ label: 'Go to Dashboard',
+ icon: 'lucide:layout-dashboard',
+ description: 'Navigate to the dashboard',
+ action: () => {
+ navigateTo(`/${defaultTeamSlug.value}`)
+ },
+ keywords: ['home', 'dashboard', 'main'],
+ },
+ {
+ id: 'nav-environments',
+ label: 'Environments',
+ icon: 'lucide:cloud',
+ description: 'Navigate to the environments page',
+ action: () => {
+ navigateTo(`/${defaultTeamSlug.value}/environments`)
+ },
+ keywords: ['environments', 'projects', 'variables'],
+ },
+ {
+ id: 'nav-settings',
+ label: 'Settings',
+ icon: 'lucide:settings',
+ description: 'Open settings page',
+ action: () => {
+ navigateTo('/user/settings')
+ },
+ keywords: ['settings', 'preferences', 'config'],
+ },
+ {
+ id: 'nav-profile',
+ label: 'Profile',
+ icon: 'lucide:user',
+ description: 'View your profile',
+ action: () => {
+ navigateTo('/user/profile')
+ },
+ keywords: ['profile', 'account', 'user'],
+ },
+ ])
+
+ // Team commands
+ const teamCommands = computed(() => {
+ return teams.value.map(team => ({
+ id: `team-${team.id}`,
+ label: team.name,
+ icon: team.logo || 'lucide:users',
+ isAvatar: Boolean(team.logo),
+ description: `Switch to ${team.name} team`,
+ action: () => selectTeam(team),
+ keywords: ['team', 'switch', team.name],
+ active: team.id === currentTeam.value?.id,
+ }))
+ })
+
+ // Group all commands
+ const commandGroups = computed(() => [
+ {
+ id: 'teams',
+ label: 'Teams',
+ items: teamCommands.value
+ },
+ {
+ id: 'navigation',
+ label: 'Navigation',
+ items: navigationCommands.value
+ },
+ {
+ id: 'theme',
+ label: 'Theme',
+ items: themeCommands.value
+ }
+ ])
+
+ // Helper function to create a team
+ const createTeamFromSearch = async (teamName: string) => {
+ if (!teamName) return
+ await createTeam(teamName)
+ }
+
+ return {
+ commandGroups,
+ createTeamFromSearch,
+ version
+ }
+}
diff --git a/apps/shelve/app/composables/useCommandPalette.ts b/apps/shelve/app/composables/useCommandPalette.ts
new file mode 100644
index 00000000..333c20a8
--- /dev/null
+++ b/apps/shelve/app/composables/useCommandPalette.ts
@@ -0,0 +1,136 @@
+import type { CommandGroup, CommandItem, CommandProviderOptions } from '@types'
+
+export function useCommandPalette(
+ searchQuery: Ref,
+ commandGroups: Ref,
+ options: CommandProviderOptions = {}
+) {
+ const selectedIndex = ref(0)
+ const scrollContainerRef = ref(null)
+
+ // Filter commands based on search query
+ const filteredCommandGroups = computed(() => {
+ if (!searchQuery.value) {
+ return commandGroups.value
+ }
+
+ const searchLower = searchQuery.value.toLowerCase()
+
+ return commandGroups.value.map(group => {
+ const filteredItems = group.items.filter(item => {
+ if (item.label.toLowerCase().includes(searchLower)) {
+ return true
+ }
+
+ if (item.keywords?.some(keyword => keyword.toLowerCase().includes(searchLower))) {
+ return true
+ }
+
+ return !!(item.description && item.description.toLowerCase().includes(searchLower))
+ })
+
+ return {
+ ...group,
+ items: filteredItems,
+ }
+ }).filter(group => group.items.length > 0)
+ })
+
+ // Flatten all filtered items for easier navigation
+ const allFilteredItems = computed(() => {
+ return filteredCommandGroups.value.flatMap(group => group.items)
+ })
+
+ // Get global index from group and item indices
+ const getItemGlobalIndex = (groupIndex: number, itemIndex: number): number => {
+ let globalIndex = 0
+ for (let i = 0; i < groupIndex; i++) {
+ globalIndex += filteredCommandGroups.value[i]?.items.length || 0
+ }
+ return globalIndex + itemIndex
+ }
+
+ // Scroll to keep selected item visible
+ const scrollToSelectedItem = () => {
+ nextTick(() => {
+ const selectedElement = document.querySelector('.command-item.selected')
+ if (selectedElement && scrollContainerRef.value) {
+ const container = scrollContainerRef.value
+ const containerRect = container.getBoundingClientRect()
+ const elementRect = selectedElement.getBoundingClientRect()
+
+ if (elementRect.bottom > containerRect.bottom) {
+ // Element is below the visible area
+ const scrollOffset = elementRect.bottom - containerRect.bottom + 8
+ container.scrollTop += scrollOffset
+ } else if (elementRect.top < containerRect.top) {
+ // Element is above the visible area
+ const scrollOffset = elementRect.top - containerRect.top - 8
+ container.scrollTop += scrollOffset
+ }
+ }
+ })
+ }
+
+ // Navigation handlers
+ const navigateUp = () => {
+ if (selectedIndex.value > 0) {
+ selectedIndex.value--
+ } else {
+ selectedIndex.value = allFilteredItems.value.length - 1
+ }
+ scrollToSelectedItem()
+ }
+
+ const navigateDown = () => {
+ if (selectedIndex.value < allFilteredItems.value.length - 1) {
+ selectedIndex.value++
+ } else {
+ selectedIndex.value = 0
+ }
+ scrollToSelectedItem()
+ }
+
+ const selectCurrentItem = async () => {
+ if (allFilteredItems.value.length > 0) {
+ const item = allFilteredItems.value[selectedIndex.value]
+ if (item?.action) {
+ await item.action()
+ if (options.onClose) {
+ options.onClose()
+ }
+ }
+ }
+ }
+
+ // Reset selection when search changes
+ watch(searchQuery, () => {
+ selectedIndex.value = 0
+ nextTick(() => {
+ if (scrollContainerRef.value) {
+ scrollContainerRef.value.scrollTop = 0
+ }
+ })
+ })
+
+ // Reset scroll when command groups change
+ watch(commandGroups, () => {
+ nextTick(() => {
+ if (scrollContainerRef.value) {
+ scrollContainerRef.value.scrollTop = 0
+ }
+ })
+ }, { deep: true })
+
+ return {
+ selectedIndex,
+ scrollContainerRef,
+ filteredCommandGroups,
+ allFilteredItems,
+ getItemGlobalIndex,
+ navigateUp,
+ navigateDown,
+ selectCurrentItem,
+ scrollToSelectedItem
+ }
+}
diff --git a/apps/shelve/server/api/teams/[slug]/index.delete.ts b/apps/shelve/server/api/teams/[slug]/index.delete.ts
index 20d99cd5..b0d004f2 100644
--- a/apps/shelve/server/api/teams/[slug]/index.delete.ts
+++ b/apps/shelve/server/api/teams/[slug]/index.delete.ts
@@ -1,7 +1,7 @@
export default eventHandler(async (event) => {
const team = useCurrentTeam(event)
- await new TeamsService().deleteTeam({ teamId: team.id })
+ await new TeamsService().deleteTeam({ teamId: team.id, slug: team.slug })
return {
statusCode: 200,
diff --git a/apps/shelve/server/services/teams.ts b/apps/shelve/server/services/teams.ts
index e7e109da..d90235f3 100644
--- a/apps/shelve/server/services/teams.ts
+++ b/apps/shelve/server/services/teams.ts
@@ -127,8 +127,9 @@ export class TeamsService {
}
async deleteTeam(input: DeleteTeamInput): Promise {
- const { teamId } = input
+ const { teamId, slug } = input
await clearCache('Team', teamId)
+ await clearCache('Team', slug)
const [team] = await useDrizzle().delete(tables.teams)
.where(eq(tables.teams.id, teamId))
.returning({ id: tables.teams.id, slug: tables.teams.slug })
diff --git a/packages/types/index.ts b/packages/types/index.ts
index 7143c19a..a4c61b00 100644
--- a/packages/types/index.ts
+++ b/packages/types/index.ts
@@ -9,3 +9,4 @@ export * from './src/Environment'
export * from './src/Stats'
export * from './src/integrations/Github'
export * from './src/Navigation'
+export * from './src/Command'
diff --git a/packages/types/src/Command.ts b/packages/types/src/Command.ts
new file mode 100644
index 00000000..3aeac249
--- /dev/null
+++ b/packages/types/src/Command.ts
@@ -0,0 +1,20 @@
+export interface CommandItem {
+ id: string
+ label: string
+ icon: string
+ isAvatar?: boolean
+ description?: string
+ action: () => void | Promise
+ keywords?: string[]
+ active?: boolean
+}
+
+export interface CommandGroup {
+ id: string
+ label: string
+ items: CommandItem[]
+}
+
+export interface CommandProviderOptions {
+ onClose?: () => void
+}
diff --git a/packages/types/src/Team.ts b/packages/types/src/Team.ts
index fec8fec5..0d20ab1e 100644
--- a/packages/types/src/Team.ts
+++ b/packages/types/src/Team.ts
@@ -41,7 +41,8 @@ export type UpdateTeamInput = {
}
export type DeleteTeamInput = {
- teamId: number
+ teamId: number,
+ slug: string
}
export type AddMemberInput = {