feat(notes): notes get their own list, a day log and an Enter chain

The editor was reachable from one button that only appeared in My Day,
which is how it got forgotten. It is now a permanent sidebar entry next
to My Day and Planned, and it fills the tasks island rather than the
details pane, because a note list is content.

Inside, the date picker is gone: today sits at the top with the capture
box, older days follow as dated groups. Enter in a note saves it and
opens the next one, Shift+Enter breaks the line so notes can be several,
Backspace on an empty note deletes it and steps back up. Deleting also
has a visible hover button instead of only "clear the text and hope".
Agent-written notes carry a marker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
CubeGameLP
2026-08-28 21:19:37 +02:00
co-authored by Claude Opus 5
parent ae0c7d3771
commit 0f04b796f4
34 changed files with 533 additions and 192 deletions
@@ -125,17 +125,12 @@
DragCompleted="OnSplitterDragCompleted"/>
</Grid>
<!-- Empty state: no task selected, outside Notes/Prep mode -->
<!-- Empty state: no task selected, outside Prep mode -->
<TextBlock IsVisible="{Binding IsEmptyStateVisible}"
Text="{loc:Tr details.emptyState}"
Foreground="{DynamicResource TextMuteBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Notes mode -->
<Panel IsVisible="{Binding IsNotesMode}">
<islands:NotesEditorView DataContext="{Binding Notes}"/>
</Panel>
<!-- Daily-prep mode -->
<Panel IsVisible="{Binding IsPrepMode}">
<DockPanel>
@@ -1,38 +1,110 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.NotesEditorView"
x:DataType="vm:NotesEditorViewModel">
<DockPanel Margin="16">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8">
<Button Classes="btn" Content="&#x2039;" Command="{Binding PrevDayCommand}"/>
<ctl:ThemedDatePicker SelectedDate="{Binding CurrentDate, Mode=TwoWay}"/>
<Button Classes="btn" Content="&#x203A;" Command="{Binding NextDayCommand}"/>
<Button Classes="btn" Content="{loc:Tr notes.today}" Command="{Binding TodayCommand}"/>
<TextBlock Classes="meta" VerticalAlignment="Center" Text="{Binding CurrentDayLabel}"/>
</StackPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,12,0,8">
<Button DockPanel.Dock="Right" Classes="btn" Content="{loc:Tr notes.add}" Margin="8,0,0,0"
Command="{Binding AddBulletCommand}"/>
<TextBox PlaceholderText="{loc:Tr notes.newNotePlaceholder}" Text="{Binding NewBulletText}">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddBulletCommand}"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
<UserControl.Styles>
<!-- A note reads as text, not as a form field: the box only shows itself under the pointer
and while it has focus. -->
<Style Selector="TextBox.note">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="4,3"/>
<Setter Property="MinHeight" Value="0"/>
<Setter Property="AcceptsReturn" Value="True"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
<Style Selector="TextBox.note /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Bullets}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:NoteBulletViewModel">
<TextBox Text="{Binding Text}" Margin="0,2"
LostFocus="OnBulletLostFocus"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Style Selector="Border.note-row">
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="10,1,6,1"/>
<Setter Property="Margin" Value="4,1"/>
</Style>
<Style Selector="Border.note-row:pointerover">
<Setter Property="Background" Value="{DynamicResource PeatSoftBrush}"/>
</Style>
<!-- Row actions stay out of the way until the row is hovered. -->
<Style Selector="Border.note-row Button.note-del">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="Border.note-row:pointerover Button.note-del">
<Setter Property="Opacity" Value="1"/>
</Style>
</UserControl.Styles>
<DockPanel LastChildFill="True">
<!-- Today's capture box: Enter files the note and leaves the caret here for the next one. -->
<Border DockPanel.Dock="Top" Classes="add-task" Margin="16,14,16,10">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="add-task-plus" VerticalAlignment="Center">
<PathIcon Width="12" Height="12" Data="{StaticResource Icon.Plus}"
Foreground="{DynamicResource TextFaintBrush}"/>
</Border>
<TextBox Grid.Column="1" x:Name="NewNoteBox" Classes="add-task-input"
PlaceholderText="{loc:Tr notes.newNotePlaceholder}"
Text="{Binding NewBulletText, Mode=TwoWay}"
VerticalAlignment="Center" Margin="12,0,0,0">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddBulletCommand}"/>
</TextBox.KeyBindings>
</TextBox>
<Border Grid.Column="2" Classes="kbd kbd-enter" VerticalAlignment="Center"
IsVisible="{Binding #NewNoteBox.IsFocused}">
<TextBlock Text="{loc:Tr tasks.enterKey}"/>
</Border>
</Grid>
</Border>
<Panel>
<ScrollViewer>
<ItemsControl x:Name="RowsHost" ItemsSource="{Binding Rows}" Margin="10,0,10,12">
<ItemsControl.DataTemplates>
<DataTemplate DataType="vm:NoteDayHeaderViewModel">
<TextBlock Classes="eyebrow section-label" Margin="14,12,14,4" Text="{Binding Label}"/>
</DataTemplate>
<DataTemplate DataType="vm:NoteBulletViewModel">
<Border Classes="note-row">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<Ellipse Grid.Column="0" Width="4" Height="4" Margin="0,0,10,0"
VerticalAlignment="Center"
Fill="{DynamicResource TextFaintBrush}"/>
<TextBox Grid.Column="1" Classes="note" Text="{Binding Text, Mode=TwoWay}"
LostFocus="OnBulletLostFocus" KeyDown="OnBulletKeyDown"/>
<PathIcon Grid.Column="2" Width="12" Height="12" Margin="6,0"
VerticalAlignment="Center"
IsVisible="{Binding FromAgent}"
ToolTip.Tip="{loc:Tr notes.fromAgentTip}"
Data="{StaticResource Icon.AgentSuggested}"
Foreground="{DynamicResource TextFaintBrush}"/>
<Button Grid.Column="3" Classes="icon-btn note-del"
VerticalAlignment="Center"
ToolTip.Tip="{loc:Tr notes.deleteTip}"
Click="OnDeleteBulletClick">
<PathIcon Classes="danger" Data="{StaticResource Icon.Trash}" Width="12" Height="12"/>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</ScrollViewer>
<TextBlock IsVisible="{Binding IsEmpty}"
Text="{loc:Tr notes.emptyHint}"
Foreground="{DynamicResource TextMuteBrush}"
MaxWidth="260" TextAlignment="Center" TextWrapping="Wrap"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
</Panel>
</DockPanel>
</UserControl>
@@ -1,12 +1,53 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.Islands;
public partial class NotesEditorView : UserControl
{
public NotesEditorView() => InitializeComponent();
private NotesEditorViewModel? _vm;
public NotesEditorView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_vm is not null) _vm.FocusRequested -= FocusBullet;
_vm = DataContext as NotesEditorViewModel;
if (_vm is not null) _vm.FocusRequested += FocusBullet;
}
/// <summary>Rows the VM just inserted have no container yet — wait for the layout pass that
/// creates it before reaching for the TextBox inside.</summary>
private void FocusBullet(NoteBulletViewModel bullet) =>
Dispatcher.UIThread.Post(
() => RowsHost.ContainerFromItem(bullet)?.FindDescendantOfType<TextBox>()?.Focus(),
DispatcherPriority.Loaded);
private void OnBulletKeyDown(object? sender, KeyEventArgs e)
{
if (sender is not TextBox { DataContext: NoteBulletViewModel bullet } box
|| DataContext is not NotesEditorViewModel vm) return;
// Shift+Enter falls through to the TextBox and breaks the line — a note can be several.
if (e.Key == Key.Enter && !e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
e.Handled = true;
vm.SplitBulletCommand.Execute(bullet);
}
else if (e.Key == Key.Back && string.IsNullOrEmpty(box.Text))
{
e.Handled = true;
vm.DeleteBulletCommand.Execute(bullet);
}
}
private void OnBulletLostFocus(object? sender, RoutedEventArgs e)
{
@@ -15,4 +56,11 @@ public partial class NotesEditorView : UserControl
&& vm.CommitBulletCommand.CanExecute(bullet))
vm.CommitBulletCommand.Execute(bullet);
}
private void OnDeleteBulletClick(object? sender, RoutedEventArgs e)
{
if (sender is Button { DataContext: NoteBulletViewModel bullet }
&& DataContext is NotesEditorViewModel vm)
vm.DeleteBulletCommand.Execute(bullet);
}
}
@@ -68,7 +68,8 @@
</Border>
<!-- Add-task row -->
<Border DockPanel.Dock="Top" Classes="add-task" Margin="16,14,16,8">
<Border DockPanel.Dock="Top" Classes="add-task" Margin="16,14,16,8"
IsVisible="{Binding !IsNotesList}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="add-task-plus" VerticalAlignment="Center">
<PathIcon Width="12" Height="12" Data="{StaticResource Icon.Plus}"
@@ -90,14 +91,6 @@
</Grid>
</Border>
<!-- Notes pinned row (My Day only) -->
<Button DockPanel.Dock="Top"
Classes="btn" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Margin="16,0,16,8"
IsVisible="{Binding ShowNotesRow}"
Command="{Binding OpenNotesCommand}"
Content="{loc:Tr tasks.notesPinnedRow}"/>
<!-- Task list: one flat, virtualized ListBox. Rows is HeaderRow | TaskRowViewModel — group
headers are regular entries, not a separate ItemsControl per section, so a
VirtualizingStackPanel actually bounds the realized container count.
@@ -106,6 +99,7 @@
unreachable. -->
<Panel>
<ListBox x:Name="RowsListBox"
IsVisible="{Binding !IsNotesList}"
ItemsSource="{Binding Rows}"
Background="Transparent"
BorderThickness="0">
@@ -172,6 +166,11 @@
Foreground="{DynamicResource TextMuteBrush}"
TextAlignment="Center" TextWrapping="Wrap"/>
</StackPanel>
<!-- The notes list is not a task list: same island slot, different content. -->
<Panel IsVisible="{Binding IsNotesList}">
<islands:NotesEditorView DataContext="{Binding Notes}"/>
</Panel>
</Panel>
</DockPanel>