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
2 changes: 2 additions & 0 deletions app/ui/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,8 @@
"explain_commits": "Explain commits",
"n_commits_selected": "{count} commits selected",
"close_details": "Close Details",
"dock_bottom": "Dock to bottom",
"dock_right": "Dock to right",
"tab_information": "INFORMATION",
"tab_changes": "CHANGES",
"tab_submodule": "SUBMODULE",
Expand Down
2 changes: 2 additions & 0 deletions app/ui/src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@
"explain_commits": "Explicar commits",
"n_commits_selected": "{count} commits seleccionados",
"close_details": "Cerrar detalles",
"dock_bottom": "Anclar abajo",
"dock_right": "Anclar a la derecha",
"tab_information": "INFORMACIÓN",
"tab_changes": "CAMBIOS",
"tab_submodule": "SUBMÓDULO",
Expand Down
2 changes: 2 additions & 0 deletions app/ui/src/i18n/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,8 @@
"explain_commits": "Explicar commits",
"n_commits_selected": "{count} commits selecionados",
"close_details": "Fechar detalhes",
"dock_bottom": "Encaixar embaixo",
"dock_right": "Encaixar à direita",
"tab_information": "INFORMAÇÃO",
"tab_changes": "ALTERAÇÕES",
"tab_submodule": "SUBMÓDULO",
Expand Down
6 changes: 5 additions & 1 deletion app/ui/src/layouts/History/HistoryView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
historyAuthorWidth,
historyDateWidth,
detailsWidth,
detailsOrientation,
layoutRefs
} from '../../services/layoutService';
import { appendCommitGraph, createGraphState, GraphState } from '../../GraphBuilder';
Expand Down Expand Up @@ -526,7 +527,10 @@ async function onRemoveFilter(name: string) {
v-if="effectiveLogRefs.length"
:refs="effectiveLogRefs"
@remove="onRemoveFilter" />
<div class="flex flex-row flex-1 min-h-0 min-w-0 overflow-hidden">
<div
class="flex flex-1 min-h-0 min-w-0 overflow-hidden"
:class="detailsOrientation === 'bottom' ? 'flex-col' : 'flex-row'"
>
<div class="flex-1 min-w-0 flex flex-col min-h-0">
<HistoryCommitList
ref="commitListRef"
Expand Down
73 changes: 64 additions & 9 deletions app/ui/src/layouts/History/components/HistoryDetailPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import Tabs from '../../../components/Common/Tabs.vue';
import Tab from '../../../components/Common/Tab.vue';
import Tooltip from '../../../components/Common/Tooltip.vue';
import Resizer from '../../../components/Common/Resizer.vue';
import { detailsWidth, layoutRefs } from '../../../services/layoutService';
import { detailsOrientation, layoutRefs } from '../../../services/layoutService';
import { submodules } from '../../../services/gitService';
import { cn } from '../../../utils/cn';

const props = defineProps<{
selectedCommits: Commit[];
Expand Down Expand Up @@ -43,6 +44,12 @@ const emit = defineEmits<{
const { t } = useI18n();
const activeDetailTab = ref('information');

const isBottom = computed(() => detailsOrientation.value === 'bottom');

function toggleOrientation() {
detailsOrientation.value = isBottom.value ? 'right' : 'bottom';
}

// Let the parent drive the active tab (e.g. an AI file link jumps to "changes").
watch(() => props.requestedTab, (tab) => {
if (tab) activeDetailTab.value = tab;
Expand Down Expand Up @@ -76,9 +83,31 @@ function onSetTab(tab: string) {
</script>

<template>
<div class="flex flex-col bg-overlay border-l border-line flex-shrink-0 relative shadow-none dark:shadow-2xl z-10 min-h-0 overflow-hidden" :style="{ width: layoutRefs.detailsWidth.value + 'px' }">
<Resizer :target="layoutRefs.detailsWidth" :options="{ invert: true, min: 200, max: 1200, clampToContainer: true, reserve: 260 }" class="absolute left-0 top-0 bottom-0 -translate-x-1/2 z-30" />

<div
:class="cn(
'flex flex-col bg-overlay flex-shrink-0 relative shadow-none dark:shadow-2xl z-10 min-h-0 overflow-hidden',
isBottom
? 'w-full border-t border-neutral-200 dark:border-transparent'
: 'border-l border-neutral-200 dark:border-transparent',
)"
:style="isBottom
? { height: layoutRefs.detailsHeight.value + 'px' }
: { width: layoutRefs.detailsWidth.value + 'px' }"
>
<Resizer
v-if="isBottom"
vertical
:target="layoutRefs.detailsHeight"
:options="{ axis: 'y', invert: true, min: 160, max: 1200, clampToContainer: true, reserve: 160 }"
class="absolute top-0 left-0 right-0 -translate-y-1/2 z-30"
/>
<Resizer
v-else
:target="layoutRefs.detailsWidth"
:options="{ invert: true, min: 200, max: 1200, clampToContainer: true, reserve: 260 }"
class="absolute left-0 top-0 bottom-0 -translate-x-1/2 z-30"
/>

<!-- Top Title Bar like SourceGit: commit pinned left, explain as an icon
button on the right. Both flex so they adapt when the panel is resized. -->
<div v-if="selectedCommits.length === 1" class="flex-shrink-0 bg-surface border-b border-line flex items-center justify-between gap-2 px-3 h-[42px] min-w-0">
Expand All @@ -98,16 +127,42 @@ function onSetTab(tab: string) {
<div v-if="selectedCommits.length > 1" class="flex-shrink-0 bg-surface border-b border-line flex items-center justify-between gap-2 px-3 h-[42px] min-w-0">
<span class="text-xs text-content-strong font-bold truncate min-w-0">{{ t('history_detail.n_commits_selected', { count: selectedCommits.length }) }}</span>

<Tooltip :text="t('history_detail.explain_commits')" position="bottom">
<button @click="emit('explain')" class="h-7 w-7 flex items-center justify-center bg-accent hover:bg-accent-hover text-accent-fg rounded transition-colors outline-none shadow-sm shrink-0">
<Icon icon="lucide:sparkles" class="text-sm" />
</button>
</Tooltip>
<div class="flex items-center gap-1 shrink-0">
<Tooltip :text="isBottom ? t('history_detail.dock_right') : t('history_detail.dock_bottom')" position="bottom">
<button
@click="toggleOrientation"
class="h-7 w-7 flex items-center justify-center rounded text-content-muted hover:text-content-strong hover:bg-surface-hover transition-colors outline-none"
>
<Icon :icon="isBottom ? 'lucide:panel-right' : 'lucide:panel-bottom'" class="text-base" />
</button>
</Tooltip>
<Tooltip :text="t('history_detail.explain_commits')" position="bottom">
<button @click="emit('explain')" class="h-7 w-7 flex items-center justify-center bg-accent hover:bg-accent-hover text-accent-fg rounded transition-colors outline-none shadow-sm shrink-0">
<Icon icon="lucide:sparkles" class="text-sm" />
</button>
</Tooltip>
<Tooltip :text="t('history_detail.close_details')" position="bottom">
<button
@click="emit('close')"
class="h-7 w-7 flex items-center justify-center rounded text-content-muted hover:text-content-strong hover:bg-surface-hover transition-colors outline-none"
>
<Icon icon="lucide:x" class="text-base" />
</button>
</Tooltip>
</div>
</div>

<div v-if="selectedCommits.length === 1" class="flex-1 overflow-hidden flex flex-col">
<Tabs v-model="activeDetailTab" @change="emit('setTab', $event)">
<template #tab-actions>
<Tooltip :text="isBottom ? t('history_detail.dock_right') : t('history_detail.dock_bottom')" position="bottom">
<button
@click="toggleOrientation"
class="h-7 w-7 flex items-center justify-center rounded text-content-muted hover:text-content-strong hover:bg-surface-hover transition-colors outline-none"
>
<Icon :icon="isBottom ? 'lucide:panel-right' : 'lucide:panel-bottom'" class="text-base" />
</button>
</Tooltip>
<Tooltip :text="t('history_detail.close_details')" position="bottom">
<button @click="emit('close')"
class="h-7 w-7 flex items-center justify-center rounded text-content-muted hover:text-content-strong hover:bg-surface-hover transition-colors outline-none">
Expand Down
20 changes: 20 additions & 0 deletions app/ui/src/services/layoutService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,26 @@ function persistedSize(key: string, fallback: number, min = 60, max = 2000): Ref
return size;
}

export type DetailsOrientation = 'right' | 'bottom';

function persistedString<T extends string>(key: string, fallback: T, allowed: readonly T[]): Ref<T> {
const saved = getItem(LAYOUT_KEY + key);
const initial = (allowed.includes(saved as T) ? saved : fallback) as T;
const value = ref(initial) as Ref<T>;
watch(value, (v) => {
setItem(LAYOUT_KEY + key, v);
});
return value;
}

export const sidebarWidth = persistedSize('sidebarWidth', 256, 160);
export const detailsWidth = persistedSize('detailsWidth', 340, 200);
export const detailsHeight = persistedSize('detailsHeight', 320, 160);
export const detailsOrientation = persistedString<DetailsOrientation>(
'detailsOrientation',
'right',
['right', 'bottom'] as const,
);
export const statusWidth = persistedSize('statusWidth', 300, 160);
export const unstagedHeight = persistedSize('unstagedHeight', 300, 100);
export const stashFilesHeight = persistedSize('stashFilesHeight', 300, 100);
Expand All @@ -56,6 +74,8 @@ export const submoduleDetailHeight = persistedSize('submoduleDetailHeight', 400,
export const layoutRefs = {
sidebarWidth,
detailsWidth,
detailsHeight,
detailsOrientation,
statusWidth,
unstagedHeight,
stashFilesHeight,
Expand Down
Loading