공개: alpha.4 기준선 갱신
This commit is contained in:
parent
debf62f76e
commit
b63832706b
37 changed files with 1839 additions and 822 deletions
|
|
@ -26,9 +26,22 @@ public partial class App : Application
|
|||
DataContext = viewModel,
|
||||
};
|
||||
|
||||
_ = viewModel.InitializeAsync();
|
||||
_ = InitializeDesktopAsync(viewModel);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private static async Task InitializeDesktopAsync(MainWindowViewModel viewModel)
|
||||
{
|
||||
await viewModel.InitializeAsync();
|
||||
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("KOTALK_DESKTOP_OPEN_SAMPLE_WINDOW"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
await viewModel.OpenDetachedConversationFromShortcutAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ namespace PhysOn.Desktop.Models;
|
|||
public sealed record DesktopWorkspaceLayout(
|
||||
bool IsCompactDensity,
|
||||
bool IsInspectorVisible,
|
||||
bool IsConversationPaneCollapsed);
|
||||
bool IsConversationPaneCollapsed,
|
||||
double ConversationPaneWidth = 348);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ public partial class ConversationWindowViewModel : ViewModelBase, IAsyncDisposab
|
|||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("KOTALK_DESKTOP_SAMPLE_MODE"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
LoadSampleConversation();
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadMessagesAsync();
|
||||
|
||||
try
|
||||
|
|
@ -78,6 +84,47 @@ public partial class ConversationWindowViewModel : ViewModelBase, IAsyncDisposab
|
|||
partial void OnErrorTextChanged(string? value) => OnPropertyChanged(nameof(HasErrorText));
|
||||
partial void OnConversationTitleChanged(string value) => OnPropertyChanged(nameof(ConversationGlyph));
|
||||
|
||||
private void LoadSampleConversation()
|
||||
{
|
||||
Messages.Clear();
|
||||
StatusText = "●";
|
||||
ErrorText = null;
|
||||
|
||||
foreach (var item in new[]
|
||||
{
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "detached-1",
|
||||
SenderName = "민지",
|
||||
Text = "이 창은 대화를 따로 두고 확인할 수 있게 분리했습니다.",
|
||||
MetaText = "09:10",
|
||||
IsMine = false,
|
||||
ServerSequence = 1
|
||||
},
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "detached-2",
|
||||
SenderName = _launchContext.DisplayName,
|
||||
Text = "검수하면서도 메인 받은함은 그대로 둘 수 있어요.",
|
||||
MetaText = "09:11",
|
||||
IsMine = true,
|
||||
ServerSequence = 2
|
||||
},
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "detached-3",
|
||||
SenderName = "민지",
|
||||
Text = "작업용 대화만 따로 띄워두기엔 이 구성이 훨씬 낫네요.",
|
||||
MetaText = "09:12",
|
||||
IsMine = false,
|
||||
ServerSequence = 3
|
||||
}
|
||||
})
|
||||
{
|
||||
Messages.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadMessagesAsync()
|
||||
{
|
||||
if (IsBusy)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
ShowAllConversationsCommand = new RelayCommand(() => SelectedListFilter = "all");
|
||||
ShowUnreadConversationsCommand = new RelayCommand(() => SelectedListFilter = "unread");
|
||||
ShowPinnedConversationsCommand = new RelayCommand(() => SelectedListFilter = "pinned");
|
||||
ApplyAckDraftCommand = new RelayCommand(() => ApplyQuickDraft("확인했습니다."));
|
||||
ApplyShareDraftCommand = new RelayCommand(() => ApplyQuickDraft("공유드립니다.\n- "));
|
||||
ApplyTaskDraftCommand = new RelayCommand(() => ApplyQuickDraft("할 일\n- "));
|
||||
ToggleCompactModeCommand = new RelayCommand(() => IsCompactDensity = !IsCompactDensity);
|
||||
ToggleInspectorCommand = new RelayCommand(() => IsInspectorVisible = !IsInspectorVisible);
|
||||
ToggleConversationPaneCommand = new RelayCommand(() => IsConversationPaneCollapsed = !IsConversationPaneCollapsed);
|
||||
|
|
@ -80,6 +83,9 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
public IRelayCommand ShowAllConversationsCommand { get; }
|
||||
public IRelayCommand ShowUnreadConversationsCommand { get; }
|
||||
public IRelayCommand ShowPinnedConversationsCommand { get; }
|
||||
public IRelayCommand ApplyAckDraftCommand { get; }
|
||||
public IRelayCommand ApplyShareDraftCommand { get; }
|
||||
public IRelayCommand ApplyTaskDraftCommand { get; }
|
||||
public IRelayCommand ToggleCompactModeCommand { get; }
|
||||
public IRelayCommand ToggleInspectorCommand { get; }
|
||||
public IRelayCommand ToggleConversationPaneCommand { get; }
|
||||
|
|
@ -102,7 +108,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
[ObservableProperty] private string selectedListFilter = "all";
|
||||
[ObservableProperty] private string composerText = string.Empty;
|
||||
[ObservableProperty] private string selectedConversationTitle = "KoTalk";
|
||||
[ObservableProperty] private string selectedConversationSubtitle = "대화를 열면 바로 이어집니다.";
|
||||
[ObservableProperty] private string selectedConversationSubtitle = "준비";
|
||||
[ObservableProperty] private ConversationRowViewModel? selectedConversation;
|
||||
[ObservableProperty] private bool hasErrorText;
|
||||
[ObservableProperty] private bool hasFilteredConversations;
|
||||
|
|
@ -110,6 +116,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
[ObservableProperty] private bool isCompactDensity = true;
|
||||
[ObservableProperty] private bool isInspectorVisible;
|
||||
[ObservableProperty] private bool isConversationPaneCollapsed;
|
||||
[ObservableProperty] private double conversationPaneWidthValue = 348;
|
||||
[ObservableProperty] private int detachedWindowCount;
|
||||
|
||||
public bool ShowOnboarding => !IsAuthenticated;
|
||||
|
|
@ -153,26 +160,26 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
public string DetachedWindowActionGlyph => HasDetachedWindows ? DetachedWindowBadgeText : "↗";
|
||||
public bool HasDetachedWindows => DetachedWindowCount > 0;
|
||||
public bool IsConversationPaneExpanded => !IsConversationPaneCollapsed;
|
||||
public double ConversationPaneWidth => IsConversationPaneCollapsed ? 0 : (IsCompactDensity ? 296 : 340);
|
||||
public double ConversationPaneWidth => IsConversationPaneCollapsed ? 0 : ConversationPaneWidthValue;
|
||||
public double InspectorPaneWidth => IsInspectorVisible ? (IsCompactDensity ? 92 : 108) : 0;
|
||||
public Thickness ConversationRowPadding => IsCompactDensity ? new Thickness(6, 6) : new Thickness(8, 7);
|
||||
public Thickness MessageBubblePadding => IsCompactDensity ? new Thickness(10, 8) : new Thickness(12, 10);
|
||||
public double ConversationAvatarSize => IsCompactDensity ? 26 : 30;
|
||||
public Thickness ConversationRowPadding => IsCompactDensity ? new Thickness(6, 5) : new Thickness(8, 6);
|
||||
public Thickness MessageBubblePadding => IsCompactDensity ? new Thickness(10, 7) : new Thickness(12, 9);
|
||||
public double ConversationAvatarSize => IsCompactDensity ? 28 : 32;
|
||||
public double ComposerMinHeight => IsCompactDensity ? 48 : 58;
|
||||
public string ComposerCounterText => $"{ComposerText.Trim().Length}";
|
||||
public string SearchWatermark => "대화 검색";
|
||||
public string SearchWatermark => "검색";
|
||||
public string InspectorStatusText => HasDetachedWindows
|
||||
? $"{RealtimeStatusGlyph} {DetachedWindowBadgeText}"
|
||||
: RealtimeStatusGlyph;
|
||||
public string WorkspaceModeText => HasDetachedWindows ? $"분리 창 {DetachedWindowBadgeText}" : "단일 창";
|
||||
public string StatusSummaryText => string.IsNullOrWhiteSpace(StatusLine) ? RealtimeStatusText : StatusLine;
|
||||
public string ComposerPlaceholderText => HasSelectedConversation ? "메시지 보내기" : "왼쪽에서 대화를 고르세요";
|
||||
public string ComposerPlaceholderText => HasSelectedConversation ? "메시지" : "대화 선택";
|
||||
public string ComposerActionText => Messages.Count == 0 ? "시작" : "보내기";
|
||||
public bool ShowMessageEmptyState => Messages.Count == 0;
|
||||
public string MessageEmptyStateTitle => HasSelectedConversation ? "첫 메시지부터 시작" : "대화를 먼저 고르세요";
|
||||
public string MessageEmptyStateTitle => HasSelectedConversation ? "첫 메시지" : "대화 선택";
|
||||
public string MessageEmptyStateText => HasSelectedConversation
|
||||
? "짧게 한 줄만 남겨도 바로 이어집니다."
|
||||
: "받은함에서 대화를 고르거나 창으로 분리해 집중할 수 있습니다.";
|
||||
? "짧게 남기세요."
|
||||
: "목록에서 선택";
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
|
|
@ -270,6 +277,11 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
OnPropertyChanged(nameof(InspectorPaneWidth));
|
||||
_ = PersistWorkspaceLayoutAsync();
|
||||
}
|
||||
partial void OnConversationPaneWidthValueChanged(double value)
|
||||
{
|
||||
OnPropertyChanged(nameof(ConversationPaneWidth));
|
||||
_ = PersistWorkspaceLayoutAsync();
|
||||
}
|
||||
partial void OnIsConversationPaneCollapsedChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(ConversationPaneGlyph));
|
||||
|
|
@ -292,7 +304,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
UpdateSelectedConversationState(value?.ConversationId);
|
||||
SelectedConversationTitle = value?.Title ?? "KoTalk";
|
||||
SelectedConversationSubtitle = value?.Subtitle ?? "받은함과 대화를 한 화면에서 관리합니다.";
|
||||
SelectedConversationSubtitle = value?.Subtitle ?? "대화";
|
||||
OnPropertyChanged(nameof(SelectedConversationGlyph));
|
||||
OnPropertyChanged(nameof(HasSelectedConversation));
|
||||
OnPropertyChanged(nameof(HasSelectedConversationUnread));
|
||||
|
|
@ -317,7 +329,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
$"desktop-{Environment.MachineName.ToLowerInvariant()}",
|
||||
"windows",
|
||||
Environment.MachineName,
|
||||
"0.1.0"));
|
||||
"0.1.0-alpha.4"));
|
||||
|
||||
var response = await _apiClient.RegisterAlphaQuickAsync(apiBaseUrl, request, CancellationToken.None);
|
||||
ApiBaseUrl = apiBaseUrl;
|
||||
|
|
@ -370,7 +382,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
SelectedListFilter = "all";
|
||||
SelectedConversation = null;
|
||||
SelectedConversationTitle = "KoTalk";
|
||||
SelectedConversationSubtitle = "대화를 열면 바로 이어집니다.";
|
||||
SelectedConversationSubtitle = "준비";
|
||||
NotifyConversationMetricsChanged();
|
||||
}
|
||||
|
||||
|
|
@ -381,10 +393,18 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
Messages.Clear();
|
||||
NotifyMessageStateChanged();
|
||||
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
var items = await _apiClient.GetMessagesAsync(_session.ApiBaseUrl, _session.AccessToken, value.ConversationId, CancellationToken.None);
|
||||
|
||||
if (!string.Equals(SelectedConversation?.ConversationId, value.ConversationId, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Messages.Clear();
|
||||
foreach (var item in items.Items)
|
||||
{
|
||||
|
|
@ -667,6 +687,27 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
OnPropertyChanged(nameof(PinnedConversationCount));
|
||||
}
|
||||
|
||||
public void UpdateConversationPaneWidth(double width)
|
||||
{
|
||||
if (IsConversationPaneCollapsed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var clamped = Math.Clamp(Math.Round(width), 280, 480);
|
||||
if (Math.Abs(clamped - ConversationPaneWidthValue) > 1)
|
||||
{
|
||||
ConversationPaneWidthValue = clamped;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyQuickDraft(string template)
|
||||
{
|
||||
ComposerText = string.IsNullOrWhiteSpace(ComposerText)
|
||||
? template
|
||||
: ComposerText.TrimEnd() + Environment.NewLine + template;
|
||||
}
|
||||
|
||||
private void LoadSampleWorkspace()
|
||||
{
|
||||
Conversations.Clear();
|
||||
|
|
@ -676,6 +717,13 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
CurrentUserDisplayName = "이안";
|
||||
DisplayName = "이안";
|
||||
InviteCode = string.Empty;
|
||||
_currentUserId = "sample-user";
|
||||
_session = new DesktopSession(
|
||||
DefaultApiBaseUrl,
|
||||
"sample-access",
|
||||
"sample-refresh",
|
||||
CurrentUserDisplayName,
|
||||
"sample-ops");
|
||||
RealtimeState = RealtimeConnectionState.Connected;
|
||||
RealtimeStatusText = "연결됨";
|
||||
StatusLine = "준비";
|
||||
|
|
@ -683,6 +731,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
IsCompactDensity = true;
|
||||
IsInspectorVisible = false;
|
||||
IsConversationPaneCollapsed = false;
|
||||
ConversationPaneWidthValue = 348;
|
||||
DetachedWindowCount = 1;
|
||||
ErrorText = null;
|
||||
|
||||
|
|
@ -691,8 +740,8 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
ConversationId = "sample-ops",
|
||||
Title = "제품 운영",
|
||||
Subtitle = "스크린샷 기준으로 레이아웃을 다시 정리했습니다.",
|
||||
LastMessageText = "스크린샷 기준으로 레이아웃을 다시 정리했습니다.",
|
||||
Subtitle = "레이아웃 검수 메모를 확인해 주세요.",
|
||||
LastMessageText = "레이아웃 검수 메모를 확인해 주세요.",
|
||||
MetaText = FormatConversationMeta(now.AddMinutes(-5), 2),
|
||||
UnreadCount = 2,
|
||||
IsPinned = true,
|
||||
|
|
@ -703,8 +752,8 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
ConversationId = "sample-review",
|
||||
Title = "디자인 리뷰",
|
||||
Subtitle = "오후 2시에 검수 포인트만 다시 볼게요.",
|
||||
LastMessageText = "오후 2시에 검수 포인트만 다시 볼게요.",
|
||||
Subtitle = "오후 2시에 포인트만 다시 볼게요.",
|
||||
LastMessageText = "오후 2시에 포인트만 다시 볼게요.",
|
||||
MetaText = FormatConversationMeta(now.AddMinutes(-22), 0),
|
||||
UnreadCount = 0,
|
||||
IsPinned = false,
|
||||
|
|
@ -715,14 +764,38 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
ConversationId = "sample-friends",
|
||||
Title = "주말 약속",
|
||||
Subtitle = "토요일 브런치 장소만 정하면 끝.",
|
||||
LastMessageText = "토요일 브런치 장소만 정하면 끝.",
|
||||
Subtitle = "브런치 장소만 정하면 끝.",
|
||||
LastMessageText = "브런치 장소만 정하면 끝.",
|
||||
MetaText = FormatConversationMeta(now.AddMinutes(-54), 0),
|
||||
UnreadCount = 0,
|
||||
IsPinned = false,
|
||||
LastReadSequence = 3,
|
||||
SortKey = now.AddMinutes(-54)
|
||||
});
|
||||
Conversations.Add(new ConversationRowViewModel
|
||||
{
|
||||
ConversationId = "sample-team",
|
||||
Title = "운영 팀",
|
||||
Subtitle = "오후 공유본만 마지막으로 확인해 주세요.",
|
||||
LastMessageText = "오후 공유본만 마지막으로 확인해 주세요.",
|
||||
MetaText = FormatConversationMeta(now.AddHours(-2), 1),
|
||||
UnreadCount = 1,
|
||||
IsPinned = false,
|
||||
LastReadSequence = 7,
|
||||
SortKey = now.AddHours(-2)
|
||||
});
|
||||
Conversations.Add(new ConversationRowViewModel
|
||||
{
|
||||
ConversationId = "sample-files",
|
||||
Title = "자료 모음",
|
||||
Subtitle = "최신 캡처와 빌드 경로를 정리해 두었습니다.",
|
||||
LastMessageText = "최신 캡처와 빌드 경로를 정리해 두었습니다.",
|
||||
MetaText = FormatConversationMeta(now.AddHours(-5), 0),
|
||||
UnreadCount = 0,
|
||||
IsPinned = true,
|
||||
LastReadSequence = 9,
|
||||
SortKey = now.AddHours(-5)
|
||||
});
|
||||
|
||||
NotifyConversationMetricsChanged();
|
||||
RefreshConversationFilter("sample-ops");
|
||||
|
|
@ -735,7 +808,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
MessageId = "sample-msg-1",
|
||||
SenderName = "민지",
|
||||
Text = "회의 전에 이슈만 짧게 정리해 주세요.",
|
||||
Text = "회의 전에 레이아웃 이슈만 짧게 정리해 주세요.",
|
||||
MetaText = "08:54",
|
||||
IsMine = false,
|
||||
ServerSequence = 13
|
||||
|
|
@ -744,7 +817,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
MessageId = "sample-msg-2",
|
||||
SenderName = "이안",
|
||||
Text = "레이아웃 구조를 다시 줄였습니다. 우측 빈 패널도 없앴어요.",
|
||||
Text = "리스트 폭을 다시 줄이고 우측 빈 패널도 없앴어요.",
|
||||
MetaText = "08:56",
|
||||
IsMine = true,
|
||||
ServerSequence = 14
|
||||
|
|
@ -753,7 +826,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
MessageId = "sample-msg-3",
|
||||
SenderName = "민지",
|
||||
Text = "좋아요. 지금 화면이면 바로 검수할 수 있겠네요.",
|
||||
Text = "좋아요. 지금 화면이면 검수하기 좋겠네요.",
|
||||
MetaText = "08:58",
|
||||
IsMine = false,
|
||||
ServerSequence = 15
|
||||
|
|
@ -762,7 +835,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
MessageId = "sample-msg-4",
|
||||
SenderName = "이안",
|
||||
Text = "스크린샷 기준으로 레이아웃도 바로 수정했습니다.",
|
||||
Text = "스크린샷 기준으로 밀도도 같이 맞췄습니다.",
|
||||
MetaText = "09:05",
|
||||
IsMine = true,
|
||||
ServerSequence = 16
|
||||
|
|
@ -771,7 +844,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
MessageId = "sample-msg-5",
|
||||
SenderName = "민지",
|
||||
Text = "좋아요. 바로 확인 가능한 흐름으로 정리됐어요.",
|
||||
Text = "좋아요. 확인 흐름이 더 짧아졌어요.",
|
||||
MetaText = "09:06",
|
||||
IsMine = false,
|
||||
ServerSequence = 17
|
||||
|
|
@ -780,7 +853,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
MessageId = "sample-msg-6",
|
||||
SenderName = "이안",
|
||||
Text = "분리 창도 한 번에 열리도록 남겨 두었습니다.",
|
||||
Text = "분리 창은 상단 액션으로 남겨 두었습니다.",
|
||||
MetaText = "09:07",
|
||||
IsMine = true,
|
||||
ServerSequence = 18
|
||||
|
|
@ -793,6 +866,42 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
MetaText = "09:08",
|
||||
IsMine = false,
|
||||
ServerSequence = 19
|
||||
},
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "sample-msg-8",
|
||||
SenderName = "이안",
|
||||
Text = "검색과 필터는 한 줄 안에서 끝나도록 다시 정리할게요.",
|
||||
MetaText = "09:10",
|
||||
IsMine = true,
|
||||
ServerSequence = 20
|
||||
},
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "sample-msg-9",
|
||||
SenderName = "민지",
|
||||
Text = "좋아요. 설명보다 눌리는 구조가 더 중요해요.",
|
||||
MetaText = "09:11",
|
||||
IsMine = false,
|
||||
ServerSequence = 21
|
||||
},
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "sample-msg-10",
|
||||
SenderName = "이안",
|
||||
Text = "작성창도 짧은 액션만 남기고 텍스트는 줄였습니다.",
|
||||
MetaText = "09:12",
|
||||
IsMine = true,
|
||||
ServerSequence = 22
|
||||
},
|
||||
new MessageRowViewModel
|
||||
{
|
||||
MessageId = "sample-msg-11",
|
||||
SenderName = "민지",
|
||||
Text = "이제 목록과 대화가 한 화면에서 훨씬 빠르게 읽히네요.",
|
||||
MetaText = "09:13",
|
||||
IsMine = false,
|
||||
ServerSequence = 23
|
||||
}
|
||||
})
|
||||
{
|
||||
|
|
@ -1021,6 +1130,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
IsCompactDensity = true;
|
||||
IsInspectorVisible = false;
|
||||
IsConversationPaneCollapsed = false;
|
||||
ConversationPaneWidthValue = 348;
|
||||
StatusLine = "초기화";
|
||||
}
|
||||
|
||||
|
|
@ -1029,6 +1139,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
IsCompactDensity = layout.IsCompactDensity;
|
||||
IsInspectorVisible = layout.IsInspectorVisible;
|
||||
IsConversationPaneCollapsed = layout.IsConversationPaneCollapsed;
|
||||
ConversationPaneWidthValue = Math.Clamp(layout.ConversationPaneWidth, 280, 480);
|
||||
}
|
||||
|
||||
private Task PersistWorkspaceLayoutAsync()
|
||||
|
|
@ -1036,7 +1147,8 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
return _workspaceLayoutStore.SaveAsync(new DesktopWorkspaceLayout(
|
||||
IsCompactDensity,
|
||||
IsInspectorVisible,
|
||||
IsConversationPaneCollapsed));
|
||||
IsConversationPaneCollapsed,
|
||||
ConversationPaneWidthValue));
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
|
|
|
|||
|
|
@ -3,95 +3,98 @@
|
|||
xmlns:vm="using:PhysOn.Desktop.ViewModels"
|
||||
x:Class="PhysOn.Desktop.Views.ConversationWindow"
|
||||
x:DataType="vm:ConversationWindowViewModel"
|
||||
Width="460"
|
||||
Height="760"
|
||||
MinWidth="360"
|
||||
Width="404"
|
||||
Height="748"
|
||||
MinWidth="340"
|
||||
MinHeight="520"
|
||||
Background="#F6F7F8"
|
||||
Background="#F3F4F6"
|
||||
Title="{Binding ConversationTitle}">
|
||||
|
||||
<Window.Styles>
|
||||
<Style Selector="Border.surface">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Background" Value="#FFFFFF" />
|
||||
<Setter Property="BorderBrush" Value="#E6E8EB" />
|
||||
<Setter Property="BorderBrush" Value="#E5E7EB" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<Style Selector="Border.soft">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Background" Value="#F7F8F9" />
|
||||
<Setter Property="BorderBrush" Value="#ECEEF1" />
|
||||
<Style Selector="Border.muted">
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Background" Value="#F7F8FA" />
|
||||
<Setter Property="BorderBrush" Value="#E8EAEE" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<Style Selector="Button.icon">
|
||||
<Setter Property="CornerRadius" Value="9" />
|
||||
<Setter Property="Padding" Value="9,7" />
|
||||
<Setter Property="Background" Value="#FFFFFF" />
|
||||
<Setter Property="BorderBrush" Value="#D9DDE2" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<Style Selector="Button.primary">
|
||||
<Setter Property="CornerRadius" Value="9" />
|
||||
<Setter Property="Padding" Value="12,9" />
|
||||
<Setter Property="Background" Value="#111418" />
|
||||
<Setter Property="Foreground" Value="#FFFFFF" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.caption">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="#6A7380" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.body">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="#151A20" />
|
||||
</Style>
|
||||
<Style Selector="Border.bubble">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="8,7" />
|
||||
<Setter Property="Margin" Value="0,0,0,5" />
|
||||
<Setter Property="Background" Value="#F6F7F8" />
|
||||
<Setter Property="BorderBrush" Value="#EAEDF0" />
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Padding" Value="9,7" />
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
<Setter Property="Background" Value="#F7F8FA" />
|
||||
<Setter Property="BorderBrush" Value="#E4E7EB" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
</Style>
|
||||
<Style Selector="Border.bubble.mine">
|
||||
<Setter Property="Background" Value="#ECEFF3" />
|
||||
<Setter Property="Background" Value="#EEF1F4" />
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
</Style>
|
||||
<Style Selector="Button.icon">
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Padding" Value="8,6" />
|
||||
<Setter Property="Background" Value="#F7F8FA" />
|
||||
<Setter Property="BorderBrush" Value="#D9DDE2" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<Style Selector="Button.primary">
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Padding" Value="11,8" />
|
||||
<Setter Property="Background" Value="#111418" />
|
||||
<Setter Property="Foreground" Value="#FFFFFF" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.caption">
|
||||
<Setter Property="FontSize" Value="11.5" />
|
||||
<Setter Property="Foreground" Value="#69727D" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.body">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="#111418" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Grid Margin="16" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="10">
|
||||
<Border Classes="surface" Padding="12">
|
||||
<Grid ColumnDefinitions="42,*,Auto" ColumnSpacing="12">
|
||||
<Border Width="42" Height="42" Classes="soft">
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="8">
|
||||
<Border Classes="surface" Padding="10">
|
||||
<Grid ColumnDefinitions="40,*,Auto,Auto" ColumnSpacing="10">
|
||||
<Border Width="40" Height="40" Classes="muted">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding ConversationGlyph}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#151A20" />
|
||||
Foreground="#111418" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="2">
|
||||
<TextBlock Text="{Binding ConversationTitle}"
|
||||
FontSize="15"
|
||||
FontSize="14.5"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#151A20"
|
||||
Foreground="#111418"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding ConversationSubtitle}"
|
||||
Classes="caption"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
||||
<Border Classes="soft" Padding="8,5">
|
||||
<TextBlock Text="{Binding StatusText}" Classes="caption" />
|
||||
</Border>
|
||||
<Button Classes="icon"
|
||||
Command="{Binding ReloadCommand}"
|
||||
Content="↻" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="muted" Padding="8,4">
|
||||
<TextBlock Text="{Binding StatusText}" Classes="caption" />
|
||||
</Border>
|
||||
|
||||
<Button Grid.Column="3"
|
||||
Classes="icon"
|
||||
Command="{Binding ReloadCommand}"
|
||||
Content="↻" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1"
|
||||
Classes="surface"
|
||||
Padding="10"
|
||||
Padding="9"
|
||||
IsVisible="{Binding HasErrorText}">
|
||||
<TextBlock Text="{Binding ErrorText}"
|
||||
Foreground="#C62828"
|
||||
|
|
@ -99,8 +102,8 @@
|
|||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Classes="surface" Padding="12">
|
||||
<ScrollViewer Name="MessagesScrollViewer">
|
||||
<Border Grid.Row="2" Classes="surface" Padding="8">
|
||||
<ScrollViewer Name="MessagesScrollViewer" MaxWidth="360" HorizontalAlignment="Center">
|
||||
<ItemsControl ItemsSource="{Binding Messages}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MessageRowViewModel">
|
||||
|
|
@ -123,17 +126,18 @@
|
|||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="3" Classes="surface" Padding="10">
|
||||
<Border Grid.Row="3" Classes="muted" Padding="8" MaxWidth="360" HorizontalAlignment="Center">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="10">
|
||||
<TextBox Name="ComposerTextBox"
|
||||
PlaceholderText="메시지"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="46"
|
||||
MinHeight="44"
|
||||
Text="{Binding ComposerText}"
|
||||
KeyDown="ComposerTextBox_OnKeyDown" />
|
||||
<Button Grid.Column="1"
|
||||
Classes="primary"
|
||||
MinWidth="72"
|
||||
Command="{Binding SendMessageCommand}"
|
||||
Content="보내기" />
|
||||
</Grid>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -66,6 +66,14 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
|
||||
private void ConversationPaneHost_OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel viewModel && sender is Control control && control.IsVisible)
|
||||
{
|
||||
viewModel.UpdateConversationPaneWidth(control.Bounds.Width);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
if (_boundViewModel is not null)
|
||||
|
|
|
|||
4
src/PhysOn.Web/package-lock.json
generated
4
src/PhysOn.Web/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "physon-web",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "physon-web",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.4",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "physon-web",
|
||||
"private": true,
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@
|
|||
|
||||
.onboarding {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 16px 16px calc(20px + env(safe-area-inset-bottom));
|
||||
padding: 12px 12px calc(16px + env(safe-area-inset-bottom));
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.onboarding__chrome,
|
||||
|
|
@ -25,7 +27,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.brand-lockup,
|
||||
|
|
@ -33,7 +35,7 @@
|
|||
.chat-appbar__leading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +51,7 @@
|
|||
.appbar__title h2,
|
||||
.chat-appbar__title strong {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
|
@ -84,9 +86,9 @@
|
|||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.brand-mark svg,
|
||||
|
|
@ -105,9 +107,9 @@
|
|||
}
|
||||
|
||||
.brand-mark--small {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.surface-badge,
|
||||
|
|
@ -117,10 +119,10 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 999px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
}
|
||||
|
||||
|
|
@ -128,13 +130,13 @@
|
|||
.onboarding__panel,
|
||||
.pane {
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
}
|
||||
|
||||
.onboarding__hero,
|
||||
.onboarding__panel {
|
||||
padding: 20px 18px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.onboarding__hero {
|
||||
|
|
@ -197,7 +199,7 @@
|
|||
|
||||
.panel__heading {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.panel__label {
|
||||
|
|
@ -207,8 +209,8 @@
|
|||
|
||||
.onboarding__form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.field {
|
||||
|
|
@ -227,10 +229,10 @@
|
|||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
color: var(--text-strong);
|
||||
padding: 13px 14px;
|
||||
padding: 11px 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
|
@ -262,8 +264,8 @@
|
|||
}
|
||||
|
||||
.primary-button {
|
||||
min-height: 48px;
|
||||
border-radius: 12px;
|
||||
min-height: 42px;
|
||||
border-radius: 2px;
|
||||
background: var(--text-strong);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
|
|
@ -271,9 +273,9 @@
|
|||
|
||||
.secondary-button,
|
||||
.ghost-button {
|
||||
min-height: 44px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
|
|
@ -298,7 +300,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
|
|
@ -309,13 +311,13 @@
|
|||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
width: 100%;
|
||||
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
||||
padding-bottom: calc(66px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.pane {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto auto 1fr;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
min-height: 100svh;
|
||||
|
|
@ -324,11 +326,11 @@
|
|||
}
|
||||
|
||||
.pane--list {
|
||||
padding: 18px 16px 0;
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.pane--chat {
|
||||
padding: 18px 16px 14px;
|
||||
padding: 12px;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
|
|
@ -339,13 +341,13 @@
|
|||
.appbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
|
|
@ -358,7 +360,7 @@
|
|||
.toolbar-strip__group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
|
|
@ -373,10 +375,10 @@
|
|||
.icon-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
|
@ -386,16 +388,16 @@
|
|||
}
|
||||
|
||||
.icon-button--soft {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
color: var(--text-soft);
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +410,7 @@
|
|||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
border-radius: 2px;
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
|
|
@ -429,10 +431,10 @@
|
|||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 14px;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
|
|
@ -444,14 +446,14 @@
|
|||
|
||||
.search-field input {
|
||||
border: 0;
|
||||
padding: 12px 0;
|
||||
padding: 10px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.quick-grid--triple {
|
||||
|
|
@ -460,11 +462,11 @@
|
|||
|
||||
.mini-panel {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-height: 84px;
|
||||
padding: 14px 12px;
|
||||
gap: 4px;
|
||||
min-height: 72px;
|
||||
padding: 11px 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 14px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -494,21 +496,25 @@
|
|||
.conversation-list {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.conversation-list--saved {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.conversation-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -520,6 +526,11 @@
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-results--discovery,
|
||||
.search-results--matches {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.saved-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -543,11 +554,11 @@
|
|||
|
||||
.search-result {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -593,16 +604,16 @@
|
|||
}
|
||||
|
||||
.avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 2px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.avatar--header {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.conversation-row__body,
|
||||
|
|
@ -666,7 +677,7 @@
|
|||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
border-radius: 2px;
|
||||
background: var(--text-strong);
|
||||
color: #fff;
|
||||
font-style: normal;
|
||||
|
|
@ -677,7 +688,7 @@
|
|||
.row-pin {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
border-radius: 2px;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
|
|
@ -691,8 +702,8 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
min-height: 32px;
|
||||
gap: 8px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.chat-overview span {
|
||||
|
|
@ -710,9 +721,9 @@
|
|||
.mini-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
min-height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-muted);
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
|
@ -720,10 +731,10 @@
|
|||
.message-stream {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
padding: 4px 0 12px;
|
||||
padding: 2px 0 10px;
|
||||
}
|
||||
|
||||
.message-stream--empty {
|
||||
|
|
@ -744,10 +755,10 @@
|
|||
.empty-panel,
|
||||
.profile-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
}
|
||||
|
||||
|
|
@ -789,9 +800,9 @@
|
|||
}
|
||||
|
||||
.avatar--profile {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.profile-card__body {
|
||||
|
|
@ -845,9 +856,9 @@
|
|||
}
|
||||
|
||||
.message-bubble__body {
|
||||
padding: 11px 12px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
|
|
@ -868,8 +879,8 @@
|
|||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
|
|
@ -877,7 +888,7 @@
|
|||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.composer__field {
|
||||
|
|
@ -885,7 +896,7 @@
|
|||
}
|
||||
|
||||
.composer textarea {
|
||||
min-height: 48px;
|
||||
min-height: 44px;
|
||||
max-height: 120px;
|
||||
resize: none;
|
||||
}
|
||||
|
|
@ -893,9 +904,9 @@
|
|||
.send-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 2px;
|
||||
background: var(--text-strong);
|
||||
color: #fff;
|
||||
}
|
||||
|
|
@ -903,8 +914,8 @@
|
|||
.bottom-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
|
||||
gap: 6px;
|
||||
padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
|
@ -923,16 +934,18 @@
|
|||
justify-items: center;
|
||||
align-content: center;
|
||||
gap: 2px;
|
||||
min-height: 46px;
|
||||
min-height: 42px;
|
||||
padding: 6px 4px;
|
||||
border-radius: 12px;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.nav-button span {
|
||||
font-size: 11px;
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-button--active {
|
||||
|
|
@ -940,14 +953,18 @@
|
|||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.nav-button--active span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-button--active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 18%;
|
||||
width: 64%;
|
||||
top: 4px;
|
||||
left: 22%;
|
||||
width: 56%;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
border-radius: 2px;
|
||||
background: var(--text-strong);
|
||||
}
|
||||
|
||||
|
|
@ -959,7 +976,7 @@
|
|||
max-width: calc(100vw - 32px);
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 12px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
color: var(--text-soft);
|
||||
font-size: 13px;
|
||||
|
|
@ -995,37 +1012,56 @@
|
|||
|
||||
@media (min-width: 900px) {
|
||||
.onboarding {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(360px, 400px);
|
||||
align-items: stretch;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1480px;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
grid-template-columns: 88px 340px minmax(640px, 1fr);
|
||||
gap: 18px;
|
||||
grid-template-columns: 64px minmax(272px, 340px) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
padding: 18px;
|
||||
padding-bottom: 18px;
|
||||
padding: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.pane {
|
||||
min-height: calc(100svh - 36px);
|
||||
min-height: calc(100svh - 20px);
|
||||
margin: 0;
|
||||
border-radius: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pane--list {
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.pane--chat {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.pane--search .search-results--discovery,
|
||||
.pane--search .search-results--matches,
|
||||
.pane--saved .conversation-list--saved {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pane--search .saved-section,
|
||||
.pane--saved .saved-section {
|
||||
align-content: start;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 2px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.pane--me .profile-card {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.pane--hidden {
|
||||
display: grid;
|
||||
}
|
||||
|
|
@ -1034,13 +1070,13 @@
|
|||
position: sticky;
|
||||
inset: 0 auto auto 0;
|
||||
width: auto;
|
||||
height: calc(100svh - 36px);
|
||||
padding: 12px 10px;
|
||||
height: calc(100svh - 20px);
|
||||
padding: 8px 6px;
|
||||
grid-template-columns: 1fr;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 14px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
}
|
||||
|
||||
|
|
@ -1055,17 +1091,21 @@
|
|||
}
|
||||
|
||||
.nav-button {
|
||||
min-height: 58px;
|
||||
padding: 8px 4px;
|
||||
border-radius: 12px;
|
||||
gap: 4px;
|
||||
min-height: 48px;
|
||||
padding: 7px 4px;
|
||||
border-radius: 2px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bottom-bar--shell .nav-button span {
|
||||
display: block;
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bottom-bar--shell .nav-button--active span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.appbar__title span,
|
||||
.chat-appbar__title span {
|
||||
display: none;
|
||||
|
|
@ -1073,21 +1113,22 @@
|
|||
|
||||
.appbar,
|
||||
.chat-appbar {
|
||||
min-height: 48px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.toolbar-strip {
|
||||
overflow: visible;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-results,
|
||||
.saved-section,
|
||||
.saved-section__body {
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ type IconName =
|
|||
| 'group'
|
||||
|
||||
const DEFAULT_API_BASE_URL = import.meta.env.VITE_API_BASE_URL?.trim() ?? ''
|
||||
const APP_VERSION = 'web-0.1.0-alpha.3'
|
||||
const APP_VERSION = 'web-0.1.0-alpha.4'
|
||||
|
||||
const CONNECTION_LABEL: Record<ConnectionState, string> = {
|
||||
idle: '준비 중',
|
||||
|
|
@ -81,6 +81,18 @@ function sortConversations(items: ConversationSummaryDto[]): ConversationSummary
|
|||
return [...items].sort(compareConversations)
|
||||
}
|
||||
|
||||
function dedupeConversationsById(items: ConversationSummaryDto[]): ConversationSummaryDto[] {
|
||||
const seen = new Set<string>()
|
||||
return items.filter((item) => {
|
||||
if (seen.has(item.conversationId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
seen.add(item.conversationId)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function upsertConversation(
|
||||
items: ConversationSummaryDto[],
|
||||
nextConversation: ConversationSummaryDto,
|
||||
|
|
@ -415,10 +427,6 @@ function App() {
|
|||
messages: messageMatches.slice(0, 8),
|
||||
}
|
||||
}, [conversations, messagesByConversation, normalizedSearchQuery])
|
||||
const savedConversations = useMemo(
|
||||
() => conversations.filter((conversation) => conversation.isPinned || conversation.unreadCount > 0),
|
||||
[conversations],
|
||||
)
|
||||
const replyNeededConversations = useMemo(
|
||||
() => conversations.filter((conversation) => conversation.unreadCount > 0).slice(0, 4),
|
||||
[conversations],
|
||||
|
|
@ -431,6 +439,24 @@ function App() {
|
|||
() => conversations.slice(0, 4),
|
||||
[conversations],
|
||||
)
|
||||
const savedReplyQueue = replyNeededConversations
|
||||
const savedPinnedQueue = useMemo(
|
||||
() => dedupeConversationsById(pinnedConversations.filter((conversation) => !replyNeededConversations.some((item) => item.conversationId === conversation.conversationId))),
|
||||
[pinnedConversations, replyNeededConversations],
|
||||
)
|
||||
const savedRecentQueue = useMemo(
|
||||
() => dedupeConversationsById(
|
||||
recentConversations.filter((conversation) =>
|
||||
!replyNeededConversations.some((item) => item.conversationId === conversation.conversationId) &&
|
||||
!pinnedConversations.some((item) => item.conversationId === conversation.conversationId),
|
||||
),
|
||||
),
|
||||
[pinnedConversations, recentConversations, replyNeededConversations],
|
||||
)
|
||||
const savedConversations = useMemo(
|
||||
() => dedupeConversationsById([...savedReplyQueue, ...savedPinnedQueue, ...savedRecentQueue]),
|
||||
[savedPinnedQueue, savedRecentQueue, savedReplyQueue],
|
||||
)
|
||||
const searchResultTotal = searchResults.conversations.length + searchResults.messages.length
|
||||
const primaryResumeConversation = selectedConversation ?? conversations[0] ?? null
|
||||
|
||||
|
|
@ -958,11 +984,11 @@ function App() {
|
|||
const destinationMeta: Record<BottomDestination, { title: string; subtitle: string }> = {
|
||||
inbox: {
|
||||
title: '받은함',
|
||||
subtitle: me?.displayName ? `${me.displayName}` : '최근 대화',
|
||||
subtitle: me?.displayName ? `${me.displayName}` : '최근',
|
||||
},
|
||||
search: {
|
||||
title: '검색',
|
||||
subtitle: '대화 다시 찾기',
|
||||
subtitle: '다시 찾기',
|
||||
},
|
||||
saved: {
|
||||
title: '보관',
|
||||
|
|
@ -970,7 +996,7 @@ function App() {
|
|||
},
|
||||
me: {
|
||||
title: '내 공간',
|
||||
subtitle: '세션과 기기',
|
||||
subtitle: '세션',
|
||||
},
|
||||
}
|
||||
const activeDestinationMeta = destinationMeta[bottomDestination]
|
||||
|
|
@ -1038,41 +1064,19 @@ function App() {
|
|||
</span>
|
||||
<div className="brand-lockup__text">
|
||||
<strong>KoTalk</strong>
|
||||
<span>가볍게 이어지는 대화</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="surface-badge">WEB</span>
|
||||
</header>
|
||||
|
||||
<section className="onboarding__hero">
|
||||
<p className="eyebrow">KO · TALK</p>
|
||||
<h1>
|
||||
열면
|
||||
<br />
|
||||
바로 대화.
|
||||
</h1>
|
||||
<p className="onboarding__summary">표시 이름과 참여 키만 넣고 바로 시작합니다.</p>
|
||||
|
||||
<div className="summary-strip summary-strip--hero" aria-label="핵심 특성">
|
||||
<span className="summary-chip"><Icon name="spark" /> 빠른 시작</span>
|
||||
<span className="summary-chip"><Icon name="chat" /> 작업 대화</span>
|
||||
<span className="summary-chip"><Icon name="session" /> 기기 기억</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="onboarding__panel">
|
||||
<div className="panel__heading">
|
||||
<span className="panel__label">Join</span>
|
||||
<h2>시작</h2>
|
||||
</div>
|
||||
|
||||
<form className="onboarding__form" onSubmit={handleRegister}>
|
||||
<label className="field">
|
||||
<span>표시 이름</span>
|
||||
<input
|
||||
aria-label="표시 이름"
|
||||
autoComplete="nickname"
|
||||
maxLength={20}
|
||||
placeholder="이름"
|
||||
placeholder="표시 이름"
|
||||
required
|
||||
value={displayName}
|
||||
onChange={(event) => setDisplayName(event.target.value)}
|
||||
|
|
@ -1080,10 +1084,10 @@ function App() {
|
|||
</label>
|
||||
|
||||
<label className="field">
|
||||
<span>참여 키</span>
|
||||
<input
|
||||
aria-label="참여 키"
|
||||
autoCapitalize="characters"
|
||||
placeholder="참여 키 입력"
|
||||
placeholder="참여 키"
|
||||
required
|
||||
value={inviteCode}
|
||||
onChange={(event) => setInviteCode(event.target.value.toUpperCase())}
|
||||
|
|
@ -1096,10 +1100,10 @@ function App() {
|
|||
|
||||
{showAdvanced ? (
|
||||
<label className="field">
|
||||
<span>서버 주소</span>
|
||||
<input
|
||||
aria-label="서버 주소"
|
||||
inputMode="url"
|
||||
placeholder="기본 서버를 바꾸는 경우만 입력"
|
||||
placeholder="서버 주소"
|
||||
value={apiBaseUrl}
|
||||
onChange={(event) => setApiBaseUrl(event.target.value)}
|
||||
/>
|
||||
|
|
@ -1107,15 +1111,10 @@ function App() {
|
|||
) : null}
|
||||
|
||||
<button className="primary-button" disabled={registering} type="submit">
|
||||
{registering ? '입장 중...' : '대화 열기'}
|
||||
{registering ? '입장 중...' : '열기'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="panel__foot">
|
||||
<span>세션은 이 기기에만</span>
|
||||
<span>서버는 고급에서만</span>
|
||||
</div>
|
||||
|
||||
{statusMessage ? <p className="status-text">{statusMessage}</p> : null}
|
||||
</section>
|
||||
</main>
|
||||
|
|
@ -1156,7 +1155,7 @@ function App() {
|
|||
</button>
|
||||
</aside>
|
||||
|
||||
<section className={`pane pane--list ${mobileView === 'chat' ? 'pane--hidden' : ''}`}>
|
||||
<section className={`pane pane--list pane--${bottomDestination} ${mobileView === 'chat' ? 'pane--hidden' : ''}`}>
|
||||
<header className="appbar">
|
||||
<div className="appbar__leading">
|
||||
<span className="brand-mark brand-mark--small" aria-hidden="true">
|
||||
|
|
@ -1190,32 +1189,6 @@ function App() {
|
|||
{bottomDestination === 'inbox' ? (
|
||||
<>
|
||||
<div className="toolbar-strip" aria-label="받은함 빠른 막대">
|
||||
<div className="toolbar-strip__group">
|
||||
<span className={`status-chip status-chip--${connectionState}`}>
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
{CONNECTION_DESCRIPTION[connectionState]}
|
||||
</span>
|
||||
<span className="status-panel__time">{formatRelativeConnection(storedSession?.savedAt ?? null)}</span>
|
||||
</div>
|
||||
<div className="toolbar-strip__group toolbar-strip__group--actions">
|
||||
<button
|
||||
className="icon-button icon-button--soft"
|
||||
type="button"
|
||||
aria-label="검색 열기"
|
||||
onClick={() => openDestination('search')}
|
||||
>
|
||||
<Icon name="search" />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button icon-button--soft"
|
||||
type="button"
|
||||
aria-label="내 공간 열기"
|
||||
onClick={() => openDestination('me')}
|
||||
>
|
||||
<Icon name="me" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`summary-chip ${listFilter === 'all' ? 'summary-chip--active' : ''}`}
|
||||
type="button"
|
||||
|
|
@ -1237,6 +1210,24 @@ function App() {
|
|||
>
|
||||
<Icon name="pin" /> {pinnedTotal}
|
||||
</button>
|
||||
<div className="toolbar-strip__group toolbar-strip__group--actions">
|
||||
<button
|
||||
className="icon-button icon-button--soft"
|
||||
type="button"
|
||||
aria-label="검색"
|
||||
onClick={() => openDestination('search')}
|
||||
>
|
||||
<Icon name="search" />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button icon-button--soft"
|
||||
type="button"
|
||||
aria-label="새로고침"
|
||||
onClick={handleReconnect}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="conversation-list">
|
||||
|
|
@ -1290,7 +1281,7 @@ function App() {
|
|||
<button
|
||||
className="icon-button icon-button--soft"
|
||||
type="button"
|
||||
aria-label="최근 대화 열기"
|
||||
aria-label="최근 대화"
|
||||
onClick={() => {
|
||||
if (primaryResumeConversation) {
|
||||
selectConversation(primaryResumeConversation.conversationId, 'chat')
|
||||
|
|
@ -1326,13 +1317,41 @@ function App() {
|
|||
|
||||
<div className="conversation-list">
|
||||
{!normalizedSearchQuery ? (
|
||||
<p className="empty-state empty-state--inline">대화와 메시지를 다시 찾아보세요.</p>
|
||||
<div className="search-results search-results--discovery">
|
||||
{recentConversations.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
<strong>최근</strong>
|
||||
<span>{recentConversations.length}개</span>
|
||||
</div>
|
||||
<div className="saved-section__body">{renderConversationRows(recentConversations)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
{replyNeededConversations.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
<strong>안읽음</strong>
|
||||
<span>{replyNeededConversations.length}개</span>
|
||||
</div>
|
||||
<div className="saved-section__body">{renderConversationRows(replyNeededConversations)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
{pinnedConversations.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
<strong>고정</strong>
|
||||
<span>{pinnedConversations.length}개</span>
|
||||
</div>
|
||||
<div className="saved-section__body">{renderConversationRows(pinnedConversations)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{normalizedSearchQuery && searchResultTotal === 0 ? (
|
||||
<p className="empty-state empty-state--inline">결과가 없습니다. 다른 단어로 다시 찾아보세요.</p>
|
||||
) : null}
|
||||
{normalizedSearchQuery && searchResultTotal > 0 ? (
|
||||
<div className="search-results">
|
||||
<div className="search-results search-results--matches">
|
||||
{searchResults.messages.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
|
|
@ -1365,42 +1384,42 @@ function App() {
|
|||
{bottomDestination === 'saved' ? (
|
||||
<>
|
||||
<div className="toolbar-strip toolbar-strip--utility" aria-label="보관함 요약">
|
||||
<span className="status-chip"><Icon name="spark" /> {replyNeededConversations.length}</span>
|
||||
<span className="status-chip"><Icon name="pin" /> {pinnedConversations.length}</span>
|
||||
<span className="status-chip"><Icon name="spark" /> {savedReplyQueue.length}</span>
|
||||
<span className="status-chip"><Icon name="pin" /> {savedPinnedQueue.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="conversation-list">
|
||||
<div className="conversation-list conversation-list--saved">
|
||||
{savedConversations.length === 0 ? (
|
||||
<p className="empty-state empty-state--inline">지금 보관된 후속 작업이 없습니다.</p>
|
||||
) : null}
|
||||
|
||||
{replyNeededConversations.length > 0 ? (
|
||||
{savedReplyQueue.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
<strong>답장</strong>
|
||||
<span>{replyNeededConversations.length}개</span>
|
||||
<span>{savedReplyQueue.length}개</span>
|
||||
</div>
|
||||
<div className="saved-section__body">{renderConversationRows(replyNeededConversations)}</div>
|
||||
<div className="saved-section__body">{renderConversationRows(savedReplyQueue)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{pinnedConversations.length > 0 ? (
|
||||
{savedPinnedQueue.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
<strong>고정</strong>
|
||||
<span>{pinnedConversations.length}개</span>
|
||||
<strong>중요</strong>
|
||||
<span>{savedPinnedQueue.length}개</span>
|
||||
</div>
|
||||
<div className="saved-section__body">{renderConversationRows(pinnedConversations)}</div>
|
||||
<div className="saved-section__body">{renderConversationRows(savedPinnedQueue)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{recentConversations.length > 0 ? (
|
||||
{savedRecentQueue.length > 0 ? (
|
||||
<section className="saved-section">
|
||||
<div className="saved-section__header">
|
||||
<strong>최근</strong>
|
||||
<span>{recentConversations.length}개</span>
|
||||
<span>{savedRecentQueue.length}개</span>
|
||||
</div>
|
||||
<div className="saved-section__body">{renderConversationRows(recentConversations)}</div>
|
||||
<div className="saved-section__body">{renderConversationRows(savedRecentQueue)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -1481,8 +1500,7 @@ function App() {
|
|||
|
||||
{loadingConversationId !== selectedConversation.conversationId && selectedMessages.length === 0 ? (
|
||||
<div className="empty-panel empty-panel--chat">
|
||||
<strong>{selectedConversation.memberCount <= 1 ? '첫 메모부터 시작' : '첫 메시지부터 시작'}</strong>
|
||||
<p>짧게 한 줄만 남겨도 대화가 바로 이어집니다.</p>
|
||||
<strong>{selectedConversation.memberCount <= 1 ? '첫 메모' : '첫 메시지'}</strong>
|
||||
<div className="empty-panel__actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
|
|
@ -1527,13 +1545,13 @@ function App() {
|
|||
<form className="composer" onSubmit={handleSendMessage}>
|
||||
<div className="composer-shortcuts" aria-label="빠른 입력">
|
||||
<button className="ghost-button" type="button" onClick={() => applyQuickDraft('확인 후 답드릴게요.')}>
|
||||
확인 후 회신
|
||||
확인
|
||||
</button>
|
||||
<button className="ghost-button" type="button" onClick={() => applyQuickDraft('자료 공유드립니다.\n- ')}>
|
||||
자료 공유
|
||||
공유
|
||||
</button>
|
||||
<button className="ghost-button" type="button" onClick={() => applyQuickDraft('잠시 뒤 다시 말씀드릴게요.')}>
|
||||
잠시 후
|
||||
나중
|
||||
</button>
|
||||
</div>
|
||||
<label className="composer__field">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue