@orioncactuscorp/ui 1.13.0 → 1.15.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/CHANGELOG.md +155 -0
- package/README.md +133 -36
- package/dist/components/BackgroundIconButton/BackgroundIconButton.css +1 -1
- package/dist/components/Button/Button.css +1 -1
- package/dist/components/CheckMark/CheckMark.js +19 -17
- package/dist/components/Checkbox/Checkbox.css +1 -1
- package/dist/components/Checkbox/Checkbox.js +38 -36
- package/dist/components/FloatingWindow/FloatingWindow.css +1 -0
- package/dist/components/FloatingWindow/FloatingWindow.js +693 -0
- package/dist/components/FloatingWindow/FloatingWindow.module.scss.js +17 -0
- package/dist/components/FloatingWindow/FloatingWindowManager.js +36 -0
- package/dist/components/FloatingWindow/contexts.js +25 -0
- package/dist/components/FloatingWindow/geometry.js +222 -0
- package/dist/components/FloatingWindow/stack.js +31 -0
- package/dist/components/FloatingWindow.d.ts +6 -0
- package/dist/components/FloatingWindow.js +18 -0
- package/dist/components/Interaction/Interaction.css +1 -1
- package/dist/components/Menu/Menu.css +1 -1
- package/dist/components/Menu/Menu.js +95 -87
- package/dist/components/Modal/Modal.css +1 -1
- package/dist/components/Modal/Modal.js +558 -723
- package/dist/components/Modal/Modal.module.scss.js +69 -60
- package/dist/components/Modal/initialFocus.js +11 -0
- package/dist/components/Modal/keyboardSession.js +104 -0
- package/dist/components/Modal/snapPoints.js +95 -82
- package/dist/components/Modal/useModalDrag.js +730 -762
- package/dist/components/Modal/useModalKeyboardAvoidance.js +159 -0
- package/dist/components/Modal/useModalViewportGeometry.js +36 -0
- package/dist/components/PositionSnap/PositionSnap.css +1 -0
- package/dist/components/PositionSnap/PositionSnap.js +330 -0
- package/dist/components/PositionSnap/PositionSnap.module.scss.js +9 -0
- package/dist/components/PositionSnap/contexts.js +12 -0
- package/dist/components/PositionSnap/geometry.js +75 -0
- package/dist/components/PositionSnap/gesture.js +7 -0
- package/dist/components/PositionSnap/policy.js +60 -0
- package/dist/components/PositionSnap.d.ts +6 -0
- package/dist/components/PositionSnap.js +7 -0
- package/dist/components/ProgressIndicator/ProgressIndicator.css +1 -0
- package/dist/components/ProgressIndicator/ProgressIndicator.js +45 -0
- package/dist/components/ProgressIndicator/ProgressIndicator.module.scss.js +13 -0
- package/dist/components/ProgressIndicator.d.ts +6 -0
- package/dist/components/ProgressIndicator.js +4 -0
- package/dist/components/Radio/Radio.css +1 -1
- package/dist/components/Radio/Radio.js +26 -24
- package/dist/components/Switch/Switch.css +1 -1
- package/dist/components/Switch/Switch.js +18 -16
- package/dist/components/Tabs/Tabs.css +1 -1
- package/dist/components/Tooltip/Tooltip.css +1 -1
- package/dist/components/Tooltip/Tooltip.js +59 -56
- package/dist/components/Tooltip/Tooltip.module.scss.js +6 -4
- package/dist/components/internal/ModalPresentation.js +361 -0
- package/dist/components/internal/OverlayPortalContext.js +20 -0
- package/dist/index.js +135 -112
- package/dist/styles.css +13 -10
- package/dist/ui/src/components/CheckMark/CheckMark.d.ts +1 -1
- package/dist/ui/src/components/CheckMark/CheckMark.d.ts.map +1 -1
- package/dist/ui/src/components/CheckMark/types.d.ts +2 -1
- package/dist/ui/src/components/CheckMark/types.d.ts.map +1 -1
- package/dist/ui/src/components/Checkbox/Checkbox.d.ts +1 -1
- package/dist/ui/src/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/ui/src/components/Checkbox/types.d.ts +2 -1
- package/dist/ui/src/components/Checkbox/types.d.ts.map +1 -1
- package/dist/ui/src/components/FloatingWindow/FloatingWindow.d.ts +32 -0
- package/dist/ui/src/components/FloatingWindow/FloatingWindow.d.ts.map +1 -0
- package/dist/ui/src/components/FloatingWindow/FloatingWindowManager.d.ts +4 -0
- package/dist/ui/src/components/FloatingWindow/FloatingWindowManager.d.ts.map +1 -0
- package/dist/ui/src/components/FloatingWindow/contexts.d.ts +36 -0
- package/dist/ui/src/components/FloatingWindow/contexts.d.ts.map +1 -0
- package/dist/ui/src/components/FloatingWindow/geometry.d.ts +31 -0
- package/dist/ui/src/components/FloatingWindow/geometry.d.ts.map +1 -0
- package/dist/ui/src/components/FloatingWindow/index.d.ts +4 -0
- package/dist/ui/src/components/FloatingWindow/index.d.ts.map +1 -0
- package/dist/ui/src/components/FloatingWindow/stack.d.ts +9 -0
- package/dist/ui/src/components/FloatingWindow/stack.d.ts.map +1 -0
- package/dist/ui/src/components/FloatingWindow/types.d.ts +121 -0
- package/dist/ui/src/components/FloatingWindow/types.d.ts.map +1 -0
- package/dist/ui/src/components/Interaction/index.d.ts +1 -1
- package/dist/ui/src/components/Interaction/index.d.ts.map +1 -1
- package/dist/ui/src/components/Interaction/types.d.ts +9 -1
- package/dist/ui/src/components/Interaction/types.d.ts.map +1 -1
- package/dist/ui/src/components/Menu/Menu.d.ts +2 -2
- package/dist/ui/src/components/Menu/Menu.d.ts.map +1 -1
- package/dist/ui/src/components/Menu/types.d.ts +9 -2
- package/dist/ui/src/components/Menu/types.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/Modal.d.ts +2 -2
- package/dist/ui/src/components/Modal/Modal.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/contexts.d.ts +3 -0
- package/dist/ui/src/components/Modal/contexts.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/initialFocus.d.ts +18 -0
- package/dist/ui/src/components/Modal/initialFocus.d.ts.map +1 -0
- package/dist/ui/src/components/Modal/keyboardSession.d.ts +46 -0
- package/dist/ui/src/components/Modal/keyboardSession.d.ts.map +1 -0
- package/dist/ui/src/components/Modal/snapPoints.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/types.d.ts +17 -2
- package/dist/ui/src/components/Modal/types.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/useModalDrag.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/useModalKeyboardAvoidance.d.ts +17 -0
- package/dist/ui/src/components/Modal/useModalKeyboardAvoidance.d.ts.map +1 -0
- package/dist/ui/src/components/Modal/useModalViewportGeometry.d.ts +9 -0
- package/dist/ui/src/components/Modal/useModalViewportGeometry.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/PositionSnap.d.ts +11 -0
- package/dist/ui/src/components/PositionSnap/PositionSnap.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/contexts.d.ts +17 -0
- package/dist/ui/src/components/PositionSnap/contexts.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/geometry.d.ts +31 -0
- package/dist/ui/src/components/PositionSnap/geometry.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/gesture.d.ts +2 -0
- package/dist/ui/src/components/PositionSnap/gesture.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/index.d.ts +3 -0
- package/dist/ui/src/components/PositionSnap/index.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/policy.d.ts +24 -0
- package/dist/ui/src/components/PositionSnap/policy.d.ts.map +1 -0
- package/dist/ui/src/components/PositionSnap/types.d.ts +31 -0
- package/dist/ui/src/components/PositionSnap/types.d.ts.map +1 -0
- package/dist/ui/src/components/ProgressIndicator/ProgressIndicator.d.ts +4 -0
- package/dist/ui/src/components/ProgressIndicator/ProgressIndicator.d.ts.map +1 -0
- package/dist/ui/src/components/ProgressIndicator/index.d.ts +3 -0
- package/dist/ui/src/components/ProgressIndicator/index.d.ts.map +1 -0
- package/dist/ui/src/components/ProgressIndicator/types.d.ts +17 -0
- package/dist/ui/src/components/ProgressIndicator/types.d.ts.map +1 -0
- package/dist/ui/src/components/Radio/Radio.d.ts +1 -1
- package/dist/ui/src/components/Radio/Radio.d.ts.map +1 -1
- package/dist/ui/src/components/Radio/types.d.ts +2 -1
- package/dist/ui/src/components/Radio/types.d.ts.map +1 -1
- package/dist/ui/src/components/Switch/Switch.d.ts +1 -1
- package/dist/ui/src/components/Switch/Switch.d.ts.map +1 -1
- package/dist/ui/src/components/Switch/types.d.ts +2 -1
- package/dist/ui/src/components/Switch/types.d.ts.map +1 -1
- package/dist/ui/src/components/Tooltip/Tooltip.d.ts +1 -1
- package/dist/ui/src/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/ui/src/components/Tooltip/types.d.ts +6 -0
- package/dist/ui/src/components/Tooltip/types.d.ts.map +1 -1
- package/dist/ui/src/components/internal/ModalPresentation.d.ts +62 -0
- package/dist/ui/src/components/internal/ModalPresentation.d.ts.map +1 -0
- package/dist/ui/src/components/internal/OverlayPortalContext.d.ts +10 -0
- package/dist/ui/src/components/internal/OverlayPortalContext.d.ts.map +1 -0
- package/dist/ui/src/index.d.ts +7 -1
- package/dist/ui/src/index.d.ts.map +1 -1
- package/dist/ui/src/utils/motion.d.ts.map +1 -1
- package/dist/utils/motion.js +194 -135
- package/docs/api-conventions.md +4 -0
- package/docs/motion.md +4 -0
- package/docs/overlay-stacking.md +48 -0
- package/docs/responsive-foundation-profile.md +1 -1
- package/docs/scss-helpers.md +110 -5
- package/docs/selector-contract.md +126 -2
- package/docs/tokens.md +2 -0
- package/package.json +1 -1
- package/src/scss/foundations/atomic.scss +9 -8
- package/src/scss/index.scss +2 -0
- package/src/scss/mixins/_fluid.scss +57 -0
- package/src/scss/mixins/_fluid.test.ts +87 -0
- package/src/scss/mixins/_gradient.scss +99 -0
- package/src/scss/mixins/_gradient.test.ts +111 -0
- package/src/scss/mixins/_responsive.scss +2 -1
- package/src/scss/mixins/_typo.scss +2 -1
- package/src/scss/mixins/_units.scss +14 -0
- package/src/scss/mixins/_utils.scss +3 -22
package/docs/scss-helpers.md
CHANGED
|
@@ -116,6 +116,101 @@ oc-alpha($color, $opacity)
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## oc-easing-gradient / oc-gradient-mask — 부드러운 gradient 경계
|
|
120
|
+
|
|
121
|
+
`@orioncactuscorp/ui/scss/mixins/gradient`
|
|
122
|
+
|
|
123
|
+
```scss
|
|
124
|
+
oc-easing-gradient($direction, $from, $to, $start: 0%, $end: 100%)
|
|
125
|
+
|
|
126
|
+
@include oc-gradient-mask(
|
|
127
|
+
$direction,
|
|
128
|
+
$start-fade-size: 0px,
|
|
129
|
+
$end-fade-size: 0px,
|
|
130
|
+
$content-size: 100%
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
@include oc-gradient-mask-edges(
|
|
134
|
+
$top-fade-size: 0px,
|
|
135
|
+
$right-fade-size: 0px,
|
|
136
|
+
$bottom-fade-size: 0px,
|
|
137
|
+
$left-fade-size: 0px,
|
|
138
|
+
$inline-content-size: 100%,
|
|
139
|
+
$block-content-size: 100%
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`oc-easing-gradient`는 gradient line을 따라 색상을 선형 비율로 섞지 않고 smoothstep easing으로 샘플링한 중간 color stop을 생성합니다. 경계의 시작과 끝에서 색 변화 속도가 부드럽게 이어져 content fade, scrim, 이미지 overlay에 사용할 수 있습니다. 8개 구간은 부드러운 경계와 생성되는 CSS 크기 사이의 균형을 위한 고정값입니다.
|
|
144
|
+
|
|
145
|
+
- `$direction`: `top | right | bottom | left` 물리 방향
|
|
146
|
+
- `$from`, `$to`: Sass color, `transparent`, CSS `var(...)`
|
|
147
|
+
- `$start`, `$end`: gradient 변화가 시작하고 끝나는 위치
|
|
148
|
+
|
|
149
|
+
```scss
|
|
150
|
+
@use '@orioncactuscorp/ui/scss/mixins/gradient' as *;
|
|
151
|
+
|
|
152
|
+
.scrim {
|
|
153
|
+
background-image: oc-easing-gradient(
|
|
154
|
+
bottom,
|
|
155
|
+
transparent,
|
|
156
|
+
var(--oc-color-theme-background-normal),
|
|
157
|
+
0%,
|
|
158
|
+
var(--scrim-size)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`oc-gradient-mask`는 같은 easing curve로 시작·끝 경계를 감추고 Safari용 `-webkit-mask-image`와 표준 `mask-image`를 함께 출력합니다. fade 길이와 전체 content 길이는 호출부가 소유한 CSS 변수를 전달해 runtime에 조정합니다. `$start-fade-size + $end-fade-size`는 `$content-size`를 넘지 않도록 설정하세요.
|
|
164
|
+
|
|
165
|
+
```scss
|
|
166
|
+
.scrollViewport {
|
|
167
|
+
--example-start-fade-size: 0px;
|
|
168
|
+
--example-end-fade-size: 3rem;
|
|
169
|
+
--example-content-size: 100%;
|
|
170
|
+
|
|
171
|
+
@include oc-gradient-mask(
|
|
172
|
+
right,
|
|
173
|
+
var(--example-start-fade-size),
|
|
174
|
+
var(--example-end-fade-size),
|
|
175
|
+
var(--example-content-size)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
2D 스크롤 영역은 `oc-gradient-mask-edges`로 block·inline mask를 교차 합성합니다. 앞의 네 인자는 CSS shorthand와 같은 `top → right → bottom → left` 순서이며 fade를 독립적으로 조절합니다. 표준 `mask-composite: intersect`와 WebKit용 `source-in`을 함께 출력합니다. 각 축의 양쪽 fade 합은 해당 content 길이를 넘지 않도록 설정하세요.
|
|
181
|
+
|
|
182
|
+
```scss
|
|
183
|
+
.canvasViewport {
|
|
184
|
+
--example-top-fade-size: 0px;
|
|
185
|
+
--example-right-fade-size: 3rem;
|
|
186
|
+
--example-bottom-fade-size: 3rem;
|
|
187
|
+
--example-left-fade-size: 0px;
|
|
188
|
+
|
|
189
|
+
@include oc-gradient-mask-edges(
|
|
190
|
+
var(--example-top-fade-size),
|
|
191
|
+
var(--example-right-fade-size),
|
|
192
|
+
var(--example-bottom-fade-size),
|
|
193
|
+
var(--example-left-fade-size)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
마스크 상태를 전환할 때는 `mask-image` 자체를 transition하지 않고 fade 길이 custom property를 `@property`로 등록한 뒤 `oc-motion(..., fade)`를 적용합니다. gradient의 공간적 색 easing과 상태 변화의 시간적 motion을 별도 계약으로 유지합니다.
|
|
199
|
+
|
|
200
|
+
```scss
|
|
201
|
+
@use '@orioncactuscorp/ui/scss/mixins/motion' as *;
|
|
202
|
+
|
|
203
|
+
@property --example-start-fade-size {
|
|
204
|
+
syntax: '<length>';
|
|
205
|
+
inherits: false;
|
|
206
|
+
initial-value: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.scrollViewport {
|
|
210
|
+
@include oc-motion(--example-start-fade-size, fade, $pace: quick);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
119
214
|
## typo — typography 토큰 조합
|
|
120
215
|
|
|
121
216
|
`@orioncactuscorp/ui/scss/mixins/typo`
|
|
@@ -161,24 +256,34 @@ named breakpoint: `sm`(480px), `md`(768px), `lg`(1280px), `xl`(1600px), `hg`(192
|
|
|
161
256
|
|
|
162
257
|
oc-ui SCSS는 mobile-first가 기본입니다: base rule은 narrow viewport 기준으로 쓰고, desktop 이상 차이는 `respond-to(min, md)`에서 더합니다. `respond-to(md)`(max-width)는 `Mobile(md)` 전용 예외 보정에만 사용합니다.
|
|
163
258
|
|
|
164
|
-
## fluidClamp / toRem — fluid 값 유틸
|
|
259
|
+
## oc-fluid / fluidClamp / toRem — fluid 값 유틸
|
|
165
260
|
|
|
166
|
-
`@orioncactuscorp/ui/scss/mixins/utils`
|
|
261
|
+
`@orioncactuscorp/ui/scss/mixins/fluid`, `@orioncactuscorp/ui/scss/mixins/utils`
|
|
167
262
|
|
|
168
263
|
```scss
|
|
169
|
-
|
|
264
|
+
oc-fluid($min, $max, $bp, $anchor: to)
|
|
170
265
|
```
|
|
171
266
|
|
|
172
|
-
`clamp($
|
|
267
|
+
`clamp($min, <viewport 비례식>, $max)`를 출력합니다. `$min`과 `$max`는 `px` 또는 `rem` 단위 값을 사용하고, `$bp`는 기준 viewport(px)입니다. `$bp`에는 Sass breakpoint 변수 또는 직접 입력한 값(`375px`, `375`)을 사용할 수 있습니다. `$anchor`는 비례식이 어느 끝값에 정렬될지 정하며 기본값은 `to`입니다.
|
|
173
268
|
|
|
174
269
|
```scss
|
|
270
|
+
@use '@orioncactuscorp/ui/scss/mixins/fluid' as *;
|
|
271
|
+
@use '@orioncactuscorp/ui/scss/mixins/breakpoint' as *;
|
|
175
272
|
@use '@orioncactuscorp/ui/scss/mixins/utils' as *;
|
|
176
273
|
|
|
177
274
|
.hero {
|
|
178
|
-
padding-block:
|
|
275
|
+
padding-block: oc-fluid(toRem(24), toRem(64), 1280px);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.radius {
|
|
279
|
+
border-radius: oc-fluid(toRem(10), toRem(12), $breakpoint-md, from);
|
|
179
280
|
}
|
|
180
281
|
```
|
|
181
282
|
|
|
283
|
+
`anchor: to`는 `$bp`에서 `$max`에 도달하며, `anchor: from`은 `$bp`에서 `$min`을 기준으로 이후 viewport에서 증가합니다. `oc-fluid()`는 값 생성 함수이므로 `font-size`, `line-height`, `padding`, `gap`, `border-radius` 등 CSS length 속성에 조합해서 사용할 수 있습니다.
|
|
284
|
+
|
|
285
|
+
기존 `fluidClamp($from, $bp, $anchor: to, $to)`도 호환성을 위해 유지됩니다. 신규 코드는 `oc-fluid()`를 사용하세요.
|
|
286
|
+
|
|
182
287
|
`toRem($px)`는 `16px = 1rem` 기준으로 숫자를 rem으로 변환합니다.
|
|
183
288
|
|
|
184
289
|
## Motion / Spring
|
|
@@ -34,6 +34,25 @@
|
|
|
34
34
|
- 스타일 override는 paint property를 직접 재선언하기보다 컴포넌트가 노출하는 contract 변수(`--oc-{component}-*`)를 덮는 방식을 우선하세요. 상태 전이·모션과의 일관성이 유지됩니다.
|
|
35
35
|
- 방향성 계약(`--oc-*` 변수, `data-oc-*` 값, prop)은 물리 방향(top/right/bottom/left)이 아니라 논리 방향(`block-start`, `inline-end`, `start`, `end` 등)을 사용합니다. RTL에서 자동으로 뒤집힙니다.
|
|
36
36
|
|
|
37
|
+
## 채워진 컨트롤 색상 계약
|
|
38
|
+
|
|
39
|
+
채워진 컨트롤은 배경과 그 위의 전경을 한 쌍의 component contract 변수로 노출합니다. 기본값은 package theme과 같고, consumer는 내부 slot이나 상태 selector를 복제하지 않고 컴포넌트 루트에서 두 색을 함께 조정할 수 있습니다.
|
|
40
|
+
|
|
41
|
+
| Component | Background contract | Foreground contract | 적용 상태 |
|
|
42
|
+
| ---------------------- | ---------------------------------------------- | ----------------------------------- | ------------------------------- |
|
|
43
|
+
| `Checkbox` | `--oc-checkbox-background-color` | `--oc-checkbox-color` | checked, indeterminate |
|
|
44
|
+
| `Radio` | `--oc-radio-background-color` | `--oc-radio-color` | checked |
|
|
45
|
+
| `Switch` | `--oc-switch-background-color` | `--oc-switch-color` | checked track, thumb |
|
|
46
|
+
| `BackgroundIconButton` | `--oc-background-icon-button-background-color` | `--oc-background-icon-button-color` | 모든 variant의 background, icon |
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
.darkPrimaryControl {
|
|
50
|
+
--oc-radio-background-color: var(--oc-color-theme-label-strong);
|
|
51
|
+
--oc-radio-color: var(--oc-color-theme-background-normal-normal);
|
|
52
|
+
--oc-interaction-color: var(--oc-radio-color);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
37
56
|
## Interaction 프리미티브
|
|
38
57
|
|
|
39
58
|
`Interaction`은 hover/press/focus 피드백을 그리는 passive 시각 레이어입니다. `pointer-events: none`이고 자기 상태를 갖지 않으며, 부모 컴포넌트가 자기 상태 selector로 자식 Interaction의 opacity를 제어합니다.
|
|
@@ -52,7 +71,40 @@
|
|
|
52
71
|
}
|
|
53
72
|
```
|
|
54
73
|
|
|
55
|
-
variant
|
|
74
|
+
`Interaction variant="light|normal|strong"`는 primitive 인스턴스의 기본 강도를 정합니다. 색을 커스텀한 host나 합성 영역에서 강도를 함께 바꾸려면 Interaction을 직접 찾지 말고 host에 `data-oc-interaction-variant="light|normal|strong"`를 설정하세요. host selector가 primitive 기본값보다 우선하며 custom property 상속을 통해 모든 하위 Interaction에 적용됩니다.
|
|
75
|
+
|
|
76
|
+
상태 레이어 색은 host-scoped contract인 `--oc-interaction-color`로 지정합니다. 기본 fallback은 `--oc-color-theme-label-normal`이며 Button처럼 appearance나 variant별 기본값이 fallback과 다른 owner component만 같은 변수를 설정합니다. 별도 매핑이 없는 owner는 primitive fallback과 host 상속을 그대로 사용합니다. consumer는 나중에 로드되는 단일 host 클래스에서 이 변수를 덮을 수 있습니다. 채워진 surface를 커스텀할 때 상태 레이어가 전경색을 따르게 하려면 `--oc-interaction-color: var(--oc-{component}-color)`처럼 명시적으로 연결하세요. foreground와 state-layer를 강제로 결합하지 않으므로 기존 기본 시각과 별도 상태색 customization을 함께 유지할 수 있습니다.
|
|
77
|
+
|
|
78
|
+
`:root` 또는 바깥 theme scope에서 선언한 값도 상속되지만, owner가 appearance나 variant 기본값을 같은 변수에 직접 선언하면 더 가까운 owner 값이 우선합니다. 따라서 `--oc-interaction-color`는 모든 Interaction을 일괄 교체하는 global theme token이 아니며, 일관된 customization이 필요하면 대상 component host에 설정합니다.
|
|
79
|
+
|
|
80
|
+
커스텀 React host의 props를 선언할 때는 package root에서 `InteractionHostProps`를 import해 같은 named contract를 재사용할 수 있습니다.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<Button
|
|
84
|
+
className={styles.brandButton}
|
|
85
|
+
data-oc-interaction-variant='strong'
|
|
86
|
+
>
|
|
87
|
+
확인
|
|
88
|
+
</Button>
|
|
89
|
+
|
|
90
|
+
<ActionArea data-oc-interaction-variant='strong'>
|
|
91
|
+
<ActionAreaButton priority='main'>저장</ActionAreaButton>
|
|
92
|
+
<ActionAreaButton priority='sub'>취소</ActionAreaButton>
|
|
93
|
+
</ActionArea>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
중첩 영역은 `normal`을 명시해 바깥 scope를 재설정할 수 있습니다. `Menu`는 content가 portal에 렌더링되므로 trigger나 `MenuRoot`가 아니라 `MenuContent`에 selector를 설정합니다.
|
|
97
|
+
|
|
98
|
+
| Contract | 기본값 | 용도 |
|
|
99
|
+
| ----------------------------------------------------- | ------------------------------------------------ | ----------------------------------------- |
|
|
100
|
+
| `--oc-interaction-color` | `--oc-color-theme-label-normal` | host와 하위 Interaction의 상태 레이어 색 |
|
|
101
|
+
| `data-oc-interaction-variant="light\|normal\|strong"` | 미설정 | host와 하위 Interaction의 named 강도 선택 |
|
|
102
|
+
| `--oc-interaction-opacity-factor` | primitive `variant`의 `0.75\|1\|1.5` | host 범위의 직접 강도 조정 escape hatch |
|
|
103
|
+
| `--oc-interaction-opacity-hover` | `--oc-color-global-opacity-5 × resolved factor` | Interaction이 제공하는 hover 출력값 |
|
|
104
|
+
| `--oc-interaction-opacity-focus` | `--oc-color-global-opacity-8 × resolved factor` | Interaction이 제공하는 focus 출력값 |
|
|
105
|
+
| `--oc-interaction-opacity-active` | `--oc-color-global-opacity-12 × resolved factor` | Interaction이 제공하는 active 출력값 |
|
|
106
|
+
|
|
107
|
+
`--oc-interaction-opacity-*`는 custom host가 상태 selector에서 읽는 resolved output 계약입니다. 임의 강도를 지정할 때는 이 출력값을 자식 selector로 덮지 말고 host에서 `--oc-interaction-opacity-factor`를 설정하세요.
|
|
56
108
|
|
|
57
109
|
## Badge 계약
|
|
58
110
|
|
|
@@ -113,6 +165,16 @@ slot content 프리미티브는 독립 컴포넌트 selector를 노출합니다.
|
|
|
113
165
|
- `verticalAlign=top`은 첫 content 줄 기준 정렬, `center`는 전체 content 높이 기준 중앙 정렬입니다.
|
|
114
166
|
- `interactive=true`일 때 row 활성화와 별개로 동작해야 하는 내부 control은 `data-oc-cell-control`로 opt-in한 경우에만 row 클릭이 활성화합니다.
|
|
115
167
|
|
|
168
|
+
## PositionSnap 계약
|
|
169
|
+
|
|
170
|
+
`PositionSnapPositioner`는 위치와 drag 상태를 소유하며, 내부 control의 transform이나 시맨틱을 변경하지 않습니다.
|
|
171
|
+
|
|
172
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
173
|
+
| -------------- | ----------------------------------- | -------------------------------------------------------------------------- | ------------------------------------ |
|
|
174
|
+
| `PositionSnap` | `data-oc-component="position-snap"` | `data-oc-drag="content\|handle"`, `data-oc-dragging`, `data-oc-positioned` | `positioner`, `position-snap-handle` |
|
|
175
|
+
|
|
176
|
+
위치 좌표는 PositionSnap runtime의 내부 계약이며 consumer는 `position`, `boundary`, `boundaryPadding` prop으로 제어합니다. 기본 paint order는 docked·raised control 단계인 `--oc-zindex-2`이며, owner 문맥에서 조정해야 할 때만 공개 계약인 `--oc-position-snap-z-index`를 덮습니다.
|
|
177
|
+
|
|
116
178
|
## Runtime Motion 계약
|
|
117
179
|
|
|
118
180
|
runtime motion wrapper는 SCSS `oc-motion`과 같은 intent/phase/pace/reduced vocabulary를 사용하며, 시맨틱 상태와 진행 중 애니메이션 상태를 분리해 노출합니다.
|
|
@@ -193,6 +255,16 @@ Tab content는 inline padding 없이 배치되고 배경색이 transparent인 se
|
|
|
193
255
|
|
|
194
256
|
`Skeleton`은 기본 장식이라 `aria-hidden="true"`를 강제하고 consumer의 `role`/`tabIndex` prop을 무시합니다. 폭·높이·radius·색·opacity는 `--oc-skeleton-*` 변수로 노출됩니다.
|
|
195
257
|
|
|
258
|
+
## Progress indicator 계약
|
|
259
|
+
|
|
260
|
+
`ProgressIndicator`는 알려진 범위 안의 진행률을 표시하는 읽기 전용 선형 요소입니다. `value`와 `max`는 시각적 길이와 `aria-valuenow`/`aria-valuemax`에 함께 반영되며, consumer는 `aria-label` 또는 `aria-labelledby`로 접근 가능한 이름을 제공합니다.
|
|
261
|
+
|
|
262
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
263
|
+
| ------------------- | ---------------------------------------- | ---------------- | ------------ |
|
|
264
|
+
| `ProgressIndicator` | `data-oc-component="progress-indicator"` | `data-oc-size` | `indicator` |
|
|
265
|
+
|
|
266
|
+
배경 track과 진행 indicator 색, 선 두께, radius는 `--oc-progress-indicator-background-color`, `--oc-progress-indicator-color`, `--oc-progress-indicator-block-size`, `--oc-progress-indicator-radius` 변수로 조정할 수 있습니다.
|
|
267
|
+
|
|
196
268
|
## Action Area 계약
|
|
197
269
|
|
|
198
270
|
`ActionAreaLayout`은 스크롤 가능한 body와 고정 footer slot 사이의 scroll 경계를 소유합니다.
|
|
@@ -207,6 +279,53 @@ Tab content는 inline padding 없이 배치되고 배경색이 transparent인 se
|
|
|
207
279
|
- `data-oc-scrollable="true"`는 body에 보이는 scrollport 밖 overflow가 있다는 뜻입니다. 이 상태에서 직속 footer `ActionArea`는 block-end padding을 유지하고 `--oc-action-area-padding-block-start: 0`을 받으며, 대응하는 block-start 간격은 body에 예약됩니다.
|
|
208
280
|
- `ActionAreaLayout.ActionArea background="auto"`는 scroll 상태를 자식 `ActionArea`의 background 계약에 매핑합니다: body가 스크롤 가능하고 바닥에 닿지 않은 동안만 `data-oc-background="true"`가 방출됩니다. `data-oc-background`는 시각 gradient/background 상태이며 body 예약의 근거로 사용하지 마세요.
|
|
209
281
|
|
|
282
|
+
## FloatingWindow 계약
|
|
283
|
+
|
|
284
|
+
FloatingWindow는 anchor 기반 geometry와 dialog 시맨틱을 Modal 기본 surface presentation에 결합합니다. 기본 `FloatingWindowRoot`는 modeless이고 `ModalWindowRoot`는 modal focus 정책, 배경 입력을 차단하는 backdrop, Modal popup scale/fade motion을 적용합니다. ModalWindow backdrop은 기본적으로 렌더링하며 필요한 경우 `backdrop="hidden"`으로 시각적 dimmer만 숨길 수 있습니다.
|
|
285
|
+
|
|
286
|
+
| Layer/slot | Selector | 책임 |
|
|
287
|
+
| ----------- | -------------------------------------------------------------------------- | -------------------------------------------- |
|
|
288
|
+
| Backdrop | `data-oc-component="floating-window-backdrop"` + `data-oc-part="backdrop"` | ModalWindow 기본 dimmer와 배경 pointer 차단 |
|
|
289
|
+
| Positioner | `data-oc-component="floating-window-positioner"` | fixed positioning과 manager 주입 stack level |
|
|
290
|
+
| Window | `data-oc-component="floating-window"` + `data-oc-part="window"` | dialog 시맨틱과 anchor/free 좌표 |
|
|
291
|
+
| Drag handle | `data-oc-part="drag-handle"` + `data-oc-draggable="true\|false"` | 명시적인 pointer drag 시작 영역 |
|
|
292
|
+
| Surface | `data-oc-part="surface-motion"` + `data-oc-part="surface"` | Modal popup presentation과 `data-oc-resize` |
|
|
293
|
+
| Header | `data-oc-component="modal-header"` | Modal 기본 header slot |
|
|
294
|
+
| Navigation | `data-oc-component="modal-navigation-section"` | Modal 기본 disclosure navigation |
|
|
295
|
+
| Body | `data-oc-component="modal-body"` | Modal 기본 scrollable body |
|
|
296
|
+
| Footer | `data-oc-component="modal-footer"` | Modal 기본 footer slot |
|
|
297
|
+
| Close | `data-oc-component="modal-close"` | dialog close action |
|
|
298
|
+
|
|
299
|
+
Window은 `data-oc-positioned="true\|false"`, `data-oc-window-positioning="anchored\|free"`, `data-oc-window-presentation="floating\|modal"`, `data-oc-window-modality="modeless\|trap-focus\|modal"`, `data-oc-side`, `data-oc-alignment`를 노출합니다. presentation은 사용한 Root 구성을 구분하는 호환 메타데이터이며 모든 presentation은 동일한 Modal popup surface motion을 사용합니다. modality는 focus와 background interaction 계약을 나타냅니다. pointer drag 중에는 `data-oc-window-dragging="true"`가 설정됩니다. `data-oc-side`는 collision flip 뒤 실제 side이고, `data-oc-alignment`는 alignment flip 뒤의 semantic alignment입니다. alignment shift는 같은 alignment 의도를 유지한 채 좌표만 boundary 안으로 보정합니다.
|
|
300
|
+
|
|
301
|
+
Drag handle 내부의 `button`, `a`, form control, `contenteditable` 요소는 drag를 시작하지 않습니다. 그 밖의 custom interactive target은 `data-oc-window-no-drag`로 같은 동작을 선언할 수 있습니다. 이 attribute는 drag gesture의 opt-out 계약이며 시각 selector가 아닙니다.
|
|
302
|
+
|
|
303
|
+
Manager는 일반 pointer와 focus intent를 window activation으로 해석하지만 `data-oc-part="close"` intent는 activation에서 제외합니다. 따라서 클릭 가능한 비활성 modeless window의 Close는 stack 순서를 바꾸지 않고 해당 위치에서 종료 모션을 실행합니다. modal backdrop과 inert 영역 뒤의 window는 이 예외와 관계없이 상호작용할 수 없습니다.
|
|
304
|
+
|
|
305
|
+
`FloatingWindowHeader`와 `FloatingWindowNavigationSection`은 surface 최상단을 소유하는 대체 관계의 chrome입니다. 기본 조합에서는 둘을 함께 렌더링하지 않습니다. `FloatingWindowNavigationSection`은 기본적으로 title을 dialog title로 등록하며, 별도의 custom title owner를 제공할 때만 `titleAsDialogTitle={false}`를 사용합니다.
|
|
306
|
+
|
|
307
|
+
modeless `FloatingWindowRoot`는 열릴 때 현재 focus를 유지합니다. `ModalWindowRoot`는 Modal과 같은 초기 focus resolver를 사용해 등록된 visible title을 우선하고, title이 없으면 첫 tabbable 요소로 fallback합니다. 이 정책은 DOM상 close button이 먼저 있다는 이유만으로 닫기 동작을 자동 강조하지 않기 위한 dialog interaction 계약입니다.
|
|
308
|
+
|
|
309
|
+
FloatingWindow surface 크기·radius·padding은 Modal의 `--oc-modal-*` popup 계약을 사용합니다. `resize="fixed"`는 Modal fixed surface와 같은 내부 body scroll contract를 활성화하며, surface의 `data-oc-scrollable`, Header의 `data-oc-sticky`, Footer의 `data-oc-background`를 실제 scroll state에 맞춰 갱신합니다. Footer의 ActionArea 조합은 Modal과 같은 direct-child selector 계약을 사용합니다.
|
|
310
|
+
|
|
311
|
+
`FloatingWindowManager` 안의 window는 고유한 `windowId`로 등록되며 다음 managed state를 노출합니다.
|
|
312
|
+
|
|
313
|
+
| Selector | 의미 |
|
|
314
|
+
| ----------------------------------- | -------------------------------------------- |
|
|
315
|
+
| `[data-oc-window-active='true']` | 현재 manager stack의 활성 window |
|
|
316
|
+
| `[data-oc-window-active='false']` | 열려 있지만 비활성인 managed window |
|
|
317
|
+
| `--oc-floating-window-stack-offset` | manager가 정규화한 backdrop/window 쌍 offset |
|
|
318
|
+
|
|
319
|
+
Manager는 modeless window보다 modal과 `trap-focus` window를 위에 유지합니다. 동일 modality tier에서는 open, pointer, focus 활성화 순서가 stack을 결정하며 Escape와 outside dismissal은 활성 window 하나에만 허용됩니다. Manager 밖에서는 `data-oc-window-active`를 출력하지 않습니다.
|
|
320
|
+
|
|
321
|
+
여러 focus-owning window가 열려도 Base UI modal/inert ownership은 활성 window 하나에만 부여됩니다. 비활성 modal과 `trap-focus` window는 요청 modality와 stack tier를 보존하지만 아래에 있는 동안 document inert를 중복 적용하지 않습니다.
|
|
322
|
+
|
|
323
|
+
| Variable | Default | Scope |
|
|
324
|
+
| ----------------------------------- | ---------------------------------------------------- | ----------------------------- |
|
|
325
|
+
| `--oc-floating-window-stack-offset` | `0`; `stackLevel` prop이 manager-owned offset을 주입 | backdrop/window stacking pair |
|
|
326
|
+
|
|
327
|
+
`--oc-floating-window-x`, `--oc-floating-window-y`는 geometry runtime이 소유하는 live 좌표이므로 consumer style override 대상으로 사용하지 않습니다.
|
|
328
|
+
|
|
210
329
|
## Modal 레이어 계약
|
|
211
330
|
|
|
212
331
|
Modal은 transition motion과 시각 clipping을 분리합니다.
|
|
@@ -217,7 +336,7 @@ Modal은 transition motion과 시각 clipping을 분리합니다.
|
|
|
217
336
|
| Motion layer | `data-oc-component="modal-content"` + `data-oc-part="surface-motion"` | dialog 시맨틱, Base UI transition 상태, opacity/transform/drag translate |
|
|
218
337
|
| Visual surface | `data-oc-part="surface"` | background, radius, overflow clipping, layout, header/body/footer slot, `data-oc-scrollable="true\|false"` |
|
|
219
338
|
| Bottom fill | `data-oc-component="modal-bottom-fill"` + `data-oc-part="bottom-fill"` | bottom sheet overshoot fill |
|
|
220
|
-
| Header | `data-oc-component="modal-header"` + `data-oc-part="header"` | 타이틀
|
|
339
|
+
| Header | `data-oc-component="modal-header"` + `data-oc-part="header"` | 타이틀 영역, optional drag handle, `header-bottom` 보조 chrome slot |
|
|
221
340
|
| Navigation | `data-oc-component="modal-navigation-section"` | 고정 chrome row + 확장 panel |
|
|
222
341
|
| Body | `data-oc-component="modal-body"` + `data-oc-part="body"` | scroll area wrapper |
|
|
223
342
|
| Body content | `data-oc-component="modal-body-content"` + `data-oc-part="body-content"` + `data-oc-layout="content\|fill"` | padded content 영역 |
|
|
@@ -225,7 +344,11 @@ Modal은 transition motion과 시각 clipping을 분리합니다.
|
|
|
225
344
|
|
|
226
345
|
시각 커스터마이즈는 `data-oc-part="surface"` 또는 component contract 변수를 대상으로 하세요. `surface-motion`은 transition/placement/drag 동작을 의도적으로 바꿀 때만 대상입니다.
|
|
227
346
|
|
|
347
|
+
Modal의 기본 `initialFocus="auto"`는 `ModalHeader`의 visible title을 `tabIndex={-1}`인 정적 문맥 시작점으로 사용합니다. title이 없으면 첫 tabbable 요소로 fallback합니다. `first-interactive`, `title`, ref/function으로 업무 의미에 맞는 대상을 명시할 수 있으며, 되돌리기 어려운 확인 dialog는 가장 덜 파괴적인 action을 지정하는 것이 권장됩니다. title focus는 일반 Tab 순서에 추가되지 않으며 첫 Tab부터 기존 control 순환이 시작됩니다.
|
|
348
|
+
|
|
228
349
|
- `ModalContent`의 `className`은 motion layer, visual surface, bottom fill 세 레이어에 적용됩니다 — 공개 `--oc-modal-*` 변수 override가 layout 계산까지 도달하게 하기 위함입니다. `style`은 visual surface에 적용되며, `style`의 CSS custom property는 motion/bottom fill 레이어로도 전달됩니다.
|
|
350
|
+
- `ModalHeader bottomContent`는 기본 header row 아래 `data-oc-part="header-bottom"`에 보조 chrome을 렌더링합니다. Modal은 이 slot을 header 높이에 포함하고 `--oc-modal-container-padding-block-start`를 현재 size의 `--oc-modal-container-padding-block`으로 자동 설정해 body와 scroll 시작 간격을 예약합니다. 명시적인 `ModalContent style` 변수는 이 기본값을 override할 수 있습니다.
|
|
351
|
+
- `ModalHeader bottomContent`의 직계 `ProgressIndicator`는 edge-attached navigation chrome으로 취급해 `--oc-progress-indicator-radius: 0`을 자동 적용합니다. 별도 wrapper 없이 slot에 직접 배치하는 구성이 canonical usage입니다.
|
|
229
352
|
- `data-starting-style`/`data-ending-style`은 Base UI가 motion layer에 방출합니다.
|
|
230
353
|
- `data-oc-modal-backdrop="hidden"`으로 정착하면 backdrop 레이어는 렌더링되지 않습니다. 전환 중 잠시 남는 exiting backdrop 요소에 layout/hit-testing/커스터마이즈를 의존하지 마세요.
|
|
231
354
|
- `backdropFrom` snap 임계 아래에 정착한 bottom sheet는 backdrop 레이어에 `data-oc-modal-backdrop-passthrough="true"`를 방출합니다. 이 상태는 non-modal이며 배경이 상호작용 가능합니다. attribute는 드래그 프레임 단위가 아니라 정착(rest) 시점에 바뀝니다.
|
|
@@ -236,6 +359,7 @@ Modal은 transition motion과 시각 clipping을 분리합니다.
|
|
|
236
359
|
- `ModalBody layout="fill"`은 header/footer/content padding 예약을 유지한 채 body content slot을 iframe·map·viewer 같은 embedded 표면용 fill container로 바꿉니다.
|
|
237
360
|
- size별 공개 변수: `--oc-modal-small-width`/`--oc-modal-medium-width`/`--oc-modal-large-width`와 `--oc-modal-{size}-padding-*`이 내부 `--oc-modal-container-padding-*` 계약으로 매핑됩니다. header/body/footer padding은 기본적으로 container padding을 상속하며, `--oc-modal-header-padding-*`/`--oc-modal-footer-padding-*`로 slot별 예외를 둘 수 있습니다. edge 정렬 chrome control용으로 `--oc-modal-header-edge-offset-inline`이 있습니다.
|
|
238
361
|
- 닫기 시도가 차단되어 피드백 중일 때 motion layer에 `data-oc-modal-feedback="reject"`가 방출됩니다. transform을 직접 덮지 말고 `--oc-modal-reject-distance` 같은 contract 변수를 사용하세요.
|
|
362
|
+
- bottom placement에서 소프트 키보드 회피 세션이 활성일 때 viewport 레이어(`data-oc-part="viewport"`)에 `data-oc-modal-keyboard-avoiding="true"`가 방출됩니다. 키보드 geometry 추격 transition의 게이트로 쓰이는 내부 지향 상태이며, 소비자는 이 attribute의 방출 타이밍(세션 시작/해제 시점의 지연 포함)에 의존하지 않는 것이 좋습니다.
|
|
239
363
|
- `ModalNavigationSection`은 body content가 아니라 Modal chrome입니다. 루트는 `data-oc-state="open|closed"`, toggle row는 `data-oc-component="modal-navigation"` + `data-oc-part="navigation"` + `aria-expanded`/`aria-controls`, indicator는 `data-oc-part="indicator"`/`indicator-frame`, panel은 `data-oc-component="modal-navigation-panel"`(collapsed 동안 `aria-hidden`/`inert`), panel content는 `data-oc-part="panel-inner"`를 노출합니다.
|
|
240
364
|
|
|
241
365
|
## Provider 소유 예외
|
package/docs/tokens.md
CHANGED
|
@@ -543,6 +543,8 @@ spring preset의 timing/duration scale입니다. 자세한 사용법은 [Spring
|
|
|
543
543
|
|
|
544
544
|
## Z-index
|
|
545
545
|
|
|
546
|
+
브라우저 paint order 단계이며 시각적 elevation과 별도입니다. 의미별 단계와 중첩 portal 규칙은 [Overlay Stacking](./overlay-stacking.md)을 참조하세요.
|
|
547
|
+
|
|
546
548
|
- `--oc-zindex-0`
|
|
547
549
|
- `--oc-zindex-1`
|
|
548
550
|
- `--oc-zindex-2`
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
@use '../mixins/breakpoint' as *;
|
|
2
2
|
@use '../mixins/color' as *;
|
|
3
|
+
@use '../mixins/fluid' as *;
|
|
3
4
|
@use '../mixins/radius' as *;
|
|
4
5
|
@use '../mixins/utils' as *;
|
|
5
6
|
|
|
6
7
|
:root {
|
|
7
8
|
--oc-atomic-radius-factor: 1;
|
|
8
9
|
--oc-atomic-radius-max: #{oc-radius(10000px)};
|
|
9
|
-
--oc-atomic-radius-huge: #{oc-radius(
|
|
10
|
-
--oc-atomic-radius-xlarge: #{oc-radius(
|
|
11
|
-
--oc-atomic-radius-large: #{oc-radius(
|
|
12
|
-
--oc-atomic-radius-medium: #{oc-radius(
|
|
13
|
-
--oc-atomic-radius-small: #{oc-radius(
|
|
14
|
-
--oc-atomic-radius-xsmall: #{oc-radius(
|
|
15
|
-
--oc-atomic-radius-mini: #{oc-radius(
|
|
16
|
-
--oc-atomic-radius-tiny: #{oc-radius(
|
|
10
|
+
--oc-atomic-radius-huge: #{oc-radius(oc-fluid(toRem(24), toRem(64), $breakpoint-md, from))};
|
|
11
|
+
--oc-atomic-radius-xlarge: #{oc-radius(oc-fluid(toRem(12), toRem(32), $breakpoint-md, from))};
|
|
12
|
+
--oc-atomic-radius-large: #{oc-radius(oc-fluid(toRem(12), toRem(24), $breakpoint-md, from))};
|
|
13
|
+
--oc-atomic-radius-medium: #{oc-radius(oc-fluid(toRem(12), toRem(16), $breakpoint-md, from))};
|
|
14
|
+
--oc-atomic-radius-small: #{oc-radius(oc-fluid(toRem(10), toRem(12), $breakpoint-md, from))};
|
|
15
|
+
--oc-atomic-radius-xsmall: #{oc-radius(oc-fluid(toRem(8), toRem(10), $breakpoint-md, from))};
|
|
16
|
+
--oc-atomic-radius-mini: #{oc-radius(oc-fluid(toRem(6), toRem(8), $breakpoint-md, from))};
|
|
17
|
+
--oc-atomic-radius-tiny: #{oc-radius(oc-fluid(toRem(4), toRem(6), $breakpoint-md, from))};
|
|
17
18
|
--oc-atomic-radius-micro: #{oc-radius(toRem(4))};
|
|
18
19
|
|
|
19
20
|
--oc-atomic-width-content: 35em;
|
package/src/scss/index.scss
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
@forward './foundations/reset';
|
|
12
12
|
@forward './mixins/breakpoint';
|
|
13
13
|
@forward './mixins/utils';
|
|
14
|
+
@forward './mixins/fluid';
|
|
14
15
|
@forward './mixins/responsive';
|
|
15
16
|
@forward './mixins/color';
|
|
17
|
+
@forward './mixins/gradient';
|
|
16
18
|
@forward './mixins/typo';
|
|
17
19
|
@forward './mixins/element';
|
|
18
20
|
@forward './mixins/focus';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@use 'sass:math';
|
|
2
|
+
@use 'sass:meta';
|
|
3
|
+
@use './units' as *;
|
|
4
|
+
|
|
5
|
+
@function _oc-fluid-px-value($value) {
|
|
6
|
+
@if meta.type-of($value) != 'number' {
|
|
7
|
+
@error 'oc-fluid() min and max values must be Sass numbers with px or rem units.';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@if math.is-unitless($value) {
|
|
11
|
+
@error 'oc-fluid() min and max values must use px or rem units.';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@if math.unit($value) == 'px' {
|
|
15
|
+
@return stripUnit($value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@if math.unit($value) == 'rem' {
|
|
19
|
+
@return stripUnit($value) * $rem-unit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@error 'oc-fluid() min and max values must use px or rem units.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@function _oc-fluid-viewport-value($bp) {
|
|
26
|
+
@if meta.type-of($bp) != 'number' {
|
|
27
|
+
@error 'oc-fluid() $bp must be a Sass number in px or unitless form.';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@if not math.is-unitless($bp) and math.unit($bp) != 'px' {
|
|
31
|
+
@error 'oc-fluid() $bp must be a Sass number in px or unitless form.';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
$bp-num: stripUnit($bp);
|
|
35
|
+
|
|
36
|
+
@if $bp-num <= 0 {
|
|
37
|
+
@error 'oc-fluid() $bp must be greater than zero.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@return $bp-num;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@function oc-fluid($min, $max, $bp, $anchor: to) {
|
|
44
|
+
$min-px: _oc-fluid-px-value($min);
|
|
45
|
+
$max-px: _oc-fluid-px-value($max);
|
|
46
|
+
$basis-px: $max-px;
|
|
47
|
+
|
|
48
|
+
@if $anchor == from {
|
|
49
|
+
$basis-px: $min-px;
|
|
50
|
+
} @else if $anchor != to {
|
|
51
|
+
@error "oc-fluid() anchor must be 'from' or 'to'.";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$bp-num: _oc-fluid-viewport-value($bp);
|
|
55
|
+
|
|
56
|
+
@return clamp(#{$min}, calc(#{$basis-px} / #{$bp-num} * 100vw), #{$max});
|
|
57
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { compileString } from 'sass';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const compileScss = (source: string) =>
|
|
5
|
+
compileString(source, {
|
|
6
|
+
loadPaths: ['src/scss/mixins'],
|
|
7
|
+
style: 'compressed',
|
|
8
|
+
}).css.replace(/:\s+/g, ':');
|
|
9
|
+
|
|
10
|
+
describe('oc-fluid function', () => {
|
|
11
|
+
it('defaults to the to anchor and accepts a direct pixel viewport basis', () => {
|
|
12
|
+
const css = compileScss(`
|
|
13
|
+
@use 'fluid' as *;
|
|
14
|
+
@use 'units' as *;
|
|
15
|
+
|
|
16
|
+
.title {
|
|
17
|
+
font-size: oc-fluid(toRem(16), toRem(20), 375px);
|
|
18
|
+
}
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
expect(css).toContain('.title{font-size:clamp(1rem,20/375*100vw,1.25rem)}');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('accepts a named breakpoint value and the from anchor', () => {
|
|
25
|
+
const css = compileScss(`
|
|
26
|
+
@use 'breakpoint' as *;
|
|
27
|
+
@use 'fluid' as *;
|
|
28
|
+
@use 'units' as units;
|
|
29
|
+
|
|
30
|
+
.radius {
|
|
31
|
+
border-radius: oc-fluid(units.toRem(10), units.toRem(12), $breakpoint-md, from);
|
|
32
|
+
}
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
expect(css).toContain(
|
|
36
|
+
'.radius{border-radius:clamp(0.625rem,10/768*100vw,0.75rem)}',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('supports pixel endpoints and mixed compatible length units', () => {
|
|
41
|
+
const css = compileScss(`
|
|
42
|
+
@use 'fluid' as *;
|
|
43
|
+
|
|
44
|
+
.pixel {
|
|
45
|
+
font-size: oc-fluid(16px, 20px, 375px);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.mixed {
|
|
49
|
+
padding-block: oc-fluid(1rem, 20px, 375px);
|
|
50
|
+
}
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
expect(css).toContain('.pixel{font-size:clamp(16px,20/375*100vw,20px)}');
|
|
54
|
+
expect(css).toContain(
|
|
55
|
+
'.mixed{padding-block:clamp(1rem,20/375*100vw,20px)}',
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rejects an unknown anchor', () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
compileScss(`
|
|
62
|
+
@use 'fluid' as *;
|
|
63
|
+
@use 'units' as *;
|
|
64
|
+
|
|
65
|
+
.invalid {
|
|
66
|
+
font-size: oc-fluid(toRem(16), toRem(20), 375px, center);
|
|
67
|
+
}
|
|
68
|
+
`),
|
|
69
|
+
).toThrow("oc-fluid() anchor must be 'from' or 'to'.");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('fluidClamp compatibility function', () => {
|
|
74
|
+
it('preserves the legacy argument order', () => {
|
|
75
|
+
const css = compileScss(`
|
|
76
|
+
@use 'utils' as *;
|
|
77
|
+
|
|
78
|
+
.legacy {
|
|
79
|
+
font-size: fluidClamp(toRem(16), 375px, to, toRem(20));
|
|
80
|
+
}
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
expect(css).toContain(
|
|
84
|
+
'.legacy{font-size:clamp(1rem,20/375*100vw,1.25rem)}',
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
@use 'sass:list';
|
|
2
|
+
@use 'sass:math';
|
|
3
|
+
@use 'sass:meta';
|
|
4
|
+
@use 'sass:string';
|
|
5
|
+
|
|
6
|
+
@function _oc-gradient-require-direction($direction) {
|
|
7
|
+
@if list.index((top, right, bottom, left), $direction) == null {
|
|
8
|
+
@error 'Unknown gradient direction: #{$direction}. Use top, right, bottom, or left.';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@return $direction;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@function _oc-gradient-position($start, $end, $progress) {
|
|
15
|
+
@if $progress == 0 {
|
|
16
|
+
@return $start;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@if $progress == 1 {
|
|
20
|
+
@return $end;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@return string.unquote('calc(#{$start} + (#{$end} - #{$start}) * #{$progress})');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@function _oc-gradient-color($from, $to, $progress) {
|
|
27
|
+
@if $progress == 0 {
|
|
28
|
+
@return $from;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@if $progress == 1 {
|
|
32
|
+
@return $to;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$from-weight: math.percentage(1 - $progress);
|
|
36
|
+
|
|
37
|
+
@return string.unquote('color-mix(in srgb, #{$from} #{$from-weight}, #{$to})');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@function _oc-easing-gradient-stops($from, $to, $start, $end) {
|
|
41
|
+
// Eight intervals keep smoothstep boundaries visually soft without making every generated gradient excessively large.
|
|
42
|
+
$stepCount: 8;
|
|
43
|
+
$stops: ();
|
|
44
|
+
|
|
45
|
+
@for $index from 0 through $stepCount {
|
|
46
|
+
$linear-progress: math.div($index, $stepCount);
|
|
47
|
+
|
|
48
|
+
// Smoothstep keeps the color-change velocity at both boundaries continuous.
|
|
49
|
+
$eased-progress: 3 * math.pow($linear-progress, 2) - 2 * math.pow($linear-progress, 3);
|
|
50
|
+
$color: _oc-gradient-color($from, $to, $eased-progress);
|
|
51
|
+
$position: _oc-gradient-position($start, $end, $linear-progress);
|
|
52
|
+
|
|
53
|
+
$stops: list.append($stops, $color $position, comma);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@return $stops;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@function oc-easing-gradient($direction, $from, $to, $start: 0%, $end: 100%) {
|
|
60
|
+
$validated-direction: _oc-gradient-require-direction($direction);
|
|
61
|
+
$stops: _oc-easing-gradient-stops($from, $to, $start, $end);
|
|
62
|
+
|
|
63
|
+
@return linear-gradient(to $validated-direction, $stops);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@function _oc-gradient-mask-image($direction, $start-fade-size, $end-fade-size, $content-size) {
|
|
67
|
+
$validated-direction: _oc-gradient-require-direction($direction);
|
|
68
|
+
$start-stops: _oc-easing-gradient-stops(transparent, black, 0px, $start-fade-size);
|
|
69
|
+
$end-fade-start: string.unquote('calc(#{$content-size} - #{$end-fade-size})');
|
|
70
|
+
$end-stops: _oc-easing-gradient-stops(black, transparent, $end-fade-start, $content-size);
|
|
71
|
+
$stops: list.join($start-stops, $end-stops, comma);
|
|
72
|
+
|
|
73
|
+
@return linear-gradient(to $validated-direction, $stops);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@mixin oc-gradient-mask($direction, $start-fade-size: 0px, $end-fade-size: 0px, $content-size: 100%) {
|
|
77
|
+
$mask: _oc-gradient-mask-image($direction, $start-fade-size, $end-fade-size, $content-size);
|
|
78
|
+
|
|
79
|
+
// stylelint-disable-next-line property-no-vendor-prefix -- Safari still needs the prefixed mask property for gradient fades.
|
|
80
|
+
-webkit-mask-image: $mask;
|
|
81
|
+
// stylelint-disable-next-line declaration-block-no-duplicate-properties -- Keep the standard property after the Safari fallback.
|
|
82
|
+
mask-image: $mask;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@mixin oc-gradient-mask-edges($top-fade-size: 0px, $right-fade-size: 0px, $bottom-fade-size: 0px, $left-fade-size: 0px, $inline-content-size: 100%, $block-content-size: 100%) {
|
|
86
|
+
$block-mask: _oc-gradient-mask-image(bottom, $top-fade-size, $bottom-fade-size, $block-content-size);
|
|
87
|
+
$inline-mask: _oc-gradient-mask-image(right, $left-fade-size, $right-fade-size, $inline-content-size);
|
|
88
|
+
|
|
89
|
+
// Both layers encode only black alpha, so intersection multiplies corner alpha and is order-independent.
|
|
90
|
+
$masks: $block-mask, $inline-mask;
|
|
91
|
+
|
|
92
|
+
// stylelint-disable-next-line property-no-vendor-prefix -- WebKit uses its legacy Porter-Duff keyword for mask intersection.
|
|
93
|
+
-webkit-mask-composite: source-in;
|
|
94
|
+
// stylelint-disable-next-line property-no-vendor-prefix -- Safari still needs the prefixed mask property for gradient fades.
|
|
95
|
+
-webkit-mask-image: $masks;
|
|
96
|
+
mask-composite: intersect;
|
|
97
|
+
// stylelint-disable-next-line declaration-block-no-duplicate-properties -- Keep the standard property after the Safari fallback.
|
|
98
|
+
mask-image: $masks;
|
|
99
|
+
}
|