@orioncactuscorp/ui 1.9.2 → 1.11.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.
Files changed (99) hide show
  1. package/README.md +119 -54
  2. package/dist/components/Accordion/Accordion.css +1 -1
  3. package/dist/components/Accordion/Accordion.js +169 -177
  4. package/dist/components/ActionArea/ActionAreaLayout.js +20 -24
  5. package/dist/components/Cell/Cell.css +1 -1
  6. package/dist/components/Cell/Cell.js +57 -58
  7. package/dist/components/CheckMark/CheckMark.css +1 -1
  8. package/dist/components/Checkbox/Checkbox.css +1 -1
  9. package/dist/components/Checkbox/Checkbox.js +50 -51
  10. package/dist/components/Menu/Menu.css +1 -1
  11. package/dist/components/Menu/Menu.module.scss.js +20 -22
  12. package/dist/components/Modal/Modal.css +1 -1
  13. package/dist/components/Modal/Modal.js +585 -575
  14. package/dist/components/Modal/bottomSheetGeometry.js +46 -33
  15. package/dist/components/Modal/modalScrollArbitration.js +71 -49
  16. package/dist/components/Modal/snapPoints.js +42 -38
  17. package/dist/components/Modal/useModalContentBlockTransition.js +49 -44
  18. package/dist/components/Modal/useModalDrag.js +937 -854
  19. package/dist/components/Radio/Radio.css +1 -1
  20. package/dist/components/ScrollArea/ScrollArea.js +197 -200
  21. package/dist/components/Select/Select.js +31 -34
  22. package/dist/components/Switch/Switch.css +1 -1
  23. package/dist/components/Tabs/Tabs.css +1 -0
  24. package/dist/components/Tabs/Tabs.js +208 -0
  25. package/dist/components/Tabs/Tabs.module.scss.js +27 -0
  26. package/dist/components/Tabs.d.ts +6 -0
  27. package/dist/components/Tabs.js +8 -0
  28. package/dist/components/TextArea/TextArea.js +57 -58
  29. package/dist/components/TextInput/TextInput.js +30 -33
  30. package/dist/components/Tooltip/Tooltip.css +1 -1
  31. package/dist/foundations/color.theme.dark.css +1 -1
  32. package/dist/foundations/color.theme.light.css +1 -1
  33. package/dist/foundations/focus.css +1 -0
  34. package/dist/foundations/reset.css +1 -1
  35. package/dist/foundations.css +4 -3
  36. package/dist/index.js +69 -64
  37. package/dist/react/motion.js +31 -35
  38. package/dist/reset.css +1 -1
  39. package/dist/styles.css +14 -12
  40. package/dist/ui/src/components/Accordion/Accordion.d.ts.map +1 -1
  41. package/dist/ui/src/components/ActionArea/ActionAreaLayout.d.ts.map +1 -1
  42. package/dist/ui/src/components/Cell/Cell.d.ts.map +1 -1
  43. package/dist/ui/src/components/CheckMark/types.d.ts +10 -0
  44. package/dist/ui/src/components/CheckMark/types.d.ts.map +1 -1
  45. package/dist/ui/src/components/Checkbox/Checkbox.d.ts.map +1 -1
  46. package/dist/ui/src/components/Checkbox/types.d.ts +16 -0
  47. package/dist/ui/src/components/Checkbox/types.d.ts.map +1 -1
  48. package/dist/ui/src/components/Modal/Modal.d.ts +1 -1
  49. package/dist/ui/src/components/Modal/Modal.d.ts.map +1 -1
  50. package/dist/ui/src/components/Modal/bottomSheetGeometry.d.ts +13 -0
  51. package/dist/ui/src/components/Modal/bottomSheetGeometry.d.ts.map +1 -1
  52. package/dist/ui/src/components/Modal/modalScrollArbitration.d.ts.map +1 -1
  53. package/dist/ui/src/components/Modal/snapPoints.d.ts +3 -1
  54. package/dist/ui/src/components/Modal/snapPoints.d.ts.map +1 -1
  55. package/dist/ui/src/components/Modal/types.d.ts +8 -0
  56. package/dist/ui/src/components/Modal/types.d.ts.map +1 -1
  57. package/dist/ui/src/components/Modal/useModalContentBlockTransition.d.ts.map +1 -1
  58. package/dist/ui/src/components/Modal/useModalDrag.d.ts +27 -10
  59. package/dist/ui/src/components/Modal/useModalDrag.d.ts.map +1 -1
  60. package/dist/ui/src/components/Radio/types.d.ts +16 -0
  61. package/dist/ui/src/components/Radio/types.d.ts.map +1 -1
  62. package/dist/ui/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  63. package/dist/ui/src/components/Select/Select.d.ts.map +1 -1
  64. package/dist/ui/src/components/Switch/types.d.ts +10 -0
  65. package/dist/ui/src/components/Switch/types.d.ts.map +1 -1
  66. package/dist/ui/src/components/Tabs/Tabs.d.ts +7 -0
  67. package/dist/ui/src/components/Tabs/Tabs.d.ts.map +1 -0
  68. package/dist/ui/src/components/Tabs/index.d.ts +3 -0
  69. package/dist/ui/src/components/Tabs/index.d.ts.map +1 -0
  70. package/dist/ui/src/components/Tabs/types.d.ts +26 -0
  71. package/dist/ui/src/components/Tabs/types.d.ts.map +1 -0
  72. package/dist/ui/src/components/TextArea/TextArea.d.ts.map +1 -1
  73. package/dist/ui/src/components/TextInput/TextInput.d.ts.map +1 -1
  74. package/dist/ui/src/index.d.ts +2 -0
  75. package/dist/ui/src/index.d.ts.map +1 -1
  76. package/dist/ui/src/react/motion/index.d.ts.map +1 -1
  77. package/dist/ui/src/utils/mergeRefs.d.ts +13 -0
  78. package/dist/ui/src/utils/mergeRefs.d.ts.map +1 -0
  79. package/dist/utils/mergeRefs.js +24 -0
  80. package/dist/vscode/oc-ui-vars.css +13 -0
  81. package/docs/editor-setup.md +42 -0
  82. package/docs/motion.md +665 -0
  83. package/docs/responsive-foundation-profile.md +230 -0
  84. package/docs/scss-helpers.md +189 -0
  85. package/docs/selector-contract.md +260 -0
  86. package/docs/spring.md +382 -0
  87. package/docs/stability.md +62 -0
  88. package/docs/tokens.md +559 -0
  89. package/package.json +9 -4
  90. package/src/scss/foundations/color-theme.test.ts +14 -0
  91. package/src/scss/foundations/color.theme.dark.scss +1 -0
  92. package/src/scss/foundations/color.theme.light.scss +1 -0
  93. package/src/scss/foundations/focus.scss +4 -0
  94. package/src/scss/foundations/focus.test.ts +48 -0
  95. package/src/scss/foundations/reset.scss +9 -4
  96. package/src/scss/foundations/reset.test.ts +32 -0
  97. package/src/scss/index.scss +2 -0
  98. package/src/scss/mixins/_focus.scss +11 -0
  99. package/src/scss/mixins/_focus.test.ts +58 -0
package/docs/motion.md ADDED
@@ -0,0 +1,665 @@
1
+ # Semantic Motion
2
+
3
+ `oc-motion`은 duration/easing preset wrapper가 아니라 UI motion을 semantic contract로 선언하기 위한 helper입니다. 컴포넌트는 "0.24s ease"를 고르는 대신 무엇이 움직이고, 전환의 어느 순간이며, 같은 motion을 얼마나 빠르게 보정할지만 선언합니다.
4
+
5
+ ```scss
6
+ oc-motion($properties, $intent, $phase, $pace)
7
+ ```
8
+
9
+ - `$intent`: 무엇이 움직이는가. UI의 역할과 공간 모델을 고릅니다.
10
+ - `$phase`: 언제 움직이는가. 전환의 시점과 방향을 고릅니다.
11
+ - `$pace`: 같은 motion을 빠르게, 기본값으로, 느리게 보정합니다.
12
+ - escape hatch: semantic contract로 부족할 때만 국소 조정합니다.
13
+
14
+ 실제 duration token, spring profile, reduced-motion 정책은 디자인 시스템이 중앙에서 관리합니다.
15
+
16
+ 일반 컴포넌트와 consumer SCSS에서는 `oc-motion`을 우선 사용합니다. `oc-spring`은 직접 물리 spring을 설계해야 하는 고급 escape hatch로 남깁니다.
17
+
18
+ React runtime에서 height/expand처럼 CSS transition만으로 안정적으로 다루기 어려운 motion은 `@orioncactuscorp/ui/utils/motion`과 `@orioncactuscorp/ui/react/motion` subpath를 사용합니다. 이 runtime layer도 같은 intent, phase, pace, reduced-motion policy, `--oc-motion-duration-factor*` token vocabulary를 공유하고, 실제 프레임 계산은 low-level `ocSpring` engine에 위임합니다.
19
+
20
+ ## 빠른 시작
21
+
22
+ ```scss
23
+ @use '@orioncactuscorp/ui/scss/mixins/motion' as *;
24
+
25
+ .button {
26
+ @include oc-motion((background-color, color), feedback);
27
+ }
28
+
29
+ .notice {
30
+ @include oc-motion(opacity, fade);
31
+ }
32
+
33
+ .popover {
34
+ @include oc-motion((opacity, transform), disclosure, $phase: enter);
35
+ }
36
+
37
+ .popover[data-ending-style] {
38
+ @include oc-motion((opacity, transform), disclosure, $phase: exit);
39
+ }
40
+
41
+ .bottom-sheet {
42
+ @include oc-motion(transform, sheet, $phase: enter);
43
+ }
44
+
45
+ .bottom-sheet[data-drag-release] {
46
+ @include oc-motion(transform, sheet, $phase: release);
47
+ }
48
+ ```
49
+
50
+ `disclosure`, `surface`, `sheet`, `expand`처럼 lifecycle이 있는 motion은 들어올 때 `enter`, 나갈 때 `exit`를 명시하는 것을 기본으로 합니다. lifecycle selector가 없거나 양방향으로 같은 transition을 써도 되는 단순 케이스에서만 기본값인 `change`를 fallback으로 사용합니다.
51
+
52
+ `@include oc-motion(...)`은 기본 transition과 `prefers-reduced-motion: reduce` 대응을 함께 출력합니다. reduced-motion 대응이 필요 없는 순수 함수 값만 필요할 때는 `transition: oc-motion(...)`을 직접 사용할 수 있습니다.
53
+
54
+ ```scss
55
+ .simple {
56
+ transition: oc-motion(opacity, feedback);
57
+ }
58
+ ```
59
+
60
+ React runtime에서는 같은 vocabulary를 TS로 해석할 수 있습니다.
61
+
62
+ ```tsx
63
+ import { MotionExpand } from '@orioncactuscorp/ui/react/motion';
64
+ import { ocMotion } from '@orioncactuscorp/ui/utils/motion';
65
+
66
+ const config = ocMotion.resolve({
67
+ intent: 'expand',
68
+ phase: 'enter',
69
+ pace: 'normal',
70
+ reduced: 'auto',
71
+ });
72
+
73
+ export function Panel({ open, children }) {
74
+ return (
75
+ <MotionExpand open={open} intent='expand' forceMount>
76
+ {children}
77
+ </MotionExpand>
78
+ );
79
+ }
80
+ ```
81
+
82
+ ## 왜 semantic contract로 작성하는가
83
+
84
+ motion을 컴포넌트마다 `0.2s ease`, `0.3s linear(...)`처럼 직접 쓰면 다음 문제가 생깁니다.
85
+
86
+ - 같은 종류의 움직임이 컴포넌트마다 조금씩 달라집니다.
87
+ - reduced-motion 정책을 컴포넌트마다 따로 관리합니다.
88
+ - fade, disclosure, surface, sheet, expand처럼 비슷해 보이지만 체감 기준이 다른 motion의 경계가 흐려집니다.
89
+ - enter, exit, release가 같은 duration/easing으로 묶여 닫힘이나 drag release가 어색해집니다.
90
+ - 장기적으로 duration/easing을 조정할 때 전체 surface를 다시 찾아야 합니다.
91
+
92
+ `oc-motion`은 motion을 `intent + phase + pace`로 표현합니다. 컴포넌트는 motion의 의미를 선언하고, 실제 값은 foundation token과 helper policy가 결정합니다.
93
+
94
+ ## Mental model
95
+
96
+ | 축 | 답하는 질문 | 예시 |
97
+ | ------------ | ------------------------------------------------ | --------------------------------------- |
98
+ | `intent` | 이 UI는 어떤 역할과 공간 모델로 움직이는가? | `disclosure`, `surface`, `sheet` |
99
+ | `phase` | 전환의 어느 순간인가? | `enter`, `exit`, `release` |
100
+ | `pace` | 이 instance는 기본보다 빠른가, 느린가? | `quick`, `normal`, `slow` |
101
+ | escape hatch | semantic API로 부족한 한 컴포넌트 보정이 있는가? | `$duration-factor`, `$extra-bounce` |
102
+ | amplitude | motion의 거리나 scale 강도를 token으로 줄일까? | `oc-motion-scale`, `oc-motion-distance` |
103
+
104
+ `phase`는 standalone animation이 아닙니다. `enter`만으로는 motion이 정해지지 않고, `disclosure enter`, `surface enter`, `sheet enter`, `expand enter`처럼 intent와 결합해야 체감 기준이 생깁니다.
105
+
106
+ ## oc-motion contract
107
+
108
+ 가장 일반적인 사용은 mixin입니다. base transition과 reduced-motion override를 함께 출력합니다.
109
+
110
+ ### `@include oc-motion(...)`
111
+
112
+ ```scss
113
+ @include oc-motion(
114
+ $properties,
115
+ $intent,
116
+ $phase: change,
117
+ $pace: normal,
118
+ $duration-factor: 1,
119
+ $reduced: auto,
120
+ $duration-token: null,
121
+ $extra-bounce: null
122
+ );
123
+ ```
124
+
125
+ 컴포넌트 스타일에서는 이 mixin을 기본으로 사용합니다.
126
+
127
+ ### `oc-motion(...)`
128
+
129
+ ```scss
130
+ transition: oc-motion(
131
+ $properties,
132
+ $intent,
133
+ $phase: change,
134
+ $pace: normal,
135
+ $duration-factor: 1,
136
+ $duration-token: null,
137
+ $extra-bounce: null
138
+ );
139
+ ```
140
+
141
+ transition item list만 반환합니다. `@media (prefers-reduced-motion: reduce)`는 출력하지 않습니다.
142
+
143
+ ### `oc-motion-duration(...)`
144
+
145
+ ```scss
146
+ transition-duration: oc-motion-duration(
147
+ $intent,
148
+ $phase: change,
149
+ $pace: normal,
150
+ $duration-factor: 1,
151
+ $duration-factor-token: var(--oc-motion-duration-factor),
152
+ $duration-token: null
153
+ );
154
+ ```
155
+
156
+ duration token, 전역 duration factor token, pace factor, 숫자 duration factor를 합친 `calc(...)` 값을 반환합니다.
157
+
158
+ ### `oc-motion-timing(...)`
159
+
160
+ ```scss
161
+ transition-timing-function: oc-motion-timing($intent, $phase: change);
162
+ ```
163
+
164
+ intent와 phase에 맞는 token timing을 반환합니다.
165
+
166
+ ### `oc-motion-scale(...)`
167
+
168
+ ```scss
169
+ transform: scale(
170
+ oc-motion-scale($scale, $intent: feedback, $scale-factor-token: null)
171
+ );
172
+ ```
173
+
174
+ scale amplitude를 runtime token에 연결합니다. 기본 token 값 `1`에서는 입력한 scale을 그대로 유지하고, token을 `0`으로 두면 `scale(1)`이 되어 scale 효과가 사라집니다. `0.5`처럼 0과 1 사이의 값은 더 얕은 scale, `1`보다 큰 값은 더 강한 scale을 만듭니다. 전체 scale은 `--oc-motion-scale-factor`로 조정하고, feedback 계열은 `--oc-motion-scale-factor-feedback`, spatial 계열 intent는 `--oc-motion-scale-factor-spatial`로 override할 수 있습니다. `fade`도 intent vocabulary completeness를 위해 amplitude helper에서 사용할 수 있지만, 권장 contract는 opacity 중심입니다. scale/translate가 motion 의미를 설명하기 시작하면 `disclosure`, `surface`, `sheet`, `move` 같은 역할 intent를 먼저 검토합니다.
175
+
176
+ ### `oc-motion-distance(...)`
177
+
178
+ ```scss
179
+ transform: translateY(
180
+ oc-motion-distance($distance, $intent, $distance-factor-token: null)
181
+ );
182
+ ```
183
+
184
+ translate 또는 keyframe offset처럼 motion 거리 자체가 피드백 강도에 해당하는 값을 runtime token에 연결합니다. 기본 token 값 `1`에서는 입력한 거리를 그대로 유지하고, token을 `0`으로 두면 translate/reject shake 거리가 사라집니다. 전체 distance는 `--oc-motion-distance-factor`로 조정하고, `disclosure`/`surface`/`sheet`/`expand`/`move` 계열은 `--oc-motion-distance-factor-spatial`, `feedback`/`reject`/`gesture` 계열은 `--oc-motion-distance-factor-reject`로 override할 수 있습니다.
185
+
186
+ ## 인자 역할
187
+
188
+ | 구분 | 인자 | 값 | 역할과 사용 기준 |
189
+ | ---------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
190
+ | Required | `$properties` | `opacity`, `transform`, `(opacity, transform)` | transition 대상 property입니다. reduced-motion에서 property별 보존 여부를 판단하는 기준이 됩니다. |
191
+ | Required | `$intent` | `feedback`, `fade`, `reject`, `disclosure`, `surface`, `sheet`, `expand`, `move`, `gesture`, `ambient`, `continuous` | UI motion의 역할과 공간 모델입니다. duration, spring profile, 기본 reduced policy를 결정합니다. |
192
+ | Timing axis | `$phase` | `change`, `enter`, `exit`, `release`, `loop` | 같은 intent 안에서 전환의 시점과 방향을 구분합니다. 생략하면 `change`입니다. |
193
+ | Timing axis | `$pace` | `quick`, `normal`, `slow` | 공유 vocabulary로 duration을 상대 보정합니다. 먼저 `$pace`를 고르고, 그래도 부족할 때만 `$duration-factor`를 사용합니다. |
194
+ | Reduced axis | `$reduced` | `auto`, `preserve`, `fade`, `static`, `feedback` | reduced-motion에서 무엇을 남길지 정합니다. 기본은 intent의 policy를 따르는 `auto`입니다. |
195
+ | Escape hatch | `$duration-factor` | unitless number | 한 컴포넌트만 미세 조정하는 multiplier입니다. 음수는 금지하고, `0`은 instant completion 용도로만 사용합니다. |
196
+ | Escape hatch | `$duration-token` | CSS duration token | 기존 component-local duration CSS variable과 연결해야 할 때만 사용합니다. spring-backed motion에서는 사용할 수 없습니다. |
197
+ | Escape hatch | `$extra-bounce` | unitless number | spring-backed motion의 bounce만 국소 override합니다. transform entrance가 너무 건조할 때처럼 제한적으로 사용합니다. |
198
+ | Advanced token | `$duration-factor-token` | CSS number token | `oc-motion-duration(...)` 전용입니다. reduced-motion 내부 정책처럼 multiplier token 자체를 바꿔야 할 때만 사용합니다. |
199
+ | Amplitude helper | `$scale-factor-token` | CSS number token | `oc-motion-scale(...)` 전용입니다. scale amplitude를 어떤 token으로 줄일지 바꿉니다. |
200
+ | Amplitude helper | `$distance-factor-token` | CSS number token | `oc-motion-distance(...)` 전용입니다. translate/keyframe distance amplitude를 어떤 token으로 줄일지 바꿉니다. |
201
+
202
+ ## Intent 선택
203
+
204
+ 선택 기준은 컴포넌트 이름이 아니라 motion의 역할과 공간 모델입니다. 일반 content show/hide는 `fade`, Tooltip과 Menu는 `disclosure`, Popup Modal은 `surface`, Bottom Modal은 `sheet`, Accordion은 `expand`를 사용합니다.
205
+
206
+ | Intent | 역할 | 대표 사용처 | 자주 쓰는 phase | 기본 reduced-motion 정책 |
207
+ | ------------ | ----------------------------------------- | ----------------------------------- | -------------------------- | ------------------------ |
208
+ | `feedback` | 즉각적인 시각 반응 | hover, focus, active | `change` | `feedback` |
209
+ | `fade` | 일반 content의 단순 표시/숨김 | Notice, helper text, loaded content | `change` | `fade` |
210
+ | `reject` | 허용되지 않은 시도를 알려주는 문제 피드백 | blocked dismiss, invalid | `change`, `enter` | `feedback` |
211
+ | `disclosure` | trigger에 붙은 작은 transient surface | Menu, Tooltip, Popover | `enter`, `exit` | `fade` |
212
+ | `surface` | 화면 맥락 위의 작업 focus surface | Popup Modal | `enter`, `exit` | `fade` |
213
+ | `sheet` | 화면 edge에 고정된 spatial panel | Bottom Modal, drawer | `enter`, `exit`, `release` | `static` |
214
+ | `expand` | layout 흐름 안에서 펼쳐지는 content | Accordion | `enter`, `exit` | `static` |
215
+ | `move` | 같은 control 안에서 위치가 바뀌는 요소 | Switch thumb, indicator | `change` | `static` |
216
+ | `gesture` | 사용자 조작 후 정착하는 자유 surface | Popup drag, drag snap | `release` | `feedback` |
217
+ | `ambient` | 은은한 대기 상태 반복 표현 | Skeleton, status pulse | `loop` | `static` |
218
+ | `continuous` | 일정 속도 반복 motion | Loading rotate, progress sweep | `loop` | `static` |
219
+
220
+ ### Reject feedback
221
+
222
+ `reject`는 사용자의 시도가 처리되지 않았음을 즉시 알려야 할 때 사용합니다. 예를 들어 닫을 수 없는 Alert의 background를 눌렀거나, 잘못된 입력으로 다음 단계 이동이 막혔거나, 현재 상태에서 허용되지 않는 전환을 시도한 경우가 여기에 해당합니다.
223
+
224
+ 단순 hover, press, selected 같은 반응은 `feedback`을 사용합니다. 시도가 실패했거나 의도적으로 차단되었음을 알려야 할 때만 `reject`를 사용합니다.
225
+
226
+ `reject` motion은 보통 surface 전체가 짧게 흔들리는 형태입니다. 좌우 이동 거리처럼 motion 강도 자체가 거리로 표현되는 값은 `oc-motion-distance(..., reject)`로 감싸고, component-local 기본 거리와 global distance scale이 함께 작동하게 둡니다.
227
+
228
+ ```scss
229
+ .dialog {
230
+ --dialog-reject-distance: #{oc-motion-distance(0.75rem, reject)};
231
+
232
+ animation: oc-dialog-reject-shake oc-motion-duration(reject)
233
+ oc-motion-timing(reject) both;
234
+ }
235
+ ```
236
+
237
+ 현재 Alert는 background dismissal이 차단된 상태에서 backdrop을 누르면 Modal surface에 transient `data-oc-modal-feedback='reject'` state를 부여합니다. 기본 흔들림 거리는 `--oc-modal-reject-distance`로 조정할 수 있고, 전체 reject 거리 강도는 `--oc-motion-distance-factor-reject`가 담당합니다.
238
+
239
+ ## Phase 선택
240
+
241
+ `phase`는 standalone animation 이름이 아니라 intent에 붙는 시점입니다. 같은 `enter`라도 `disclosure enter`는 작은 surface가 민첩하게 열리는 motion이고, `sheet enter`는 edge-anchored panel이 공간을 설명하며 들어오는 motion입니다.
242
+
243
+ | Phase | 답하는 질문 | 대표 조합 |
244
+ | --------- | ----------------------------------- | ---------------------------------- |
245
+ | `change` | 값이나 상태가 바뀌었는가? | `feedback change`, `move change` |
246
+ | `enter` | 화면에 나타나거나 열리는가? | `disclosure enter`, `sheet enter` |
247
+ | `exit` | 화면에서 사라지거나 닫히는가? | `disclosure exit`, `surface exit` |
248
+ | `release` | 사용자가 drag를 놓은 뒤 정착하는가? | `sheet release`, `gesture release` |
249
+ | `loop` | 자동으로 반복되는가? | `ambient loop`, `continuous loop` |
250
+
251
+ `settle`은 public phase로 쓰지 않습니다. 사용자가 일으킨 단계는 `release`이고, 최종 상태에 수렴하는 물리 결과는 runtime이나 spring 내부 구현에서만 `settle`이라는 이름을 사용할 수 있습니다.
252
+
253
+ `fade`는 일반 transition처럼 `change` 기본값을 사용합니다. 같은 opacity transition이 `0 -> 1`, `1 -> 0` 양방향에 적용되므로, exit timing을 별도로 조정해야 할 때만 `$phase: exit` 같은 override를 추가합니다.
254
+
255
+ `change`는 lifecycle phase를 대체하는 이름이 아니라 fallback입니다. Base UI의 `data-starting-style`/`data-ending-style`, `data-state`, class 같은 lifecycle selector가 있으면 해당 selector에서 `enter`/`exit`를 명시하고, selector를 구분하기 어려운 단순 state transition에서만 `change`를 사용합니다. `oc-motion`은 lifecycle selector를 생성하지 않고, 어떤 selector가 어느 phase를 의미하는지는 컴포넌트나 primitive가 소유합니다.
256
+
257
+ `disclosure`, `surface`, `sheet`, `expand`는 역할별 profile이 다릅니다. `disclosure enter/change`의 transform은 작은 surface가 민첩하게 열리도록 낮은 snappy bounce를 허용하고, `disclosure exit`은 spring 없이 `0.14s`와 `cubic-bezier(0.8, 0, 1, 1)`로 초반 관성을 낮춘 뒤 빠르게 정리합니다. `surface`, `sheet`, `expand`는 기본적으로 smooth/no-bounce입니다. Popup Modal은 1보다 커지는 scale overshoot를 피하고, `surface exit`은 smooth `0.24`를 기준으로 닫힘을 정리합니다. Bottom Sheet는 iOS bottom sheet reference에 맞춰 `sheet enter` smooth `0.26`, `sheet exit/release` smooth `0.33`을 기준으로 둡니다. Accordion은 뜨는 surface가 아니라 문서 흐름이 펼쳐지는 motion이므로 `expand`를 사용합니다.
258
+
259
+ `reject`는 사용자의 시도가 거부되었음을 알려야 하는 surface shake motion에 사용하며, keyframes 자체가 좌우 반동을 만들기 때문에 timing은 smooth spring을 사용합니다. 좌우로 방향을 여러 번 바꿔야 하는 shake는 keyframes로 변위를 정의하고 `oc-motion-duration(reject)`와 `oc-motion-timing(reject)`를 animation duration/timing에 연결합니다. 단일 transform 복귀 transition만 필요한 경우에는 `oc-motion(transform, reject)`를 사용할 수 있습니다.
260
+
261
+ `move`는 Switch thumb, indicator, Accordion chevron처럼 같은 control 안에서 위치나 방향이 바뀌는 motion에 사용합니다. hover, press, selected 같은 즉각 반응은 `feedback change`를 유지합니다. `ambient loop`는 Skeleton pulse처럼 은은하게 반복되는 상태 표현에 사용하고, spinner 회전이나 progress sweep처럼 일정 속도 자체가 의미인 반복은 `continuous loop`를 사용합니다. 두 반복 intent는 spring-backed motion이 아니며 reduced-motion에서는 기본 static으로 둡니다. `gesture release`는 Popup drag snap처럼 작은 자유 surface가 손을 놓은 뒤 빠르게 정착해야 하는 경우에 사용하며, SCSS transition helper와 TS runtime animator 모두 snappy spring profile을 사용합니다.
262
+
263
+ ```scss
264
+ .menu {
265
+ @include oc-motion((opacity, transform), disclosure, $phase: enter);
266
+ }
267
+
268
+ .menu[data-ending-style] {
269
+ @include oc-motion((opacity, transform), disclosure, $phase: exit);
270
+ }
271
+
272
+ .bottom-sheet {
273
+ @include oc-motion(transform, sheet, $phase: enter);
274
+ }
275
+
276
+ .accordion-details {
277
+ @include oc-motion(block-size, expand, $phase: enter);
278
+ }
279
+
280
+ .dialog[data-oc-feedback='reject'] {
281
+ animation: oc-dialog-reject-shake oc-motion-duration(reject)
282
+ oc-motion-timing(reject) both;
283
+ }
284
+
285
+ .chevron {
286
+ @include oc-motion(transform, move, $pace: quick);
287
+ }
288
+ ```
289
+
290
+ ## Extra bounce override
291
+
292
+ `$extra-bounce`는 spring-backed transition의 반동 강도를 컴포넌트 한 곳에서만 조정해야 할 때 쓰는 escape hatch입니다. 기본 profile을 우선 사용하고, 같은 intent 안에서도 transform entrance가 너무 건조하거나 과하게 느껴지는 경우에만 사용합니다.
293
+
294
+ ```scss
295
+ .small-menu {
296
+ @include oc-motion(transform, disclosure, $phase: enter, $extra-bounce: 0.08);
297
+ }
298
+
299
+ .tooltip {
300
+ @include oc-motion(transform, disclosure, $phase: enter, $extra-bounce: 0);
301
+ }
302
+ ```
303
+
304
+ `$extra-bounce`는 `oc-spring`의 preset bounce에 더해지는 unitless 값입니다. `disclosure enter/change`의 `transform`은 기본적으로 약한 extra bounce를 갖고, override를 넘기면 property 기본값보다 명시 값이 우선합니다. 여러 property를 함께 넘기면 같은 override가 모든 spring-backed property에 적용되므로, opacity처럼 overshoot가 의미 없는 property와 섞기보다 transform transition에 국소적으로 쓰는 편이 좋습니다.
305
+
306
+ non-spring motion에서는 `$extra-bounce`가 의미 없으므로 helper가 에러를 냅니다. `feedback change`처럼 token timing path를 쓰는 motion은 `$pace`, `$duration-factor`, `oc-motion-scale`, `oc-motion-distance`로 조정합니다. Reduced-motion에서는 transform spring이 제거될 수 있으므로 extra bounce는 일반 motion profile의 국소 조정으로만 봅니다.
307
+
308
+ ## Pace와 duration factor
309
+
310
+ `$pace`는 반복적으로 쓰는 semantic 선택지입니다.
311
+
312
+ ```scss
313
+ .fast-feedback {
314
+ @include oc-motion(opacity, feedback, $pace: quick);
315
+ }
316
+
317
+ .calm-surface {
318
+ @include oc-motion((opacity, transform), surface, $pace: slow);
319
+ }
320
+ ```
321
+
322
+ foundation의 `--oc-motion-pace-*` token은 unitless duration multiplier입니다.
323
+
324
+ ```scss
325
+ --oc-motion-pace-quick: 0.75;
326
+ --oc-motion-pace-normal: 1;
327
+ --oc-motion-pace-slow: 1.35;
328
+ ```
329
+
330
+ `$duration-factor`은 개별 컴포넌트에서 token 체계를 벗어나지 않고 미세 조정해야 할 때만 사용합니다.
331
+
332
+ ```scss
333
+ .compact {
334
+ @include oc-motion(opacity, feedback, $duration-factor: 0.85);
335
+ }
336
+ ```
337
+
338
+ `$pace`와 `$duration-factor`은 같은 개념이 아닙니다. `$pace`는 공유 vocabulary이고, `$duration-factor`은 국소 조정을 위한 숫자 multiplier입니다. 장기 유지보수 관점에서는 `$pace`를 먼저 선택하고, 정말 필요한 경우에만 `$duration-factor`을 추가합니다.
339
+
340
+ spring-backed Sass transition에서는 두 종류의 시간이 보입니다. `sheet enter 0.26s`, `sheet release 0.33s` 같은 값은 spring curve를 샘플링하는 semantic 기준 duration이고, CSS에 출력되는 transition-duration은 그 spring이 안정적으로 settle되는 시간입니다. 그래서 compiled CSS나 Storybook preview에서는 `0.26s`가 더 긴 `calc(...)` 값으로 보일 수 있습니다. `$pace`와 `$duration-factor`는 이 출력 duration에 곱해지는 instance 보정입니다.
341
+
342
+ ## Reduced Motion
343
+
344
+ `@include oc-motion(...)`은 항상 reduced-motion block을 함께 출력합니다.
345
+
346
+ ```scss
347
+ .tooltip {
348
+ @include oc-motion((opacity, transform), disclosure);
349
+ }
350
+ ```
351
+
352
+ reduced-motion에서 `disclosure`와 `surface`는 opacity fade만 남기고 transform은 제거합니다. base transition이 spring-backed transition이어도 reduced block에서는 `transition-timing-function`을 `oc-motion-timing(...)` 기반 token timing으로 명시해 spring/overshoot timing이 남지 않게 합니다. `sheet`와 `expand`는 기본적으로 static입니다. Bottom Sheet처럼 공간 구조 이해에 꼭 필요한 translate는 컴포넌트가 별도 invariant로 보존할 수 있습니다.
353
+
354
+ ```css
355
+ @media (prefers-reduced-motion: reduce) {
356
+ .tooltip {
357
+ transition-property: opacity;
358
+ transition-duration: calc(
359
+ var(--oc-motion-duration-disclosure) *
360
+ var(--oc-motion-duration-factor-reduced) *
361
+ var(--oc-motion-pace-normal) * 1
362
+ );
363
+ transition-timing-function: var(--oc-motion-timing-disclosure);
364
+ transform: none;
365
+ }
366
+ }
367
+ ```
368
+
369
+ ### Reduced policy
370
+
371
+ | Policy | 동작 |
372
+ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
373
+ | `auto` | intent의 기본 reduced-motion 정책을 사용합니다. |
374
+ | `fade` | feedback-safe property만 transition하고 transform은 제거합니다. |
375
+ | `feedback` | `fade`와 같은 방식으로 paint/opacity feedback만 남깁니다. |
376
+ | `static` | transition을 제거합니다. transform이 있으면 `transform: none`도 출력합니다. |
377
+ | `preserve` | motion을 유지하되 `--oc-motion-duration-factor-preserve`와 token timing을 사용합니다. 공간 구조 이해에 꼭 필요한 motion에만 사용합니다. |
378
+
379
+ feedback-safe property는 다음으로 제한합니다.
380
+
381
+ ```scss
382
+ opacity
383
+ color
384
+ background-color
385
+ border-color
386
+ box-shadow
387
+ outline-color
388
+ fill
389
+ stroke
390
+ ```
391
+
392
+ 예를 들어 `@include oc-motion(transform, feedback)`처럼 feedback-safe property가 하나도 없으면 reduced-motion에서는 transition을 제거합니다.
393
+
394
+ `transform`을 `:active`, checked selector, `[data-state]` 같은 더 구체적인 state selector에서 바꾸는 경우에는 reduced-motion override도 같은 state selector에서 보완해야 합니다. `oc-motion`이 출력하는 `transform: none`은 helper가 include된 selector에만 적용되므로, 더 높은 specificity의 state transform까지 자동으로 이기지는 않습니다.
395
+
396
+ ## Component-local duration token
397
+
398
+ 기존 component contract를 유지해야 할 때 `$duration-token`을 사용할 수 있습니다.
399
+
400
+ ```scss
401
+ .interaction {
402
+ --oc-interaction-time: 0.2s;
403
+
404
+ @include oc-motion(
405
+ (opacity, transform),
406
+ feedback,
407
+ $duration-token: var(
408
+ --oc-interaction-time,
409
+ var(--oc-motion-duration-feedback)
410
+ )
411
+ );
412
+ }
413
+ ```
414
+
415
+ 이 옵션은 non-spring motion에서만 허용합니다. spring-backed motion은 Sass가 compile-time duration으로 spring curve를 샘플링해야 하므로 CSS custom property duration을 받을 수 없습니다.
416
+
417
+ ```scss
418
+ // Invalid: surface enter/change is spring-backed.
419
+ .invalid {
420
+ @include oc-motion(opacity, surface, $duration-token: var(--local-time));
421
+ }
422
+ ```
423
+
424
+ ## Runtime API
425
+
426
+ CSS transition만으로 안정적으로 다루기 어려운 height, measured layout, gesture release motion은 runtime API를 사용할 수 있습니다. runtime도 같은 `intent + phase + pace` vocabulary를 사용합니다.
427
+
428
+ ```tsx
429
+ import { ocMotion } from '@orioncactuscorp/ui/utils/motion';
430
+
431
+ const config = ocMotion.resolve({
432
+ intent: 'sheet',
433
+ phase: 'release',
434
+ pace: 'normal',
435
+ reduced: 'auto',
436
+ reducedMotion: prefersReducedMotion,
437
+ property: 'transform',
438
+ element: surfaceElement,
439
+ });
440
+ ```
441
+
442
+ | Option | 역할 |
443
+ | ---------------- | ------------------------------------------------------------------------------------------------------------ |
444
+ | `intent` | UI motion의 역할과 공간 모델입니다. Sass `$intent`와 같은 vocabulary를 사용합니다. |
445
+ | `phase` | 전환 시점입니다. 생략하면 `change`입니다. |
446
+ | `pace` | semantic speed modifier입니다. 생략하면 `normal`입니다. |
447
+ | `durationFactor` | 한 instance만 duration을 미세 조정하는 숫자 multiplier입니다. |
448
+ | `extraBounce` | spring-backed motion의 bounce를 국소 override합니다. non-spring motion에서는 에러를 냅니다. |
449
+ | `reduced` | 어떤 reduced-motion policy를 적용할지 고릅니다. `auto`는 intent 기본 policy를 사용합니다. |
450
+ | `reducedMotion` | 현재 환경이나 호출 맥락이 reduced-motion인지 전달합니다. policy 이름이 아니라 runtime 상태입니다. |
451
+ | `property` | reduced-motion에서 animate 여부와 property별 spring preset을 판단하는 기준입니다. 가능하면 명시합니다. |
452
+ | `element` | computed CSS custom property를 읽을 대상 element입니다. 전달하지 않으면 default token snapshot을 사용합니다. |
453
+ | `tokens` | 테스트나 특수 runtime에서 element 대신 직접 넘기는 token snapshot override입니다. |
454
+
455
+ `reduced`와 `reducedMotion`은 다른 값입니다. `reduced`는 정책이고, `reducedMotion`은 현재 motion을 줄여야 하는 상태입니다. `property`도 중요합니다. 예를 들어 `feedback` reduced policy는 opacity, color, background-color 같은 feedback-safe property만 유지하고 `transform`은 제거할 수 있습니다.
456
+
457
+ `$duration-token`은 Sass-only escape hatch입니다. runtime에서는 `element`의 computed CSS custom property나 `tokens` override를 통해 duration token을 읽습니다.
458
+
459
+ ## Token customization
460
+
461
+ consumer는 foundation CSS custom property를 override해 전체 motion 톤을 조정할 수 있습니다.
462
+
463
+ ```scss
464
+ :root {
465
+ --oc-motion-duration-factor: 0.9;
466
+ --oc-motion-scale-factor: 0.8;
467
+ --oc-motion-scale-factor-feedback: 0.6;
468
+ --oc-motion-scale-factor-spatial: 0;
469
+ --oc-motion-distance-factor: 0.75;
470
+ --oc-motion-distance-factor-reject: 1;
471
+ --oc-motion-distance-factor-spatial: 0.5;
472
+ --oc-motion-duration-feedback: 0.16s;
473
+ --oc-motion-pace-slow: 1.25;
474
+ }
475
+
476
+ @media (prefers-reduced-motion: reduce) {
477
+ :root {
478
+ --oc-motion-duration-factor-reduced: 0.65;
479
+ }
480
+ }
481
+ ```
482
+
483
+ amplitude token은 parent-child 구조를 따릅니다. 전체 톤을 조정할 때는 parent token을 먼저 바꾸고, 특정 intent만 다르게 가져가야 할 때 child token을 override합니다.
484
+
485
+ ```scss
486
+ :root {
487
+ --oc-motion-scale-factor: 0.75;
488
+ --oc-motion-distance-factor: 0.75;
489
+ }
490
+
491
+ .dense-tool {
492
+ --oc-motion-scale-factor-feedback: 0.5;
493
+ }
494
+
495
+ .calm-surface {
496
+ --oc-motion-distance-factor-spatial: 0.25;
497
+ }
498
+ ```
499
+
500
+ `--oc-motion-scale-factor-feedback`, `--oc-motion-scale-factor-spatial`, `--oc-motion-distance-factor-reject`, `--oc-motion-distance-factor-spatial`은 기본적으로 각 parent token을 참조합니다. 따라서 child token을 지정하지 않으면 전체 scale/distance factor 정책을 그대로 따릅니다.
501
+
502
+ 주요 token은 다음과 같습니다.
503
+
504
+ ```scss
505
+ --oc-motion-duration-factor
506
+ --oc-motion-duration-factor-reduced
507
+ --oc-motion-duration-factor-preserve
508
+ --oc-motion-scale-factor
509
+ --oc-motion-scale-factor-feedback
510
+ --oc-motion-scale-factor-spatial
511
+ --oc-motion-distance-factor
512
+ --oc-motion-distance-factor-reject
513
+ --oc-motion-distance-factor-spatial
514
+ --oc-motion-pace-quick
515
+ --oc-motion-pace-normal
516
+ --oc-motion-pace-slow
517
+ --oc-motion-duration-feedback
518
+ --oc-motion-duration-fade
519
+ --oc-motion-duration-reject
520
+ --oc-motion-duration-disclosure
521
+ --oc-motion-duration-disclosure-exit
522
+ --oc-motion-duration-surface
523
+ --oc-motion-duration-surface-exit
524
+ --oc-motion-duration-sheet
525
+ --oc-motion-duration-sheet-exit
526
+ --oc-motion-duration-sheet-release
527
+ --oc-motion-duration-expand
528
+ --oc-motion-duration-expand-exit
529
+ --oc-motion-duration-move
530
+ --oc-motion-duration-gesture
531
+ --oc-motion-duration-gesture-release
532
+ --oc-motion-duration-ambient
533
+ --oc-motion-duration-continuous
534
+ --oc-motion-timing-feedback
535
+ --oc-motion-timing-fade
536
+ --oc-motion-timing-reject
537
+ --oc-motion-timing-disclosure
538
+ --oc-motion-timing-disclosure-exit
539
+ --oc-motion-timing-surface
540
+ --oc-motion-timing-surface-exit
541
+ --oc-motion-timing-sheet
542
+ --oc-motion-timing-sheet-exit
543
+ --oc-motion-timing-sheet-release
544
+ --oc-motion-timing-expand
545
+ --oc-motion-timing-expand-exit
546
+ --oc-motion-timing-move
547
+ --oc-motion-timing-gesture
548
+ --oc-motion-timing-gesture-release
549
+ --oc-motion-timing-ambient
550
+ --oc-motion-timing-continuous
551
+ ```
552
+
553
+ spring-backed Sass `@include oc-motion(...)`은 `--oc-motion-duration-sheet` 같은 intent duration token을 직접 읽어 curve를 다시 만들지 않습니다. spring timing curve는 Sass compile-time의 semantic duration으로 샘플링하고, 출력 transition-duration에는 settle duration, `--oc-motion-duration-factor`, `--oc-motion-pace-*`, `$duration-factor`만 남깁니다. 개별 `--oc-motion-duration-*` token은 non-spring path, reduced-motion fallback, `oc-motion-duration(...)`, TS runtime token snapshot에서 사용합니다.
554
+
555
+ TS runtime `ocMotion.resolve(...)`도 동일하게 element의 computed CSS custom property를 읽어 duration을 계산합니다. `extraBounce` option은 Sass `$extra-bounce`와 같은 정책을 따르며 spring-backed motion에서만 사용할 수 있습니다. reduced-motion 환경에서는 `preserve` 정책만 `--oc-motion-duration-factor-preserve`를 사용하고, `fade`와 `feedback` 정책은 `--oc-motion-duration-factor-reduced`를 사용합니다. 일반 content show/hide는 `fade`라 reduced-motion에서도 opacity fade만 남깁니다. Accordion처럼 `block-size` 자체를 움직이는 `expand` motion은 reduced-motion에서 즉시 끝냅니다. Menu/Tooltip은 `disclosure`, Popup Modal은 `surface`라 reduced-motion에서도 opacity fade만 남깁니다. Bottom Modal의 open/drag release처럼 sheet 위치 자체가 인터랙션의 의미인 경우에는 별도 product invariant로 사용자 설정과 무관하게 동일한 공간 이동을 유지합니다.
556
+
557
+ ## `oc-motion`과 `oc-spring`
558
+
559
+ 대부분의 컴포넌트 transition은 `oc-motion`을 사용합니다.
560
+
561
+ ```scss
562
+ .content {
563
+ @include oc-motion((opacity, transform), surface);
564
+ }
565
+ ```
566
+
567
+ 직접 spring의 preset, bounce, physical parameter를 설계해야 하는 낮은 수준의 motion은 `oc-spring`을 사용합니다.
568
+
569
+ ```scss
570
+ @use '@orioncactuscorp/ui/scss/mixins/spring' as *;
571
+
572
+ .custom-sheet {
573
+ transition: oc-spring(transform, 0.5s, bouncy, 0.1);
574
+ }
575
+ ```
576
+
577
+ `oc-spring`을 사용할 때도 reduced-motion 정책은 직접 작성해야 합니다. public 컴포넌트에서는 특별한 이유가 없다면 `oc-motion`에 새 intent/profile을 추가하는 쪽이 장기적으로 더 유지보수하기 쉽습니다.
578
+
579
+ 자세한 low-level spring 사용법은 [Spring Motion](./spring.md)을 참고하세요.
580
+
581
+ ## 권장 패턴
582
+
583
+ ```scss
584
+ // Good: intent와 reduced-motion 정책을 중앙에서 관리합니다.
585
+ .notice {
586
+ @include oc-motion(opacity, fade);
587
+ }
588
+
589
+ // Good: 작은 floating surface는 disclosure intent를 사용합니다.
590
+ .menu {
591
+ @include oc-motion((opacity, transform), disclosure);
592
+ }
593
+
594
+ // Good: disclosure translate 거리는 distance factor token으로 전역 조정할 수 있습니다.
595
+ .menu[data-starting-style] {
596
+ transform: scale(oc-motion-scale(0.82, disclosure))
597
+ translateY(oc-motion-distance(-1rem, disclosure));
598
+ }
599
+
600
+ // Good: exit은 phase로 표현합니다.
601
+ .menu[data-ending-style] {
602
+ @include oc-motion((opacity, transform), disclosure, $phase: exit);
603
+ }
604
+
605
+ // Good: modal 형태별 motion intent를 나눕니다.
606
+ .popup-modal {
607
+ @include oc-motion((opacity, transform), surface, $phase: enter);
608
+ }
609
+
610
+ .bottom-sheet {
611
+ @include oc-motion(transform, sheet, $phase: enter);
612
+ }
613
+
614
+ .accordion-panel {
615
+ @include oc-motion(block-size, expand, $phase: enter);
616
+ }
617
+
618
+ // Good: 단순 feedback은 feedback intent를 사용합니다.
619
+ .button {
620
+ @include oc-motion((background-color, color, box-shadow), feedback);
621
+ }
622
+
623
+ // Good: 거부된 시도 후 surface shake는 reject duration/timing을 사용합니다.
624
+ .dialog {
625
+ --dialog-reject-distance: #{oc-motion-distance(0.75rem, reject)};
626
+
627
+ animation: oc-dialog-reject-shake oc-motion-duration(reject)
628
+ oc-motion-timing(reject) both;
629
+ }
630
+ ```
631
+
632
+ ## 피해야 할 패턴
633
+
634
+ ```scss
635
+ // Avoid: 같은 disclosure motion이 컴포넌트마다 다른 raw easing으로 흩어집니다.
636
+ .tooltip {
637
+ transition:
638
+ opacity 0.2s ease,
639
+ transform 0.2s cubic-bezier(0.2, 0, 0, 1);
640
+ }
641
+
642
+ // Avoid: public 컴포넌트가 raw spring과 reduced-motion 정책을 따로 관리합니다.
643
+ .menu {
644
+ transition: oc-spring((opacity, transform), 0.3s, snappy);
645
+ }
646
+
647
+ // Avoid: pace 대신 숫자 scale만 공유 vocabulary처럼 사용합니다.
648
+ .surface {
649
+ @include oc-motion(opacity, surface, $duration-factor: 1.35);
650
+ }
651
+ ```
652
+
653
+ ## 테스트 팁
654
+
655
+ SCSS helper나 public component transition을 바꿀 때는 Sass compilation test로 다음을 확인합니다.
656
+
657
+ - 기본 transition이 의도한 intent/phase/pace token 또는 spring profile을 사용하는지
658
+ - function-only `oc-motion(...)` 호출이 reduced-motion media를 출력하지 않는지
659
+ - mixin `@include oc-motion(...)` 호출이 reduced-motion media를 출력하는지
660
+ - spring-backed disclosure/surface가 reduced-motion에서 token timing으로 바뀌는지
661
+ - spring-backed motion의 `$extra-bounce` override와 non-spring error가 의도대로 동작하는지
662
+ - transform이 reduced-motion에서 제거되어야 하는 policy인지
663
+ - invalid intent, phase, pace, reduced policy, negative duration factor이 compile error로 실패하는지
664
+
665
+ 컴포넌트 테스트에서는 selector와 상태별 CSS 결과를 함께 확인합니다. Menu, Tooltip처럼 Base UI의 `data-starting-style`, `data-ending-style`, `data-side`에 의존하는 컴포넌트는 helper 출력과 handwritten override의 cascade 관계도 테스트 또는 주석으로 남깁니다.