diff --git a/src/SheetMe.Designer/Controls/MinThumbTrack.cs b/src/SheetMe.Designer/Controls/MinThumbTrack.cs
new file mode 100644
index 0000000..b5af102
--- /dev/null
+++ b/src/SheetMe.Designer/Controls/MinThumbTrack.cs
@@ -0,0 +1,106 @@
+// 출처: [200]SheetMe src/Ui/SheetMe.Ui/MinThumbTrack.cs 복사 이식(vendoring).
+// 두 저장소가 분리돼 ProjectReference 가 불가능하고, 스크롤 썸 산술은 변할 이유가 없는 고정 자산이다.
+// 원본이 바뀌면 여기도 함께 갱신할 것.
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+
+namespace SheetMe.Designer.Controls;
+
+///
+/// 썸(thumb) 최소 길이를 '확실히' 보장하는 Track.
+/// 기본 WPF Track 은 콘텐츠가 많을수록 썸을 비례로 줄이며 Thumb.MinHeight 를 반영하지 않아
+/// (아이템 1000+ 리스트에서 실처럼 가늘어짐), 여기서 직접 arrange 하여 하한()을 둔다.
+/// 드래그(Thumb.DragDelta → ScrollBar 이 Track.ValueFromDistance 호출)도 자체 density 로 정확히 매핑한다.
+/// 스크롤바 표준 동작: 세로는 value=Min→썸 위, 가로는 value=Min→썸 왼쪽(IsDirectionReversed 는 직접 처리).
+///
+public class MinThumbTrack : Track
+{
+ public static readonly DependencyProperty MinThumbLengthProperty =
+ DependencyProperty.Register(nameof(MinThumbLength), typeof(double), typeof(MinThumbTrack),
+ new FrameworkPropertyMetadata(44.0, FrameworkPropertyMetadataOptions.AffectsArrange));
+
+ /// 썸 최소 길이(px). 트랙이 이보다 짧으면 트랙 길이로 클램프.
+ public double MinThumbLength
+ {
+ get => (double)GetValue(MinThumbLengthProperty);
+ set => SetValue(MinThumbLengthProperty, value);
+ }
+
+ private double _density; // 값/픽셀 (드래그 매핑용)
+ private double _thumbLength; // 현재 썸 길이(px)
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ double viewport = ViewportSize;
+ var thumb = Thumb;
+ if (double.IsNaN(viewport) || thumb is null)
+ return base.ArrangeOverride(finalSize); // 슬라이더 모드 등은 기본 동작
+
+ bool vertical = Orientation == Orientation.Vertical;
+ double trackLength = vertical ? finalSize.Height : finalSize.Width;
+ if (trackLength <= 0.0) return finalSize;
+
+ double min = Minimum, max = Maximum;
+ double range = Math.Max(0.0, max - min);
+ double value = Math.Max(min, Math.Min(max, Value));
+ viewport = Math.Max(0.0, viewport);
+
+ double thumbLength, decreaseLength;
+ if (range <= 0.0 || viewport <= 0.0)
+ {
+ thumbLength = trackLength; // 스크롤 불필요 → 썸이 트랙 전체
+ decreaseLength = 0.0;
+ _density = 0.0;
+ }
+ else
+ {
+ double extent = range + viewport;
+ thumbLength = trackLength * viewport / extent; // 비례 크기
+ double minThumb = Math.Min(MinThumbLength, trackLength); // 트랙보다 길 수 없음
+ if (thumbLength < minThumb) thumbLength = minThumb; // ← 하한 강제
+ if (thumbLength > trackLength) thumbLength = trackLength;
+
+ double remaining = Math.Max(0.0, trackLength - thumbLength);
+ _density = remaining > 0.0 ? range / remaining : 0.0;
+ decreaseLength = remaining * ((value - min) / range); // 시작(위/왼쪽)~썸 사이 여백
+ }
+ _thumbLength = thumbLength;
+ double increaseLength = Math.Max(0.0, trackLength - thumbLength - decreaseLength);
+
+ var dec = DecreaseRepeatButton;
+ var inc = IncreaseRepeatButton;
+ if (vertical)
+ {
+ double w = finalSize.Width;
+ dec?.Arrange(new Rect(0, 0, w, decreaseLength));
+ thumb.Arrange(new Rect(0, decreaseLength, w, thumbLength));
+ inc?.Arrange(new Rect(0, decreaseLength + thumbLength, w, increaseLength));
+ }
+ else
+ {
+ double h = finalSize.Height;
+ dec?.Arrange(new Rect(0, 0, decreaseLength, h));
+ thumb.Arrange(new Rect(decreaseLength, 0, thumbLength, h));
+ inc?.Arrange(new Rect(decreaseLength + thumbLength, 0, increaseLength, h));
+ }
+ return finalSize;
+ }
+
+ /// 썸 드래그 거리 → 값 변화. 아래/오른쪽으로 끌면 value 증가(스크롤 내려감).
+ public override double ValueFromDistance(double horizontal, double vertical)
+ {
+ double dist = Orientation == Orientation.Vertical ? vertical : horizontal;
+ return dist * _density;
+ }
+
+ public override double ValueFromPoint(Point pt)
+ {
+ bool vertical = Orientation == Orientation.Vertical;
+ double trackLength = vertical ? ActualHeight : ActualWidth;
+ double remaining = Math.Max(1.0, trackLength - _thumbLength);
+ double pos = (vertical ? pt.Y : pt.X) - _thumbLength / 2.0;
+ double f = Math.Max(0.0, Math.Min(1.0, pos / remaining));
+ return Minimum + f * (Maximum - Minimum);
+ }
+}
diff --git a/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs b/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs
index 7eb9248..8714946 100644
--- a/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs
+++ b/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs
@@ -16,17 +16,18 @@ public static class PaletteIconCatalog
["Label"] = ("type", "표시"),
["PictureBox"] = ("image", "표시"),
["Line"] = ("minus", "표시"),
+ // MaskedTextBox 는 TextBox 와 아이콘이 겹쳐 구분이 안 됐다 — [200] 어휘로 분리
["TextBox"] = ("text-cursor-input", "입력"),
- ["MaskedTextBox"] = ("text-cursor-input", "입력"),
- ["ComboBox"] = ("chevron-down", "입력"),
- ["ListBox"] = ("list", "입력"),
+ ["MaskedTextBox"] = ("type", "입력"),
+ ["ComboBox"] = ("list", "입력"),
+ ["ListBox"] = ("list-checks", "입력"),
["CheckList"] = ("list-checks", "입력"),
["DateTimePicker"] = ("calendar", "입력"),
["CalcBox"] = ("sigma", "입력"),
["CheckBox"] = ("square-check", "선택"),
["RadioButton"] = ("circle-dot", "선택"),
- ["Panel"] = ("box", "컨테이너"),
- ["GroupBox"] = ("package", "컨테이너"),
+ ["Panel"] = ("package", "컨테이너"),
+ ["GroupBox"] = ("box", "컨테이너"),
["Button"] = ("mouse-pointer-click", "동작/데이터"),
["DataTable"] = ("table", "동작/데이터"),
};
@@ -41,7 +42,39 @@ public static class PaletteIconCatalog
#endregion
}
-/// 팔레트 타입명 → Lucide 아이콘 비주얼(FrameworkElement) 컨버터. parameter=크기(기본 18)
+///
+/// 아이콘 컨버터 공용 파라미터 해석 — "크기" 또는 "크기|브러시키".
+///
+/// 브러시 키를 생략하면 종전처럼 B.Muted 를 쓴다(하위호환). 색을 인자로 받을 수 있어야
+/// 활성 문서 탭 아이콘을 강조색으로, 레이어 타입 아이콘을 본문색으로 굽는 것이 가능해진다.
+///
+internal static class IconParam
+{
+ public static (double Size, Brush Brush) Parse(object? parameter, double defaultSize)
+ {
+ var size = defaultSize;
+ var key = "B.Muted";
+
+ if (parameter is string text && text.Length > 0)
+ {
+ var bar = text.IndexOf('|');
+ var sizePart = bar >= 0 ? text[..bar] : text;
+ if (bar >= 0 && bar + 1 < text.Length)
+ {
+ key = text[(bar + 1)..].Trim();
+ }
+ if (double.TryParse(sizePart, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
+ {
+ size = parsed;
+ }
+ }
+
+ var brush = System.Windows.Application.Current?.TryFindResource(key) as Brush ?? Brushes.Gray;
+ return (size, brush);
+ }
+}
+
+/// 팔레트 타입명 → Lucide 아이콘 비주얼(FrameworkElement) 컨버터. parameter="크기" 또는 "크기|브러시키"(기본 18, B.Muted)
public sealed class TypeToIconConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
@@ -50,8 +83,7 @@ public sealed class TypeToIconConverter : IValueConverter
{
return null;
}
- var size = parameter is string s && double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 18;
- var brush = System.Windows.Application.Current.TryFindResource("B.Muted") as Brush ?? Brushes.Gray;
+ var (size, brush) = IconParam.Parse(parameter, 18);
return LucideIcons.Icon(PaletteIconCatalog.IconOf(type), size, brush);
}
@@ -82,7 +114,7 @@ public sealed class InverseBoolConverter : IValueConverter
///
/// Lucide 아이콘명 → 비주얼 컨버터(문서 탭 file-text, 플로팅 바 layout-grid 등 고정 아이콘용).
/// Binding Source 에 아이콘명 문자열을 넣어 사용 — 항목마다 새 비주얼이 생성되어 공유 부모 충돌이 없다.
-/// parameter=크기(기본 14).
+/// parameter="크기" 또는 "크기|브러시키"(기본 14, B.Muted).
///
public sealed class IconNameToVisualConverter : IValueConverter
{
@@ -92,8 +124,7 @@ public sealed class IconNameToVisualConverter : IValueConverter
{
return null;
}
- var size = parameter is string s && double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 14;
- var brush = System.Windows.Application.Current.TryFindResource("B.Muted") as Brush ?? Brushes.Gray;
+ var (size, brush) = IconParam.Parse(parameter, 14);
return LucideIcons.Icon(name, size, brush);
}
diff --git a/src/SheetMe.Designer/Themes/DesignerTheme.xaml b/src/SheetMe.Designer/Themes/DesignerTheme.xaml
index 82670fe..3b3f695 100644
--- a/src/SheetMe.Designer/Themes/DesignerTheme.xaml
+++ b/src/SheetMe.Designer/Themes/DesignerTheme.xaml
@@ -1,6 +1,7 @@
+ xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
+ xmlns:ctl="clr-namespace:SheetMe.Designer.Controls">
-
+
@@ -386,8 +391,9 @@
+
@@ -400,6 +406,7 @@
+
@@ -461,6 +468,34 @@
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SheetMe.Designer/Themes/Tokens.Dark.xaml b/src/SheetMe.Designer/Themes/Tokens.Dark.xaml
index e344cba..858a998 100644
--- a/src/SheetMe.Designer/Themes/Tokens.Dark.xaml
+++ b/src/SheetMe.Designer/Themes/Tokens.Dark.xaml
@@ -43,4 +43,7 @@
+
+
+
diff --git a/src/SheetMe.Designer/Themes/Tokens.Light.xaml b/src/SheetMe.Designer/Themes/Tokens.Light.xaml
index e1e06ca..f7b8b83 100644
--- a/src/SheetMe.Designer/Themes/Tokens.Light.xaml
+++ b/src/SheetMe.Designer/Themes/Tokens.Light.xaml
@@ -43,4 +43,7 @@
+
+
+
diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml
index 97f3c82..149c1ff 100644
--- a/src/SheetMe.Designer/Views/MainView.xaml
+++ b/src/SheetMe.Designer/Views/MainView.xaml
@@ -442,20 +442,21 @@
+
-
+
-
-
+
+ Content="{Binding Source=hand, Converter={StaticResource IconName}, ConverterParameter=18|B.Ink}"/>