스타일 가이드 적용 완료. UTKCOlorPicker, UTKDatePicker 확인해야 함

This commit is contained in:
logonkhi
2026-01-12 20:16:17 +09:00
parent 6ae48ff30e
commit e1f2ac5b02
31 changed files with 1854 additions and 660 deletions

View File

@@ -37,16 +37,19 @@
=================================== */
.utk-foldout > .unity-foldout__toggle > .unity-toggle__input > .unity-foldout__checkmark {
width: 12px;
height: 12px;
margin-right: var(--space-s);
width: 16px;
height: 16px;
-unity-background-image-tint-color: var(--color-text-secondary);
transition-duration: var(--anim-fast);
transition-property: rotate;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
margin-right: var(--space-xs);
}
.utk-foldout > .unity-foldout__toggle:checked > .unity-toggle__input > .unity-foldout__checkmark {
rotate: 90deg;
rotate: 0deg;
}
/* ===================================
@@ -57,13 +60,15 @@
font-size: var(--font-size-body2);
color: var(--color-text-primary);
-unity-font-style: bold;
margin: 0;
padding: var(--space-s) 0;
}
/* ===================================
Content Container
=================================== */
.utk-foldout > .unity-foldout__content {
.utk-foldout .unity-foldout__content {
margin-top: var(--space-s);
margin-left: var(--space-m);
padding: var(--space-m);
@@ -73,6 +78,11 @@
border-color: var(--color-border);
}
.utk-foldout .unity-foldout__content {
font-size: var(--font-size-body2);
color: var(--color-text-primary);
}
/* ===================================
Collapsed State - Remove content border radius on top
=================================== */
@@ -82,7 +92,7 @@
border-bottom-right-radius: 0;
}
.utk-foldout:checked > .unity-foldout__content {
.utk-foldout:checked .unity-foldout__content {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;

View File

@@ -29,78 +29,29 @@
padding: 0;
}
/* ===================================
Content Text
=================================== */
.utk-scrollview .unity-label {
font-size: var(--font-size-body2);
color: var(--color-text-primary);
}
/* ===================================
Vertical Scroller
=================================== */
.utk-scrollview .unity-scroller--vertical {
width: 8px;
margin-left: var(--space-xs);
}
.utk-scrollview .unity-scroller--vertical > .unity-scroller__low-button,
.utk-scrollview .unity-scroller--vertical > .unity-scroller__high-button {
display: none;
}
.utk-scrollview .unity-scroller--vertical > .unity-scroller__slider {
width: 8px;
width: 6px;
margin: 0;
}
.utk-scrollview .unity-scroller--vertical .unity-base-slider__tracker {
background-color: var(--color-bg-secondary);
border-radius: 4px;
border-width: 0;
}
.utk-scrollview .unity-scroller--vertical .unity-base-slider__dragger {
width: 8px;
border-radius: 4px;
background-color: var(--color-text-disabled);
border-width: 0;
transition-duration: var(--anim-fast);
transition-property: background-color;
}
.utk-scrollview .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-text-secondary);
}
/* ===================================
Horizontal Scroller
=================================== */
.utk-scrollview .unity-scroller--horizontal {
height: 8px;
margin-top: var(--space-xs);
}
.utk-scrollview .unity-scroller--horizontal > .unity-scroller__low-button,
.utk-scrollview .unity-scroller--horizontal > .unity-scroller__high-button {
display: none;
}
.utk-scrollview .unity-scroller--horizontal > .unity-scroller__slider {
height: 8px;
height: 6px;
margin: 0;
}
.utk-scrollview .unity-scroller--horizontal .unity-base-slider__tracker {
background-color: var(--color-bg-secondary);
border-radius: 4px;
border-width: 0;
}
.utk-scrollview .unity-scroller--horizontal .unity-base-slider__dragger {
height: 8px;
border-radius: 4px;
background-color: var(--color-text-disabled);
border-width: 0;
transition-duration: var(--anim-fast);
transition-property: background-color;
}
.utk-scrollview .unity-scroller--horizontal .unity-base-slider__dragger:hover {
background-color: var(--color-text-secondary);
}

View File

@@ -11,7 +11,7 @@
}
.utk-tooltip-label {
color: var(--color-text-primary);
color: #FFFFFF;
font-size: var(--font-size-body2);
white-space: normal;
-unity-text-align: middle-left;

View File

@@ -71,19 +71,6 @@
background-color: transparent;
}
.accordion-tree-view .unity-scroller--vertical {
width: 8px;
}
.accordion-tree-view .unity-scroller--vertical .unity-base-slider__dragger {
background-color: var(--color-scroller-dragger-normal);
border-radius: var(--radius-m);
}
.accordion-tree-view .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-scroller-dragger-hover);
}
.accordion-tree-view .unity-tree-view__item {
padding: 0;
margin: 0;

View File

@@ -60,19 +60,6 @@
background-color: transparent;
}
#main-list-view .unity-scroller--vertical {
width: 8px;
}
#main-list-view .unity-scroller--vertical .unity-base-slider__dragger {
background-color: rgb(80, 80, 82);
border-radius: 4px;
}
#main-list-view .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: rgb(100, 100, 102);
}
.image-list-row {
flex-direction: row;
justify-content: flex-start;

View File

@@ -30,8 +30,8 @@
=================================== */
.utk-listview .unity-list-view__item {
padding-top: var(--space-s);
padding-bottom: var(--space-s);
padding-top: 0;
padding-bottom: 0;
padding-left: var(--space-m);
padding-right: var(--space-m);
margin: 0;
@@ -40,14 +40,10 @@
transition-property: background-color;
font-size: var(--font-size-body2);
height: auto;
min-height: 28px;
min-height: 24px;
-unity-text-align: middle-left;
}
.utk-listview .unity-collection-view__item--selected {
color: var(--color-base-01);
}
/* Unity USS does not support :last-child pseudo-class */
/* .utk-listview .unity-list-view__item:last-child {
border-bottom-width: 0;
@@ -57,10 +53,13 @@
background-color: var(--color-btn-hover);
}
/* Unity ListView uses unity-collection-view__item--selected class internally */
.utk-listview .unity-collection-view__item--selected,
.utk-listview .unity-list-view__item--selected {
background-color: var(--color-btn-primary);
}
.utk-listview .unity-collection-view__item--selected:hover,
.utk-listview .unity-list-view__item--selected:hover {
background-color: var(--color-btn-primary-hover);
}
@@ -78,9 +77,11 @@
padding-bottom: 0 !important;
}
.utk-listview .unity-collection-view__item--selected .unity-label,
.utk-listview .unity-collection-view__item--selected .unity-text-element,
.utk-listview .unity-list-view__item--selected .unity-label,
.utk-listview .unity-list-view__item--selected .unity-text-element {
color: var(--color-base-01) !important;
color: var(--color-text-on-primary) !important;
}
/* ===================================
@@ -90,31 +91,6 @@
/* Note: Alternating rows must be handled in C# code
Unity USS does not support :nth-child() pseudo-class */
/* ===================================
Scrollbar
=================================== */
.utk-listview .unity-scroller--vertical > .unity-scroller__low-button,
.utk-listview .unity-scroller--vertical > .unity-scroller__high-button {
display: none;
}
.utk-listview .unity-scroller--vertical .unity-base-slider__tracker {
background-color: transparent;
border-width: 0;
}
.utk-listview .unity-scroller--vertical .unity-base-slider__dragger {
width: 6px;
border-radius: 3px;
background-color: var(--color-text-disabled);
border-width: 0;
}
.utk-listview .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-text-secondary);
}
/* ===================================
Empty State
=================================== */

View File

@@ -46,7 +46,11 @@
.utk-multicolumn-listview .unity-multi-column-header__column-title {
font-size: var(--font-size-body2);
-unity-font-style: bold;
color: var(--color-text-primary);
color: var(--color-text-primary) !important;
}
.utk-multicolumn-listview .unity-multi-column-header__column .unity-text-element {
color: var(--color-text-primary) !important;
}
/* ===================================
@@ -92,7 +96,7 @@
=================================== */
.utk-multicolumn-listview .unity-multi-column-view__cell {
padding: var(--space-s) var(--space-m);
padding: 0 var(--space-m);
border-right-width: 1px;
border-right-color: var(--color-border-light);
}
@@ -105,6 +109,8 @@
.utk-multicolumn-listview .unity-multi-column-view__cell > .unity-label {
font-size: var(--font-size-body2);
color: var(--color-text-primary);
padding-top: 0;
padding-bottom: 0;
}
.utk-multicolumn-listview .unity-list-view__item--selected .unity-multi-column-view__cell > .unity-label {
@@ -118,35 +124,6 @@
/* Note: Alternating rows must be handled in C# code
Unity USS does not support :nth-child() pseudo-class */
/* ===================================
Scrollbar
=================================== */
.utk-multicolumn-listview .unity-scroller--vertical {
width: 8px;
}
.utk-multicolumn-listview .unity-scroller--vertical > .unity-scroller__low-button,
.utk-multicolumn-listview .unity-scroller--vertical > .unity-scroller__high-button {
display: none;
}
.utk-multicolumn-listview .unity-scroller--vertical .unity-base-slider__tracker {
background-color: transparent;
border-width: 0;
}
.utk-multicolumn-listview .unity-scroller--vertical .unity-base-slider__dragger {
width: 6px;
border-radius: 3px;
background-color: var(--color-text-disabled);
border-width: 0;
}
.utk-multicolumn-listview .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-text-secondary);
}
/* ===================================
Sort Indicator
=================================== */

View File

@@ -46,7 +46,11 @@
.utk-multicolumn-treeview .unity-multi-column-header__column-title {
font-size: var(--font-size-body2);
-unity-font-style: bold;
color: var(--color-text-primary);
color: var(--color-text-primary) !important;
}
.utk-multicolumn-treeview .unity-multi-column-header__column .unity-text-element {
color: var(--color-text-primary) !important;
}
/* ===================================
@@ -100,10 +104,6 @@
transition-property: rotate;
}
.utk-multicolumn-treeview .unity-tree-view__item-toggle:checked {
rotate: 90deg;
}
.utk-multicolumn-treeview .unity-tree-view__item--selected .unity-tree-view__item-toggle {
-unity-background-image-tint-color: var(--color-text-on-primary);
}
@@ -125,9 +125,10 @@
=================================== */
.utk-multicolumn-treeview .unity-multi-column-view__cell {
padding: var(--space-s) var(--space-m);
padding: 0 var(--space-m);
border-right-width: 1px;
border-right-color: var(--color-border-light);
align-self: center;
}
/* Unity USS does not support :last-child pseudo-class */
@@ -144,35 +145,6 @@
color: var(--color-text-on-primary);
}
/* ===================================
Scrollbar
=================================== */
.utk-multicolumn-treeview .unity-scroller--vertical {
width: 8px;
}
.utk-multicolumn-treeview .unity-scroller--vertical > .unity-scroller__low-button,
.utk-multicolumn-treeview .unity-scroller--vertical > .unity-scroller__high-button {
display: none;
}
.utk-multicolumn-treeview .unity-scroller--vertical .unity-base-slider__tracker {
background-color: transparent;
border-width: 0;
}
.utk-multicolumn-treeview .unity-scroller--vertical .unity-base-slider__dragger {
width: 6px;
border-radius: 3px;
background-color: var(--color-text-disabled);
border-width: 0;
}
.utk-multicolumn-treeview .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-text-secondary);
}
/* ===================================
Sort Indicator
=================================== */

View File

@@ -22,7 +22,7 @@
=================================== */
.utk-treeview .unity-tree-view__item {
padding: var(--space-xs) 0;
padding: 0;
background-color: transparent;
transition-duration: var(--anim-fast);
transition-property: background-color;
@@ -32,10 +32,13 @@
background-color: var(--color-btn-hover);
}
/* Unity TreeView uses unity-collection-view__item--selected class internally */
.utk-treeview .unity-collection-view__item--selected,
.utk-treeview .unity-tree-view__item--selected {
background-color: var(--color-btn-primary);
}
.utk-treeview .unity-collection-view__item--selected:hover,
.utk-treeview .unity-tree-view__item--selected:hover {
background-color: var(--color-btn-primary-hover);
}
@@ -47,20 +50,30 @@
.utk-treeview .unity-tree-view__item-toggle {
width: 16px;
height: 16px;
margin-right: var(--space-xs);
-unity-background-image-tint-color: var(--color-text-secondary);
transition-duration: var(--anim-fast);
transition-property: rotate;
}
.utk-treeview .unity-tree-view__item-toggle:checked {
rotate: 0deg;
/* Override #unity-checkmark white color from UTKDefaultStyle.uss */
.utk-treeview .unity-tree-view__item-toggle #unity-checkmark {
-unity-background-image-tint-color: var(--color-text-secondary);
}
.utk-treeview .unity-tree-view__item-toggle:hover #unity-checkmark {
-unity-background-image-tint-color: var(--color-text-primary);
}
.utk-treeview .unity-collection-view__item--selected .unity-tree-view__item-toggle,
.utk-treeview .unity-tree-view__item--selected .unity-tree-view__item-toggle {
-unity-background-image-tint-color: var(--color-text-on-primary);
}
.utk-treeview .unity-collection-view__item--selected .unity-tree-view__item-toggle #unity-checkmark,
.utk-treeview .unity-tree-view__item--selected .unity-tree-view__item-toggle #unity-checkmark {
-unity-background-image-tint-color: var(--color-text-on-primary);
}
/* ===================================
Indentation
=================================== */
@@ -88,35 +101,7 @@
color: var(--color-text-primary);
}
.utk-treeview .unity-collection-view__item--selected .unity-tree-view__item-content > .unity-label,
.utk-treeview .unity-tree-view__item--selected .unity-tree-view__item-content > .unity-label {
color: var(--color-text-on-primary);
}
/* ===================================
Scrollbar
=================================== */
.utk-treeview .unity-scroller--vertical {
width: 8px;
}
.utk-treeview .unity-scroller--vertical > .unity-scroller__low-button,
.utk-treeview .unity-scroller--vertical > .unity-scroller__high-button {
display: none;
}
.utk-treeview .unity-scroller--vertical .unity-base-slider__tracker {
background-color: transparent;
border-width: 0;
}
.utk-treeview .unity-scroller--vertical .unity-base-slider__dragger {
width: 6px;
border-radius: 3px;
background-color: var(--color-text-disabled);
border-width: 0;
}
.utk-treeview .unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-text-secondary);
}

View File

@@ -11,10 +11,12 @@
.utk-alert {
flex-direction: column;
align-items: center;
min-width: 280px;
max-width: 400px;
padding: var(--space-xl);
padding-top: var(--space-m);
padding-left: var(--space-xl);
padding-right: var(--space-xl);
padding-bottom: var(--space-xl);
background-color: var(--color-bg-modal);
border-radius: var(--radius-l);
border-width: var(--border-width);
@@ -22,91 +24,33 @@
}
/* ===================================
Icon Container
=================================== */
.utk-alert__icon-container {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
align-items: center;
justify-content: center;
margin-bottom: var(--space-l);
}
.utk-alert__icon {
font-size: 24px;
-unity-text-align: middle-center;
}
/* ===================================
Type Variants - Icons
=================================== */
.utk-alert--info .utk-alert__icon-container {
background-color: var(--color-state-info);
}
.utk-alert--info .utk-alert__icon {
color: var(--color-text-on-primary);
}
.utk-alert--success .utk-alert__icon-container {
background-color: var(--color-state-success);
}
.utk-alert--success .utk-alert__icon {
color: var(--color-text-on-primary);
}
.utk-alert--warning .utk-alert__icon-container {
background-color: var(--color-state-warning);
}
.utk-alert--warning .utk-alert__icon {
color: var(--color-text-on-primary);
}
.utk-alert--error .utk-alert__icon-container {
background-color: var(--color-state-error);
}
.utk-alert--error .utk-alert__icon {
color: var(--color-text-on-primary);
}
.utk-alert--confirm .utk-alert__icon-container {
background-color: var(--color-btn-primary);
}
.utk-alert--confirm .utk-alert__icon {
color: var(--color-text-on-primary);
}
/* ===================================
Content
=================================== */
.utk-alert__content {
flex-direction: column;
align-items: center;
margin-bottom: var(--space-l);
}
/* ===================================
Title
Title (상단 왼쪽)
=================================== */
.utk-alert__title {
font-size: var(--font-size-h3);
color: var(--color-text-primary);
-unity-font-style: bold;
-unity-text-align: middle-center;
margin-bottom: var(--space-s);
-unity-text-align: middle-left;
margin: 0;
padding: 0;
align-self: flex-start;
}
/* ===================================
Message
Content (가운데)
=================================== */
.utk-alert__content {
flex-direction: column;
align-items: center;
flex-grow: 1;
margin-top: var(--space-l);
margin-bottom: var(--space-l);
}
/* ===================================
Message (가운데)
=================================== */
.utk-alert__message {
@@ -130,3 +74,27 @@
margin-left: var(--space-s);
margin-right: var(--space-s);
}
/* ===================================
Type Variants - Title Color
=================================== */
.utk-alert--info .utk-alert__title {
color: var(--color-text-primary);
}
.utk-alert--success .utk-alert__title {
color: var(--color-state-success);
}
.utk-alert--warning .utk-alert__title {
color: var(--color-state-warning);
}
.utk-alert--error .utk-alert__title {
color: var(--color-state-error);
}
.utk-alert--confirm .utk-alert__title {
color: var(--color-text-primary);
}

View File

@@ -21,6 +21,7 @@
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-l);
cursor: move;
}
.utk-color-picker__title {

View File

@@ -187,6 +187,67 @@
color: var(--color-text-disabled);
}
/* ===================================
Date Range Selection Styles
=================================== */
.utk-date-picker__day-btn--range-start {
background-color: var(--color-calendar-selected);
color: var(--color-text-on-primary);
-unity-font-style: bold;
border-top-left-radius: var(--radius-s);
border-bottom-left-radius: var(--radius-s);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.utk-date-picker__day-btn--range-start:hover {
background-color: var(--color-btn-primary-hover);
}
.utk-date-picker__day-btn--range-end {
background-color: var(--color-calendar-selected);
color: var(--color-text-on-primary);
-unity-font-style: bold;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: var(--radius-s);
border-bottom-right-radius: var(--radius-s);
}
.utk-date-picker__day-btn--range-end:hover {
background-color: var(--color-btn-primary-hover);
}
/* 시작일과 종료일이 같은 경우 */
.utk-date-picker__day-btn--range-start.utk-date-picker__day-btn--range-end {
border-radius: var(--radius-s);
}
.utk-date-picker__day-btn--in-range {
background-color: var(--color-calendar-range);
color: var(--color-text-primary);
border-radius: 0;
}
.utk-date-picker__day-btn--in-range:hover {
background-color: var(--color-calendar-range-hover);
}
/* ===================================
Range Info Label
=================================== */
.utk-date-picker__range-info {
margin-top: var(--space-m);
padding: var(--space-s) var(--space-m);
background-color: var(--color-bg-secondary);
border-radius: var(--radius-s);
font-size: var(--font-size-body2);
color: var(--color-text-secondary);
-unity-text-align: middle-center;
}
/* ===================================
Time Picker Row
=================================== */

View File

@@ -95,6 +95,11 @@
padding: var(--space-l);
}
.utk-panel__content .unity-label {
font-size: var(--font-size-body2);
color: var(--color-text-primary);
}
.utk-panel--collapsed .utk-panel__content {
display: none;
}

View File

@@ -20,56 +20,24 @@
}
/* ===================================
Icon
Type Variants - Message Color
Toast는 항상 어두운 배경을 사용하므로 밝은 색상 직접 지정
=================================== */
.utk-toast__icon {
width: 20px;
height: 20px;
margin-right: var(--space-m);
font-size: 14px;
-unity-text-align: middle-center;
border-radius: var(--radius-full);
.utk-toast--info .utk-toast__message {
color: #FFFFFF;
}
/* ===================================
Type Variants
=================================== */
.utk-toast--info {
border-left-width: 3px;
border-left-color: var(--color-state-info);
.utk-toast--success .utk-toast__message {
color: #4CAF50;
}
.utk-toast--info .utk-toast__icon {
color: var(--color-state-info);
.utk-toast--warning .utk-toast__message {
color: #FFB74D;
}
.utk-toast--success {
border-left-width: 3px;
border-left-color: var(--color-state-success);
}
.utk-toast--success .utk-toast__icon {
color: var(--color-state-success);
}
.utk-toast--warning {
border-left-width: 3px;
border-left-color: var(--color-state-warning);
}
.utk-toast--warning .utk-toast__icon {
color: var(--color-state-warning);
}
.utk-toast--error {
border-left-width: 3px;
border-left-color: var(--color-state-error);
}
.utk-toast--error .utk-toast__icon {
color: var(--color-state-error);
.utk-toast--error .utk-toast__message {
color: #EF5350;
}
/* ===================================
@@ -79,7 +47,7 @@
.utk-toast__message {
flex-grow: 1;
font-size: var(--font-size-body2);
color: var(--color-text-primary);
color: #FFFFFF;
white-space: normal;
}

View File

@@ -354,13 +354,17 @@ ListView/TreeView 항목 텍스트 스타일
color: var(--color-base-01) !important;
}
.unity-tree-view__item-content {
align-self: center;
}
/* TreeView 항목 텍스트 색상 */
.unity-tree-view__item .unity-label,
.unity-tree-view__item .unity-text-element {
color: var(--color-text-primary) !important;
-unity-text-align: middle-left;
padding-top: 0;
padding-bottom: 0;
padding: 0;
margin: 0;
}
.unity-tree-view__item--selected .unity-label,
@@ -376,12 +380,16 @@ TreeView 항목 스타일
/* TreeView 펼치기/접기 화살표 토글 - 크기 및 정렬 조정 */
.unity-tree-view__item-toggle {
margin-right: 4px;
margin-top: 0;
margin-bottom: 0;
margin-right: 0;
margin-left: 0;
width: 16px;
height: 16px;
min-width: 16px;
min-height: 16px;
padding: 0;
align-self: center;
-unity-background-image-tint-color: var(--color-text-secondary);
}
@@ -557,6 +565,10 @@ SetupDraggerEvents() 메서드처럼 코드로 MouseEnterEvent/MouseLeaveEvent
transition-property: background-color;
}
.unity-scroller--vertical .unity-base-slider__dragger:hover {
background-color: var(--color-scroller-dragger-hover);
}
/* 상하 화살표 버튼 - 완전히 숨김 처리 */
.unity-scroller--vertical .unity-repeat-button {
display: none;
@@ -566,6 +578,11 @@ SetupDraggerEvents() 메서드처럼 코드로 MouseEnterEvent/MouseLeaveEvent
min-height: 0;
}
.unity-scroller--vertical > .unity-scroller__low-button,
.unity-scroller--vertical > .unity-scroller__high-button {
display: none;
}
/* 슬라이더 - 여백 제거 */
.unity-scroller--vertical .unity-slider {
margin: 0;
@@ -614,6 +631,10 @@ SetupDraggerEvents() 메서드처럼 코드로 MouseEnterEvent/MouseLeaveEvent
transition-property: background-color;
}
.unity-scroller--horizontal .unity-base-slider__dragger:hover {
background-color: var(--color-scroller-dragger-hover);
}
/* 좌우 화살표 버튼 - 완전히 숨김 처리 */
.unity-scroller--horizontal .unity-repeat-button {
display: none;
@@ -623,6 +644,11 @@ SetupDraggerEvents() 메서드처럼 코드로 MouseEnterEvent/MouseLeaveEvent
min-height: 0;
}
.unity-scroller--horizontal > .unity-scroller__low-button,
.unity-scroller--horizontal > .unity-scroller__high-button {
display: none;
}
/* 슬라이더 - 여백 제거 */
.unity-scroller--horizontal .unity-slider {
margin: 0;

View File

@@ -161,6 +161,8 @@
--color-calendar-saturday: var(--color-blue-02);
--color-calendar-today: var(--color-seti-orange);
--color-calendar-selected: var(--color-blue-06);
--color-calendar-range: rgba(66, 133, 244, 0.25);
--color-calendar-range-hover: rgba(66, 133, 244, 0.35);
/* ===================================
Semantic Colors - State (color-state-*)

View File

@@ -168,6 +168,8 @@
--color-calendar-saturday: var(--color-blue-02);
--color-calendar-today: var(--color-orange-02);
--color-calendar-selected: var(--color-blue-02);
--color-calendar-range: rgba(66, 133, 244, 0.15);
--color-calendar-range-hover: rgba(66, 133, 244, 0.25);
/* ===================================
Semantic Colors - State (color-state-*)

View File

@@ -1,95 +0,0 @@
/*
* ===================================
* UTKTab.uss
* Unity Tab 래핑 스타일
* ===================================
*/
/* ===================================
Base Tab
=================================== */
.utk-tab {
flex-direction: row;
align-items: center;
justify-content: center;
padding: var(--space-m) var(--space-l);
background-color: transparent;
border-width: 0;
border-bottom-width: 2px;
border-bottom-color: transparent;
cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
transition-duration: var(--anim-fast);
transition-property: background-color, border-color, color;
}
.utk-tab:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.utk-tab--selected {
border-bottom-color: var(--color-btn-primary);
}
/* ===================================
Tab Header (Label Container)
=================================== */
.utk-tab > .unity-tab__header {
flex-direction: row;
align-items: center;
justify-content: center;
background-color: transparent;
border-width: 0;
padding: 0;
margin: 0;
}
/* ===================================
Tab Header Label
=================================== */
.utk-tab > .unity-tab__header > .unity-tab__header-label {
font-size: var(--font-size-body2);
color: var(--color-text-secondary);
margin: 0;
padding: 0;
}
.utk-tab--selected > .unity-tab__header > .unity-tab__header-label {
color: var(--color-btn-primary);
-unity-font-style: bold;
}
/* ===================================
Tab Content Container
=================================== */
.utk-tab > .unity-tab__content-container {
flex-grow: 1;
padding: var(--space-m);
}
/* ===================================
Hide Tab Underline (Use Custom)
=================================== */
.utk-tab > .unity-tab__header > .unity-tab__header-underline {
display: none;
}
/* ===================================
Disabled State
=================================== */
.utk-tab--disabled {
cursor: arrow;
}
.utk-tab--disabled:hover {
background-color: transparent;
}
.utk-tab--disabled > .unity-tab__header > .unity-tab__header-label {
color: var(--color-text-disabled);
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 26349aa08d395b241b70ebcbf9b99671
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -12,6 +12,7 @@
.utk-tabview {
flex-direction: column;
flex-grow: 1;
color: var(--color-text-secondary);
}
/* ===================================
@@ -42,47 +43,57 @@
padding: var(--space-l);
}
/* Tab Content inside unity-tab__content-container */
.utk-tabview .unity-tab__content-container {
background-color: transparent;
}
.utk-tabview .unity-tab__content-container .unity-label {
font-size: var(--font-size-body2);
color: var(--color-text-primary);
}
/* ===================================
Tab styling inside TabView
Tab Header inside TabView
=================================== */
.utk-tabview > .unity-tab-view__header-container > .unity-tab {
padding: var(--space-m) var(--space-l);
background-color: transparent;
.utk-tabview .unity-tab__header {
background-color: transparent !important;
border-width: 0;
border-bottom-width: 2px;
border-bottom-color: transparent;
padding: var(--space-s) var(--space-l);
margin-right: var(--space-s);
transition-duration: var(--anim-fast);
transition-property: background-color, border-color;
transition-property: background-color;
}
.utk-tabview > .unity-tab-view__header-container > .unity-tab:hover {
background-color: rgba(255, 255, 255, 0.05);
.utk-tabview .unity-tab__header:hover {
background-color: var(--color-btn-hover) !important;
}
.utk-tabview > .unity-tab-view__header-container > .unity-tab:checked {
border-bottom-color: var(--color-btn-primary);
.utk-tabview .unity-tab__header:checked {
background-color: transparent !important;
}
/* Tab Header inside TabView */
.utk-tabview > .unity-tab-view__header-container > .unity-tab > .unity-tab__header {
background-color: transparent;
border-width: 0;
padding: 0;
margin: 0;
}
.utk-tabview > .unity-tab-view__header-container > .unity-tab > .unity-tab__header > .unity-tab__header-label {
.utk-tabview .unity-tab__header-label {
font-size: var(--font-size-body2);
color: var(--color-text-secondary);
color: var(--color-text-secondary) !important;
}
.utk-tabview > .unity-tab-view__header-container > .unity-tab:checked > .unity-tab__header > .unity-tab__header-label {
color: var(--color-btn-primary);
.utk-tabview .unity-tab__header:checked .unity-tab__header-label {
color: var(--color-btn-primary) !important;
-unity-font-style: bold;
}
/* Hide underline (use custom border) */
.utk-tabview > .unity-tab-view__header-container > .unity-tab > .unity-tab__header > .unity-tab__header-underline {
display: none;
/* Tab Underline */
.utk-tabview .unity-tab__header-underline {
height: 2px;
background-color: transparent !important;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.utk-tabview .unity-tab__header:checked > .unity-tab__header-underline {
background-color: var(--color-btn-primary) !important;
}

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Modal;
@@ -105,7 +106,7 @@ namespace UVC.Sample.UIToolkit
alphaRow.style.marginBottom = 15;
_useAlphaToggle = new Toggle("Use Alpha Channel");
_useAlphaToggle.value = false;
_useAlphaToggle.value = true;
_useAlphaToggle.style.color = Color.white;
alphaRow.Add(_useAlphaToggle);
@@ -117,6 +118,12 @@ namespace UVC.Sample.UIToolkit
openPickerBtn.style.marginBottom = 10;
container.Add(openPickerBtn);
// Async 버튼
var openPickerAsyncBtn = new Button(() => OpenColorPickerAsync().Forget()) { text = "Open Color Picker (Async)" };
openPickerAsyncBtn.style.height = 32;
openPickerAsyncBtn.style.marginBottom = 10;
container.Add(openPickerAsyncBtn);
// 프리셋 색상 버튼들
var presetLabel = new Label("Preset Colors:");
presetLabel.style.color = Color.white;
@@ -162,6 +169,30 @@ namespace UVC.Sample.UIToolkit
_currentPicker.OnColorSelected += OnColorSelected;
}
private async UniTaskVoid OpenColorPickerAsync()
{
if (_root == null) return;
bool useAlpha = _useAlphaToggle?.value ?? true;
// ShowAsync를 사용하여 색상 선택 대기
// OK 클릭 시 선택된 색상 반환, 취소/닫기 시 _currentColor 반환
Color selectedColor = await UTKColorPicker.ShowAsync(_root, _currentColor, "Select Color (Async)", useAlpha);
// 결과 처리
_currentColor = selectedColor;
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = selectedColor;
}
if (_colorLabel != null)
{
_colorLabel.text = ColorToHex(selectedColor);
}
Debug.Log($"[Async] Color Result: {ColorToHex(selectedColor)}");
}
private void OnColorChanged(Color color)
{
// 실시간 미리보기 업데이트

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Locale;
@@ -18,9 +19,12 @@ namespace UVC.Sample.UIToolkit
private VisualElement? _root;
private Label? _dateLabel;
private Label? _dateTimeLabel;
private Label? _dateRangeLabel;
private DateTime _selectedDate = DateTime.Today;
private DateTime _selectedDateTime = DateTime.Now;
private DateTime _rangeStartDate = DateTime.Today;
private DateTime _rangeEndDate = DateTime.Today.AddDays(7);
private UTKDatePicker? _currentPicker;
private void Start()
@@ -78,6 +82,44 @@ namespace UVC.Sample.UIToolkit
_dateTimeLabel.text = FormatDateTime(_selectedDateTime);
container.Add(dateTimeSection.container);
// Async 섹션
var asyncSection = new VisualElement();
asyncSection.style.marginBottom = 15;
var asyncLabel = new Label("Async/Await Mode");
asyncLabel.style.color = new Color(0.8f, 0.8f, 0.8f);
asyncLabel.style.fontSize = 12;
asyncLabel.style.marginBottom = 5;
asyncSection.Add(asyncLabel);
var asyncBtn = new Button(() => OpenDatePickerAsync().Forget()) { text = "Open Date Picker (Async)" };
asyncBtn.style.height = 32;
asyncSection.Add(asyncBtn);
container.Add(asyncSection);
// Date Range 섹션
var rangeSection = CreateSection("Date Range Mode", "Open Range Picker", OpenDateRangePicker);
_dateRangeLabel = rangeSection.label;
_dateRangeLabel.text = FormatDateRange(_rangeStartDate, _rangeEndDate);
container.Add(rangeSection.container);
// Date Range Async 섹션
var rangeAsyncSection = new VisualElement();
rangeAsyncSection.style.marginBottom = 15;
var rangeAsyncLabel = new Label("Date Range Async Mode");
rangeAsyncLabel.style.color = new Color(0.8f, 0.8f, 0.8f);
rangeAsyncLabel.style.fontSize = 12;
rangeAsyncLabel.style.marginBottom = 5;
rangeAsyncSection.Add(rangeAsyncLabel);
var rangeAsyncBtn = new Button(() => OpenDateRangePickerAsync().Forget()) { text = "Open Range Picker (Async)" };
rangeAsyncBtn.style.height = 32;
rangeAsyncSection.Add(rangeAsyncBtn);
container.Add(rangeAsyncSection);
// 구분선
var separator = new VisualElement();
separator.style.height = 1;
@@ -185,6 +227,34 @@ namespace UVC.Sample.UIToolkit
_currentPicker.OnClosed += OnPickerClosed;
}
private async UniTaskVoid OpenDatePickerAsync()
{
if (_root == null) return;
// ShowAsync를 사용하여 날짜 선택 대기
// OK 클릭 시 선택된 날짜 반환, 취소/닫기 시 null 반환
DateTime? result = await UTKDatePicker.ShowAsync(
_root,
_selectedDate,
UTKDatePicker.PickerMode.DateOnly,
"Select Date (Async)"
);
if (result.HasValue)
{
_selectedDate = result.Value;
if (_dateLabel != null)
{
_dateLabel.text = FormatDate(result.Value);
}
Debug.Log($"[Async] Date Result: {FormatDate(result.Value)}");
}
else
{
Debug.Log("[Async] Date selection cancelled");
}
}
private void OnDateSelected(DateTime date)
{
_selectedDate = date;
@@ -245,12 +315,75 @@ namespace UVC.Sample.UIToolkit
return dateTime.ToString("yyyy-MM-dd HH:mm");
}
private string FormatDateRange(DateTime start, DateTime end)
{
return $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}";
}
private void OpenDateRangePicker()
{
if (_root == null || _currentPicker != null) return;
_currentPicker = UTKDatePicker.ShowRange(
_root,
_rangeStartDate,
_rangeEndDate,
false,
"Select Date Range"
);
_currentPicker.OnDateRangeSelected += OnDateRangeSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private async UniTaskVoid OpenDateRangePickerAsync()
{
if (_root == null) return;
// ShowRangeAsync를 사용하여 날짜 범위 선택 대기
// OK 클릭 시 선택된 범위 반환, 취소/닫기 시 null 반환
var result = await UTKDatePicker.ShowRangeAsync(
_root,
_rangeStartDate,
_rangeEndDate,
false,
"Select Date Range (Async)"
);
if (result.HasValue)
{
_rangeStartDate = result.Value.Start;
_rangeEndDate = result.Value.End;
if (_dateRangeLabel != null)
{
_dateRangeLabel.text = FormatDateRange(result.Value.Start, result.Value.End);
}
Debug.Log($"[Async] Range Result: {FormatDateRange(result.Value.Start, result.Value.End)}");
}
else
{
Debug.Log("[Async] Date range selection cancelled");
}
}
private void OnDateRangeSelected(DateTime start, DateTime end)
{
_rangeStartDate = start;
_rangeEndDate = end;
if (_dateRangeLabel != null)
{
_dateRangeLabel.text = FormatDateRange(start, end);
}
Debug.Log($"Date Range Selected: {FormatDateRange(start, end)}");
}
private void OnDestroy()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnDateSelected;
_currentPicker.OnDateSelected -= OnDateTimeSelected;
_currentPicker.OnDateRangeSelected -= OnDateRangeSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Dispose();
_currentPicker = null;

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
@@ -787,6 +788,21 @@ public class UTKStyleGuideSample : MonoBehaviour
}),
}),
new TreeViewItemData<string>(9, "Parent 3"),
new TreeViewItemData<string>(10, "Parent 4", new List<TreeViewItemData<string>>
{
new TreeViewItemData<string>(11, "Child 4-1"),
new TreeViewItemData<string>(12, "Child 4-2"),
}),
new TreeViewItemData<string>(13, "Parent 5", new List<TreeViewItemData<string>>
{
new TreeViewItemData<string>(14, "Child 5-1"),
new TreeViewItemData<string>(15, "Child 5-2", new List<TreeViewItemData<string>>
{
new TreeViewItemData<string>(16, "Grandchild 5-2-1"),
new TreeViewItemData<string>(17, "Grandchild 5-2-2"),
}),
}),
new TreeViewItemData<string>(18, "Parent 6"),
};
treeView.SetRootItems(treeItems);
@@ -794,31 +810,138 @@ public class UTKStyleGuideSample : MonoBehaviour
container.Add(treeView);
}
/// <summary>
/// MultiColumnListView 샘플 데이터 클래스
/// </summary>
private class SampleListItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Status { get; set; } = "";
public int Progress { get; set; }
}
private void CreateMultiColumnListViewPreview(VisualElement container)
{
AddDescription(container, "멀티 컬럼 리스트 뷰 (정렬 가능한 테이블)");
var placeholder = new Label("MultiColumnListView requires Columns and data binding setup.");
placeholder.style.color = new Color(0.6f, 0.6f, 0.6f);
container.Add(placeholder);
var listView = new UTKMultiColumnListView();
listView.style.height = 200;
listView.style.width = 400;
listView.style.width = 450;
// 컬럼 정의
listView.columns.Add(new Column { name = "id", title = "ID", width = 50 });
listView.columns.Add(new Column { name = "name", title = "Name", width = 150, stretchable = true });
listView.columns.Add(new Column { name = "status", title = "Status", width = 100 });
listView.columns.Add(new Column { name = "progress", title = "Progress", width = 80 });
// 샘플 데이터
var items = new List<SampleListItem>
{
new() { Id = 1, Name = "Task Alpha", Status = "Active", Progress = 75 },
new() { Id = 2, Name = "Task Beta", Status = "Pending", Progress = 30 },
new() { Id = 3, Name = "Task Gamma", Status = "Completed", Progress = 100 },
new() { Id = 4, Name = "Task Delta", Status = "Active", Progress = 50 },
new() { Id = 5, Name = "Task Epsilon", Status = "Cancelled", Progress = 0 },
new() { Id = 6, Name = "Task Zeta", Status = "Active", Progress = 90 },
new() { Id = 7, Name = "Task Eta", Status = "Pending", Progress = 15 },
new() { Id = 8, Name = "Task Theta", Status = "Completed", Progress = 100 },
};
listView.itemsSource = items;
listView.fixedItemHeight = 24;
// 셀 생성 및 바인딩
listView.columns["id"].makeCell = () => new Label();
listView.columns["id"].bindCell = (e, i) => ((Label)e).text = items[i].Id.ToString();
listView.columns["name"].makeCell = () => new Label();
listView.columns["name"].bindCell = (e, i) => ((Label)e).text = items[i].Name;
listView.columns["status"].makeCell = () => new Label();
listView.columns["status"].bindCell = (e, i) => ((Label)e).text = items[i].Status;
listView.columns["progress"].makeCell = () => new Label();
listView.columns["progress"].bindCell = (e, i) => ((Label)e).text = $"{items[i].Progress}%";
container.Add(listView);
}
/// <summary>
/// MultiColumnTreeView 샘플 데이터 클래스
/// </summary>
private class SampleTreeItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public string Size { get; set; } = "";
}
private void CreateMultiColumnTreeViewPreview(VisualElement container)
{
AddDescription(container, "멀티 컬럼 트리 뷰 (계층적 테이블)");
var placeholder = new Label("MultiColumnTreeView requires Columns and TreeViewItemData setup.");
placeholder.style.color = new Color(0.6f, 0.6f, 0.6f);
container.Add(placeholder);
var treeView = new UTKMultiColumnTreeView();
treeView.style.height = 200;
treeView.style.width = 400;
treeView.style.height = 220;
treeView.style.width = 450;
// 컬럼 정의
treeView.columns.Add(new Column { name = "name", title = "Name", width = 200, stretchable = true });
treeView.columns.Add(new Column { name = "type", title = "Type", width = 100 });
treeView.columns.Add(new Column { name = "size", title = "Size", width = 80 });
// 샘플 데이터 (계층 구조)
var rootItems = new List<TreeViewItemData<SampleTreeItem>>
{
new(1, new SampleTreeItem { Id = 1, Name = "Documents", Type = "Folder", Size = "2.5 GB" },
new List<TreeViewItemData<SampleTreeItem>>
{
new(11, new SampleTreeItem { Id = 11, Name = "Reports", Type = "Folder", Size = "500 MB" },
new List<TreeViewItemData<SampleTreeItem>>
{
new(111, new SampleTreeItem { Id = 111, Name = "Q1_Report.pdf", Type = "PDF", Size = "2.3 MB" }),
new(112, new SampleTreeItem { Id = 112, Name = "Q2_Report.pdf", Type = "PDF", Size = "1.8 MB" }),
}),
new(12, new SampleTreeItem { Id = 12, Name = "Images", Type = "Folder", Size = "1.2 GB" }),
}),
new(2, new SampleTreeItem { Id = 2, Name = "Projects", Type = "Folder", Size = "5.0 GB" },
new List<TreeViewItemData<SampleTreeItem>>
{
new(21, new SampleTreeItem { Id = 21, Name = "ProjectA", Type = "Folder", Size = "2.0 GB" }),
new(22, new SampleTreeItem { Id = 22, Name = "ProjectB", Type = "Folder", Size = "3.0 GB" }),
}),
new(3, new SampleTreeItem { Id = 3, Name = "Config.json", Type = "JSON", Size = "4 KB" }),
};
treeView.SetRootItems(rootItems);
treeView.fixedItemHeight = 22;
// 셀 생성 및 바인딩
treeView.columns["name"].makeCell = () => new Label();
treeView.columns["name"].bindCell = (e, i) =>
{
var item = treeView.GetItemDataForIndex<SampleTreeItem>(i);
((Label)e).text = item.Name;
};
treeView.columns["type"].makeCell = () => new Label();
treeView.columns["type"].bindCell = (e, i) =>
{
var item = treeView.GetItemDataForIndex<SampleTreeItem>(i);
((Label)e).text = item.Type;
};
treeView.columns["size"].makeCell = () => new Label();
treeView.columns["size"].bindCell = (e, i) =>
{
var item = treeView.GetItemDataForIndex<SampleTreeItem>(i);
((Label)e).text = item.Size;
};
// 기본 확장
treeView.ExpandAll();
container.Add(treeView);
}
@@ -840,16 +963,50 @@ public class UTKStyleGuideSample : MonoBehaviour
{
AddDescription(container, "스크롤 뷰 컴포넌트");
var scrollView = new UTKScrollView();
scrollView.style.height = 150;
scrollView.style.width = 300;
var row = CreateRow(container, "ScrollViews");
// 세로 스크롤
var verticalScroll = new UTKScrollView();
verticalScroll.style.height = 150;
verticalScroll.style.width = 200;
for (int i = 1; i <= 15; i++)
{
scrollView.Add(new Label($"Scrollable Item {i}"));
verticalScroll.Add(new Label($"Vertical Item {i}"));
}
row.Add(verticalScroll);
container.Add(scrollView);
// 가로 스크롤
var horizontalScroll = new UTKScrollView();
horizontalScroll.mode = ScrollViewMode.Horizontal;
horizontalScroll.style.height = 150;
horizontalScroll.style.width = 200;
var horizontalContent = new VisualElement();
horizontalContent.style.flexDirection = FlexDirection.Row;
for (int i = 1; i <= 15; i++)
{
var item = new Label($"H-Item {i}");
item.style.minWidth = 80;
item.style.marginRight = 8;
horizontalContent.Add(item);
}
horizontalScroll.Add(horizontalContent);
row.Add(horizontalScroll);
// 양방향 스크롤
var bothScroll = new UTKScrollView();
bothScroll.mode = ScrollViewMode.VerticalAndHorizontal;
bothScroll.style.height = 150;
bothScroll.style.width = 200;
for (int i = 1; i <= 10; i++)
{
var item = new Label($"This is a very long scrollable item number {i} that requires horizontal scrolling");
item.style.whiteSpace = WhiteSpace.NoWrap;
bothScroll.Add(item);
}
row.Add(bothScroll);
}
#endregion
@@ -909,23 +1066,102 @@ public class UTKStyleGuideSample : MonoBehaviour
{
if (_root == null) return;
AddDescription(container, "알림 다이얼로그");
AddDescription(container, "알림 다이얼로그 (async/await 지원)");
var row = CreateRow(container, "Alerts");
// Alert Types (Async)
var row1 = CreateRow(container, "Alert Types (Async)");
var infoBtn = new UTKButton("Show Info", "", UTKButton.ButtonVariant.Primary);
infoBtn.OnClicked += () => UTKAlert.ShowInfo(_root, "Information", "This is an info message.");
row.Add(infoBtn);
UTKAlert.SetRoot(_root);
var warnBtn = new UTKButton("Show Warning", "", UTKButton.ButtonVariant.Normal);
warnBtn.OnClicked += () => UTKAlert.ShowWarning(_root, "Warning", "This is a warning message.");
row.Add(warnBtn);
var infoBtn = new UTKButton("Info", "", UTKButton.ButtonVariant.Primary);
infoBtn.OnClicked += () => ShowInfoAlertAsync().Forget();
row1.Add(infoBtn);
var confirmBtn = new UTKButton("Show Confirm", "", UTKButton.ButtonVariant.Normal);
confirmBtn.OnClicked += () => UTKAlert.ShowConfirm(_root, "Confirm", "Are you sure?",
() => Debug.Log("Confirmed!"),
() => Debug.Log("Cancelled!"));
row.Add(confirmBtn);
var successBtn = new UTKButton("Success", "", UTKButton.ButtonVariant.Normal);
successBtn.OnClicked += () => ShowSuccessAlertAsync().Forget();
row1.Add(successBtn);
var warnBtn = new UTKButton("Warning", "", UTKButton.ButtonVariant.Normal);
warnBtn.OnClicked += () => ShowWarningAlertAsync().Forget();
row1.Add(warnBtn);
var errorBtn = new UTKButton("Error", "", UTKButton.ButtonVariant.Danger);
errorBtn.OnClicked += () => ShowErrorAlertAsync().Forget();
row1.Add(errorBtn);
// Confirm Dialog (Async)
var row2 = CreateRow(container, "Confirm Dialog (Async)");
var confirmBtn = new UTKButton("Confirm", "", UTKButton.ButtonVariant.Normal);
confirmBtn.OnClicked += () => ShowConfirmAlertAsync().Forget();
row2.Add(confirmBtn);
var confirmCustomBtn = new UTKButton("Custom Labels", "", UTKButton.ButtonVariant.Normal);
confirmCustomBtn.OnClicked += () => ShowConfirmCustomLabelsAsync().Forget();
row2.Add(confirmCustomBtn);
// Callback Style (Non-Async)
var row3 = CreateRow(container, "Callback Style");
var callbackBtn = new UTKButton("With Callback", "", UTKButton.ButtonVariant.OutlineNormal);
callbackBtn.OnClicked += () =>
{
UTKAlert.ShowInfo(_root, "Callback Style", "This uses callback instead of async.",
onClose: () => Debug.Log("Alert closed via callback"));
};
row3.Add(callbackBtn);
var confirmCallbackBtn = new UTKButton("Confirm Callback", "", UTKButton.ButtonVariant.OutlineNormal);
confirmCallbackBtn.OnClicked += () =>
{
UTKAlert.ShowConfirm(_root, "Confirm", "Do you want to proceed?",
onConfirm: () => Debug.Log("Confirmed via callback!"),
onCancel: () => Debug.Log("Cancelled via callback!"));
};
row3.Add(confirmCallbackBtn);
}
private async UniTaskVoid ShowInfoAlertAsync()
{
if (_root == null) return;
await UTKAlert.ShowInfoAsync("Information", "This is an info message.");
Debug.Log("Info alert closed");
}
private async UniTaskVoid ShowSuccessAlertAsync()
{
if (_root == null) return;
await UTKAlert.ShowSuccessAsync("Success", "Operation completed successfully!", closeOnBlockerClick: true);
Debug.Log("Success alert closed");
}
private async UniTaskVoid ShowWarningAlertAsync()
{
if (_root == null) return;
await UTKAlert.ShowWarningAsync("Warning", "This is a warning message.");
Debug.Log("Warning alert closed");
}
private async UniTaskVoid ShowErrorAlertAsync()
{
if (_root == null) return;
await UTKAlert.ShowErrorAsync("Error", "An error has occurred!");
Debug.Log("Error alert closed");
}
private async UniTaskVoid ShowConfirmAlertAsync()
{
if (_root == null) return;
bool result = await UTKAlert.ShowConfirmAsync("Confirm", "Are you sure?");
Debug.Log(result ? "Confirmed!" : "Cancelled!");
}
private async UniTaskVoid ShowConfirmCustomLabelsAsync()
{
if (_root == null) return;
bool result = await UTKAlert.ShowConfirmAsync("Delete Item", "Are you sure you want to delete this item?",
confirmLabel: "Delete", cancelLabel: "Keep");
Debug.Log(result ? "Item deleted!" : "Item kept!");
}
private void CreateToastPreview(VisualElement container)
@@ -936,16 +1172,22 @@ public class UTKStyleGuideSample : MonoBehaviour
var row = CreateRow(container, "Toasts");
UTKToast.SetRoot(_root);
var infoBtn = new UTKButton("Info Toast", "", UTKButton.ButtonVariant.Primary);
infoBtn.OnClicked += () => UTKToast.ShowInfo(_root, "This is an info toast!", 3000);
infoBtn.OnClicked += () => UTKToast.Show("This is an info toast!This is an info toast!This is an info toast!This is an info toast!This is an info toast!");
row.Add(infoBtn);
var successBtn = new UTKButton("Success Toast", "", UTKButton.ButtonVariant.Normal);
successBtn.OnClicked += () => UTKToast.ShowSuccess(_root, "Operation successful!", 3000);
successBtn.OnClicked += () => UTKToast.ShowSuccess("Operation successful!");
row.Add(successBtn);
var warningBtn = new UTKButton("Warning Toast", "", UTKButton.ButtonVariant.Normal);
warningBtn.OnClicked += () => UTKToast.ShowWarning("Warning: Check your input!");
row.Add(warningBtn);
var errorBtn = new UTKButton("Error Toast", "", UTKButton.ButtonVariant.Danger);
errorBtn.OnClicked += () => UTKToast.ShowError(_root, "An error occurred!", 3000);
errorBtn.OnClicked += () => UTKToast.ShowError("An error occurred!");
row.Add(errorBtn);
}
@@ -968,37 +1210,227 @@ public class UTKStyleGuideSample : MonoBehaviour
#region Picker Previews
private Color _selectedColor = Color.white;
private VisualElement? _colorPreviewBox;
private Label? _colorHexLabel;
private void CreateColorPickerPreview(VisualElement container)
{
AddDescription(container, "색상 선택 컴포넌트");
if (_root == null) return;
var colorLabel = new Label("Selected: #FFFFFFFF");
container.Add(colorLabel);
AddDescription(container, "색상 선택 컴포넌트 (버튼 클릭으로 모달 표시)");
var colorPicker = new UTKColorPicker();
colorPicker.style.position = Position.Relative;
colorPicker.SetColor(Color.white);
colorPicker.OnColorChanged += (color) =>
{
colorLabel.text = $"Selected: #{ColorUtility.ToHtmlStringRGBA(color)}";
};
container.Add(colorPicker);
// 현재 색상 미리보기
var previewRow = CreateRow(container, "Current Color");
_colorPreviewBox = new VisualElement();
_colorPreviewBox.style.width = 60;
_colorPreviewBox.style.height = 30;
_colorPreviewBox.style.backgroundColor = _selectedColor;
_colorPreviewBox.style.borderTopLeftRadius = 4;
_colorPreviewBox.style.borderTopRightRadius = 4;
_colorPreviewBox.style.borderBottomLeftRadius = 4;
_colorPreviewBox.style.borderBottomRightRadius = 4;
_colorPreviewBox.style.borderTopWidth = 1;
_colorPreviewBox.style.borderBottomWidth = 1;
_colorPreviewBox.style.borderLeftWidth = 1;
_colorPreviewBox.style.borderRightWidth = 1;
_colorPreviewBox.style.borderTopColor = new Color(0.4f, 0.4f, 0.4f);
_colorPreviewBox.style.borderBottomColor = new Color(0.4f, 0.4f, 0.4f);
_colorPreviewBox.style.borderLeftColor = new Color(0.4f, 0.4f, 0.4f);
_colorPreviewBox.style.borderRightColor = new Color(0.4f, 0.4f, 0.4f);
previewRow.Add(_colorPreviewBox);
_colorHexLabel = new Label($"#{ColorUtility.ToHtmlStringRGBA(_selectedColor)}");
_colorHexLabel.style.marginLeft = 10;
previewRow.Add(_colorHexLabel);
// 버튼들
var row1 = CreateRow(container, "With Alpha");
var withAlphaBtn = new UTKButton("Open Color Picker (Alpha)", "", UTKButton.ButtonVariant.Primary);
withAlphaBtn.OnClicked += () => OpenColorPicker(true);
row1.Add(withAlphaBtn);
var row2 = CreateRow(container, "Without Alpha");
var withoutAlphaBtn = new UTKButton("Open Color Picker (No Alpha)", "", UTKButton.ButtonVariant.Normal);
withoutAlphaBtn.OnClicked += () => OpenColorPicker(false);
row2.Add(withoutAlphaBtn);
// Async 버튼
var row3 = CreateRow(container, "Async/Await");
var asyncBtn = new UTKButton("Open Color Picker (Async)", "", UTKButton.ButtonVariant.Normal);
asyncBtn.OnClicked += () => OpenColorPickerAsync().Forget();
row3.Add(asyncBtn);
}
private void OpenColorPicker(bool useAlpha)
{
if (_root == null) return;
var picker = UTKColorPicker.Show(_root, _selectedColor, "Select Color", useAlpha);
picker.OnColorChanged += (color) =>
{
// 실시간 미리보기 업데이트
if (_colorPreviewBox != null)
_colorPreviewBox.style.backgroundColor = color;
if (_colorHexLabel != null)
_colorHexLabel.text = useAlpha
? $"#{ColorUtility.ToHtmlStringRGBA(color)}"
: $"#{ColorUtility.ToHtmlStringRGB(color)}";
};
picker.OnColorSelected += (color) =>
{
_selectedColor = color;
Debug.Log($"Color Selected: #{(useAlpha ? ColorUtility.ToHtmlStringRGBA(color) : ColorUtility.ToHtmlStringRGB(color))}");
};
}
private async UniTaskVoid OpenColorPickerAsync()
{
if (_root == null) return;
// ShowAsync를 사용하여 색상 선택 대기
// OK 클릭 시 선택된 색상 반환, 취소/닫기 시 _selectedColor 반환
Color result = await UTKColorPicker.ShowAsync(_root, _selectedColor, "Select Color (Async)", useAlpha: true);
// 결과 처리
_selectedColor = result;
if (_colorPreviewBox != null)
_colorPreviewBox.style.backgroundColor = result;
if (_colorHexLabel != null)
_colorHexLabel.text = $"#{ColorUtility.ToHtmlStringRGBA(result)}";
Debug.Log($"[Async] Color Result: #{ColorUtility.ToHtmlStringRGBA(result)}");
}
private DateTime _selectedDate = DateTime.Today;
private DateTime _rangeStartDate = DateTime.Today;
private DateTime _rangeEndDate = DateTime.Today.AddDays(7);
private Label? _dateLabel;
private Label? _rangeDateLabel;
private void CreateDatePickerPreview(VisualElement container)
{
AddDescription(container, "날짜 선택 컴포넌트");
if (_root == null) return;
var dateLabel = new Label($"Selected: {DateTime.Now:yyyy-MM-dd}");
container.Add(dateLabel);
AddDescription(container, "날짜 선택 컴포넌트 (버튼 클릭으로 모달 표시)");
var datePicker = new UTKDatePicker();
datePicker.style.position = Position.Relative;
datePicker.OnDateSelected += (date) =>
// 현재 선택된 날짜 표시
var previewRow = CreateRow(container, "Current Date");
_dateLabel = new Label($"Selected: {_selectedDate:yyyy-MM-dd}");
previewRow.Add(_dateLabel);
// 날짜만 선택 버튼
var row1 = CreateRow(container, "Date Only");
var dateOnlyBtn = new UTKButton("Open Date Picker", "", UTKButton.ButtonVariant.Primary);
dateOnlyBtn.OnClicked += () => OpenDatePicker(UTKDatePicker.PickerMode.DateOnly);
row1.Add(dateOnlyBtn);
// 날짜 + 시간 선택 버튼
var row2 = CreateRow(container, "Date & Time");
var dateTimeBtn = new UTKButton("Open DateTime Picker", "", UTKButton.ButtonVariant.Normal);
dateTimeBtn.OnClicked += () => OpenDatePicker(UTKDatePicker.PickerMode.DateAndTime);
row2.Add(dateTimeBtn);
// Async 버튼
var row3 = CreateRow(container, "Async/Await");
var asyncBtn = new UTKButton("Open Date Picker (Async)", "", UTKButton.ButtonVariant.Normal);
asyncBtn.OnClicked += () => OpenDatePickerAsync().Forget();
row3.Add(asyncBtn);
// 날짜 범위 표시
var rangePreviewRow = CreateRow(container, "Date Range");
_rangeDateLabel = new Label($"Range: {_rangeStartDate:yyyy-MM-dd} ~ {_rangeEndDate:yyyy-MM-dd}");
rangePreviewRow.Add(_rangeDateLabel);
// 날짜 범위 선택 버튼
var row4 = CreateRow(container, "Date Range");
var rangeBtn = new UTKButton("Open Range Picker", "", UTKButton.ButtonVariant.Normal);
rangeBtn.OnClicked += OpenDateRangePicker;
row4.Add(rangeBtn);
// 날짜 범위 Async 버튼
var row5 = CreateRow(container, "Range Async");
var rangeAsyncBtn = new UTKButton("Open Range Picker (Async)", "", UTKButton.ButtonVariant.Normal);
rangeAsyncBtn.OnClicked += () => OpenDateRangePickerAsync().Forget();
row5.Add(rangeAsyncBtn);
}
private void OpenDatePicker(UTKDatePicker.PickerMode mode)
{
if (_root == null) return;
string title = mode == UTKDatePicker.PickerMode.DateOnly ? "Select Date" : "Select Date & Time";
var picker = UTKDatePicker.Show(_root, _selectedDate, mode, title);
picker.OnDateSelected += (date) =>
{
dateLabel.text = $"Selected: {date:yyyy-MM-dd HH:mm}";
_selectedDate = date;
if (_dateLabel != null)
{
_dateLabel.text = mode == UTKDatePicker.PickerMode.DateOnly
? $"Selected: {date:yyyy-MM-dd}"
: $"Selected: {date:yyyy-MM-dd HH:mm}";
}
Debug.Log($"Date Selected: {date:yyyy-MM-dd HH:mm}");
};
container.Add(datePicker);
}
private async UniTaskVoid OpenDatePickerAsync()
{
if (_root == null) return;
// ShowAsync를 사용하여 날짜 선택 대기
// OK 클릭 시 선택된 날짜 반환, 취소/닫기 시 null 반환
DateTime? result = await UTKDatePicker.ShowAsync(_root, _selectedDate, UTKDatePicker.PickerMode.DateOnly, "Select Date (Async)");
if (result.HasValue)
{
_selectedDate = result.Value;
if (_dateLabel != null)
_dateLabel.text = $"Selected: {result.Value:yyyy-MM-dd}";
Debug.Log($"[Async] Date Result: {result.Value:yyyy-MM-dd}");
}
else
{
Debug.Log("[Async] Date selection cancelled");
}
}
private void OpenDateRangePicker()
{
if (_root == null) return;
var picker = UTKDatePicker.ShowRange(_root, _rangeStartDate, _rangeEndDate, false, "Select Date Range");
picker.OnDateRangeSelected += (start, end) =>
{
_rangeStartDate = start;
_rangeEndDate = end;
if (_rangeDateLabel != null)
_rangeDateLabel.text = $"Range: {start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}";
Debug.Log($"Date Range Selected: {start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}");
};
}
private async UniTaskVoid OpenDateRangePickerAsync()
{
if (_root == null) return;
// ShowRangeAsync를 사용하여 날짜 범위 선택 대기
// OK 클릭 시 선택된 범위 반환, 취소/닫기 시 null 반환
var result = await UTKDatePicker.ShowRangeAsync(_root, _rangeStartDate, _rangeEndDate, false, "Select Date Range (Async)");
if (result.HasValue)
{
_rangeStartDate = result.Value.Start;
_rangeEndDate = result.Value.End;
if (_rangeDateLabel != null)
_rangeDateLabel.text = $"Range: {result.Value.Start:yyyy-MM-dd} ~ {result.Value.End:yyyy-MM-dd}";
Debug.Log($"[Async] Range Result: {result.Value.Start:yyyy-MM-dd} ~ {result.Value.End:yyyy-MM-dd}");
}
else
{
Debug.Log("[Async] Date range selection cancelled");
}
}
#endregion

View File

@@ -181,6 +181,7 @@ namespace UVC.UIToolkit
// Actions
_actions = new VisualElement { name = "actions" };
_actions.AddToClassList("utk-card__actions");
_actions.style.display = DisplayStyle.None; // 기본적으로 숨김
hierarchy.Add(_actions);
UpdateVariant();
@@ -281,6 +282,13 @@ namespace UVC.UIToolkit
public void AddAction(VisualElement element)
{
_actions?.Add(element);
UpdateActionsVisibility();
}
private void UpdateActionsVisibility()
{
if (_actions == null) return;
_actions.style.display = _actions.childCount > 0 ? DisplayStyle.Flex : DisplayStyle.None;
}
/// <summary>

View File

@@ -159,6 +159,7 @@ namespace UVC.UIToolkit.Common
_tooltipLabel.text = displayText;
_tooltipContainer.style.display = DisplayStyle.Flex;
_tooltipContainer.BringToFront();
_isVisible = true;
// 다음 프레임에 위치 조정 (레이아웃 계산 후)
@@ -217,11 +218,18 @@ namespace UVC.UIToolkit.Common
_tooltipRegistry[element] = tooltip;
// 이벤트 콜백 생성 및 등록
// 참고: evt.position은 로컬 좌표이므로, 패널 기준 좌표로 변환 필요
EventCallback<PointerEnterEvent> enterCallback = evt =>
{
if (_tooltipRegistry.TryGetValue(element, out var text))
{
ShowDelayed(text, evt.position).Forget();
// 로컬 좌표를 root 좌표로 변환
var rootPosition = element.LocalToWorld(evt.localPosition);
if (_root != null)
{
rootPosition = _root.WorldToLocal(rootPosition);
}
ShowDelayed(text, rootPosition).Forget();
}
};
@@ -231,7 +239,13 @@ namespace UVC.UIToolkit.Common
{
if (_isVisible)
{
AdjustPosition(evt.position);
// 로컬 좌표를 root 좌표로 변환
var rootPosition = element.LocalToWorld(evt.localPosition);
if (_root != null)
{
rootPosition = _root.WorldToLocal(rootPosition);
}
AdjustPosition(rootPosition);
}
};

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Modal;
@@ -18,10 +19,10 @@ namespace UVC.UIToolkit
#endregion
#region Fields
private static VisualElement? _root;
private bool _disposed;
private UTKModalBlocker? _blocker;
private VisualElement? _iconContainer;
private Label? _iconLabel;
private Label? _titleLabel;
private Label? _messageLabel;
private VisualElement? _buttonContainer;
@@ -29,6 +30,11 @@ namespace UVC.UIToolkit
private string _title = "";
private string _message = "";
private AlertType _alertType = AlertType.Info;
private string _confirmLabel = "OK";
private string _cancelLabel = "Cancel";
private EventCallback<KeyDownEvent>? _keyDownCallback;
private VisualElement? _keyEventTarget;
#endregion
#region Events
@@ -116,45 +122,234 @@ namespace UVC.UIToolkit
}
#endregion
#region Static Factory
#region Static Methods
/// <summary>
/// 기본 루트 요소 설정
/// </summary>
/// <param name="root">Alert를 표시할 기본 루트 요소</param>
public static void SetRoot(VisualElement root)
{
_root = root;
}
/// <summary>
/// 기본 루트 요소 반환
/// </summary>
public static VisualElement? GetRoot() => _root;
#endregion
#region Static Factory (without parent)
/// <summary>
/// Info 알림 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKAlert ShowInfo(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return Show(_root!, title, message, AlertType.Info, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Success 알림 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKAlert ShowSuccess(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return Show(_root!, title, message, AlertType.Success, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Warning 알림 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKAlert ShowWarning(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return Show(_root!, title, message, AlertType.Warning, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Error 알림 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKAlert ShowError(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return Show(_root!, title, message, AlertType.Error, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Confirm 대화상자 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKAlert ShowConfirm(string title, string message, Action? onConfirm, Action? onCancel = null, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
ValidateRoot();
var alert = Show(_root!, title, message, AlertType.Confirm, null, closeOnBlockerClick, confirmLabel, cancelLabel);
alert.OnConfirm = onConfirm;
alert.OnCancel = onCancel;
return alert;
}
/// <summary>
/// 알림 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKAlert Show(string title, string message, AlertType type = AlertType.Info, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
ValidateRoot();
return Show(_root!, title, message, type, onClose, closeOnBlockerClick, confirmLabel, cancelLabel);
}
private static void ValidateRoot()
{
if (_root == null)
{
throw new InvalidOperationException("UTKAlert.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다.");
}
}
#endregion
#region Static Factory Async (without parent)
/// <summary>
/// Info 알림 표시 (비동기, SetRoot로 설정된 루트 사용)
/// </summary>
public static UniTask ShowInfoAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return ShowAsync(_root!, title, message, AlertType.Info, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Success 알림 표시 (비동기, SetRoot로 설정된 루트 사용)
/// </summary>
public static UniTask ShowSuccessAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return ShowAsync(_root!, title, message, AlertType.Success, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Warning 알림 표시 (비동기, SetRoot로 설정된 루트 사용)
/// </summary>
public static UniTask ShowWarningAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return ShowAsync(_root!, title, message, AlertType.Warning, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Error 알림 표시 (비동기, SetRoot로 설정된 루트 사용)
/// </summary>
public static UniTask ShowErrorAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
ValidateRoot();
return ShowAsync(_root!, title, message, AlertType.Error, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Confirm 대화상자 표시 (비동기, SetRoot로 설정된 루트 사용)
/// </summary>
/// <returns>확인 버튼 클릭 시 true, 취소 버튼 클릭 시 false</returns>
public static UniTask<bool> ShowConfirmAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
ValidateRoot();
return ShowConfirmAsync(_root!, title, message, closeOnBlockerClick, confirmLabel, cancelLabel);
}
#endregion
#region Static Factory Async (with parent)
/// <summary>
/// Info 알림 표시 (비동기)
/// </summary>
public static UniTask ShowInfoAsync(VisualElement parent, string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return ShowAsync(parent, title, message, AlertType.Info, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Success 알림 표시 (비동기)
/// </summary>
public static UniTask ShowSuccessAsync(VisualElement parent, string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return ShowAsync(parent, title, message, AlertType.Success, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Warning 알림 표시 (비동기)
/// </summary>
public static UniTask ShowWarningAsync(VisualElement parent, string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return ShowAsync(parent, title, message, AlertType.Warning, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Error 알림 표시 (비동기)
/// </summary>
public static UniTask ShowErrorAsync(VisualElement parent, string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return ShowAsync(parent, title, message, AlertType.Error, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Confirm 대화상자 표시 (비동기)
/// </summary>
/// <returns>확인 버튼 클릭 시 true, 취소 버튼 클릭 시 false</returns>
public static UniTask<bool> ShowConfirmAsync(VisualElement parent, string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
var tcs = new UniTaskCompletionSource<bool>();
var alert = Show(parent, title, message, AlertType.Confirm, null, closeOnBlockerClick, confirmLabel, cancelLabel);
alert.OnConfirm = () => tcs.TrySetResult(true);
alert.OnCancel = () => tcs.TrySetResult(false);
alert.OnClosed = () => tcs.TrySetResult(false);
return tcs.Task;
}
/// <summary>
/// 알림 표시 (비동기)
/// </summary>
public static UniTask ShowAsync(VisualElement parent, string title, string message, AlertType type, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
var tcs = new UniTaskCompletionSource();
var alert = Show(parent, title, message, type, () => tcs.TrySetResult(), closeOnBlockerClick, confirmLabel, cancelLabel);
return tcs.Task;
}
#endregion
#region Static Factory (with parent)
/// <summary>
/// Info 알림 표시
/// </summary>
public static UTKAlert ShowInfo(VisualElement parent, string title, string message, Action? onClose = null)
public static UTKAlert ShowInfo(VisualElement parent, string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return Show(parent, title, message, AlertType.Info, onClose);
return Show(parent, title, message, AlertType.Info, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Success 알림 표시
/// </summary>
public static UTKAlert ShowSuccess(VisualElement parent, string title, string message, Action? onClose = null)
public static UTKAlert ShowSuccess(VisualElement parent, string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return Show(parent, title, message, AlertType.Success, onClose);
return Show(parent, title, message, AlertType.Success, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Warning 알림 표시
/// </summary>
public static UTKAlert ShowWarning(VisualElement parent, string title, string message, Action? onClose = null)
public static UTKAlert ShowWarning(VisualElement parent, string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return Show(parent, title, message, AlertType.Warning, onClose);
return Show(parent, title, message, AlertType.Warning, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Error 알림 표시
/// </summary>
public static UTKAlert ShowError(VisualElement parent, string title, string message, Action? onClose = null)
public static UTKAlert ShowError(VisualElement parent, string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK")
{
return Show(parent, title, message, AlertType.Error, onClose);
return Show(parent, title, message, AlertType.Error, onClose, closeOnBlockerClick, confirmLabel);
}
/// <summary>
/// Confirm 대화상자 표시
/// </summary>
public static UTKAlert ShowConfirm(VisualElement parent, string title, string message, Action? onConfirm, Action? onCancel = null)
public static UTKAlert ShowConfirm(VisualElement parent, string title, string message, Action? onConfirm, Action? onCancel = null, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
var alert = Show(parent, title, message, AlertType.Confirm, null);
var alert = Show(parent, title, message, AlertType.Confirm, null, closeOnBlockerClick, confirmLabel, cancelLabel);
alert.OnConfirm = onConfirm;
alert.OnCancel = onCancel;
return alert;
@@ -163,13 +358,32 @@ namespace UVC.UIToolkit
/// <summary>
/// 알림 표시
/// </summary>
public static UTKAlert Show(VisualElement parent, string title, string message, AlertType type, Action? onClose)
/// <param name="parent">부모 요소</param>
/// <param name="title">제목</param>
/// <param name="message">메시지</param>
/// <param name="type">알림 유형</param>
/// <param name="onClose">닫힘 콜백</param>
/// <param name="closeOnBlockerClick">배경 클릭 시 닫힘 여부</param>
/// <param name="confirmLabel">확인 버튼 레이블</param>
/// <param name="cancelLabel">취소 버튼 레이블 (Confirm 타입에서만 사용)</param>
public static UTKAlert Show(VisualElement parent, string title, string message, AlertType type, Action? onClose, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel")
{
var alert = new UTKAlert(title, message, type);
alert.OnClosed = onClose;
alert._confirmLabel = confirmLabel;
alert._cancelLabel = cancelLabel;
alert.UpdateButtons();
alert._blocker = UTKModalBlocker.Show(parent, 0.5f, true);
alert._blocker.OnBlockerClicked += alert.Close;
alert._blocker = UTKModalBlocker.Show(parent, 0.5f, closeOnBlockerClick);
if (closeOnBlockerClick)
{
alert._blocker.OnBlockerClicked += alert.Close;
}
else
{
// closeOnBlockerClick이 false일 때 blocker 클릭 시 Alert로 포커스 유지
alert._blocker.OnBlockerClicked += () => alert.Focus();
}
alert._blocker.Add(alert);
// 중앙 정렬
@@ -178,6 +392,13 @@ namespace UVC.UIToolkit
alert.style.top = Length.Percent(50);
alert.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
// ESC 키 이벤트 등록
alert.RegisterEscapeKey(parent);
// Alert에 포커스 설정 (키보드 이벤트 수신을 위해)
alert.focusable = true;
alert.schedule.Execute(() => alert.Focus());
return alert;
}
#endregion
@@ -187,26 +408,21 @@ namespace UVC.UIToolkit
{
AddToClassList("utk-alert");
_iconContainer = new VisualElement { name = "icon-container" };
_iconContainer.AddToClassList("utk-alert__icon-container");
Add(_iconContainer);
_iconLabel = new Label { name = "icon" };
_iconLabel.AddToClassList("utk-alert__icon");
_iconContainer.Add(_iconLabel);
// Title (상단 왼쪽)
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-alert__title");
Add(_titleLabel);
// Content (가운데 메시지)
var contentContainer = new VisualElement { name = "content" };
contentContainer.AddToClassList("utk-alert__content");
Add(contentContainer);
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-alert__title");
contentContainer.Add(_titleLabel);
_messageLabel = new Label { name = "message" };
_messageLabel.AddToClassList("utk-alert__message");
contentContainer.Add(_messageLabel);
// Buttons
_buttonContainer = new VisualElement { name = "buttons" };
_buttonContainer.AddToClassList("utk-alert__buttons");
Add(_buttonContainer);
@@ -248,19 +464,6 @@ namespace UVC.UIToolkit
};
AddToClassList(typeClass);
// 아이콘 업데이트
if (_iconLabel != null)
{
_iconLabel.text = _alertType switch
{
AlertType.Success => "✓",
AlertType.Warning => "⚠",
AlertType.Error => "✕",
AlertType.Confirm => "?",
_ => ""
};
}
// 버튼 업데이트
UpdateButtons();
}
@@ -273,7 +476,7 @@ namespace UVC.UIToolkit
if (_alertType == AlertType.Confirm)
{
var cancelBtn = new UTKButton("Cancel", "", UTKButton.ButtonVariant.Normal);
var cancelBtn = new UTKButton(_cancelLabel, "", UTKButton.ButtonVariant.Normal);
cancelBtn.AddToClassList("utk-alert__btn");
cancelBtn.OnClicked += () =>
{
@@ -282,7 +485,7 @@ namespace UVC.UIToolkit
};
_buttonContainer.Add(cancelBtn);
var confirmBtn = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary);
var confirmBtn = new UTKButton(_confirmLabel, "", UTKButton.ButtonVariant.Primary);
confirmBtn.AddToClassList("utk-alert__btn");
confirmBtn.OnClicked += () =>
{
@@ -293,18 +496,64 @@ namespace UVC.UIToolkit
}
else
{
var okBtn = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary);
var okBtn = new UTKButton(_confirmLabel, "", UTKButton.ButtonVariant.Primary);
okBtn.AddToClassList("utk-alert__btn");
okBtn.OnClicked += Close;
_buttonContainer.Add(okBtn);
}
}
/// <summary>
/// ESC 키 이벤트 등록
/// </summary>
private void RegisterEscapeKey(VisualElement parent)
{
_keyEventTarget = parent;
_keyDownCallback = evt =>
{
if (evt.keyCode == KeyCode.Escape)
{
evt.StopPropagation();
HandleEscapeKey();
}
};
// 패널에 키 이벤트 등록 (포커스와 관계없이 캡처)
_keyEventTarget.RegisterCallback(_keyDownCallback, TrickleDown.TrickleDown);
}
/// <summary>
/// ESC 키 해제
/// </summary>
private void UnregisterEscapeKey()
{
if (_keyDownCallback != null && _keyEventTarget != null)
{
_keyEventTarget.UnregisterCallback(_keyDownCallback, TrickleDown.TrickleDown);
_keyDownCallback = null;
_keyEventTarget = null;
}
}
/// <summary>
/// ESC 키 처리
/// </summary>
private void HandleEscapeKey()
{
if (_alertType == AlertType.Confirm)
{
// Confirm 타입: 취소 처리
OnCancel?.Invoke();
}
Close();
}
/// <summary>
/// 알림 닫기
/// </summary>
public void Close()
{
UnregisterEscapeKey();
OnClosed?.Invoke();
if (_blocker != null)
{

View File

@@ -11,6 +11,36 @@ namespace UVC.UIToolkit.Modal
/// UIToolkit 기반 컬러 피커 모달
/// HSV 색공간 + RGB 슬라이더 + Hex 입력 지원
/// </summary>
/// <example>
/// <code>
/// // 기본 사용법 (Alpha 포함)
/// var picker = UTKColorPicker.Show(rootVisualElement, Color.red, "Select Color");
/// picker.OnColorChanged += (color) =>
/// {
/// // 실시간 색상 변경 시 호출
/// Debug.Log($"Color changing: {color}");
/// };
/// picker.OnColorSelected += (color) =>
/// {
/// // OK 버튼 클릭 시 호출
/// Debug.Log($"Color selected: #{ColorUtility.ToHtmlStringRGBA(color)}");
/// };
///
/// // Alpha 없이 사용
/// var pickerNoAlpha = UTKColorPicker.Show(rootVisualElement, Color.blue, "Select Color", useAlpha: false);
///
/// // async/await 사용법 (UniTask)
/// Color selectedColor = await UTKColorPicker.ShowAsync(rootVisualElement, Color.red, "Select Color");
/// // OK 클릭 시 선택된 색상 반환, 취소/닫기 시 initialColor(Color.red) 반환
/// Debug.Log($"Result: #{ColorUtility.ToHtmlStringRGBA(selectedColor)}");
///
/// // 인스턴스 직접 생성
/// var colorPicker = new UTKColorPicker();
/// colorPicker.UseAlpha = true; // Alpha 슬라이더 표시 여부 (기본값: true)
/// colorPicker.SetColor(Color.green);
/// container.Add(colorPicker);
/// </code>
/// </example>
[UxmlElement]
public partial class UTKColorPicker : VisualElement, IDisposable
{
@@ -22,7 +52,7 @@ namespace UVC.UIToolkit.Modal
#region Fields
private bool _disposed;
private bool _useAlpha;
private bool _useAlpha = true;
private bool _isUpdating; // 재귀 업데이트 방지
private Color _originalColor = Color.white;
@@ -61,9 +91,15 @@ namespace UVC.UIToolkit.Modal
private Label? _titleLabel;
private Button? _closeButton;
private Button? _cancelButton;
private Button? _confirmButton;
private UTKButton? _cancelButton;
private UTKButton? _confirmButton;
private VisualElement? _alphaRow;
private VisualElement? _header;
// 드래그 관련 필드
private bool _isDragging;
private Vector2 _dragStartPosition;
private Vector2 _dragStartMousePosition;
#endregion
#region Events
@@ -77,6 +113,23 @@ namespace UVC.UIToolkit.Modal
public event Action? OnClosed;
#endregion
#region Properties
/// <summary>
/// 알파(투명도) 값을 다룰지 여부 (기본값: true)
/// </summary>
[UxmlAttribute]
public bool UseAlpha
{
get => _useAlpha;
set
{
_useAlpha = value;
SetAlphaVisible(value);
UpdateHexField();
}
}
#endregion
#region Constructor
public UTKColorPicker()
{
@@ -152,7 +205,7 @@ namespace UVC.UIToolkit.Modal
VisualElement parent,
Color initialColor,
string title = "Color Picker",
bool useAlpha = false)
bool useAlpha = true)
{
var picker = new UTKColorPicker();
picker._useAlpha = useAlpha;
@@ -175,6 +228,41 @@ namespace UVC.UIToolkit.Modal
return picker;
}
/// <summary>
/// 컬러 피커를 표시하고 색상 선택을 기다립니다.
/// OK 버튼 클릭 시 선택된 색상을 반환하고, 취소/닫기 시 initialColor를 반환합니다.
/// </summary>
/// <param name="parent">부모 VisualElement</param>
/// <param name="initialColor">초기 색상</param>
/// <param name="title">피커 제목</param>
/// <param name="useAlpha">알파 채널 사용 여부</param>
/// <returns>선택된 색상 또는 초기 색상</returns>
public static async UniTask<Color> ShowAsync(
VisualElement parent,
Color initialColor,
string title = "Color Picker",
bool useAlpha = true)
{
var tcs = new UniTaskCompletionSource<Color>();
Color resultColor = initialColor;
var picker = Show(parent, initialColor, title, useAlpha);
picker.OnColorSelected += (color) =>
{
resultColor = color;
tcs.TrySetResult(resultColor);
};
picker.OnClosed += () =>
{
// OnColorSelected가 먼저 호출된 경우 이미 완료됨
tcs.TrySetResult(resultColor);
};
return await tcs.Task;
}
#endregion
#region Public Methods
@@ -222,8 +310,8 @@ namespace UVC.UIToolkit.Modal
AddToClassList("utk-color-picker");
// Header
var header = new VisualElement { name = "header" };
header.AddToClassList("utk-color-picker__header");
_header = new VisualElement { name = "header" };
_header.AddToClassList("utk-color-picker__header");
_titleLabel = new Label("Color Picker") { name = "title" };
_titleLabel.AddToClassList("utk-color-picker__title");
@@ -231,9 +319,9 @@ namespace UVC.UIToolkit.Modal
_closeButton = new Button { name = "close-btn", text = "\u2715" }; // ✕
_closeButton.AddToClassList("utk-color-picker__close-btn");
header.Add(_titleLabel);
header.Add(_closeButton);
Add(header);
_header.Add(_titleLabel);
_header.Add(_closeButton);
Add(_header);
// SV Box + Hue Bar row
var colorArea = new VisualElement { name = "color-area" };
@@ -309,10 +397,10 @@ namespace UVC.UIToolkit.Modal
var buttonRow = new VisualElement { name = "button-row" };
buttonRow.AddToClassList("utk-color-picker__button-row");
_cancelButton = new Button { name = "cancel-btn", text = "Cancel" };
_cancelButton = new UTKButton("Cancel", "", UTKButton.ButtonVariant.Normal) { name = "cancel-btn" };
_cancelButton.AddToClassList("utk-color-picker__cancel-btn");
_confirmButton = new Button { name = "confirm-btn", text = "OK" };
_confirmButton = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary) { name = "confirm-btn" };
_confirmButton.AddToClassList("utk-color-picker__confirm-btn");
buttonRow.Add(_cancelButton);
@@ -362,17 +450,35 @@ namespace UVC.UIToolkit.Modal
_titleLabel ??= this.Q<Label>("title");
_closeButton ??= this.Q<Button>("close-btn");
_cancelButton ??= this.Q<Button>("cancel-btn");
_confirmButton ??= this.Q<Button>("confirm-btn");
_cancelButton ??= this.Q<UTKButton>("cancel-btn");
_confirmButton ??= this.Q<UTKButton>("confirm-btn");
_alphaRow ??= this.Q<VisualElement>("row-a");
}
private void SetupEvents()
{
// Close/Cancel/Confirm buttons
_closeButton?.RegisterCallback<ClickEvent>(_ => Cancel());
_cancelButton?.RegisterCallback<ClickEvent>(_ => Cancel());
_confirmButton?.RegisterCallback<ClickEvent>(_ => Confirm());
_closeButton?.RegisterCallback<ClickEvent>(evt =>
{
Cancel();
evt.StopPropagation();
});
if (_cancelButton != null)
{
_cancelButton.OnClicked += () =>
{
Cancel();
};
}
if (_confirmButton != null)
{
_confirmButton.OnClicked += () =>
{
Confirm();
};
}
// SV Box interaction
_svBox?.RegisterCallback<PointerDownEvent>(OnSVBoxPointerDown);
@@ -401,6 +507,11 @@ namespace UVC.UIToolkit.Modal
// Original preview click - 원래 색상으로 복원
_previewOriginal?.RegisterCallback<ClickEvent>(_ => SetColor(_originalColor));
// Header drag events
_header?.RegisterCallback<PointerDownEvent>(OnHeaderPointerDown);
_header?.RegisterCallback<PointerMoveEvent>(OnHeaderPointerMove);
_header?.RegisterCallback<PointerUpEvent>(OnHeaderPointerUp);
}
#endregion
@@ -408,6 +519,38 @@ namespace UVC.UIToolkit.Modal
private bool _svDragging;
private bool _hueDragging;
private void OnHeaderPointerDown(PointerDownEvent evt)
{
// 닫기 버튼 클릭은 무시
if (evt.target == _closeButton) return;
_isDragging = true;
_dragStartMousePosition = evt.position;
_dragStartPosition = new Vector2(resolvedStyle.left, resolvedStyle.top);
_header?.CapturePointer(evt.pointerId);
evt.StopPropagation();
}
private void OnHeaderPointerMove(PointerMoveEvent evt)
{
if (!_isDragging) return;
Vector2 delta = (Vector2)evt.position - _dragStartMousePosition;
style.left = _dragStartPosition.x + delta.x;
style.top = _dragStartPosition.y + delta.y;
evt.StopPropagation();
}
private void OnHeaderPointerUp(PointerUpEvent evt)
{
if (_isDragging)
{
_isDragging = false;
_header?.ReleasePointer(evt.pointerId);
evt.StopPropagation();
}
}
private void OnSVBoxPointerDown(PointerDownEvent evt)
{
if (_hueDragging) return; // Hue 드래그 중이면 무시
@@ -802,8 +945,10 @@ namespace UVC.UIToolkit.Modal
{
if (_hexField == null) return;
// 항상 RGBA 형식 (알파값 포함)
_hexField.value = ColorUtility.ToHtmlStringRGBA(_currentColor);
// UseAlpha에 따라 RGBA(8자리) 또는 RGB(6자리) 형식으로 표시
_hexField.value = _useAlpha
? ColorUtility.ToHtmlStringRGBA(_currentColor)
: ColorUtility.ToHtmlStringRGB(_currentColor);
}
private void UpdateIndicators()

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Locale;
@@ -12,6 +13,57 @@ namespace UVC.UIToolkit.Modal
/// UIToolkit 기반 날짜/시간 피커 모달
/// 캘린더 그리드 + 시간 선택 지원
/// </summary>
/// <example>
/// <code>
/// // 기본 사용법 (날짜만)
/// var picker = UTKDatePicker.Show(rootVisualElement, DateTime.Today, UTKDatePicker.PickerMode.DateOnly, "Select Date");
/// picker.OnDateSelected += (date) =>
/// {
/// Debug.Log($"Date selected: {date:yyyy-MM-dd}");
/// };
///
/// // 날짜 + 시간 선택
/// var dateTimePicker = UTKDatePicker.Show(rootVisualElement, DateTime.Now, UTKDatePicker.PickerMode.DateAndTime, "Select Date &amp; Time");
/// dateTimePicker.OnDateSelected += (date) =>
/// {
/// Debug.Log($"DateTime selected: {date:yyyy-MM-dd HH:mm}");
/// };
///
/// // async/await 사용법 (UniTask)
/// DateTime? selectedDate = await UTKDatePicker.ShowAsync(rootVisualElement, DateTime.Today);
/// if (selectedDate.HasValue)
/// {
/// Debug.Log($"Selected: {selectedDate.Value:yyyy-MM-dd}");
/// }
/// else
/// {
/// Debug.Log("Cancelled");
/// }
///
/// // 날짜 범위 선택
/// var rangePicker = UTKDatePicker.ShowRange(rootVisualElement, DateTime.Today, DateTime.Today.AddDays(7));
/// rangePicker.OnDateRangeSelected += (start, end) =>
/// {
/// Debug.Log($"Range selected: {start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}");
/// };
///
/// // 날짜 범위 async/await 사용법
/// var result = await UTKDatePicker.ShowRangeAsync(rootVisualElement, DateTime.Today, DateTime.Today.AddDays(7));
/// if (result.HasValue)
/// {
/// Debug.Log($"Range: {result.Value.Start:yyyy-MM-dd} ~ {result.Value.End:yyyy-MM-dd}");
/// }
/// else
/// {
/// Debug.Log("Cancelled");
/// }
///
/// // 인스턴스 직접 생성
/// var datePicker = new UTKDatePicker();
/// datePicker.SetDate(DateTime.Today);
/// container.Add(datePicker);
/// </code>
/// </example>
[UxmlElement]
public partial class UTKDatePicker : VisualElement, IDisposable
{
@@ -19,7 +71,18 @@ namespace UVC.UIToolkit.Modal
public enum PickerMode
{
DateOnly,
DateAndTime
DateAndTime,
DateRange,
DateTimeRange
}
/// <summary>
/// 범위 선택 시 현재 선택 중인 날짜 타입
/// </summary>
private enum RangeSelectionState
{
SelectingStart,
SelectingEnd
}
#endregion
@@ -36,9 +99,17 @@ namespace UVC.UIToolkit.Modal
private DateTime _selectedDate = DateTime.Today;
private DateTime _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
// 범위 선택 관련 필드
private DateTime? _rangeStartDate;
private DateTime? _rangeEndDate;
private RangeSelectionState _rangeState = RangeSelectionState.SelectingStart;
private UTKModalBlocker? _blocker;
private readonly List<Button> _dayButtons = new();
// 범위 선택 UI 요소
private Label? _rangeInfoLabel;
// UI 요소
private Label? _titleLabel;
private Button? _closeButton;
@@ -51,14 +122,17 @@ namespace UVC.UIToolkit.Modal
private VisualElement? _timeRow;
private UTKNumberStepper? _hourStepper;
private UTKNumberStepper? _minuteStepper;
private Button? _cancelButton;
private Button? _confirmButton;
private UTKButton? _cancelButton;
private UTKButton? _confirmButton;
#endregion
#region Events
/// <summary>날짜가 선택되었을 때 발생</summary>
public event Action<DateTime>? OnDateSelected;
/// <summary>날짜 범위가 선택되었을 때 발생 (시작일, 종료일)</summary>
public event Action<DateTime, DateTime>? OnDateRangeSelected;
/// <summary>피커가 닫힐 때 발생</summary>
public event Action? OnClosed;
#endregion
@@ -142,6 +216,155 @@ namespace UVC.UIToolkit.Modal
return picker;
}
/// <summary>
/// 날짜 피커를 표시하고 날짜 선택을 기다립니다.
/// OK 버튼 클릭 시 선택된 날짜를 반환하고, 취소/닫기 시 null을 반환합니다.
/// </summary>
/// <param name="parent">부모 VisualElement</param>
/// <param name="initialDate">초기 날짜</param>
/// <param name="mode">피커 모드 (날짜만 또는 날짜+시간)</param>
/// <param name="title">피커 제목</param>
/// <returns>선택된 날짜 또는 null (취소 시)</returns>
/// <example>
/// <code>
/// // async/await 사용법
/// DateTime? selectedDate = await UTKDatePicker.ShowAsync(rootVisualElement, DateTime.Today);
/// if (selectedDate.HasValue)
/// {
/// Debug.Log($"Selected: {selectedDate.Value:yyyy-MM-dd}");
/// }
/// else
/// {
/// Debug.Log("Cancelled");
/// }
/// </code>
/// </example>
public static async UniTask<DateTime?> ShowAsync(
VisualElement parent,
DateTime initialDate,
PickerMode mode = PickerMode.DateOnly,
string title = "Select Date")
{
var tcs = new UniTaskCompletionSource<DateTime?>();
DateTime? resultDate = null;
var picker = Show(parent, initialDate, mode, title);
picker.OnDateSelected += (date) =>
{
resultDate = date;
tcs.TrySetResult(resultDate);
};
picker.OnClosed += () =>
{
// OnDateSelected가 먼저 호출된 경우 이미 완료됨
tcs.TrySetResult(resultDate);
};
return await tcs.Task;
}
/// <summary>
/// 날짜 범위 피커를 표시합니다.
/// </summary>
/// <param name="parent">부모 VisualElement</param>
/// <param name="initialStartDate">초기 시작 날짜 (null이면 오늘)</param>
/// <param name="initialEndDate">초기 종료 날짜 (null이면 시작 날짜와 동일)</param>
/// <param name="includeTime">시간 선택 포함 여부</param>
/// <param name="title">피커 제목</param>
public static UTKDatePicker ShowRange(
VisualElement parent,
DateTime? initialStartDate = null,
DateTime? initialEndDate = null,
bool includeTime = false,
string title = "Select Date Range")
{
var picker = new UTKDatePicker();
picker._mode = includeTime ? PickerMode.DateTimeRange : PickerMode.DateRange;
var startDate = initialStartDate ?? DateTime.Today;
var endDate = initialEndDate ?? startDate;
// 시작일이 종료일보다 늦으면 스왑
if (startDate > endDate)
{
(startDate, endDate) = (endDate, startDate);
}
picker._rangeStartDate = startDate;
picker._rangeEndDate = endDate;
picker._rangeState = RangeSelectionState.SelectingStart;
picker._displayMonth = new DateTime(startDate.Year, startDate.Month, 1);
// 블로커 추가
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
picker._blocker.OnBlockerClicked += picker.Close;
// 피커 추가
parent.Add(picker);
// UI 초기화
picker.SetTitle(title);
picker.SetTimeVisible(includeTime);
picker.SetRangeInfoVisible(true);
picker.UpdateRangeInfo();
picker.UpdateCalendar();
picker.CenterOnScreen();
return picker;
}
/// <summary>
/// 날짜 범위 피커를 표시하고 범위 선택을 기다립니다.
/// OK 버튼 클릭 시 선택된 범위를 반환하고, 취소/닫기 시 null을 반환합니다.
/// </summary>
/// <param name="parent">부모 VisualElement</param>
/// <param name="initialStartDate">초기 시작 날짜</param>
/// <param name="initialEndDate">초기 종료 날짜</param>
/// <param name="includeTime">시간 선택 포함 여부</param>
/// <param name="title">피커 제목</param>
/// <returns>선택된 범위 (시작일, 종료일) 또는 null (취소 시)</returns>
/// <example>
/// <code>
/// // async/await 사용법
/// var result = await UTKDatePicker.ShowRangeAsync(rootVisualElement);
/// if (result.HasValue)
/// {
/// Debug.Log($"Range: {result.Value.Start:yyyy-MM-dd} ~ {result.Value.End:yyyy-MM-dd}");
/// }
/// else
/// {
/// Debug.Log("Cancelled");
/// }
/// </code>
/// </example>
public static async UniTask<(DateTime Start, DateTime End)?> ShowRangeAsync(
VisualElement parent,
DateTime? initialStartDate = null,
DateTime? initialEndDate = null,
bool includeTime = false,
string title = "Select Date Range")
{
var tcs = new UniTaskCompletionSource<(DateTime, DateTime)?>();
(DateTime, DateTime)? result = null;
var picker = ShowRange(parent, initialStartDate, initialEndDate, includeTime, title);
picker.OnDateRangeSelected += (start, end) =>
{
result = (start, end);
tcs.TrySetResult(result);
};
picker.OnClosed += () =>
{
tcs.TrySetResult(result);
};
return await tcs.Task;
}
#endregion
#region Public Methods
@@ -304,14 +527,20 @@ namespace UVC.UIToolkit.Modal
_timeRow.Add(_minuteStepper);
Add(_timeRow);
// Range Info Label (범위 선택 시 시작일/종료일 표시)
_rangeInfoLabel = new Label { name = "range-info" };
_rangeInfoLabel.AddToClassList("utk-date-picker__range-info");
_rangeInfoLabel.style.display = DisplayStyle.None;
Add(_rangeInfoLabel);
// Buttons
var buttonRow = new VisualElement { name = "button-row" };
buttonRow.AddToClassList("utk-date-picker__button-row");
_cancelButton = new Button { name = "cancel-btn", text = "Cancel" };
_cancelButton = new UTKButton("Cancel", "", UTKButton.ButtonVariant.Normal) { name = "cancel-btn" };
_cancelButton.AddToClassList("utk-date-picker__cancel-btn");
_confirmButton = new Button { name = "confirm-btn", text = "OK" };
_confirmButton = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary) { name = "confirm-btn" };
_confirmButton.AddToClassList("utk-date-picker__confirm-btn");
buttonRow.Add(_cancelButton);
@@ -339,8 +568,9 @@ namespace UVC.UIToolkit.Modal
_timeRow ??= this.Q<VisualElement>("time-row");
_hourStepper ??= this.Q<UTKNumberStepper>("hour-stepper");
_minuteStepper ??= this.Q<UTKNumberStepper>("minute-stepper");
_cancelButton ??= this.Q<Button>("cancel-btn");
_confirmButton ??= this.Q<Button>("confirm-btn");
_cancelButton ??= this.Q<UTKButton>("cancel-btn");
_confirmButton ??= this.Q<UTKButton>("confirm-btn");
_rangeInfoLabel ??= this.Q<Label>("range-info");
// Day buttons 쿼리
if (_dayButtons.Count == 0)
@@ -361,8 +591,8 @@ namespace UVC.UIToolkit.Modal
private void SetupEvents()
{
_closeButton?.RegisterCallback<ClickEvent>(_ => Close());
_cancelButton?.RegisterCallback<ClickEvent>(_ => Close());
_confirmButton?.RegisterCallback<ClickEvent>(_ => Confirm());
if (_cancelButton != null) _cancelButton.OnClicked += Close;
if (_confirmButton != null) _confirmButton.OnClicked += Confirm;
_prevYearButton?.RegisterCallback<ClickEvent>(_ => PreviousYear());
_prevMonthButton?.RegisterCallback<ClickEvent>(_ => PreviousMonth());
@@ -426,6 +656,9 @@ namespace UVC.UIToolkit.Modal
btn.RemoveFromClassList("utk-date-picker__day-btn--today");
btn.RemoveFromClassList("utk-date-picker__day-btn--selected");
btn.RemoveFromClassList("utk-date-picker__day-btn--other-month");
btn.RemoveFromClassList("utk-date-picker__day-btn--range-start");
btn.RemoveFromClassList("utk-date-picker__day-btn--range-end");
btn.RemoveFromClassList("utk-date-picker__day-btn--in-range");
int dayNumber = i - startDayOfWeek + 1;
@@ -444,34 +677,76 @@ namespace UVC.UIToolkit.Modal
DateTime currentDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayNumber);
DayOfWeek dow = currentDate.DayOfWeek;
// 선택된 날짜인지 확인
bool isSelected = currentDate.Date == _selectedDate.Date;
// 오늘인지 확인
bool isToday = currentDate.Date == today;
// 선택 상태 클래스
if (isSelected)
// 범위 선택 모드 처리
if (IsRangeMode)
{
btn.AddToClassList("utk-date-picker__day-btn--selected");
}
bool isRangeStart = _rangeStartDate.HasValue && currentDate.Date == _rangeStartDate.Value.Date;
bool isRangeEnd = _rangeEndDate.HasValue && currentDate.Date == _rangeEndDate.Value.Date;
bool isInRange = _rangeStartDate.HasValue && _rangeEndDate.HasValue &&
currentDate.Date > _rangeStartDate.Value.Date &&
currentDate.Date < _rangeEndDate.Value.Date;
// 오늘 클래스 (선택되지 않은 경우에만)
if (isToday && !isSelected)
{
btn.AddToClassList("utk-date-picker__day-btn--today");
}
// 요일별 클래스 (선택되지 않은 경우에만)
if (!isSelected)
{
if (dow == DayOfWeek.Sunday)
if (isRangeStart)
{
btn.AddToClassList("utk-date-picker__day-btn--sunday");
btn.AddToClassList("utk-date-picker__day-btn--range-start");
}
else if (dow == DayOfWeek.Saturday)
if (isRangeEnd)
{
btn.AddToClassList("utk-date-picker__day-btn--saturday");
btn.AddToClassList("utk-date-picker__day-btn--range-end");
}
if (isInRange)
{
btn.AddToClassList("utk-date-picker__day-btn--in-range");
}
// 오늘 클래스 (범위에 포함되지 않은 경우에만)
bool isToday = currentDate.Date == today;
if (isToday && !isRangeStart && !isRangeEnd && !isInRange)
{
btn.AddToClassList("utk-date-picker__day-btn--today");
}
// 요일별 클래스 (범위에 포함되지 않은 경우에만)
if (!isRangeStart && !isRangeEnd && !isInRange)
{
if (dow == DayOfWeek.Sunday)
{
btn.AddToClassList("utk-date-picker__day-btn--sunday");
}
else if (dow == DayOfWeek.Saturday)
{
btn.AddToClassList("utk-date-picker__day-btn--saturday");
}
}
}
else
{
// 단일 날짜 선택 모드
bool isSelected = currentDate.Date == _selectedDate.Date;
bool isToday = currentDate.Date == today;
if (isSelected)
{
btn.AddToClassList("utk-date-picker__day-btn--selected");
}
if (isToday && !isSelected)
{
btn.AddToClassList("utk-date-picker__day-btn--today");
}
if (!isSelected)
{
if (dow == DayOfWeek.Sunday)
{
btn.AddToClassList("utk-date-picker__day-btn--sunday");
}
else if (dow == DayOfWeek.Saturday)
{
btn.AddToClassList("utk-date-picker__day-btn--saturday");
}
}
}
}
@@ -493,7 +768,7 @@ namespace UVC.UIToolkit.Modal
int hour = _hourStepper?.Value ?? _selectedDate.Hour;
int minute = _minuteStepper?.Value ?? _selectedDate.Minute;
_selectedDate = new DateTime(
DateTime clickedDate = new DateTime(
_displayMonth.Year,
_displayMonth.Month,
dayNumber,
@@ -502,13 +777,63 @@ namespace UVC.UIToolkit.Modal
0
);
if (IsRangeMode)
{
// 범위 선택 모드
if (_rangeState == RangeSelectionState.SelectingStart)
{
// 시작일 선택
_rangeStartDate = clickedDate;
_rangeEndDate = null;
_rangeState = RangeSelectionState.SelectingEnd;
}
else
{
// 종료일 선택
if (_rangeStartDate.HasValue && clickedDate < _rangeStartDate.Value)
{
// 클릭한 날짜가 시작일보다 이전이면 시작일로 설정하고 기존 시작일을 종료일로
_rangeEndDate = _rangeStartDate;
_rangeStartDate = clickedDate;
}
else
{
_rangeEndDate = clickedDate;
}
_rangeState = RangeSelectionState.SelectingStart;
}
UpdateRangeInfo();
}
else
{
// 단일 날짜 선택 모드
_selectedDate = clickedDate;
}
UpdateCalendar();
}
}
private void Confirm()
{
OnDateSelected?.Invoke(GetDate());
if (IsRangeMode)
{
// 범위 선택 모드
if (_rangeStartDate.HasValue && _rangeEndDate.HasValue)
{
OnDateRangeSelected?.Invoke(_rangeStartDate.Value, _rangeEndDate.Value);
}
else if (_rangeStartDate.HasValue)
{
// 종료일이 없으면 시작일을 종료일로도 사용
OnDateRangeSelected?.Invoke(_rangeStartDate.Value, _rangeStartDate.Value);
}
}
else
{
OnDateSelected?.Invoke(GetDate());
}
Close();
}
@@ -549,6 +874,32 @@ namespace UVC.UIToolkit.Modal
style.top = (parentHeight - selfHeight) / 2;
});
}
private void SetRangeInfoVisible(bool visible)
{
if (_rangeInfoLabel != null)
{
_rangeInfoLabel.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private void UpdateRangeInfo()
{
if (_rangeInfoLabel == null) return;
string startText = _rangeStartDate?.ToString("yyyy-MM-dd") ?? "---";
string endText = _rangeEndDate?.ToString("yyyy-MM-dd") ?? "---";
string stateIndicator = _rangeState == RangeSelectionState.SelectingStart ? "▶ " : " ";
string endStateIndicator = _rangeState == RangeSelectionState.SelectingEnd ? "▶ " : " ";
_rangeInfoLabel.text = $"{stateIndicator}시작: {startText} {endStateIndicator}종료: {endText}";
}
/// <summary>
/// 범위 선택 모드인지 확인
/// </summary>
private bool IsRangeMode => _mode == PickerMode.DateRange || _mode == PickerMode.DateTimeRange;
#endregion
#region IDisposable

View File

@@ -15,12 +15,13 @@ namespace UVC.UIToolkit
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKToast";
private const int DEFAULT_DURATION_MS = 3000;
private const int DEFAULT_DURATION_MS = 1000;
#endregion
#region Fields
private static VisualElement? _root;
private bool _disposed;
private Label? _iconLabel;
private Label? _messageLabel;
private Button? _closeButton;
@@ -116,7 +117,78 @@ namespace UVC.UIToolkit
}
#endregion
#region Static Factory
#region Static Methods
/// <summary>
/// 기본 루트 요소 설정
/// </summary>
/// <param name="root">Toast를 표시할 기본 루트 요소</param>
public static void SetRoot(VisualElement root)
{
_root = root;
}
/// <summary>
/// 기본 루트 요소 반환
/// </summary>
public static VisualElement? GetRoot() => _root;
private static void ValidateRoot()
{
if (_root == null)
{
throw new InvalidOperationException("UTKToast.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다.");
}
}
#endregion
#region Static Factory (without parent)
/// <summary>
/// Info 토스트 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKToast ShowInfo(string message, int duration = DEFAULT_DURATION_MS)
{
ValidateRoot();
return Show(_root!, message, ToastType.Info, duration);
}
/// <summary>
/// Success 토스트 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKToast ShowSuccess(string message, int duration = DEFAULT_DURATION_MS)
{
ValidateRoot();
return Show(_root!, message, ToastType.Success, duration);
}
/// <summary>
/// Warning 토스트 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKToast ShowWarning(string message, int duration = DEFAULT_DURATION_MS)
{
ValidateRoot();
return Show(_root!, message, ToastType.Warning, duration);
}
/// <summary>
/// Error 토스트 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKToast ShowError(string message, int duration = DEFAULT_DURATION_MS)
{
ValidateRoot();
return Show(_root!, message, ToastType.Error, duration);
}
/// <summary>
/// 토스트 표시 (SetRoot로 설정된 루트 사용)
/// </summary>
public static UTKToast Show(string message, ToastType type = ToastType.Info, int duration = DEFAULT_DURATION_MS)
{
ValidateRoot();
return Show(_root!, message, type, duration);
}
#endregion
#region Static Factory (with parent)
/// <summary>
/// Info 토스트 표시
/// </summary>
@@ -180,10 +252,6 @@ namespace UVC.UIToolkit
{
AddToClassList("utk-toast");
_iconLabel = new Label { name = "icon" };
_iconLabel.AddToClassList("utk-toast__icon");
Add(_iconLabel);
_messageLabel = new Label { name = "message" };
_messageLabel.AddToClassList("utk-toast__message");
Add(_messageLabel);
@@ -228,17 +296,6 @@ namespace UVC.UIToolkit
_ => "utk-toast--info"
};
AddToClassList(typeClass);
if (_iconLabel != null)
{
_iconLabel.text = _type switch
{
ToastType.Success => "✓",
ToastType.Warning => "⚠",
ToastType.Error => "✕",
_ => ""
};
}
}
private async UniTaskVoid AutoCloseAsync(int delayMs)

View File

@@ -12,10 +12,6 @@ namespace UVC.UIToolkit
[UxmlElement]
public partial class UTKTab : Tab, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Tab/UTKTab";
#endregion
#region Fields
private bool _disposed;
private bool _isEnabled = true;
@@ -54,13 +50,6 @@ namespace UVC.UIToolkit
public UTKTab() : base()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SubscribeToThemeChanges();
}