@uniai-fe/uds-primitives 0.12.14 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/styles.css +330 -0
- package/package.json +1 -1
- package/src/components/fab/index.scss +1 -0
- package/src/components/fab/index.tsx +34 -0
- package/src/components/fab/markup/Base.tsx +62 -0
- package/src/components/fab/markup/Default.tsx +33 -0
- package/src/components/fab/markup/Extended.tsx +35 -0
- package/src/components/fab/markup/Label.tsx +16 -0
- package/src/components/fab/markup/Menu.tsx +114 -0
- package/src/components/fab/markup/Root.tsx +263 -0
- package/src/components/fab/markup/Template.tsx +79 -0
- package/src/components/fab/markup/Trigger.tsx +65 -0
- package/src/components/fab/markup/index.ts +7 -0
- package/src/components/fab/styles/composition.scss +80 -0
- package/src/components/fab/styles/fab.scss +140 -0
- package/src/components/fab/styles/index.scss +4 -0
- package/src/components/fab/styles/variables.scss +25 -0
- package/src/components/fab/types/composition.ts +288 -0
- package/src/components/fab/types/index.ts +3 -0
- package/src/components/fab/types/options.ts +39 -0
- package/src/components/fab/types/props.ts +124 -0
- package/src/components/tooltip/markup/Text.tsx +10 -3
- package/src/components/tooltip/types/text.ts +8 -3
- package/src/index.scss +1 -0
- package/src/index.tsx +1 -0
package/README.md
CHANGED
|
@@ -46,6 +46,74 @@ Button은 기존 priority 범위를 유지한다.
|
|
|
46
46
|
`Input.Time.Template`과 standalone `TimePicker.Template`, `TimePickerUtils`도 root public
|
|
47
47
|
entry에서 제공한다.
|
|
48
48
|
|
|
49
|
+
`Fab.Button`은 아이콘 전용 주요 액션, `Fab.Expand`는 아이콘과 라벨을 표시한다.
|
|
50
|
+
필수 `icon`은 ReactNode이며 `size`는 `small | medium | large`다. Button과 같은
|
|
51
|
+
`fill="solid" | "outlined"`와 `priority`를 독립적으로 조합한다. 두 fill 모두 `priority`가
|
|
52
|
+
필수이며 Button.Default와 같은 `primary`, `secondary`, `tertiary`, `green`, `yellow`,
|
|
53
|
+
`orange`, `teal`, `pink`, `red`, `darkGray`, `lightGray`를 지원한다. 색상과 상태별 배경은
|
|
54
|
+
Button 토큰을 재사용하며 outlined의 기본 면은 불투명 흰색이다.
|
|
55
|
+
아이콘 전용 Button에는 행동 이름인 `aria-label`도 필수다. 화면 고정 배치는 소비자가 정한다.
|
|
56
|
+
`Fab.Default`, `Fab.Extended`, `Fab.Label`은 기존 사용처와의 호환을 유지한다.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Fab } from "@uniai-fe/uds-primitives";
|
|
60
|
+
|
|
61
|
+
<Fab.Button
|
|
62
|
+
icon={<span aria-hidden="true">+</span>}
|
|
63
|
+
aria-label="항목 추가"
|
|
64
|
+
fill="solid"
|
|
65
|
+
priority="primary"
|
|
66
|
+
size="medium"
|
|
67
|
+
/>;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
관련 액션을 원형 버튼으로 펼치는 구성은 `Fab.Template`으로 시작하거나
|
|
71
|
+
`Fab.Root` 안에 `Fab.Trigger`, `Fab.Menu.List`, `Fab.Menu.Item`을 직접 조립한다.
|
|
72
|
+
`Fab.Expand`는 라벨 확장형이고, 메뉴를 펼치는 동작은 `Fab.Trigger`가 담당한다.
|
|
73
|
+
`Root`는 hover·focus·click·keyboard, 메뉴 ID·ARIA 연결, 닫기와 focus 복귀를 소유한다.
|
|
74
|
+
`direction`은 `up | down | left | right`이며 기본값은 `up`이다. 열림 상태는
|
|
75
|
+
`open`과 `onOpenChange`로 제어하거나 `defaultOpen`으로 초기화한다.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Fab.Template
|
|
79
|
+
triggerProps={{
|
|
80
|
+
icon: <span aria-hidden="true">+</span>,
|
|
81
|
+
"aria-label": "콘텐츠 추가 방법",
|
|
82
|
+
fill: "solid",
|
|
83
|
+
priority: "primary",
|
|
84
|
+
size: "medium",
|
|
85
|
+
}}
|
|
86
|
+
items={[
|
|
87
|
+
{
|
|
88
|
+
id: "write",
|
|
89
|
+
icon: <span aria-hidden="true">✎</span>,
|
|
90
|
+
"aria-label": "새 글 작성",
|
|
91
|
+
fill: "solid",
|
|
92
|
+
priority: "secondary",
|
|
93
|
+
size: "small",
|
|
94
|
+
onClick: () => console.info("새 글 작성"),
|
|
95
|
+
},
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`Root` 하나에 `Trigger` 하나와 `Menu.List` 하나를 두고 그 안에 `Menu.Item`을 조립한다.
|
|
101
|
+
이 부품들은 Root 밖에서 사용하면 오류를 발생시킨다. `Menu.Item`의
|
|
102
|
+
`aria-label`은 행동 이름과 Tooltip 문구이며 `persistentTooltips`로 열린 메뉴의
|
|
103
|
+
문구를 계속 표시할 수 있다. 항목은 native `onClick`을 실행한 뒤 닫고 trigger로
|
|
104
|
+
focus를 돌려준다. 소비자가 `event.preventDefault()`를 호출하면 이 자동 처리를 막는다.
|
|
105
|
+
`Fab.Menu.Label`은 기존 `Tooltip.Text`를 재사용하는 메뉴명 표현 부품이며 Tooltip의
|
|
106
|
+
스타일 문맥에서 사용한다. Speed Dial은 이 조합의 사용 사례 이름이며 별도 namespace가 아니다.
|
|
107
|
+
Template 항목의 `id`는 DOM ID로도 쓰이므로 같은 화면에서 고유하게 지정한다.
|
|
108
|
+
Template의 `className`, `style`, ref는 Root 하나에만 적용한다. 내부 버튼의
|
|
109
|
+
`className`은 입력에서 제외하며 런타임 입력도 무시한다. 내부 스타일은 최상위
|
|
110
|
+
`className` 아래 nested selector 또는 data attribute로 조정한다. 직접 조립하는
|
|
111
|
+
Trigger·Menu.Item의 `className`은 유지한다.
|
|
112
|
+
|
|
113
|
+
Hover로 열린 메뉴의 첫 포인터 클릭은 열림을 유지하고 이후 클릭·Enter·Space는
|
|
114
|
+
토글한다. 닫힌 Trigger에서는 모든 방향키로 첫 액션에 진입하며, 열린 메뉴의
|
|
115
|
+
반대 방향키는 Trigger에서 멈춘다.
|
|
116
|
+
|
|
49
117
|
Select와 Dropdown이 함께 사용하는 `SelectedValue`, `toSelectedValueKey`,
|
|
50
118
|
`isSameSelectedValue`, `isSameSelectedValueList`, `normalizeSingleSelectedValue`,
|
|
51
119
|
`normalizeSelectedValuesByMode`도 root public entry에서 제공한다.
|
package/dist/styles.css
CHANGED
|
@@ -417,6 +417,28 @@
|
|
|
417
417
|
--dropdown-option-padding-block-xsmall: 0px;
|
|
418
418
|
--dropdown-option-radius-xsmall: 4px;
|
|
419
419
|
--dropdown-text-weight-selected-xsmall: var(--dropdown-text-weight);
|
|
420
|
+
--fab-height-small: var(--theme-size-medium-1);
|
|
421
|
+
--fab-height-medium: var(--theme-size-medium-2);
|
|
422
|
+
--fab-height-large: var(--theme-size-medium-3);
|
|
423
|
+
--fab-content-size-small: var(--theme-size-xsmall-3);
|
|
424
|
+
--fab-content-size-medium: var(--theme-size-small-1);
|
|
425
|
+
--fab-content-size-large: var(--theme-size-small-2);
|
|
426
|
+
--fab-radius: 60px;
|
|
427
|
+
--fab-padding-inline: var(--spacing-padding-5);
|
|
428
|
+
--fab-padding-block: var(--spacing-padding-2);
|
|
429
|
+
--fab-extended-padding-inline: var(--spacing-padding-6);
|
|
430
|
+
--fab-gap: var(--spacing-gap-4);
|
|
431
|
+
--fab-font-family: var(--font-family-sans);
|
|
432
|
+
--fab-font-size-small: var(--font-body-xxsmall-size);
|
|
433
|
+
--fab-font-size-medium: var(--font-body-xsmall-size);
|
|
434
|
+
--fab-font-size-large: var(--font-body-small-size);
|
|
435
|
+
--fab-font-weight: var(--font-body-small-weight);
|
|
436
|
+
--fab-line-height: var(--font-body-small-line-height);
|
|
437
|
+
--fab-letter-spacing: var(--font-body-small-letter-spacing);
|
|
438
|
+
--fab-outlined-background: var(--button-default-outline-bg);
|
|
439
|
+
--fab-focus-color: var(--color-primary-default);
|
|
440
|
+
--fab-disabled-background: var(--button-default-neutral-disabled-bg);
|
|
441
|
+
--fab-disabled-foreground: var(--button-default-neutral-disabled-foreground);
|
|
420
442
|
--form-field-width: auto;
|
|
421
443
|
--form-field-flex: 0 0 auto;
|
|
422
444
|
--form-field-gap-x: var(--spacing-gap-5);
|
|
@@ -3770,6 +3792,314 @@ figure.chip {
|
|
|
3770
3792
|
padding: 0;
|
|
3771
3793
|
}
|
|
3772
3794
|
|
|
3795
|
+
.fab {
|
|
3796
|
+
--fab-hover-shadow: none;
|
|
3797
|
+
--fab-pressed-shadow: none;
|
|
3798
|
+
display: flex;
|
|
3799
|
+
flex: 0 0 auto;
|
|
3800
|
+
align-items: center;
|
|
3801
|
+
justify-content: center;
|
|
3802
|
+
box-sizing: border-box;
|
|
3803
|
+
width: var(--fab-height);
|
|
3804
|
+
height: var(--fab-height);
|
|
3805
|
+
padding: var(--fab-padding-block) var(--fab-padding-inline);
|
|
3806
|
+
border: 0;
|
|
3807
|
+
border-radius: var(--fab-radius);
|
|
3808
|
+
background-color: var(--fab-background);
|
|
3809
|
+
color: var(--fab-foreground);
|
|
3810
|
+
box-shadow: none;
|
|
3811
|
+
cursor: pointer;
|
|
3812
|
+
}
|
|
3813
|
+
.fab:where([data-fill="solid"][data-priority="primary"]) {
|
|
3814
|
+
--fab-background: var(--button-default-primary-solid-bg);
|
|
3815
|
+
--fab-foreground: var(--button-default-primary-solid-foreground);
|
|
3816
|
+
--fab-hover-background: var(--button-default-primary-solid-hover-bg);
|
|
3817
|
+
--fab-pressed-background: var(--button-default-primary-solid-active-bg);
|
|
3818
|
+
}
|
|
3819
|
+
.fab:where([data-fill="outlined"][data-priority="primary"]) {
|
|
3820
|
+
--fab-background: var(--fab-outlined-background);
|
|
3821
|
+
--fab-foreground: var(--button-default-primary-outline-foreground);
|
|
3822
|
+
--fab-hover-background: var(--button-default-primary-outline-hover-bg);
|
|
3823
|
+
border: 1px solid var(--button-default-primary-outline-border);
|
|
3824
|
+
}
|
|
3825
|
+
.fab:where([data-fill="solid"][data-priority="secondary"]) {
|
|
3826
|
+
--fab-background: var(--button-default-secondary-solid-bg);
|
|
3827
|
+
--fab-foreground: var(--button-default-secondary-solid-foreground);
|
|
3828
|
+
--fab-hover-background: var(--button-default-secondary-solid-hover-bg);
|
|
3829
|
+
--fab-pressed-background: var(--button-default-secondary-solid-active-bg);
|
|
3830
|
+
}
|
|
3831
|
+
.fab:where([data-fill="outlined"][data-priority="secondary"]) {
|
|
3832
|
+
--fab-background: var(--fab-outlined-background);
|
|
3833
|
+
--fab-foreground: var(--button-default-secondary-outline-foreground);
|
|
3834
|
+
--fab-hover-background: var(--button-default-secondary-outline-hover-bg);
|
|
3835
|
+
border: 1px solid var(--button-default-secondary-outline-border);
|
|
3836
|
+
}
|
|
3837
|
+
.fab:where([data-fill="solid"][data-priority="tertiary"]) {
|
|
3838
|
+
--fab-background: var(--button-default-tertiary-solid-bg);
|
|
3839
|
+
--fab-foreground: var(--button-default-tertiary-solid-foreground);
|
|
3840
|
+
--fab-hover-background: var(--button-default-tertiary-solid-hover-bg);
|
|
3841
|
+
--fab-pressed-background: var(--button-default-tertiary-solid-active-bg);
|
|
3842
|
+
}
|
|
3843
|
+
.fab:where([data-fill="outlined"][data-priority="tertiary"]) {
|
|
3844
|
+
--fab-background: var(--fab-outlined-background);
|
|
3845
|
+
--fab-foreground: var(--button-default-tertiary-outline-foreground);
|
|
3846
|
+
--fab-hover-background: var(--button-default-tertiary-outline-hover-bg);
|
|
3847
|
+
border: 1px solid var(--button-default-tertiary-outline-border);
|
|
3848
|
+
}
|
|
3849
|
+
.fab:where([data-fill="solid"][data-priority="green"]) {
|
|
3850
|
+
--fab-background: var(--button-default-green-solid-bg);
|
|
3851
|
+
--fab-foreground: var(--button-default-green-solid-foreground);
|
|
3852
|
+
--fab-hover-background: var(--button-default-green-solid-hover-bg);
|
|
3853
|
+
--fab-pressed-background: var(--button-default-green-solid-active-bg);
|
|
3854
|
+
}
|
|
3855
|
+
.fab:where([data-fill="outlined"][data-priority="green"]) {
|
|
3856
|
+
--fab-background: var(--fab-outlined-background);
|
|
3857
|
+
--fab-foreground: var(--button-default-green-outline-foreground);
|
|
3858
|
+
--fab-hover-background: var(--button-default-green-outline-hover-bg);
|
|
3859
|
+
--fab-pressed-background: var(--button-default-green-outline-active-bg);
|
|
3860
|
+
border: 1px solid var(--button-default-green-outline-border);
|
|
3861
|
+
}
|
|
3862
|
+
.fab:where([data-fill="solid"][data-priority="yellow"]) {
|
|
3863
|
+
--fab-background: var(--button-default-yellow-solid-bg);
|
|
3864
|
+
--fab-foreground: var(--button-default-yellow-solid-foreground);
|
|
3865
|
+
--fab-hover-background: var(--button-default-yellow-solid-hover-bg);
|
|
3866
|
+
--fab-pressed-background: var(--button-default-yellow-solid-active-bg);
|
|
3867
|
+
}
|
|
3868
|
+
.fab:where([data-fill="outlined"][data-priority="yellow"]) {
|
|
3869
|
+
--fab-background: var(--fab-outlined-background);
|
|
3870
|
+
--fab-foreground: var(--button-default-yellow-outline-foreground);
|
|
3871
|
+
--fab-hover-background: var(--button-default-yellow-outline-hover-bg);
|
|
3872
|
+
--fab-pressed-background: var(--button-default-yellow-outline-active-bg);
|
|
3873
|
+
border: 1px solid var(--button-default-yellow-outline-border);
|
|
3874
|
+
}
|
|
3875
|
+
.fab:where([data-fill="solid"][data-priority="orange"]) {
|
|
3876
|
+
--fab-background: var(--button-default-orange-solid-bg);
|
|
3877
|
+
--fab-foreground: var(--button-default-orange-solid-foreground);
|
|
3878
|
+
--fab-hover-background: var(--button-default-orange-solid-hover-bg);
|
|
3879
|
+
--fab-pressed-background: var(--button-default-orange-solid-active-bg);
|
|
3880
|
+
}
|
|
3881
|
+
.fab:where([data-fill="outlined"][data-priority="orange"]) {
|
|
3882
|
+
--fab-background: var(--fab-outlined-background);
|
|
3883
|
+
--fab-foreground: var(--button-default-orange-outline-foreground);
|
|
3884
|
+
--fab-hover-background: var(--button-default-orange-outline-hover-bg);
|
|
3885
|
+
--fab-pressed-background: var(--button-default-orange-outline-active-bg);
|
|
3886
|
+
border: 1px solid var(--button-default-orange-outline-border);
|
|
3887
|
+
}
|
|
3888
|
+
.fab:where([data-fill="solid"][data-priority="teal"]) {
|
|
3889
|
+
--fab-background: var(--button-default-teal-solid-bg);
|
|
3890
|
+
--fab-foreground: var(--button-default-teal-solid-foreground);
|
|
3891
|
+
--fab-hover-background: var(--button-default-teal-solid-hover-bg);
|
|
3892
|
+
--fab-pressed-background: var(--button-default-teal-solid-active-bg);
|
|
3893
|
+
}
|
|
3894
|
+
.fab:where([data-fill="outlined"][data-priority="teal"]) {
|
|
3895
|
+
--fab-background: var(--fab-outlined-background);
|
|
3896
|
+
--fab-foreground: var(--button-default-teal-outline-foreground);
|
|
3897
|
+
--fab-hover-background: var(--button-default-teal-outline-hover-bg);
|
|
3898
|
+
--fab-pressed-background: var(--button-default-teal-outline-active-bg);
|
|
3899
|
+
border: 1px solid var(--button-default-teal-outline-border);
|
|
3900
|
+
}
|
|
3901
|
+
.fab:where([data-fill="solid"][data-priority="pink"]) {
|
|
3902
|
+
--fab-background: var(--button-default-pink-solid-bg);
|
|
3903
|
+
--fab-foreground: var(--button-default-pink-solid-foreground);
|
|
3904
|
+
--fab-hover-background: var(--button-default-pink-solid-hover-bg);
|
|
3905
|
+
--fab-pressed-background: var(--button-default-pink-solid-active-bg);
|
|
3906
|
+
}
|
|
3907
|
+
.fab:where([data-fill="outlined"][data-priority="pink"]) {
|
|
3908
|
+
--fab-background: var(--fab-outlined-background);
|
|
3909
|
+
--fab-foreground: var(--button-default-pink-outline-foreground);
|
|
3910
|
+
--fab-hover-background: var(--button-default-pink-outline-hover-bg);
|
|
3911
|
+
--fab-pressed-background: var(--button-default-pink-outline-active-bg);
|
|
3912
|
+
border: 1px solid var(--button-default-pink-outline-border);
|
|
3913
|
+
}
|
|
3914
|
+
.fab:where([data-fill="solid"][data-priority="red"]) {
|
|
3915
|
+
--fab-background: var(--button-default-red-solid-bg);
|
|
3916
|
+
--fab-foreground: var(--button-default-red-solid-foreground);
|
|
3917
|
+
--fab-hover-background: var(--button-default-red-solid-hover-bg);
|
|
3918
|
+
--fab-pressed-background: var(--button-default-red-solid-active-bg);
|
|
3919
|
+
}
|
|
3920
|
+
.fab:where([data-fill="outlined"][data-priority="red"]) {
|
|
3921
|
+
--fab-background: var(--fab-outlined-background);
|
|
3922
|
+
--fab-foreground: var(--button-default-red-outline-foreground);
|
|
3923
|
+
--fab-hover-background: var(--button-default-red-outline-hover-bg);
|
|
3924
|
+
--fab-pressed-background: var(--button-default-red-outline-active-bg);
|
|
3925
|
+
border: 1px solid var(--button-default-red-outline-border);
|
|
3926
|
+
}
|
|
3927
|
+
.fab:where([data-fill="solid"][data-priority="darkGray"]) {
|
|
3928
|
+
--fab-background: var(--button-default-dark-gray-solid-bg);
|
|
3929
|
+
--fab-foreground: var(--button-default-dark-gray-solid-foreground);
|
|
3930
|
+
--fab-hover-background: var(--button-default-dark-gray-solid-hover-bg);
|
|
3931
|
+
--fab-pressed-background: var(--button-default-dark-gray-solid-active-bg);
|
|
3932
|
+
}
|
|
3933
|
+
.fab:where([data-fill="outlined"][data-priority="darkGray"]) {
|
|
3934
|
+
--fab-background: var(--fab-outlined-background);
|
|
3935
|
+
--fab-foreground: var(--button-default-dark-gray-outline-foreground);
|
|
3936
|
+
--fab-hover-background: var(--button-default-dark-gray-outline-hover-bg);
|
|
3937
|
+
--fab-pressed-background: var(--button-default-dark-gray-outline-active-bg);
|
|
3938
|
+
border: 1px solid var(--button-default-dark-gray-outline-border);
|
|
3939
|
+
}
|
|
3940
|
+
.fab:where([data-fill="solid"][data-priority="lightGray"]) {
|
|
3941
|
+
--fab-background: var(--button-default-light-gray-solid-bg);
|
|
3942
|
+
--fab-foreground: var(--button-default-light-gray-solid-foreground);
|
|
3943
|
+
--fab-hover-background: var(--button-default-light-gray-solid-hover-bg);
|
|
3944
|
+
--fab-pressed-background: var(--button-default-light-gray-solid-active-bg);
|
|
3945
|
+
}
|
|
3946
|
+
.fab:where([data-fill="outlined"][data-priority="lightGray"]) {
|
|
3947
|
+
--fab-background: var(--fab-outlined-background);
|
|
3948
|
+
--fab-foreground: var(--button-default-light-gray-outline-foreground);
|
|
3949
|
+
--fab-hover-background: var(--button-default-light-gray-outline-hover-bg);
|
|
3950
|
+
--fab-pressed-background: var(--button-default-light-gray-outline-active-bg);
|
|
3951
|
+
border: 1px solid var(--button-default-light-gray-outline-border);
|
|
3952
|
+
}
|
|
3953
|
+
.fab:where([data-fill="outlined"][data-priority="primary"]) {
|
|
3954
|
+
--fab-hover-background: var(--button-default-primary-overlay-outlined-bg);
|
|
3955
|
+
--fab-pressed-background: var(--fab-hover-background);
|
|
3956
|
+
--fab-hover-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.68);
|
|
3957
|
+
--fab-pressed-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.28);
|
|
3958
|
+
}
|
|
3959
|
+
.fab:where([data-fill="outlined"][data-priority="secondary"]) {
|
|
3960
|
+
--fab-hover-background: var(--button-default-secondary-overlay-outlined-bg);
|
|
3961
|
+
--fab-pressed-background: var(--fab-hover-background);
|
|
3962
|
+
--fab-hover-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.68);
|
|
3963
|
+
--fab-pressed-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.28);
|
|
3964
|
+
}
|
|
3965
|
+
.fab:where([data-fill="outlined"][data-priority="tertiary"]) {
|
|
3966
|
+
--fab-hover-background: var(--button-default-tertiary-overlay-outlined-bg);
|
|
3967
|
+
--fab-pressed-background: var(--fab-hover-background);
|
|
3968
|
+
--fab-hover-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.68);
|
|
3969
|
+
--fab-pressed-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.28);
|
|
3970
|
+
}
|
|
3971
|
+
.fab:where([data-fill="solid"][data-priority="tertiary"]) {
|
|
3972
|
+
--fab-hover-background: var(--button-default-tertiary-overlay-solid-bg);
|
|
3973
|
+
--fab-pressed-background: var(--fab-hover-background);
|
|
3974
|
+
--fab-hover-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.68);
|
|
3975
|
+
--fab-pressed-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.48);
|
|
3976
|
+
}
|
|
3977
|
+
.fab:where(:enabled:hover) {
|
|
3978
|
+
background-color: var(--fab-hover-background);
|
|
3979
|
+
box-shadow: var(--fab-hover-shadow);
|
|
3980
|
+
}
|
|
3981
|
+
.fab:where(:enabled:active) {
|
|
3982
|
+
background-color: var(--fab-pressed-background);
|
|
3983
|
+
box-shadow: var(--fab-pressed-shadow);
|
|
3984
|
+
}
|
|
3985
|
+
.fab:where(:focus-visible) {
|
|
3986
|
+
outline: 2px solid var(--fab-focus-color);
|
|
3987
|
+
outline-offset: 2px;
|
|
3988
|
+
}
|
|
3989
|
+
.fab:where(:disabled) {
|
|
3990
|
+
background-color: var(--fab-disabled-background);
|
|
3991
|
+
border-color: var(--fab-disabled-background);
|
|
3992
|
+
--fab-foreground: var(--fab-disabled-foreground);
|
|
3993
|
+
cursor: not-allowed;
|
|
3994
|
+
}
|
|
3995
|
+
.fab:where([data-size="small"]) {
|
|
3996
|
+
--fab-height: var(--fab-height-small);
|
|
3997
|
+
--fab-content-size: var(--fab-content-size-small);
|
|
3998
|
+
--fab-font-size: var(--fab-font-size-small);
|
|
3999
|
+
}
|
|
4000
|
+
.fab:where([data-size="medium"]) {
|
|
4001
|
+
--fab-height: var(--fab-height-medium);
|
|
4002
|
+
--fab-content-size: var(--fab-content-size-medium);
|
|
4003
|
+
--fab-font-size: var(--fab-font-size-medium);
|
|
4004
|
+
}
|
|
4005
|
+
.fab:where([data-size="large"]) {
|
|
4006
|
+
--fab-height: var(--fab-height-large);
|
|
4007
|
+
--fab-content-size: var(--fab-content-size-large);
|
|
4008
|
+
--fab-font-size: var(--fab-font-size-large);
|
|
4009
|
+
}
|
|
4010
|
+
.fab:where([data-extended="true"]) {
|
|
4011
|
+
width: fit-content;
|
|
4012
|
+
min-width: var(--fab-height);
|
|
4013
|
+
max-width: none;
|
|
4014
|
+
gap: var(--fab-gap);
|
|
4015
|
+
padding-inline: var(--fab-extended-padding-inline);
|
|
4016
|
+
}
|
|
4017
|
+
.fab :where(.fab-content) {
|
|
4018
|
+
display: flex;
|
|
4019
|
+
flex: 0 0 var(--fab-content-size);
|
|
4020
|
+
align-items: center;
|
|
4021
|
+
justify-content: center;
|
|
4022
|
+
width: var(--fab-content-size);
|
|
4023
|
+
height: var(--fab-content-size);
|
|
4024
|
+
color: var(--fab-foreground);
|
|
4025
|
+
}
|
|
4026
|
+
.fab :where(.fab-label) {
|
|
4027
|
+
color: var(--fab-foreground);
|
|
4028
|
+
font-family: var(--fab-font-family);
|
|
4029
|
+
font-size: var(--fab-font-size);
|
|
4030
|
+
font-weight: var(--fab-font-weight);
|
|
4031
|
+
line-height: var(--fab-line-height);
|
|
4032
|
+
letter-spacing: var(--fab-letter-spacing);
|
|
4033
|
+
white-space: nowrap;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
.fab-root,
|
|
4037
|
+
.fab-menu-list {
|
|
4038
|
+
display: flex;
|
|
4039
|
+
align-items: center;
|
|
4040
|
+
gap: 24px;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
.fab-menu-list[data-open="false"] {
|
|
4044
|
+
visibility: hidden;
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
.fab-root {
|
|
4048
|
+
direction: ltr;
|
|
4049
|
+
}
|
|
4050
|
+
.fab-root[data-open="false"] {
|
|
4051
|
+
pointer-events: none;
|
|
4052
|
+
}
|
|
4053
|
+
.fab-root[data-open="false"] .fab-trigger {
|
|
4054
|
+
pointer-events: auto;
|
|
4055
|
+
}
|
|
4056
|
+
.fab-root[data-direction="up"],
|
|
4057
|
+
.fab-root[data-direction="up"] .fab-menu-list {
|
|
4058
|
+
flex-direction: column-reverse;
|
|
4059
|
+
}
|
|
4060
|
+
.fab-root[data-direction="down"],
|
|
4061
|
+
.fab-root[data-direction="down"] .fab-menu-list {
|
|
4062
|
+
flex-direction: column;
|
|
4063
|
+
}
|
|
4064
|
+
.fab-root[data-direction="left"],
|
|
4065
|
+
.fab-root[data-direction="left"] .fab-menu-list {
|
|
4066
|
+
flex-direction: row-reverse;
|
|
4067
|
+
}
|
|
4068
|
+
.fab-root[data-direction="right"],
|
|
4069
|
+
.fab-root[data-direction="right"] .fab-menu-list {
|
|
4070
|
+
flex-direction: row;
|
|
4071
|
+
}
|
|
4072
|
+
.fab-root[data-open="true"] .fab-trigger .fab-content {
|
|
4073
|
+
transform: rotate(45deg);
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
.fab-trigger .fab-content {
|
|
4077
|
+
transition: transform 150ms ease;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
.fab-root[data-open="true"] .fab-menu-item {
|
|
4081
|
+
animation: fab-menu-enter 150ms ease-out;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
@keyframes fab-menu-enter {
|
|
4085
|
+
from {
|
|
4086
|
+
opacity: 0;
|
|
4087
|
+
transform: scale(0.75);
|
|
4088
|
+
}
|
|
4089
|
+
to {
|
|
4090
|
+
opacity: 1;
|
|
4091
|
+
transform: scale(1);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4095
|
+
.fab-trigger .fab-content {
|
|
4096
|
+
transition: none;
|
|
4097
|
+
}
|
|
4098
|
+
.fab-root[data-open="true"] .fab-menu-item {
|
|
4099
|
+
animation: none;
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
|
|
3773
4103
|
.form-field-container {
|
|
3774
4104
|
display: block;
|
|
3775
4105
|
width: var(--form-field-width);
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "./styles";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "./index.scss";
|
|
2
|
+
|
|
3
|
+
import { Tooltip } from "../tooltip";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
FabDefault,
|
|
7
|
+
FabExtended,
|
|
8
|
+
FabLabel,
|
|
9
|
+
FabRoot,
|
|
10
|
+
FabTrigger,
|
|
11
|
+
FabMenuList,
|
|
12
|
+
FabMenuItem,
|
|
13
|
+
FabTemplate,
|
|
14
|
+
} from "./markup";
|
|
15
|
+
|
|
16
|
+
export const Fab = {
|
|
17
|
+
Button: FabDefault,
|
|
18
|
+
Trigger: FabTrigger,
|
|
19
|
+
Expand: FabExtended,
|
|
20
|
+
Menu: {
|
|
21
|
+
List: FabMenuList,
|
|
22
|
+
Item: FabMenuItem,
|
|
23
|
+
Label: Tooltip.Text,
|
|
24
|
+
},
|
|
25
|
+
Root: FabRoot,
|
|
26
|
+
Template: FabTemplate,
|
|
27
|
+
Default: FabDefault,
|
|
28
|
+
Extended: FabExtended,
|
|
29
|
+
Label: FabLabel,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export * from "./types/options";
|
|
33
|
+
export type * from "./types";
|
|
34
|
+
export { FabContext, ARROW_KEYS, useFabContext } from "./markup";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import type { FabButtonProps } from "../types";
|
|
6
|
+
import { FabLabel } from "./Label";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fab Component; 공통 native 액션 마크업
|
|
10
|
+
* @component
|
|
11
|
+
* @property {ReactNode} props.icon 필수 장식용 콘텐츠. fab-content 안에서 aria-hidden으로 표시한다.
|
|
12
|
+
* @property {"small" | "medium" | "large"} props.size 필수 크기. 높이 40/48/56px와 아이콘 영역 16/20/24px를 적용한다.
|
|
13
|
+
* @property {"solid" | "outlined"} props.fill 필수 채움 방식. outlined의 기본 면은 불투명 흰색이다.
|
|
14
|
+
* @property {"primary" | "secondary" | "tertiary" | "green" | "yellow" | "orange" | "teal" | "pink" | "red" | "darkGray" | "lightGray"} props.priority 두 fill 모두에서 사용하는 필수 색상.
|
|
15
|
+
* @property {boolean} [props.extended=false] 라벨과 자연 너비를 표시할지 여부. Button/Expand가 각각 false/true로 고정한다.
|
|
16
|
+
* @property {ReactNode} [props.children] extended가 true일 때 표시할 라벨. 문자열·숫자는 Fab.Label로 감싼다.
|
|
17
|
+
* @property {"button" | "submit" | "reset"} [props.type="button"] native 버튼 동작.
|
|
18
|
+
* @property {boolean} [props.disabled=false] native 비활성화. 클릭·키보드 실행과 Tab 진입을 차단한다.
|
|
19
|
+
* @property {string} [props.aria-label] native 접근 가능한 이름. 아이콘 전용 공개 Button에서는 필수다.
|
|
20
|
+
* @property {React.MouseEventHandler<HTMLButtonElement>} [props.onClick] native 버튼의 실행 이벤트.
|
|
21
|
+
* @property {string} [props.className] 최상위 button에 fab 클래스와 병합할 추가 클래스.
|
|
22
|
+
* @property {CSSProperties} [props.style] 최상위 button의 inline style.
|
|
23
|
+
* @property {React.Ref<HTMLButtonElement>} [ref] 실제 native button의 ref.
|
|
24
|
+
* @see FabButtonProps
|
|
25
|
+
* @desc Button/Expand가 공유하는 내부 마크업이다. 크기·색상·라벨 표현만 적용하며 클릭·키보드·disabled는 native button에 맡긴다. 나머지 native button 속성·이벤트를 전달한다.
|
|
26
|
+
*/
|
|
27
|
+
const FabButton = forwardRef<HTMLButtonElement, FabButtonProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
icon,
|
|
31
|
+
children,
|
|
32
|
+
size,
|
|
33
|
+
fill,
|
|
34
|
+
priority,
|
|
35
|
+
extended = false,
|
|
36
|
+
className,
|
|
37
|
+
type = "button",
|
|
38
|
+
...rest
|
|
39
|
+
},
|
|
40
|
+
ref,
|
|
41
|
+
) => (
|
|
42
|
+
<button
|
|
43
|
+
{...rest}
|
|
44
|
+
ref={ref}
|
|
45
|
+
type={type}
|
|
46
|
+
className={clsx("fab", className)}
|
|
47
|
+
data-size={size}
|
|
48
|
+
data-fill={fill}
|
|
49
|
+
data-priority={priority}
|
|
50
|
+
data-extended={extended || undefined}
|
|
51
|
+
>
|
|
52
|
+
<span className="fab-content" aria-hidden="true">
|
|
53
|
+
{icon}
|
|
54
|
+
</span>
|
|
55
|
+
{extended && <FabLabel>{children}</FabLabel>}
|
|
56
|
+
</button>
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
FabButton.displayName = "FabButton";
|
|
61
|
+
|
|
62
|
+
export { FabButton };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import type { FabDefaultProps } from "../types";
|
|
5
|
+
import { FabButton } from "./Base";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fab Component; 단독으로 사용하는 아이콘 버튼
|
|
9
|
+
* @component
|
|
10
|
+
* @property {ReactNode} props.icon 필수 장식용 콘텐츠. 내부는 aria-hidden이므로 행동 이름을 별도로 제공한다.
|
|
11
|
+
* @property {"small" | "medium" | "large"} props.size 필수 크기. 버튼 높이는 각각 40/48/56px, 아이콘 영역은 16/20/24px다.
|
|
12
|
+
* @property {"solid" | "outlined"} props.fill 필수 채움 방식. outlined의 기본 면은 불투명 흰색이다.
|
|
13
|
+
* @property {"primary" | "secondary" | "tertiary" | "green" | "yellow" | "orange" | "teal" | "pink" | "red" | "darkGray" | "lightGray"} props.priority 필수 색상. 두 fill 모두에서 같은 목록을 지원한다.
|
|
14
|
+
* @property {string} props.aria-label 필수 접근 가능한 행동 이름. 아이콘의 모양보다 실행할 작업을 설명한다.
|
|
15
|
+
* @property {"button" | "submit" | "reset"} [props.type="button"] native 버튼 동작. 폼 제출·초기화는 명시적으로 선택한다.
|
|
16
|
+
* @property {boolean} [props.disabled=false] native 비활성화. 클릭·키보드 실행과 Tab 진입을 차단한다.
|
|
17
|
+
* @property {React.MouseEventHandler<HTMLButtonElement>} [props.onClick] 클릭·Enter·Space의 native 실행 이벤트.
|
|
18
|
+
* @property {string} [props.className] 최상위 button에 fab 클래스와 병합할 추가 클래스.
|
|
19
|
+
* @property {CSSProperties} [props.style] 최상위 button의 inline style.
|
|
20
|
+
* @property {React.Ref<HTMLButtonElement>} [ref] 실제 native button에 전달하는 object/callback ref.
|
|
21
|
+
* @see FabDefaultProps
|
|
22
|
+
* @desc Fab.Button과 호환 이름 Fab.Default로 공개한다. children을 받지 않는 아이콘 전용 버튼이며 나머지 native button 속성·이벤트는 그대로 전달한다. 메뉴 상태와 viewport 배치는 소비자가 정한다.
|
|
23
|
+
* @example
|
|
24
|
+
* <Fab.Button icon={<span>+</span>} aria-label="새 항목 추가"
|
|
25
|
+
* size="large" fill="solid" priority="primary" />
|
|
26
|
+
*/
|
|
27
|
+
const FabDefault = forwardRef<HTMLButtonElement, FabDefaultProps>(
|
|
28
|
+
(props, ref) => <FabButton {...props} ref={ref} extended={false} />,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
FabDefault.displayName = "FabDefault";
|
|
32
|
+
|
|
33
|
+
export { FabDefault };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import type { FabExtendedProps } from "../types";
|
|
5
|
+
import { FabButton } from "./Base";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fab Component; 아이콘과 라벨을 표시하는 주요 액션
|
|
9
|
+
* @component
|
|
10
|
+
* @property {ReactNode} props.icon 필수 장식용 콘텐츠. 내부는 aria-hidden이므로 행동 이름은 라벨로 제공한다.
|
|
11
|
+
* @property {ReactNode} props.children 필수 행동 라벨. 문자열·숫자는 Fab.Label로 감싸고 복합 JSX는 그대로 표시한다.
|
|
12
|
+
* @property {"small" | "medium" | "large"} props.size 필수 크기. 버튼 높이는 각각 40/48/56px, 아이콘 영역은 16/20/24px다.
|
|
13
|
+
* @property {"solid" | "outlined"} props.fill 필수 채움 방식. outlined의 기본 면은 불투명 흰색이다.
|
|
14
|
+
* @property {"primary" | "secondary" | "tertiary" | "green" | "yellow" | "orange" | "teal" | "pink" | "red" | "darkGray" | "lightGray"} props.priority 필수 색상. 두 fill 모두에서 같은 목록을 지원한다.
|
|
15
|
+
* @property {string} [props.aria-label] 라벨과 별도의 접근 가능한 이름. 생략하면 표시한 라벨을 사용한다.
|
|
16
|
+
* @property {"button" | "submit" | "reset"} [props.type="button"] native 버튼 동작. 폼 제출·초기화는 명시적으로 선택한다.
|
|
17
|
+
* @property {boolean} [props.disabled=false] native 비활성화. 클릭·키보드 실행과 Tab 진입을 차단한다.
|
|
18
|
+
* @property {React.MouseEventHandler<HTMLButtonElement>} [props.onClick] 클릭·Enter·Space의 native 실행 이벤트.
|
|
19
|
+
* @property {string} [props.className] 최상위 button에 fab 클래스와 병합할 추가 클래스.
|
|
20
|
+
* @property {CSSProperties} [props.style] 최상위 button의 inline style.
|
|
21
|
+
* @property {React.Ref<HTMLButtonElement>} [ref] 실제 native button에 전달하는 object/callback ref.
|
|
22
|
+
* @see FabExtendedProps
|
|
23
|
+
* @desc Fab.Expand와 호환 이름 Fab.Extended로 공개한다. 콘텐츠 너비에 좌우 16px padding을 더하는 라벨 확장형이며 액션 메뉴를 여는 기능은 갖지 않는다. 나머지 native button 속성·이벤트를 전달한다. 복합 라벨에도 접근 가능한 이름을 유지한다.
|
|
24
|
+
* @example
|
|
25
|
+
* <Fab.Expand icon={<span>+</span>} size="large" fill="solid" priority="primary">
|
|
26
|
+
* 새 항목 추가
|
|
27
|
+
* </Fab.Expand>
|
|
28
|
+
*/
|
|
29
|
+
const FabExtended = forwardRef<HTMLButtonElement, FabExtendedProps>(
|
|
30
|
+
(props, ref) => <FabButton {...props} ref={ref} extended />,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
FabExtended.displayName = "FabExtended";
|
|
34
|
+
|
|
35
|
+
export { FabExtended };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Slot } from "../../slot";
|
|
2
|
+
import type { SlotTextProps } from "../../slot/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fab Component; 라벨의 고정 타이포그래피
|
|
6
|
+
* @component
|
|
7
|
+
* @property {ReactNode} [props.children] 문자열·숫자는 span으로 감싸고 다른 ReactNode는 그대로 반환한다.
|
|
8
|
+
* @property {string} [props.title] 문자열·숫자 라벨의 native title. 생략하면 children의 문자열 값을 사용한다.
|
|
9
|
+
* @property {string} [props.id] 문자열·숫자 라벨을 감싼 span의 DOM ID.
|
|
10
|
+
* @property {CSSProperties} [props.style] 문자열·숫자 라벨을 감싼 span의 inline style.
|
|
11
|
+
* @see SlotTextProps
|
|
12
|
+
* @desc Expand의 문자열·숫자 라벨을 fab-label 클래스로 표시한다. SlotTextProps에서 className·as를 제외하며, ComponentPropsWithoutRef<"span">을 사용하므로 ref도 받지 않는다. 나머지 native span 속성은 래핑한 텍스트에만 전달한다. 복합 JSX는 wrapper와 속성 주입 없이 반환한다. 스타일 조정은 최상위 FAB의 nested selector로 한다.
|
|
13
|
+
*/
|
|
14
|
+
export function FabLabel(props: Omit<SlotTextProps, "className" | "as">) {
|
|
15
|
+
return <Slot.Text {...props} className="fab-label" />;
|
|
16
|
+
}
|