feat(ui): chromeless three-island shell

This commit is contained in:
mika kuns
2026-04-20 10:17:20 +02:00
parent 8909119d1b
commit 05404f46f2
9 changed files with 97 additions and 159 deletions

View File

@@ -22,7 +22,7 @@ public partial class App : Application
{ {
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
{ {
DataContext = Services.GetRequiredService<MainWindowViewModel>(), DataContext = Services.GetRequiredService<IslandsShellViewModel>(),
}; };
} }

View File

@@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.DetailsIslandView"
x:DataType="vm:DetailsIslandViewModel">
<TextBlock Margin="14" Text="Details (placeholder)"
Foreground="{DynamicResource TextDimBrush}"/>
</UserControl>

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.Islands;
public partial class DetailsIslandView : UserControl
{
public DetailsIslandView() { InitializeComponent(); }
}

View File

@@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
x:DataType="vm:ListsIslandViewModel">
<TextBlock Margin="14" Text="Lists (placeholder)"
Foreground="{DynamicResource TextDimBrush}"/>
</UserControl>

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.Islands;
public partial class ListsIslandView : UserControl
{
public ListsIslandView() { InitializeComponent(); }
}

View File

@@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.TasksIslandView"
x:DataType="vm:TasksIslandViewModel">
<TextBlock Margin="14" Text="Tasks (placeholder)"
Foreground="{DynamicResource TextDimBrush}"/>
</UserControl>

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.Islands;
public partial class TasksIslandView : UserControl
{
public TasksIslandView() { InitializeComponent(); }
}

View File

@@ -1,104 +1,42 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels" xmlns:vm="using:ClaudeDo.Ui.ViewModels"
xmlns:v="using:ClaudeDo.Ui.Views" xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="700"
x:Class="ClaudeDo.Ui.Views.MainWindow" x:Class="ClaudeDo.Ui.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:IslandsShellViewModel"
Title="ClaudeDo" Title="ClaudeDo"
Icon="avares://ClaudeDo.App/Assets/ClaudeTask.ico" Width="1280" Height="820" MinWidth="780" MinHeight="600"
MinWidth="800" MinHeight="500" Background="{DynamicResource VoidBrush}"
KeyDown="OnGlobalKeyDown"> SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True"
<DockPanel Background="{StaticResource WindowBgBrush}"> ExtendClientAreaTitleBarHeightHint="-1">
<v:StatusBarView DockPanel.Dock="Bottom" DataContext="{Binding StatusBar}" /> <Grid RowDefinitions="36,*">
<!-- Custom title bar -->
<Grid ColumnDefinitions="1*,2*,1.5*" Margin="8,8,8,0"> <Border Grid.Row="0" Background="{DynamicResource DeepBrush}" PointerPressed="OnTitleBarPressed">
<Grid ColumnDefinitions="*,Auto">
<!-- Lists island --> <TextBlock Grid.Column="0" Margin="14,0" VerticalAlignment="Center"
<Border Grid.Column="0" CornerRadius="12" Background="{StaticResource IslandBgBrush}" FontFamily="{DynamicResource SansFamily}" FontSize="12"
MinWidth="180" Margin="0,0,4,8" ClipToBounds="True"> Foreground="{DynamicResource TextDimBrush}" Text="ClaudeDo"/>
<DockPanel> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="0">
<TextBlock DockPanel.Dock="Top" <Button Classes="title-btn" Click="OnMinimize" Content="—"/>
Text="Lists" FontWeight="SemiBold" FontSize="13" <Button Classes="title-btn" Click="OnToggleMax" Content="▢"/>
Foreground="{StaticResource TextSecondaryBrush}" <Button Classes="title-btn close" Click="OnClose" Content="✕"/>
Margin="16,14,16,10"/>
<Border DockPanel.Dock="Bottom" Padding="8,8"
BorderThickness="0,1,0,0" BorderBrush="{StaticResource BorderSubtleBrush}">
<Button Content="+ New List"
Command="{Binding AddListCommand}"
Background="Transparent"
Foreground="{StaticResource AccentBrush}"
BorderThickness="0"
Padding="12,8"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
FontSize="13"
Cursor="Hand"/>
</Border>
<ListBox x:Name="ListsBox"
ItemsSource="{Binding Lists}"
SelectedItem="{Binding SelectedList}"
Background="Transparent"
Margin="4,0">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ListItemViewModel">
<Grid ColumnDefinitions="Auto,*" Margin="8,6"
Background="Transparent"
DoubleTapped="OnListItemDoubleTapped"
PointerPressed="OnListItemPointerPressed">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).EditListCommand}"/>
<MenuItem Header="Delete"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DeleteListCommand}"/>
<Separator/>
<MenuItem Header="New Task"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).TaskList.AddTaskCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<Ellipse Grid.Column="0" Width="8" Height="8"
Fill="{Binding DotBrush}"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Name}" FontWeight="Medium"
Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Text="{Binding WorkingDir}" FontSize="10"
Foreground="{StaticResource TextDimBrush}"
IsVisible="{Binding WorkingDir, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border> </Border>
<!-- Tasks island --> <!-- Three islands -->
<Border Grid.Column="1" CornerRadius="12" Background="{StaticResource IslandBgBrush}" <Grid Grid.Row="1" Margin="7" ColumnDefinitions="260,*,320">
Margin="4,0,4,8" ClipToBounds="True"> <Border Grid.Column="0" Classes="island" Margin="7">
<DockPanel> <islands:ListsIslandView DataContext="{Binding Lists}"/>
<TextBlock DockPanel.Dock="Top"
Text="{Binding TaskList.ListName, FallbackValue='Tasks'}"
FontWeight="SemiBold" FontSize="16"
Foreground="{StaticResource TextPrimaryBrush}"
Margin="16,14,16,10"/>
<v:TaskListView DataContext="{Binding TaskList}" />
</DockPanel>
</Border> </Border>
<Border Grid.Column="1" Classes="island" Margin="7">
<!-- Detail island --> <islands:TasksIslandView DataContext="{Binding Tasks}"/>
<Border Grid.Column="2" CornerRadius="12" Background="{StaticResource IslandBgBrush}" </Border>
MinWidth="280" Margin="4,0,0,8" ClipToBounds="True"> <Border Grid.Column="2" Classes="island" Margin="7"
<v:TaskDetailView DataContext="{Binding TaskDetail}" /> IsVisible="{Binding ShowDetails}">
<islands:DetailsIslandView DataContext="{Binding Details}"/>
</Border> </Border>
</Grid> </Grid>
</DockPanel> </Grid>
</Window> </Window>

View File

@@ -1,75 +1,27 @@
using System;
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views; namespace ClaudeDo.Ui.Views;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
public MainWindow() public MainWindow() { InitializeComponent(); }
{
InitializeComponent();
}
protected override async void OnOpened(EventArgs e) private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{ {
base.OnOpened(e); if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (DataContext is MainWindowViewModel vm) BeginMoveDrag(e);
await vm.InitializeAsync();
} }
private void OnMinimize(object? s, Avalonia.Interactivity.RoutedEventArgs e) =>
WindowState = WindowState.Minimized;
private void OnToggleMax(object? s, Avalonia.Interactivity.RoutedEventArgs e) =>
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
private void OnClose(object? s, Avalonia.Interactivity.RoutedEventArgs e) => Close();
private void OnGlobalKeyDown(object? sender, KeyEventArgs e) protected override void OnSizeChanged(SizeChangedEventArgs e)
{ {
if (DataContext is not MainWindowViewModel vm) return; base.OnSizeChanged(e);
if (DataContext is IslandsShellViewModel vm) vm.WindowWidth = Bounds.Width;
var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (ctrl && shift && e.Key == Key.N)
{
vm.AddListCommand.Execute(null);
e.Handled = true;
}
else if (ctrl && e.Key == Key.N)
{
var taskListView = this.GetVisualDescendants().OfType<TaskListView>().FirstOrDefault();
taskListView?.FocusInlineAdd();
e.Handled = true;
}
else if (ctrl && e.Key == Key.L)
{
this.FindControl<ListBox>("ListsBox")?.Focus();
e.Handled = true;
}
else if (ctrl && e.Key == Key.R)
{
if (vm.TaskList.SelectedTask is { } task)
{
task.RunNowCommand.Execute(null);
e.Handled = true;
}
}
}
private void OnListItemDoubleTapped(object? sender, TappedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
vm.EditListCommand.Execute(null);
}
private void OnListItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
var props = e.GetCurrentPoint(this).Properties;
if (!props.IsRightButtonPressed) return;
if (sender is Grid { DataContext: ListItemViewModel item }
&& DataContext is MainWindowViewModel vm)
{
vm.SelectedList = item;
}
} }
} }