@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.
- package/README.md +119 -54
- package/dist/components/Accordion/Accordion.css +1 -1
- package/dist/components/Accordion/Accordion.js +169 -177
- package/dist/components/ActionArea/ActionAreaLayout.js +20 -24
- package/dist/components/Cell/Cell.css +1 -1
- package/dist/components/Cell/Cell.js +57 -58
- package/dist/components/CheckMark/CheckMark.css +1 -1
- package/dist/components/Checkbox/Checkbox.css +1 -1
- package/dist/components/Checkbox/Checkbox.js +50 -51
- package/dist/components/Menu/Menu.css +1 -1
- package/dist/components/Menu/Menu.module.scss.js +20 -22
- package/dist/components/Modal/Modal.css +1 -1
- package/dist/components/Modal/Modal.js +585 -575
- package/dist/components/Modal/bottomSheetGeometry.js +46 -33
- package/dist/components/Modal/modalScrollArbitration.js +71 -49
- package/dist/components/Modal/snapPoints.js +42 -38
- package/dist/components/Modal/useModalContentBlockTransition.js +49 -44
- package/dist/components/Modal/useModalDrag.js +937 -854
- package/dist/components/Radio/Radio.css +1 -1
- package/dist/components/ScrollArea/ScrollArea.js +197 -200
- package/dist/components/Select/Select.js +31 -34
- package/dist/components/Switch/Switch.css +1 -1
- package/dist/components/Tabs/Tabs.css +1 -0
- package/dist/components/Tabs/Tabs.js +208 -0
- package/dist/components/Tabs/Tabs.module.scss.js +27 -0
- package/dist/components/Tabs.d.ts +6 -0
- package/dist/components/Tabs.js +8 -0
- package/dist/components/TextArea/TextArea.js +57 -58
- package/dist/components/TextInput/TextInput.js +30 -33
- package/dist/components/Tooltip/Tooltip.css +1 -1
- package/dist/foundations/color.theme.dark.css +1 -1
- package/dist/foundations/color.theme.light.css +1 -1
- package/dist/foundations/focus.css +1 -0
- package/dist/foundations/reset.css +1 -1
- package/dist/foundations.css +4 -3
- package/dist/index.js +69 -64
- package/dist/react/motion.js +31 -35
- package/dist/reset.css +1 -1
- package/dist/styles.css +14 -12
- package/dist/ui/src/components/Accordion/Accordion.d.ts.map +1 -1
- package/dist/ui/src/components/ActionArea/ActionAreaLayout.d.ts.map +1 -1
- package/dist/ui/src/components/Cell/Cell.d.ts.map +1 -1
- package/dist/ui/src/components/CheckMark/types.d.ts +10 -0
- package/dist/ui/src/components/CheckMark/types.d.ts.map +1 -1
- package/dist/ui/src/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/ui/src/components/Checkbox/types.d.ts +16 -0
- package/dist/ui/src/components/Checkbox/types.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/Modal.d.ts +1 -1
- package/dist/ui/src/components/Modal/Modal.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/bottomSheetGeometry.d.ts +13 -0
- package/dist/ui/src/components/Modal/bottomSheetGeometry.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/modalScrollArbitration.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/snapPoints.d.ts +3 -1
- package/dist/ui/src/components/Modal/snapPoints.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/types.d.ts +8 -0
- package/dist/ui/src/components/Modal/types.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/useModalContentBlockTransition.d.ts.map +1 -1
- package/dist/ui/src/components/Modal/useModalDrag.d.ts +27 -10
- package/dist/ui/src/components/Modal/useModalDrag.d.ts.map +1 -1
- package/dist/ui/src/components/Radio/types.d.ts +16 -0
- package/dist/ui/src/components/Radio/types.d.ts.map +1 -1
- package/dist/ui/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
- package/dist/ui/src/components/Select/Select.d.ts.map +1 -1
- package/dist/ui/src/components/Switch/types.d.ts +10 -0
- package/dist/ui/src/components/Switch/types.d.ts.map +1 -1
- package/dist/ui/src/components/Tabs/Tabs.d.ts +7 -0
- package/dist/ui/src/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/ui/src/components/Tabs/index.d.ts +3 -0
- package/dist/ui/src/components/Tabs/index.d.ts.map +1 -0
- package/dist/ui/src/components/Tabs/types.d.ts +26 -0
- package/dist/ui/src/components/Tabs/types.d.ts.map +1 -0
- package/dist/ui/src/components/TextArea/TextArea.d.ts.map +1 -1
- package/dist/ui/src/components/TextInput/TextInput.d.ts.map +1 -1
- package/dist/ui/src/index.d.ts +2 -0
- package/dist/ui/src/index.d.ts.map +1 -1
- package/dist/ui/src/react/motion/index.d.ts.map +1 -1
- package/dist/ui/src/utils/mergeRefs.d.ts +13 -0
- package/dist/ui/src/utils/mergeRefs.d.ts.map +1 -0
- package/dist/utils/mergeRefs.js +24 -0
- package/dist/vscode/oc-ui-vars.css +13 -0
- package/docs/editor-setup.md +42 -0
- package/docs/motion.md +665 -0
- package/docs/responsive-foundation-profile.md +230 -0
- package/docs/scss-helpers.md +189 -0
- package/docs/selector-contract.md +260 -0
- package/docs/spring.md +382 -0
- package/docs/stability.md +62 -0
- package/docs/tokens.md +559 -0
- package/package.json +9 -4
- package/src/scss/foundations/color-theme.test.ts +14 -0
- package/src/scss/foundations/color.theme.dark.scss +1 -0
- package/src/scss/foundations/color.theme.light.scss +1 -0
- package/src/scss/foundations/focus.scss +4 -0
- package/src/scss/foundations/focus.test.ts +48 -0
- package/src/scss/foundations/reset.scss +9 -4
- package/src/scss/foundations/reset.test.ts +32 -0
- package/src/scss/index.scss +2 -0
- package/src/scss/mixins/_focus.scss +11 -0
- package/src/scss/mixins/_focus.test.ts +58 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Selector Contract
|
|
2
|
+
|
|
3
|
+
`@orioncactuscorp/ui` 컴포넌트를 consumer CSS에서 커스터마이즈할 때 사용하는 안정 selector 계약입니다. CSS Module 해시 클래스는 빌드마다 바뀌므로 selector로 사용하지 않고, 컴포넌트가 노출하는 data attribute를 사용합니다.
|
|
4
|
+
|
|
5
|
+
이 문서의 selector·slot 표는 계약이며, 변경·제거 규칙은 [Stability Policy](./stability.md)를 따릅니다.
|
|
6
|
+
|
|
7
|
+
## 커스터마이즈 selector 3계층
|
|
8
|
+
|
|
9
|
+
| 계층 | 형태 | 용도 |
|
|
10
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
11
|
+
| 컴포넌트 루트 | `data-oc-component="<name>"` | 독립 시각 컴포넌트/프리미티브 식별 |
|
|
12
|
+
| 내부 slot | `data-oc-part="<slot>"` | 컴포넌트 내부의 안정 slot |
|
|
13
|
+
| 시각/상태 | `data-oc-size`, `data-oc-variant`, `data-oc-appearance`, `data-oc-state`, `data-oc-invalid`, `data-oc-orientation` 등 | 컴포넌트가 소유한 시각·상태 분기 |
|
|
14
|
+
|
|
15
|
+
값은 kebab-case입니다.
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
/* 예: invalid 상태의 TextInput content 표면 커스터마이즈 */
|
|
19
|
+
[data-oc-component='text-input'][data-oc-invalid='true']
|
|
20
|
+
[data-oc-part='content'] {
|
|
21
|
+
--oc-text-input-border-color: var(--oc-color-theme-status-negative);
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
다음은 selector로 사용하지 마세요.
|
|
26
|
+
|
|
27
|
+
- `@orioncactuscorp/ui`의 CSS Module 해시 클래스
|
|
28
|
+
- 아래 [Legacy 호환 출력](#legacy-호환-출력)에 있는 구형 attribute
|
|
29
|
+
- 계약에 없는 incidental wrapper에 의존하는 DOM 깊이 selector
|
|
30
|
+
|
|
31
|
+
## Override가 동작하는 방식
|
|
32
|
+
|
|
33
|
+
- 컴포넌트 소스는 루트 modifier selector를 `:where(...)`로 낮은 specificity로 유지합니다. 나중에 로드되는 consumer 클래스 하나로 루트 레벨 `--oc-*` component contract 변수를 덮을 수 있습니다.
|
|
34
|
+
- 스타일 override는 paint property를 직접 재선언하기보다 컴포넌트가 노출하는 contract 변수(`--oc-{component}-*`)를 덮는 방식을 우선하세요. 상태 전이·모션과의 일관성이 유지됩니다.
|
|
35
|
+
- 방향성 계약(`--oc-*` 변수, `data-oc-*` 값, prop)은 물리 방향(top/right/bottom/left)이 아니라 논리 방향(`block-start`, `inline-end`, `start`, `end` 등)을 사용합니다. RTL에서 자동으로 뒤집힙니다.
|
|
36
|
+
|
|
37
|
+
## Interaction 프리미티브
|
|
38
|
+
|
|
39
|
+
`Interaction`은 hover/press/focus 피드백을 그리는 passive 시각 레이어입니다. `pointer-events: none`이고 자기 상태를 갖지 않으며, 부모 컴포넌트가 자기 상태 selector로 자식 Interaction의 opacity를 제어합니다.
|
|
40
|
+
|
|
41
|
+
```scss
|
|
42
|
+
.myControl {
|
|
43
|
+
&:focus-visible > [data-oc-component='interaction'] {
|
|
44
|
+
opacity: var(--oc-interaction-opacity-focus);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@media (hover: hover) {
|
|
48
|
+
&:hover:not(:disabled) > [data-oc-component='interaction'] {
|
|
49
|
+
opacity: var(--oc-interaction-opacity-hover);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
variant(`light`/`normal`/`strong`)는 `--oc-interaction-opacity-*` 토큰 값만 바꿉니다.
|
|
56
|
+
|
|
57
|
+
## Field 레이아웃 계약
|
|
58
|
+
|
|
59
|
+
`Field`는 label/control/message compound 구조를 유지한 채 vertical/horizontal orientation을 노출합니다.
|
|
60
|
+
|
|
61
|
+
| Component | Root selector | Orientation selector | Part selector | Stable parts |
|
|
62
|
+
| -------------- | ----------------------------------- | -------------------------------------------- | ------------------------ | --------------------------------- |
|
|
63
|
+
| `Field` | `data-oc-component="field"` | `data-oc-orientation="vertical\|horizontal"` | - | `field-control`, message 컴포넌트 |
|
|
64
|
+
| `FieldControl` | `data-oc-component="field-control"` | - | `data-oc-part="control"` | 렌더링된 control 컴포넌트를 감쌈 |
|
|
65
|
+
|
|
66
|
+
- horizontal label 폭은 `--oc-field-label-inline-size`로 제어합니다. `labelWidth` prop의 숫자 값은 `16px = 1rem` 기준 rem으로 변환됩니다.
|
|
67
|
+
- `form`/`fieldset` 같은 상위 요소에 같은 변수를 지정하면 여러 Field가 label 폭을 공유합니다.
|
|
68
|
+
- `--oc-field-label-max-inline-size`는 label 컬럼 상한이며 기본값은 `35%`입니다.
|
|
69
|
+
- horizontal의 helper/error message는 control 컬럼에 배치되어 input 시작선에 정렬됩니다.
|
|
70
|
+
- multi-control row는 `Field` layout 책임이 아닙니다. 내부 control group을 `FieldControl` 안에서 조합하거나, 하나의 label이 여러 native control을 설명해야 하면 시맨틱 grouping 프리미티브를 사용하세요.
|
|
71
|
+
|
|
72
|
+
## Field Control slot 계약
|
|
73
|
+
|
|
74
|
+
| Component | Root selector | Native part | Content surface part | Slot parts |
|
|
75
|
+
| ----------- | -------------------------------- | ------------------------- | ------------------------ | ----------------------------------------------------------- |
|
|
76
|
+
| `TextInput` | `data-oc-component="text-input"` | `data-oc-part="input"` | `data-oc-part="content"` | `leading-content`, `trailing-content` |
|
|
77
|
+
| `TextArea` | `data-oc-component="text-area"` | `data-oc-part="textarea"` | `data-oc-part="content"` | `supporting-content`, `leading-content`, `trailing-content` |
|
|
78
|
+
| `Select` | `data-oc-component="select"` | `data-oc-part="select"` | `data-oc-part="content"` | `leading-content`, `trailing-content`, `indicator` |
|
|
79
|
+
|
|
80
|
+
slot content 프리미티브는 독립 컴포넌트 selector를 노출합니다.
|
|
81
|
+
|
|
82
|
+
| Primitive | Selector | Variants | 접근성 기본값 |
|
|
83
|
+
| ------------------ | ---------------------------------------- | ---------------------------------------------- | ------------------------------------------ |
|
|
84
|
+
| `TextInputContent` | `data-oc-component="text-input-content"` | `data-oc-variant="text\|icon\|action\|custom"` | `icon` variant는 `aria-hidden="true"` 기본 |
|
|
85
|
+
| `TextAreaContent` | `data-oc-component="text-area-content"` | `data-oc-variant="text\|icon\|action\|custom"` | `icon` variant는 `aria-hidden="true"` 기본 |
|
|
86
|
+
| `SelectContent` | `data-oc-component="select-content"` | `data-oc-variant="text\|icon\|action\|custom"` | `icon` variant는 `aria-hidden="true"` 기본 |
|
|
87
|
+
|
|
88
|
+
`TextArea`는 `supporting-content`에 `data-oc-align`을 소유합니다.
|
|
89
|
+
|
|
90
|
+
| `data-oc-align` | 의미 |
|
|
91
|
+
| --------------- | ---------------------------- |
|
|
92
|
+
| `start` | leading content만 존재 |
|
|
93
|
+
| `end` | trailing content만 존재 |
|
|
94
|
+
| `space-between` | leading과 trailing 모두 존재 |
|
|
95
|
+
|
|
96
|
+
## Cell slot 계약
|
|
97
|
+
|
|
98
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
99
|
+
| ---------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
100
|
+
| `Cell` | `data-oc-component="cell"` | `data-oc-divider="normal\|neutral\|alternative\|none"` | - |
|
|
101
|
+
| `CellItem` | `data-oc-component="cell-item"` | `data-oc-fill-width="true\|false"`, `data-oc-interactive="true"`, `data-oc-divider`, `data-oc-vertical-align="top\|center"` | `leading`, `content`, `text`, `label`, `description`, `trailing` |
|
|
102
|
+
|
|
103
|
+
- `fillWidth=true`는 row가 inline padding을 소유하고 interaction 레이어를 row 경계 안에 둡니다. `false`는 content를 flush로 두고 interaction 레이어만 inline margin으로 확장합니다.
|
|
104
|
+
- `verticalAlign=top`은 첫 content 줄 기준 정렬, `center`는 전체 content 높이 기준 중앙 정렬입니다.
|
|
105
|
+
- `interactive=true`일 때 row 활성화와 별개로 동작해야 하는 내부 control은 `data-oc-cell-control`로 opt-in한 경우에만 row 클릭이 활성화합니다.
|
|
106
|
+
|
|
107
|
+
## Runtime Motion 계약
|
|
108
|
+
|
|
109
|
+
runtime motion wrapper는 SCSS `oc-motion`과 같은 intent/phase/pace/reduced vocabulary를 사용하며, 시맨틱 상태와 진행 중 애니메이션 상태를 분리해 노출합니다.
|
|
110
|
+
|
|
111
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
112
|
+
| -------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------ |
|
|
113
|
+
| `MotionExpand` | `data-oc-component="motion-expand"` | `data-oc-state="open\|closed"`, `data-oc-motion-intent`, `data-oc-motion-state="closed\|opening\|open\|closing"` | `content` |
|
|
114
|
+
|
|
115
|
+
`prefers-reduced-motion: reduce`가 활성일 때 `data-oc-reduced-motion="true"`가 방출됩니다. reduced 상태의 `block-size` 같은 non-feedback property expand는 즉시 해소되며 mount/unmount presence 계약은 동일합니다.
|
|
116
|
+
|
|
117
|
+
## Accordion 계약
|
|
118
|
+
|
|
119
|
+
summary row는 Cell slot 계약을 시각적으로 따르되 trigger는 native button입니다. details expand는 CSS grid가 아니라 React runtime motion 레이어가 담당합니다.
|
|
120
|
+
|
|
121
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
122
|
+
| ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `Accordion` | `data-oc-component="accordion"` | `data-oc-divider="normal\|neutral\|alternative\|none"`, `data-oc-disabled="true"`, `data-oc-animation="disabled"` | - |
|
|
124
|
+
| `AccordionItem` | `data-oc-component="accordion-item"` | `data-oc-state="open\|closed"`, `data-oc-divider`, `data-oc-disabled="true"` | - |
|
|
125
|
+
| `AccordionSummary` | `data-oc-component="accordion-summary"` | `data-oc-state="open\|closed"`, `data-oc-fill-width="true\|false"`, `data-oc-vertical-align="top\|center"`, `data-oc-disabled="true"` | `header`, `leading`, `content`, `text`, `label`, `description`, `trailing`, `indicator-frame` |
|
|
126
|
+
| `AccordionDetails` | `data-oc-component="accordion-details"` | `data-oc-state="open\|closed"`, `data-oc-motion-intent`, `data-oc-motion-state`, `data-oc-reduced-motion="true"` | `panel-inner` |
|
|
127
|
+
| `AccordionDescription` | `data-oc-component="accordion-description"` | - | - |
|
|
128
|
+
| `AccordionContent` | `data-oc-component="accordion-content"` | - | - |
|
|
129
|
+
|
|
130
|
+
`AccordionSummary`가 `aria-expanded`/`aria-controls`/summary id를, `AccordionDetails`가 대응 id와 `aria-labelledby`를 소유합니다. 닫힌 force-mounted details는 `aria-hidden`과 `inert`로 접근성 트리·포커스에서 제외됩니다.
|
|
131
|
+
|
|
132
|
+
## Tabs 계약
|
|
133
|
+
|
|
134
|
+
Tabs는 Base UI의 선택·키보드·ARIA 연결을 사용하고, oc-ui가 size, layout, indicator, overflow 표현을 소유합니다.
|
|
135
|
+
|
|
136
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
137
|
+
| ----------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------- |
|
|
138
|
+
| `TabsRoot` | `data-oc-component="tabs"` | - | - |
|
|
139
|
+
| `TabsList` | `data-oc-part="list"` | `data-oc-size="small\|medium\|large"`, `data-oc-full-width="true\|false"`, `data-oc-padding="true\|false"` | `scroller`, `indicator`, `trailing` |
|
|
140
|
+
| `TabsTab` | `data-oc-part="tab"` | `aria-selected="true\|false"`, `aria-disabled="true"` | `label` |
|
|
141
|
+
| `TabsPanel` | `data-oc-part="panel"` | `hidden` | - |
|
|
142
|
+
|
|
143
|
+
`TabsTab`의 선택·disabled 스타일은 Base UI 런타임 attribute인 `data-active`/`data-disabled`가 아니라 `aria-selected`/`aria-disabled`를 기준으로 합니다. disabled tab은 `aria-disabled="true"`이지만 native `disabled`가 아니며 키보드 포커스를 받을 수 있습니다. `TabsPanel keepMounted`는 비활성 panel을 DOM에 유지하고 `hidden`으로 접근성 트리에서 제외합니다.
|
|
144
|
+
|
|
145
|
+
Base UI가 tab 관계와 키보드 탐색을 위해 생성하는 `role`, `tabIndex`, `aria-selected`, `aria-controls`, `aria-labelledby`, `aria-orientation`, panel `id`/`hidden`은 컴포넌트 소유 계약이며 소비자 prop으로 재정의할 수 없습니다.
|
|
146
|
+
|
|
147
|
+
Tab typography는 `TabsList`의 `size`에 따라 large는 `headline2/bold`, medium은 `body1/bold`, small은 `body2/bold`가 적용되고 각 `TabsTab`이 상속합니다. 소비자는 `TabsList`의 `className` 또는 `[data-oc-part='list']` selector에서 `font-size`, `line-height`, `letter-spacing`, `font-weight`를 재정의해 모든 tab item의 typography를 일괄 조정할 수 있습니다.
|
|
148
|
+
|
|
149
|
+
Tab content는 inline padding 없이 배치되고 배경색이 transparent인 self `Interaction`이 `--oc-tabs-tab-interaction-margin-inline`만큼 좌우 바깥으로 확장되어 실제 pointer hit area가 됩니다. 인접한 Interaction이 겹치지 않도록 기본 gap은 이 값의 2배이며 `padding` 상태와 무관하게 유지됩니다. `padding=false`이면 list 좌우 여백이 없고 양끝 Interaction의 바깥 확장은 list 경계에서 잘립니다. `padding=true`이면 `--oc-space-section-padding-viewport-x`가 기본 좌우 여백으로 적용됩니다.
|
|
150
|
+
|
|
151
|
+
다음 `--oc-tabs-*` 변수로 list와 tab의 spacing, 상태 색, indicator, overflow fade를 조정할 수 있습니다.
|
|
152
|
+
|
|
153
|
+
| Variable | Default | Scope |
|
|
154
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------- |
|
|
155
|
+
| `--oc-tabs-list-padding-inline` | `0px`; `padding=true`이면 `--oc-space-section-padding-viewport-x` | list 좌우 여백 |
|
|
156
|
+
| `--oc-tabs-list-padding-block` | `0px` | list 상하 여백 |
|
|
157
|
+
| `--oc-tabs-list-gap` | `--oc-tabs-tab-interaction-margin-inline`의 2배 | tab 사이 간격 |
|
|
158
|
+
| `--oc-tabs-tab-interaction-margin-inline` | `--oc-space-item-padding-xsmall` | 각 tab Interaction 좌우 확장 |
|
|
159
|
+
| `--oc-tabs-tab-padding-block` | large/medium은 `--oc-space-item-padding-mini`, small은 `--oc-space-item-padding-tiny` | 각 tab 상하 클릭 영역 |
|
|
160
|
+
| `--oc-tabs-edge-fade-size` | `3rem` | overflow edge fade 크기 |
|
|
161
|
+
| `--oc-tabs-indicator-block-size` | `0.125rem` | indicator 두께 |
|
|
162
|
+
| `--oc-tabs-indicator-radius` | `--oc-atomic-radius-max` | indicator 모서리 반경 |
|
|
163
|
+
| `--oc-tabs-indicator-color` | `--oc-color-theme-label-strong` | 선택 indicator 색 |
|
|
164
|
+
| `--oc-tabs-indicator-disabled-color` | `--oc-color-theme-label-disable` | disabled 선택 indicator 색 |
|
|
165
|
+
| `--oc-tabs-label-color` | `--oc-color-theme-label-assistive` | 기본 label 색 |
|
|
166
|
+
| `--oc-tabs-label-hover-color` | `--oc-color-theme-label-alternative` | hover label 색 |
|
|
167
|
+
| `--oc-tabs-label-selected-color` | `--oc-color-theme-label-strong` | 선택 label 색 |
|
|
168
|
+
| `--oc-tabs-label-disabled-color` | `--oc-color-theme-label-disable` | disabled label 색 |
|
|
169
|
+
|
|
170
|
+
## Icon 계약
|
|
171
|
+
|
|
172
|
+
| Primitive | Selector | 접근성 기본값 |
|
|
173
|
+
| --------- | ------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
174
|
+
| `SvgIcon` | `data-oc-component="svg-icon"` | 기본 `aria-hidden="true"`. 의미 있는 아이콘은 `role="img"` + `aria-label`/`aria-labelledby` |
|
|
175
|
+
|
|
176
|
+
## Loading placeholder 계약
|
|
177
|
+
|
|
178
|
+
상태 안내가 필요하면 `Loading`(assistive technology에 status announce), 부모 영역이 loading 시맨틱을 이미 소유하면 `Skeleton`(장식)을 사용합니다.
|
|
179
|
+
|
|
180
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
181
|
+
| ---------- | ------------------------------ | --------------------------------------------------------------------------------- | ------------ |
|
|
182
|
+
| `Loading` | `data-oc-component="loading"` | `data-oc-variant="circular"`, `data-oc-size` | `indicator` |
|
|
183
|
+
| `Skeleton` | `data-oc-component="skeleton"` | `data-oc-variant="text\|rectangle\|circle"`, `data-oc-align`, `data-oc-animation` | `fill` |
|
|
184
|
+
|
|
185
|
+
`Skeleton`은 기본 장식이라 `aria-hidden="true"`를 강제하고 consumer의 `role`/`tabIndex` prop을 무시합니다. 폭·높이·radius·색·opacity는 `--oc-skeleton-*` 변수로 노출됩니다.
|
|
186
|
+
|
|
187
|
+
## Action Area 계약
|
|
188
|
+
|
|
189
|
+
`ActionAreaLayout`은 스크롤 가능한 body와 고정 footer slot 사이의 scroll 경계를 소유합니다.
|
|
190
|
+
|
|
191
|
+
| Component | Root selector | Visual selectors | Stable parts |
|
|
192
|
+
| ------------------------- | ----------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------- |
|
|
193
|
+
| `ActionArea` | `data-oc-component="action-area"` | `data-oc-variant`, `data-oc-background="true"` | `actions`, `caption`, `extra-slot`, `compact-row`, `compact-slot` |
|
|
194
|
+
| `ActionAreaLayout` | `data-oc-component="action-area-layout"` | `data-oc-scrollable="true\|false"` | - |
|
|
195
|
+
| `ActionAreaLayout.Body` | `data-oc-component="action-area-layout-body"` | - | `body` |
|
|
196
|
+
| `ActionAreaLayout.Footer` | `data-oc-component="action-area-layout-footer"` | - | `footer` |
|
|
197
|
+
|
|
198
|
+
- `data-oc-scrollable="true"`는 body에 보이는 scrollport 밖 overflow가 있다는 뜻입니다. 이 상태에서 직속 footer `ActionArea`는 block-end padding을 유지하고 `--oc-action-area-padding-block-start: 0`을 받으며, 대응하는 block-start 간격은 body에 예약됩니다.
|
|
199
|
+
- `ActionAreaLayout.ActionArea background="auto"`는 scroll 상태를 자식 `ActionArea`의 background 계약에 매핑합니다: body가 스크롤 가능하고 바닥에 닿지 않은 동안만 `data-oc-background="true"`가 방출됩니다. `data-oc-background`는 시각 gradient/background 상태이며 body 예약의 근거로 사용하지 마세요.
|
|
200
|
+
|
|
201
|
+
## Modal 레이어 계약
|
|
202
|
+
|
|
203
|
+
Modal은 transition motion과 시각 clipping을 분리합니다.
|
|
204
|
+
|
|
205
|
+
| Layer | Selector | 책임 |
|
|
206
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
207
|
+
| Backdrop | `data-oc-component="modal-backdrop"` + `data-oc-part="backdrop"` | dimmer 표시와 backdrop press dismissal |
|
|
208
|
+
| Motion layer | `data-oc-component="modal-content"` + `data-oc-part="surface-motion"` | dialog 시맨틱, Base UI transition 상태, opacity/transform/drag translate |
|
|
209
|
+
| Visual surface | `data-oc-part="surface"` | background, radius, overflow clipping, layout, header/body/footer slot, `data-oc-scrollable="true\|false"` |
|
|
210
|
+
| Bottom fill | `data-oc-component="modal-bottom-fill"` + `data-oc-part="bottom-fill"` | bottom sheet overshoot fill |
|
|
211
|
+
| Header | `data-oc-component="modal-header"` + `data-oc-part="header"` | 타이틀 영역과 optional drag handle |
|
|
212
|
+
| Navigation | `data-oc-component="modal-navigation-section"` | 고정 chrome row + 확장 panel |
|
|
213
|
+
| Body | `data-oc-component="modal-body"` + `data-oc-part="body"` | scroll area wrapper |
|
|
214
|
+
| Body content | `data-oc-component="modal-body-content"` + `data-oc-part="body-content"` + `data-oc-layout="content\|fill"` | padded content 영역 |
|
|
215
|
+
| Footer | `data-oc-component="modal-footer"` + `data-oc-part="footer"` | 고정 overlay action/footer slot |
|
|
216
|
+
|
|
217
|
+
시각 커스터마이즈는 `data-oc-part="surface"` 또는 component contract 변수를 대상으로 하세요. `surface-motion`은 transition/placement/drag 동작을 의도적으로 바꿀 때만 대상입니다.
|
|
218
|
+
|
|
219
|
+
- `ModalContent`의 `className`은 motion layer, visual surface, bottom fill 세 레이어에 적용됩니다 — 공개 `--oc-modal-*` 변수 override가 layout 계산까지 도달하게 하기 위함입니다. `style`은 visual surface에 적용되며, `style`의 CSS custom property는 motion/bottom fill 레이어로도 전달됩니다.
|
|
220
|
+
- `data-starting-style`/`data-ending-style`은 Base UI가 motion layer에 방출합니다.
|
|
221
|
+
- `data-oc-modal-backdrop="hidden"`으로 정착하면 backdrop 레이어는 렌더링되지 않습니다. 전환 중 잠시 남는 exiting backdrop 요소에 layout/hit-testing/커스터마이즈를 의존하지 마세요.
|
|
222
|
+
- `backdropFrom` snap 임계 아래에 정착한 bottom sheet는 backdrop 레이어에 `data-oc-modal-backdrop-passthrough="true"`를 방출합니다. 이 상태는 non-modal이며 배경이 상호작용 가능합니다. attribute는 드래그 프레임 단위가 아니라 정착(rest) 시점에 바뀝니다.
|
|
223
|
+
- modal elevation shadow와 backdrop dim은 하나의 깊이 신호의 보완 관계입니다(매 프레임 `shadow = 1 − dim`). dim 상태 정착은 shadow 없음, undimmed/hidden-backdrop 정착은 full shadow이며, `backdropFrom` 경계를 지나는 이동은 둘을 crossfade합니다.
|
|
224
|
+
- visual surface의 `data-oc-scrollable`은 footer 예약 스타일링용입니다. `ActionArea` footer가 있을 때 `true`면 footer action area의 block-start padding이 body content에 예약되고, `false`면 standalone action area padding 모델이 유지되고 footer 높이만 예약됩니다.
|
|
225
|
+
- bottom placement에서 motion layer가 sheet geometry를 소유합니다: `--oc-modal-sheet-height`가 높이를 제어할 때 `data-oc-modal-sheet-sizing="managed"`, 높이 변화가 sheet motion 계약으로 전환되어야 할 때만 `data-oc-modal-sheet-transition="auto"`, snap-point 동작 중 `data-oc-modal-snapping="true"`.
|
|
226
|
+
- `resize="fixed"`는 `--oc-modal-fixed-block-size`를 요청 높이로 사용하고 `--oc-modal-fixed-max-block-size`(기본 `--oc-modal-max-height`)로 clamp합니다. popup placement는 `--oc-modal-popup-fixed-max-block-size`로 별도 clamp됩니다.
|
|
227
|
+
- `ModalBody layout="fill"`은 header/footer/content padding 예약을 유지한 채 body content slot을 iframe·map·viewer 같은 embedded 표면용 fill container로 바꿉니다.
|
|
228
|
+
- 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`이 있습니다.
|
|
229
|
+
- 닫기 시도가 차단되어 피드백 중일 때 motion layer에 `data-oc-modal-feedback="reject"`가 방출됩니다. transform을 직접 덮지 말고 `--oc-modal-reject-distance` 같은 contract 변수를 사용하세요.
|
|
230
|
+
- `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"`를 노출합니다.
|
|
231
|
+
|
|
232
|
+
## Provider 소유 예외
|
|
233
|
+
|
|
234
|
+
다음 non-`data-oc-*` attribute는 컴포넌트 소유 계약이 아니라 외부 provider/foundation 소유입니다.
|
|
235
|
+
|
|
236
|
+
| Attribute | Owner | 정책 |
|
|
237
|
+
| --------------------- | ------------------------------ | -------------------------------------- |
|
|
238
|
+
| `data-side` | Base UI / positioning provider | provider 상태 스타일링에 사용 가능 |
|
|
239
|
+
| `data-starting-style` | Base UI / transition provider | transition 상태 스타일링에 사용 가능 |
|
|
240
|
+
| `data-ending-style` | Base UI / transition provider | transition 상태 스타일링에 사용 가능 |
|
|
241
|
+
| `data-theme` | Foundation theme 계약 | 테마 선택용. 컴포넌트 selector 계약 밖 |
|
|
242
|
+
|
|
243
|
+
## Legacy 호환 출력
|
|
244
|
+
|
|
245
|
+
아래 attribute는 구형 consumer 호환용으로만 방출됩니다. 새 스타일링에 사용하지 마세요. 제거는 major 릴리스에서만 진행됩니다([Stability Policy](./stability.md) 참조).
|
|
246
|
+
|
|
247
|
+
| Component | Legacy attribute | Replacement |
|
|
248
|
+
| ------------------------------------ | ----------------------- | -------------------------- |
|
|
249
|
+
| `SectionHeader` | `data-size` | `data-oc-size` |
|
|
250
|
+
| `SectionHeader` | `data-viewport` | `data-oc-viewport` |
|
|
251
|
+
| `TextInput` | `data-invalid` | `data-oc-invalid` |
|
|
252
|
+
| `TextInput` | `data-interactive` | `data-oc-interactive` |
|
|
253
|
+
| `Select` | `data-invalid` | `data-oc-invalid` |
|
|
254
|
+
| `Checkbox` | `data-indeterminate` | `data-oc-indeterminate` |
|
|
255
|
+
| `ScrollArea` | `data-root` | `data-oc-scroll-root` |
|
|
256
|
+
| `ScrollArea` | `data-scroll` | `data-oc-scroll` |
|
|
257
|
+
| `ScrollArea` | `data-scroll-label` | `data-oc-scroll-label` |
|
|
258
|
+
| `ScrollAreaContainer` | `data-scroll-container` | `data-oc-scroll-container` |
|
|
259
|
+
| `ScrollArea` scrollbar | `data-orientation` | `data-oc-orientation` |
|
|
260
|
+
| `ScrollArea` scrollbar, track, thumb | `data-state` | `data-oc-state` |
|
package/docs/spring.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# Spring Motion
|
|
2
|
+
|
|
3
|
+
`oc-spring(...)`은 iOS spring의 perceptual duration 기준으로 CSS transition을 작성하기 위한 SCSS helper입니다. 입력한 duration은 사용자가 의도한 감각적 시간이고, 출력 CSS에는 감쇠가 안정되는 settling duration과 `linear(...)` timing function이 들어갑니다.
|
|
4
|
+
|
|
5
|
+
```scss
|
|
6
|
+
@use '@orioncactuscorp/ui/scss/spring' as *;
|
|
7
|
+
|
|
8
|
+
.sheet {
|
|
9
|
+
transition: oc-spring(transform, 0.5s, snappy);
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Sass는 위 코드를 대략 다음 형태의 CSS로 컴파일합니다.
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
.sheet {
|
|
17
|
+
transition: transform 0.815s linear(...);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 기본 사용법
|
|
22
|
+
|
|
23
|
+
단일 property는 CSS transition shorthand와 비슷하게 씁니다.
|
|
24
|
+
|
|
25
|
+
```scss
|
|
26
|
+
.smooth {
|
|
27
|
+
transition: oc-spring(transform, 0.5s, smooth);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.snappy {
|
|
31
|
+
transition: oc-spring(transform, 0.5s, snappy);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.bouncy {
|
|
35
|
+
transition: oc-spring(transform, 0.5s, bouncy);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
네 번째 positional 값은 preset bounce에 더하는 `extra-bounce`입니다.
|
|
40
|
+
|
|
41
|
+
```scss
|
|
42
|
+
.tuned {
|
|
43
|
+
transition: oc-spring(transform, 0.5s, snappy, 0.1);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
delay가 필요하면 named argument를 사용합니다.
|
|
48
|
+
|
|
49
|
+
```scss
|
|
50
|
+
.delayed {
|
|
51
|
+
transition: oc-spring(transform, 0.5s, snappy, 0.1, $delay: 80ms);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 여러 Property
|
|
56
|
+
|
|
57
|
+
같은 spring 설정을 여러 property에 적용할 수 있습니다.
|
|
58
|
+
|
|
59
|
+
```scss
|
|
60
|
+
.popover {
|
|
61
|
+
transition: oc-spring((opacity, transform), 0.3s, snappy);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
property별로 다른 설정이 필요하면 map을 사용합니다.
|
|
66
|
+
|
|
67
|
+
```scss
|
|
68
|
+
.menu {
|
|
69
|
+
transition: oc-spring(
|
|
70
|
+
(
|
|
71
|
+
opacity: (
|
|
72
|
+
0.3s,
|
|
73
|
+
snappy,
|
|
74
|
+
),
|
|
75
|
+
transform: (
|
|
76
|
+
0.3s,
|
|
77
|
+
snappy,
|
|
78
|
+
0.1,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
property별 tuple 순서는 다음과 같습니다.
|
|
86
|
+
|
|
87
|
+
```scss
|
|
88
|
+
(duration, preset, extra-bounce, delay, samples)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
예를 들어 opacity에는 짧은 snappy spring을, transform에는 extra bounce와 delay를 줄 수 있습니다.
|
|
92
|
+
|
|
93
|
+
```scss
|
|
94
|
+
.panel {
|
|
95
|
+
transition: oc-spring(
|
|
96
|
+
(
|
|
97
|
+
opacity: (
|
|
98
|
+
0.2s,
|
|
99
|
+
snappy,
|
|
100
|
+
),
|
|
101
|
+
transform: (
|
|
102
|
+
0.3s,
|
|
103
|
+
snappy,
|
|
104
|
+
0.1,
|
|
105
|
+
40ms,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 고급 옵션
|
|
113
|
+
|
|
114
|
+
`bounce`, physical spring, `initial-velocity`, `epsilon`, `stability-window`, `max-duration`처럼 의미가 더 명시적이어야 하는 옵션은 named map을 사용합니다.
|
|
115
|
+
|
|
116
|
+
```scss
|
|
117
|
+
.custom-bounce {
|
|
118
|
+
transition: oc-spring(transform, 0.6s, $bounce: 0.2);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.physical {
|
|
122
|
+
transition: oc-spring(transform, $mass: 1, $stiffness: 100, $damping: 10);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.mixed {
|
|
126
|
+
transition: oc-spring(
|
|
127
|
+
(
|
|
128
|
+
opacity: (
|
|
129
|
+
duration: 0.2s,
|
|
130
|
+
preset: snappy,
|
|
131
|
+
delay: 50ms,
|
|
132
|
+
),
|
|
133
|
+
transform: (
|
|
134
|
+
mass: 1,
|
|
135
|
+
stiffness: 100,
|
|
136
|
+
damping: 10,
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
property별 named map에서 사용할 수 있는 key는 다음과 같습니다.
|
|
144
|
+
|
|
145
|
+
```scss
|
|
146
|
+
duration
|
|
147
|
+
preset
|
|
148
|
+
extra-bounce
|
|
149
|
+
bounce
|
|
150
|
+
mass
|
|
151
|
+
stiffness
|
|
152
|
+
damping
|
|
153
|
+
initial-velocity
|
|
154
|
+
delay
|
|
155
|
+
epsilon
|
|
156
|
+
stability-window
|
|
157
|
+
max-duration
|
|
158
|
+
samples
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
알 수 없는 key나 5개를 초과하는 tuple은 컴파일 에러로 처리합니다.
|
|
162
|
+
|
|
163
|
+
## Sass 문법 주의점
|
|
164
|
+
|
|
165
|
+
직접 함수 호출에서는 Sass named argument를 사용할 수 있습니다.
|
|
166
|
+
|
|
167
|
+
```scss
|
|
168
|
+
.direct {
|
|
169
|
+
transition: oc-spring(transform, 0.5s, snappy, $extra-bounce: 0.1);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
하지만 property별 tuple 안에서는 `$extra-bounce:` 같은 named argument 문법을 사용할 수 없습니다. tuple 안에서는 positional 값을 쓰거나, named map key를 사용해야 합니다.
|
|
174
|
+
|
|
175
|
+
```scss
|
|
176
|
+
.valid-tuple {
|
|
177
|
+
transition: oc-spring(
|
|
178
|
+
(
|
|
179
|
+
transform: (
|
|
180
|
+
0.5s,
|
|
181
|
+
snappy,
|
|
182
|
+
0.1,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.valid-map {
|
|
189
|
+
transition: oc-spring(
|
|
190
|
+
(
|
|
191
|
+
transform: (
|
|
192
|
+
duration: 0.5s,
|
|
193
|
+
preset: snappy,
|
|
194
|
+
extra-bounce: 0.1,
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Prebuilt CSS에서 사용
|
|
202
|
+
|
|
203
|
+
SCSS를 컴파일하지 않고 prebuilt CSS만 쓰는 consumer는 foundation token을 사용할 수 있습니다.
|
|
204
|
+
|
|
205
|
+
```scss
|
|
206
|
+
.target {
|
|
207
|
+
transition: transform calc(0.5s * var(--oc-spring-bouncy-duration-scale))
|
|
208
|
+
var(--oc-spring-bouncy-timing);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
현재 제공되는 preset token은 다음과 같습니다.
|
|
213
|
+
|
|
214
|
+
```scss
|
|
215
|
+
--oc-spring-smooth-timing
|
|
216
|
+
--oc-spring-smooth-duration-scale
|
|
217
|
+
--oc-spring-snappy-timing
|
|
218
|
+
--oc-spring-snappy-duration-scale
|
|
219
|
+
--oc-spring-bouncy-timing
|
|
220
|
+
--oc-spring-bouncy-duration-scale
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Escape Hatches
|
|
224
|
+
|
|
225
|
+
transition shorthand를 직접 조립해야 하면 timing과 duration helper를 따로 사용할 수 있습니다.
|
|
226
|
+
|
|
227
|
+
```scss
|
|
228
|
+
.target {
|
|
229
|
+
transition-property: opacity, transform;
|
|
230
|
+
transition-duration: oc-spring-settle-duration(0.5s, snappy);
|
|
231
|
+
transition-timing-function: oc-spring-timing(0.5s, snappy);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
기존 CSS 변수 방식과 조합해야 하면 scale helper를 사용할 수 있습니다.
|
|
236
|
+
|
|
237
|
+
```scss
|
|
238
|
+
.target {
|
|
239
|
+
transition-duration: calc(0.5s * #{oc-spring-duration-scale(0.5s, snappy)});
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## 어떤 API를 써야 하나요?
|
|
244
|
+
|
|
245
|
+
- CSS transition만 필요하면 `oc-spring(...)`을 사용합니다.
|
|
246
|
+
- JS에서 transition 문자열이나 spring sample을 계산해야 하면 `ocSpring.*`를 사용합니다.
|
|
247
|
+
- gesture, drag, interrupt처럼 target이 계속 바뀌는 DOM interaction은 `createSpringAnimator`를 사용합니다.
|
|
248
|
+
- React state의 `target`을 따라가는 UI는 `useSpringTarget`으로 시작합니다.
|
|
249
|
+
- React에서 `setTarget()`, `setSpring()`, `jumpTo()`를 직접 제어해야 하면 `useSpringValue`를 사용합니다.
|
|
250
|
+
|
|
251
|
+
## Why This Exists
|
|
252
|
+
|
|
253
|
+
CSS transition은 선언적이고 가볍지만, gesture 중 target이 바뀌는 순간 현재 velocity를 이어받기 어렵습니다. 일반 easing curve는 시간에 대한 값만 정의하므로 interrupt 시점의 물리 상태를 자연스럽게 보존하지 못합니다.
|
|
254
|
+
|
|
255
|
+
orioncactus spring은 Apple Spring처럼 duration/bounce로 조율할 수 있는 물리 모델을 사용합니다. 같은 model/solver를 SCSS sampling, TypeScript runtime, React hook이 공유하기 때문에 정적인 transition과 interactive gesture가 같은 motion language를 갖습니다.
|
|
256
|
+
|
|
257
|
+
```mermaid
|
|
258
|
+
flowchart LR
|
|
259
|
+
A["duration + bounce"] --> B["spring model"]
|
|
260
|
+
C["mass / stiffness / damping"] --> B
|
|
261
|
+
B --> D["closed-form solver"]
|
|
262
|
+
D --> E["SCSS linear(...) sampling"]
|
|
263
|
+
D --> F["runtime animator"]
|
|
264
|
+
F --> G["React hooks"]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## TypeScript Runtime
|
|
268
|
+
|
|
269
|
+
런타임에서 spring 값을 계산하거나 CSS transition 문자열을 조립해야 하면 `@orioncactuscorp/ui/utils/spring`을 사용합니다. `ocSpring.*` 결과는 Apple `Spring`과 같은 물리 모델을 공유하므로 static CSS sampling과 runtime animator에 같은 spring 값을 전달할 수 있습니다.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { ocSpring } from '@orioncactuscorp/ui/utils/spring';
|
|
273
|
+
|
|
274
|
+
const spring = ocSpring.snappy({ duration: 0.5, extraBounce: 0.1 });
|
|
275
|
+
|
|
276
|
+
spring.transition('transform');
|
|
277
|
+
spring.settlingDuration;
|
|
278
|
+
spring.timingFunction;
|
|
279
|
+
spring.evaluate({ time: 0.2 }); // { value, velocity }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
진행 중인 spring을 자연스럽게 interrupt해야 하는 gesture나 drag UI는 `createSpringAnimator`를 사용합니다. animator는 frame마다 현재 value와 velocity를 추적하고, `setTarget()`이 호출되면 그 순간의 velocity를 다음 spring으로 이어받습니다.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import {
|
|
286
|
+
createSpringAnimator,
|
|
287
|
+
ocSpring,
|
|
288
|
+
} from '@orioncactuscorp/ui/utils/spring';
|
|
289
|
+
|
|
290
|
+
const spring = ocSpring.snappy({ duration: 0.5, extraBounce: 0.1 });
|
|
291
|
+
|
|
292
|
+
const animator = createSpringAnimator({
|
|
293
|
+
value: { x: 0, y: 0 },
|
|
294
|
+
target: { x: 160, y: 80 },
|
|
295
|
+
spring,
|
|
296
|
+
onUpdate: value => {
|
|
297
|
+
element.style.transform = `translate(${value.x}px, ${value.y}px)`;
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
animator.setTarget({ x: 240, y: 140 });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`value`, `target`, `velocity`는 number, number array, numeric object를 지원합니다. object를 사용할 때는 초기 value와 같은 key shape를 유지해야 합니다.
|
|
305
|
+
|
|
306
|
+
spring 자체를 바꿔야 하면 `setSpring()` 또는 `setTarget(target, { spring })`을 사용합니다. `blendDuration`을 전달하면 duration/bounce 기반 spring끼리는 duration과 bounce 값을, physical spring은 mass/stiffness/damping 값을 일정 시간 동안 섞습니다.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
animator.setTarget(
|
|
310
|
+
{ x: 320, y: 120 },
|
|
311
|
+
{
|
|
312
|
+
spring: ocSpring.bouncy({ duration: 0.45 }),
|
|
313
|
+
blendDuration: 0.2,
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## React Hook Runtime
|
|
319
|
+
|
|
320
|
+
React component에서 spring 값을 state처럼 구독하려면 `@orioncactuscorp/ui/react/spring`을 사용합니다. `useSpringTarget`은 React state의 target을 따라가는 controlled hook이고, `useSpringValue`는 현재 `value`, `velocity`, `target`과 imperative setter를 함께 반환하는 low-level hook입니다.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
'use client';
|
|
324
|
+
|
|
325
|
+
import { useState } from 'react';
|
|
326
|
+
import { useSpringTarget } from '@orioncactuscorp/ui/react/spring';
|
|
327
|
+
import { ocSpring } from '@orioncactuscorp/ui/utils/spring';
|
|
328
|
+
|
|
329
|
+
const spring = ocSpring.snappy({ duration: 0.5, extraBounce: 0.1 });
|
|
330
|
+
|
|
331
|
+
export function DragFollower() {
|
|
332
|
+
const [target, setTarget] = useState({ x: 160, y: 80 });
|
|
333
|
+
const position = useSpringTarget(target, {
|
|
334
|
+
value: { x: 0, y: 0 },
|
|
335
|
+
spring,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<button
|
|
340
|
+
type='button'
|
|
341
|
+
onPointerMove={event => {
|
|
342
|
+
setTarget({ x: event.clientX, y: event.clientY });
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<span
|
|
346
|
+
style={{
|
|
347
|
+
transform: `translate3d(${position.value.x}px, ${position.value.y}px, 0)`,
|
|
348
|
+
}}
|
|
349
|
+
/>
|
|
350
|
+
</button>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
object/array target은 reference 변경 기준으로 retarget합니다. 매 render마다 새 object를 만들면 매번 새 target으로 간주하므로, 계산된 target은 `useMemo`나 state로 안정화하는 것을 권장합니다.
|
|
356
|
+
|
|
357
|
+
`useSpringAnimator`는 React lifecycle에 맞춰 animator를 생성/정리해야 하지만 DOM write는 직접 제어하고 싶은 경우에 사용합니다. `useSpringTarget`과 `useSpringValue` 모두 초기 생성 옵션은 `createSpringAnimator`와 같은 shape를 쓰고, 세밀한 retarget과 spring 변경은 반환된 `setTarget()`, `setSpring()`, `jumpTo()`로 제어합니다.
|
|
358
|
+
|
|
359
|
+
## Architecture: Apple Spring Model
|
|
360
|
+
|
|
361
|
+
orioncactus spring은 `duration`을 CSS transition duration이 아니라 Apple Spring의 perceptual duration 관점으로 다룹니다. 사용자가 지정한 duration은 움직임이 지각되는 기본 response이고, 실제 CSS에는 값이 충분히 안정되는 settling duration을 계산해 출력합니다. 그래서 `spring.settlingDuration`은 입력 duration보다 길 수 있습니다.
|
|
362
|
+
|
|
363
|
+
duration/bounce 기반 spring은 먼저 `bounce`를 damping ratio로 바꾸고, duration을 mass/stiffness/damping으로 변환합니다. duration spring은 mass를 1로 고정해 response가 입력 duration과 맞도록 stiffness를 계산하고, damping은 damping ratio에서 파생합니다. physical spring은 이 변환을 거치지 않고 사용자가 전달한 mass/stiffness/damping을 그대로 사용합니다.
|
|
364
|
+
|
|
365
|
+
SCSS `oc-spring(...)`, TypeScript `ocSpring.*`, `createSpringAnimator`, React `useSpringTarget`, `useSpringValue`는 모두 같은 spring model과 closed-form solver를 공유합니다. CSS helper는 solver 결과를 `linear(...)` stop으로 샘플링하고, runtime animator는 frame마다 같은 solver로 현재 value와 velocity를 평가합니다. 새 API를 추가할 때도 별도 easing engine을 만들지 않고 이 model/solver를 통과해야 합니다.
|
|
366
|
+
|
|
367
|
+
runtime animator의 핵심 contract는 velocity continuity입니다. gesture 도중 `setTarget()`이 호출되면 현재 position과 velocity를 보존한 채 새 target으로 이어가므로, drag나 interrupt가 CSS transition 재시작처럼 끊기지 않습니다. `setSpring()`과 `blendDuration`도 같은 원칙을 따르며, React hook은 이 animator를 React lifecycle에 맞춰 감싼 얇은 wrapper입니다.
|
|
368
|
+
|
|
369
|
+
## 비용과 기준
|
|
370
|
+
|
|
371
|
+
`oc-spring(...)`은 spring curve를 CSS `linear(...)` stop으로 샘플링하므로 출력 CSS가 길어집니다. 컴포넌트 내부 transition처럼 재사용되는 스타일에는 적합하지만, 수십 개의 서로 다른 spring을 한 화면에서 동적으로 생성하는 용도로는 적합하지 않습니다.
|
|
372
|
+
|
|
373
|
+
기본 기준은 다음과 같습니다.
|
|
374
|
+
|
|
375
|
+
- 단순한 opacity, transform motion은 `oc-spring(property, duration, preset)` 사용
|
|
376
|
+
- preset보다 조금 더 탄성이 필요하면 네 번째 positional `extra-bounce` 사용
|
|
377
|
+
- 여러 property가 같은 rhythm이면 property list 사용
|
|
378
|
+
- property별 설정이 다르면 property map 사용
|
|
379
|
+
- physical spring과 고급 옵션은 named map 사용
|
|
380
|
+
- gesture, drag, interrupt가 중요한 런타임 상호작용은 `createSpringAnimator` 사용
|
|
381
|
+
- React state target을 따라가야 하면 `useSpringTarget` 사용
|
|
382
|
+
- React component에서 spring 값을 직접 제어해야 하면 `useSpringValue` 사용
|