@orioncactuscorp/ui 1.10.0 → 1.12.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 +136 -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.js +253 -261
- package/dist/components/Modal/useModalContentBlockTransition.js +49 -44
- package/dist/components/Radio/Radio.css +1 -1
- package/dist/components/ScrollArea/ScrollArea.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.map +1 -1
- package/dist/ui/src/components/Modal/useModalContentBlockTransition.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/colors.md +241 -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 +561 -0
- package/docs/typography.md +115 -0
- package/package.json +9 -4
- package/src/scss/foundations/color-theme.test.ts +37 -8
- package/src/scss/foundations/color.theme.dark.scss +4 -1
- package/src/scss/foundations/color.theme.light.scss +3 -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,115 @@
|
|
|
1
|
+
# Typography
|
|
2
|
+
|
|
3
|
+
oc-ui typography foundation은 19개 variant와 `normal | medium | bold` weight 조합을 제공합니다. variant 이름은 정보 위계와 읽기 맥락의 계약이고, size·line-height 값은 responsive profile과 consumer override에 따라 달라질 수 있는 기본값입니다.
|
|
4
|
+
|
|
5
|
+
## Variant hierarchy
|
|
6
|
+
|
|
7
|
+
- `display1`~`display3`: 캠페인 hero나 대형 숫자처럼 짧고 가장 강한 표현
|
|
8
|
+
- `title1`~`title3`: 페이지 표제
|
|
9
|
+
- `heading1`~`heading2`: 큰 section의 제목
|
|
10
|
+
- `headline1`~`headline2`: 내비게이션과 소섹션(sub-section)의 제목
|
|
11
|
+
- `body1`~`body2`: 일반 본문과 UI 설명. `body1`이 기본 페이지 typography
|
|
12
|
+
- `label1`~`label2`: 대상을 지칭하는 '라벨' 텍스트 — form field label, 항목명, button·menu 같은 control의 라벨
|
|
13
|
+
- `caption1`~`caption2`: metadata, timestamp, 짧은 보조 정보
|
|
14
|
+
|
|
15
|
+
variant 숫자가 작을수록 같은 계층 안에서 더 큽니다. 계층을 오직 크기 선택 도구로 사용하지 말고 문서 구조와 UI 역할에 맞춰 선택합니다.
|
|
16
|
+
|
|
17
|
+
## Reading variants
|
|
18
|
+
|
|
19
|
+
`body1reading`, `body2reading`, `label1reading`은 대응 기본 variant와 font-size·letter-spacing·weight가 같고 line-height만 더 넓습니다. 여러 줄로 이어지는 본문, 안내문, 조밀한 label 묶음처럼 읽기 호흡이 필요한 경우에 사용합니다. 한 줄 control label이나 짧은 badge에는 기본 variant를 유지합니다.
|
|
20
|
+
|
|
21
|
+
## Responsive stops
|
|
22
|
+
|
|
23
|
+
기본 profile은 `sm / md / lg / xl` 4개 stop을 사용합니다.
|
|
24
|
+
|
|
25
|
+
- `body*`, `label*`, `caption*`은 `md` stop을 포함합니다.
|
|
26
|
+
- `heading*`, `headline*`, `body*`, `label*`, `caption*`은 `xl` stop을 포함합니다.
|
|
27
|
+
- `display*`, `title*`은 `sm / lg`, `heading*`과 `headline*`은 `sm / lg / xl` 기본값을 가집니다.
|
|
28
|
+
- 정의되지 않은 stop은 responsive segment에서 인접한 유효 stop을 기준으로 보간·유지됩니다. 표의 `—`는 token이 없다는 뜻이 아니라 해당 variant map에 별도 stop 값이 없다는 뜻입니다.
|
|
29
|
+
|
|
30
|
+
## Weight policy
|
|
31
|
+
|
|
32
|
+
모든 variant는 `normal`, `medium`, `bold` 세 이름을 제공하지만 `bold`의 숫자는 계층에 따라 낮아집니다. display·title은 700, heading·headline·body는 600, label은 550, caption1은 540, caption2는 530입니다. 작은 글자에서 지나치게 뭉치는 것을 피하면서 의미상 같은 `bold` 역할을 유지하기 위한 정책입니다.
|
|
33
|
+
|
|
34
|
+
## 기본값 표
|
|
35
|
+
|
|
36
|
+
<!-- prettier-ignore-start -->
|
|
37
|
+
<!-- generated:typography:start -->
|
|
38
|
+
> 아래 표는 기본 responsive profile의 SCSS map에서 생성됩니다. 값은 고정 스펙이 아니라 기본값입니다. line-height 괄호 안 숫자는 `line-height / font-size` 비율입니다.
|
|
39
|
+
|
|
40
|
+
| Variant | Size sm | Size md | Size lg | Size xl | Line-height sm | Line-height md | Line-height lg | Line-height xl | Letter spacing | Normal | Medium | Bold |
|
|
41
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
42
|
+
| `display1` | 48px | — | 64px | — | 62px (1.29) | — | 82px (1.28) | — | 0em | 400 | 500 | 700 |
|
|
43
|
+
| `display2` | 40px | — | 48px | — | 52px (1.30) | — | 62px (1.29) | — | 0em | 400 | 500 | 700 |
|
|
44
|
+
| `display3` | 36px | — | 40px | — | 48px (1.33) | — | 52px (1.30) | — | 0em | 400 | 500 | 700 |
|
|
45
|
+
| `title1` | 32px | — | 36px | — | 44px (1.38) | — | 48px (1.33) | — | 0em | 400 | 500 | 700 |
|
|
46
|
+
| `title2` | 28px | — | 32px | — | 38px (1.36) | — | 42px (1.31) | — | 0em | 400 | 500 | 700 |
|
|
47
|
+
| `title3` | 24px | — | 28px | — | 32px (1.33) | — | 36px (1.29) | — | 0em | 400 | 500 | 700 |
|
|
48
|
+
| `heading1` | 22px | — | 24px | 26px | 30px (1.36) | — | 32px (1.33) | 34px (1.31) | 0em | 400 | 500 | 600 |
|
|
49
|
+
| `heading2` | 20px | — | 22px | 24px | 28px (1.40) | — | 30px (1.36) | 32px (1.33) | 0em | 400 | 500 | 600 |
|
|
50
|
+
| `headline1` | 18px | — | 20px | 22px | 26px (1.44) | — | 28px (1.40) | 30px (1.36) | 0em | 400 | 500 | 600 |
|
|
51
|
+
| `headline2` | 17px | — | 18px | 20px | 24px (1.41) | — | 26px (1.44) | 28px (1.40) | 0em | 400 | 500 | 600 |
|
|
52
|
+
| `body1` | 16px | 17px | 18px | 20px | 24px (1.50) | 26px (1.53) | 28px (1.56) | 30px (1.50) | 0em | 400 | 500 | 600 |
|
|
53
|
+
| `body1reading` | 16px | 17px | 18px | 20px | 26px (1.63) | 28px (1.65) | 30px (1.67) | 32px (1.60) | 0em | 400 | 500 | 600 |
|
|
54
|
+
| `body2` | 15px | 16px | 17px | 18px | 22px (1.47) | 24px (1.50) | 26px (1.53) | 28px (1.56) | 0em | 400 | 500 | 600 |
|
|
55
|
+
| `body2reading` | 15px | 16px | 17px | 18px | 24px (1.60) | 26px (1.63) | 28px (1.65) | 30px (1.67) | 0em | 400 | 500 | 600 |
|
|
56
|
+
| `label1` | 14px | 15px | 16px | 17px | 20px (1.43) | 22px (1.47) | 24px (1.50) | 26px (1.53) | 0.01em | 400 | 500 | 550 |
|
|
57
|
+
| `label1reading` | 14px | 15px | 16px | 17px | 22px (1.57) | 24px (1.60) | 26px (1.63) | 28px (1.65) | 0.01em | 400 | 500 | 550 |
|
|
58
|
+
| `label2` | 13px | 14px | 15px | 16px | 18px (1.38) | 20px (1.43) | 22px (1.47) | 24px (1.50) | 0.01em | 400 | 500 | 550 |
|
|
59
|
+
| `caption1` | 12px | 13px | 14px | 15px | 16px (1.33) | 18px (1.38) | 20px (1.43) | 22px (1.47) | 0.01em | 400 | 500 | 540 |
|
|
60
|
+
| `caption2` | 11px | 12px | 13px | 14px | 14px (1.27) | 16px (1.33) | 18px (1.38) | 20px (1.43) | 0.01em | 400 | 500 | 530 |
|
|
61
|
+
<!-- generated:typography:end -->
|
|
62
|
+
<!-- prettier-ignore-end -->
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
raw `--oc-typo-size-*`, `--oc-typo-height-*`, `--oc-typo-spacing-*`, `--oc-typo-weight-*`를 직접 조합하지 말고 반드시 `typo()` mixin을 사용합니다. mixin은 네 속성을 같은 variant contract에서 출력하고 runtime `--oc-typo-size-factor`도 반영합니다.
|
|
67
|
+
|
|
68
|
+
```scss
|
|
69
|
+
@use '@orioncactuscorp/ui/scss/mixins/typo' as *;
|
|
70
|
+
|
|
71
|
+
.pageTitle {
|
|
72
|
+
@include typo(title1, bold);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.articleBody {
|
|
76
|
+
@include typo(body1reading, normal);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.fieldLabel {
|
|
80
|
+
@include typo(label1, medium);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Customize
|
|
85
|
+
|
|
86
|
+
### Size and line-height maps
|
|
87
|
+
|
|
88
|
+
foundation을 직접 compile할 때 `$oc-typo-size-overrides`와 `$oc-typo-height-overrides`로 필요한 variant·stop만 덮습니다. 역할 이름은 유지하고 제품에 필요한 기본값만 조정합니다.
|
|
89
|
+
|
|
90
|
+
```scss
|
|
91
|
+
@use '@orioncactuscorp/ui/scss/foundations/typo' as * with (
|
|
92
|
+
$oc-typo-size-overrides: (
|
|
93
|
+
title1: (
|
|
94
|
+
sm: 36,
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
$oc-typo-height-overrides: (
|
|
98
|
+
title1: (
|
|
99
|
+
sm: 48,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Runtime size factor
|
|
106
|
+
|
|
107
|
+
`--oc-typo-size-factor`는 size와 line-height에 함께 적용되는 runtime multiplier입니다. 특정 subtree의 접근성 확대나 preview에 사용할 수 있지만, variant 간 위계를 다시 설계하는 용도로 남용하지 않습니다.
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
.largeTypeScope {
|
|
111
|
+
--oc-typo-size-factor: 1.125;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
fluid·stepped·static 출력과 active stop 범위는 [Responsive Foundation Profile](./responsive-foundation-profile.md), mixin signature는 [SCSS Helpers](./scss-helpers.md)를 참조하세요. Storybook typography matrix는 현재 foundation profile의 CSS variable을 사용하므로 override 환경의 실값을 그대로 보여줍니다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orioncactuscorp/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "orioncactus UI foundations and core primitives",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
33
|
"src/scss",
|
|
34
|
+
"docs",
|
|
34
35
|
"README.md",
|
|
35
36
|
"LICENSE"
|
|
36
37
|
],
|
|
@@ -78,14 +79,18 @@
|
|
|
78
79
|
},
|
|
79
80
|
"packageManager": "bun@1.3.6",
|
|
80
81
|
"scripts": {
|
|
81
|
-
"build": "bun run build:lib && bun run build:css && bun run check:exports && bun run check:dist-imports && bun run check:rsc-boundaries",
|
|
82
|
+
"build": "bun run build:lib && bun run build:css && bun run check:exports && bun run check:dist-imports && bun run check:css-injection && bun run check:rsc-boundaries",
|
|
82
83
|
"build:lib": "vite build && node scripts/preserve-client-directives.mjs",
|
|
83
|
-
"build:css": "sass src/scss/foundations:dist/foundations --no-source-map --style=compressed && node scripts/fix-css-module-artifacts.mjs && node scripts/bundle-foundations.mjs && node scripts/generate-vscode-css-vars.mjs",
|
|
84
|
+
"build:css": "sass src/scss/foundations:dist/foundations --no-source-map --style=compressed && node scripts/fix-css-module-artifacts.mjs && node scripts/bundle-foundations.mjs && node scripts/generate-vscode-css-vars.mjs && node scripts/generate-token-reference.mjs",
|
|
85
|
+
"docs:tokens": "node scripts/generate-token-reference.mjs",
|
|
86
|
+
"docs:tokens:check": "node scripts/generate-token-reference.mjs --check",
|
|
84
87
|
"check:exports": "node scripts/check-package-exports.mjs",
|
|
85
88
|
"check:dist-imports": "node scripts/check-dist-imports.mjs",
|
|
89
|
+
"check:css-injection": "node scripts/check-css-injection.mjs",
|
|
86
90
|
"check:rsc-boundaries": "node scripts/check-rsc-client-boundaries.mjs",
|
|
87
91
|
"test": "vitest",
|
|
88
|
-
"test:run": "vitest run",
|
|
92
|
+
"test:run": "vitest run && node --test scripts/*.test.mjs",
|
|
93
|
+
"test:scripts": "node --test scripts/*.test.mjs",
|
|
89
94
|
"test:coverage": "vitest run --coverage",
|
|
90
95
|
"test:e2e": "bun run --cwd ../.. build && bun run --cwd ../.. example:build && playwright test",
|
|
91
96
|
"test:e2e:ci": "bun run --cwd ../.. example:build && playwright test",
|
|
@@ -30,11 +30,37 @@ describe('color theme foundations', () => {
|
|
|
30
30
|
`);
|
|
31
31
|
|
|
32
32
|
expect(css).toMatch(
|
|
33
|
-
/:root\{[^}]*--oc-color-theme-background-normal-normal:var\(--oc-color-global-common-100\)/,
|
|
33
|
+
/:root\{color-scheme:light;[^}]*--oc-color-theme-background-normal-normal:var\(--oc-color-global-common-100\)/,
|
|
34
|
+
);
|
|
35
|
+
expect(css).toContain('[data-theme=light]{color-scheme:light;');
|
|
36
|
+
expect(css).toContain(
|
|
37
|
+
'@media(prefers-color-scheme:dark){:root:where(:not([data-theme=light])){color-scheme:dark;',
|
|
38
|
+
);
|
|
39
|
+
expect(css).toContain('[data-theme=dark]{color-scheme:dark;');
|
|
40
|
+
|
|
41
|
+
const lightAttributeIndex = css.indexOf('[data-theme=light]');
|
|
42
|
+
const systemPreferenceIndex = css.indexOf(
|
|
43
|
+
'@media(prefers-color-scheme:dark)',
|
|
44
|
+
);
|
|
45
|
+
const darkAttributeIndex = css.indexOf('[data-theme=dark]');
|
|
46
|
+
|
|
47
|
+
expect(lightAttributeIndex).toBeLessThan(systemPreferenceIndex);
|
|
48
|
+
expect(systemPreferenceIndex).toBeLessThan(darkAttributeIndex);
|
|
49
|
+
expect(css).not.toContain('@media(prefers-color-scheme:dark){:root{');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('aliases the focus ring color to the primary semantic in both themes', () => {
|
|
53
|
+
const css = compileScss(`
|
|
54
|
+
@use 'foundations/color.theme.light' as light;
|
|
55
|
+
@use 'foundations/color.theme.dark' as dark;
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
expect(css).toMatch(
|
|
59
|
+
/:root\{[^}]*--oc-color-theme-focus-ring:var\(--oc-color-theme-primary-normal\)/,
|
|
60
|
+
);
|
|
61
|
+
expect(css).toMatch(
|
|
62
|
+
/\[data-theme=dark\]\{[^}]*--oc-color-theme-focus-ring:var\(--oc-color-theme-primary-normal\)/,
|
|
34
63
|
);
|
|
35
|
-
expect(css).toContain('[data-theme=light]');
|
|
36
|
-
expect(css).toContain('@media(prefers-color-scheme:dark)');
|
|
37
|
-
expect(css).toContain('[data-theme=dark]');
|
|
38
64
|
});
|
|
39
65
|
|
|
40
66
|
it('emits only light when $oc-color-themes is (light)', () => {
|
|
@@ -44,7 +70,8 @@ describe('color theme foundations', () => {
|
|
|
44
70
|
@use 'foundations/color.theme.dark' as dark;
|
|
45
71
|
`);
|
|
46
72
|
|
|
47
|
-
expect(css).toContain('
|
|
73
|
+
expect(css).toContain(':root{color-scheme:light;');
|
|
74
|
+
expect(css).toContain('[data-theme=light]{color-scheme:light;');
|
|
48
75
|
expect(css).not.toContain('@media(prefers-color-scheme:dark)');
|
|
49
76
|
expect(css).not.toContain('[data-theme=dark]');
|
|
50
77
|
expect(css).not.toContain(
|
|
@@ -61,7 +88,8 @@ describe('color theme foundations', () => {
|
|
|
61
88
|
|
|
62
89
|
expect(css).not.toContain('@media(prefers-color-scheme:dark)');
|
|
63
90
|
expect(css).not.toContain('[data-theme=light]');
|
|
64
|
-
expect(css).toContain('
|
|
91
|
+
expect(css).toContain(':root{color-scheme:dark;');
|
|
92
|
+
expect(css).toContain('[data-theme=dark]{color-scheme:dark;');
|
|
65
93
|
expect(css).toMatch(
|
|
66
94
|
/:root\{[^}]*--oc-color-theme-background-normal-normal:var\(--oc-color-global-coolNeutral-15\)/,
|
|
67
95
|
);
|
|
@@ -75,8 +103,9 @@ describe('color theme foundations', () => {
|
|
|
75
103
|
`);
|
|
76
104
|
|
|
77
105
|
expect(css).not.toContain('@media(prefers-color-scheme:dark)');
|
|
78
|
-
expect(css).toContain('
|
|
79
|
-
expect(css).toContain('[data-theme=
|
|
106
|
+
expect(css).toContain(':root{color-scheme:light;');
|
|
107
|
+
expect(css).toContain('[data-theme=light]{color-scheme:light;');
|
|
108
|
+
expect(css).toContain('[data-theme=dark]{color-scheme:dark;');
|
|
80
109
|
});
|
|
81
110
|
|
|
82
111
|
it('throws when $oc-color-themes is empty', () => {
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
@use '../mixins/color' as *;
|
|
4
4
|
|
|
5
5
|
@mixin color-theme-dark {
|
|
6
|
+
color-scheme: dark;
|
|
7
|
+
|
|
6
8
|
--oc-color-theme-static-white: var(--oc-color-global-common-100);
|
|
7
9
|
--oc-color-theme-static-black: var(--oc-color-global-common-0);
|
|
8
10
|
--oc-color-theme-static-transparent: var(--oc-color-global-common-transparent);
|
|
9
11
|
--oc-color-theme-primary-normal: var(--oc-color-global-redOrange-60);
|
|
12
|
+
--oc-color-theme-focus-ring: var(--oc-color-theme-primary-normal);
|
|
10
13
|
--oc-color-theme-label-strong: var(--oc-color-global-common-100);
|
|
11
14
|
--oc-color-theme-label-normal: var(--oc-color-global-coolNeutral-99);
|
|
12
15
|
--oc-color-theme-label-neutral: #{oc-alpha(coolNeutral-90, 88)};
|
|
@@ -67,7 +70,7 @@
|
|
|
67
70
|
@include color-theme-dark;
|
|
68
71
|
}
|
|
69
72
|
} @else if config.$oc-color-system-preference {
|
|
70
|
-
:root {
|
|
73
|
+
:root:where(:not([data-theme='light'])) {
|
|
71
74
|
@media (prefers-color-scheme: dark) {
|
|
72
75
|
@include color-theme-dark;
|
|
73
76
|
}
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
@use '../mixins/color' as *;
|
|
4
4
|
|
|
5
5
|
@mixin color-theme-light {
|
|
6
|
+
color-scheme: light;
|
|
7
|
+
|
|
6
8
|
--oc-color-theme-static-white: var(--oc-color-global-common-100);
|
|
7
9
|
--oc-color-theme-static-black: var(--oc-color-global-common-0);
|
|
8
10
|
--oc-color-theme-static-transparent: var(--oc-color-global-common-transparent);
|
|
9
11
|
--oc-color-theme-primary-normal: var(--oc-color-global-redOrange-50);
|
|
12
|
+
--oc-color-theme-focus-ring: var(--oc-color-theme-primary-normal);
|
|
10
13
|
--oc-color-theme-label-strong: var(--oc-color-global-common-0);
|
|
11
14
|
--oc-color-theme-label-normal: var(--oc-color-global-coolNeutral-10);
|
|
12
15
|
--oc-color-theme-label-neutral: #{oc-alpha(coolNeutral-22, 88)};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { compileString } from 'sass';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('focus foundation', () => {
|
|
5
|
+
const compileFocus = () =>
|
|
6
|
+
compileString("@use 'foundations/focus' as *;", {
|
|
7
|
+
loadPaths: ['src/scss'],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('exposes the focus ring size tokens', () => {
|
|
11
|
+
const result = compileFocus();
|
|
12
|
+
|
|
13
|
+
expect(result.css).toContain('--oc-focus-ring-width: 2px;');
|
|
14
|
+
expect(result.css).toContain('--oc-focus-ring-offset: 2px;');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Protects the documented Full Foundation Rebuild composition
|
|
18
|
+
// (docs/responsive-foundation-profile.md): replacing foundations.css with
|
|
19
|
+
// the SCSS-source imports must still emit the focus ring tokens, or the
|
|
20
|
+
// reset-layer outline declarations become invalid at computed-value time.
|
|
21
|
+
it('keeps focus tokens in the full foundation rebuild composition', () => {
|
|
22
|
+
const result = compileString(
|
|
23
|
+
`
|
|
24
|
+
@use 'foundations/responsive.config' with (
|
|
25
|
+
$oc-responsive-mode: static,
|
|
26
|
+
$oc-responsive-static-stop: md
|
|
27
|
+
);
|
|
28
|
+
@use 'foundations/color.global' as *;
|
|
29
|
+
@use 'foundations/color.theme.light' as *;
|
|
30
|
+
@use 'foundations/color.theme.dark' as *;
|
|
31
|
+
@use 'foundations/breakpoint' as *;
|
|
32
|
+
@use 'foundations/typo' as *;
|
|
33
|
+
@use 'foundations/space' as *;
|
|
34
|
+
@use 'foundations/atomic' as *;
|
|
35
|
+
@use 'foundations/focus' as *;
|
|
36
|
+
@use 'foundations/motion' as *;
|
|
37
|
+
@use 'foundations/zindex' as *;
|
|
38
|
+
`,
|
|
39
|
+
{ loadPaths: ['src/scss'] },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result.css).toContain('--oc-focus-ring-width: 2px;');
|
|
43
|
+
expect(result.css).toContain('--oc-focus-ring-offset: 2px;');
|
|
44
|
+
expect(result.css).toContain(
|
|
45
|
+
'--oc-color-theme-focus-ring: var(--oc-color-theme-primary-normal);',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@use '../mixins/focus' as *;
|
|
2
|
+
|
|
1
3
|
/* Box sizing rules */
|
|
2
4
|
*,
|
|
3
5
|
*::before,
|
|
@@ -106,10 +108,13 @@ button {
|
|
|
106
108
|
appearance: none;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
/* Centralized keyboard focus ring. :where() keeps specificity at 0 so any
|
|
112
|
+
component-level outline declaration overrides it. Text-entry controls
|
|
113
|
+
(input/textarea/select) are excluded: they match :focus-visible on pointer
|
|
114
|
+
focus per spec, and the Field family owns focus presentation via its
|
|
115
|
+
:focus-within stroke. */
|
|
116
|
+
:where(button, a[href], summary, [tabindex]):where(:focus-visible) {
|
|
117
|
+
@include oc-focus-ring;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
textarea,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { compileString } from 'sass';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('reset foundation', () => {
|
|
5
|
+
const compileReset = () =>
|
|
6
|
+
compileString("@use 'foundations/reset';", {
|
|
7
|
+
loadPaths: ['src/scss'],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('provides the centralized keyboard focus ring for focusable elements', () => {
|
|
11
|
+
const result = compileReset();
|
|
12
|
+
|
|
13
|
+
// Both halves stay inside :where() so the whole selector is specificity 0
|
|
14
|
+
// and any local outline declaration wins.
|
|
15
|
+
expect(result.css).toContain(
|
|
16
|
+
':where(button, a[href], summary, [tabindex]):where(:focus-visible)',
|
|
17
|
+
);
|
|
18
|
+
expect(result.css).toContain(
|
|
19
|
+
'outline: var(--oc-focus-ring-width) solid var(--oc-color-theme-focus-ring);',
|
|
20
|
+
);
|
|
21
|
+
expect(result.css).toContain(
|
|
22
|
+
'outline-offset: var(--oc-focus-ring-offset);',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('no longer suppresses the native button focus outline', () => {
|
|
27
|
+
const result = compileReset();
|
|
28
|
+
const normalized = result.css.replace(/\s+/g, '');
|
|
29
|
+
|
|
30
|
+
expect(normalized).not.toContain('button:focus-visible{outline:none');
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/scss/index.scss
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
@forward './foundations/typo';
|
|
6
6
|
@forward './foundations/space';
|
|
7
7
|
@forward './foundations/atomic';
|
|
8
|
+
@forward './foundations/focus';
|
|
8
9
|
@forward './foundations/motion';
|
|
9
10
|
@forward './foundations/zindex';
|
|
10
11
|
@forward './foundations/reset';
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
@forward './mixins/color';
|
|
15
16
|
@forward './mixins/typo';
|
|
16
17
|
@forward './mixins/element';
|
|
18
|
+
@forward './mixins/focus';
|
|
17
19
|
@forward './mixins/radius';
|
|
18
20
|
@forward './mixins/motion';
|
|
19
21
|
@forward './spring';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard focus ring contract. The ring is drawn with `outline` so it
|
|
3
|
+
* follows the host element's own border-radius, stays out of layout, and
|
|
4
|
+
* survives forced-colors mode. The Interaction primitive keeps owning the
|
|
5
|
+
* surface tint; it cannot carry the ring because its element opacity (the
|
|
6
|
+
* tint mechanism) would multiply into anything painted in its subtree.
|
|
7
|
+
*/
|
|
8
|
+
@mixin oc-focus-ring($color: var(--oc-color-theme-focus-ring), $width: var(--oc-focus-ring-width), $offset: var(--oc-focus-ring-offset)) {
|
|
9
|
+
outline: $width solid $color;
|
|
10
|
+
outline-offset: $offset;
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { compileString } from 'sass';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('focus utilities', () => {
|
|
5
|
+
const compileFocus = (scss: string) =>
|
|
6
|
+
compileString(
|
|
7
|
+
`
|
|
8
|
+
@use 'mixins/focus' as *;
|
|
9
|
+
|
|
10
|
+
${scss}
|
|
11
|
+
`,
|
|
12
|
+
{
|
|
13
|
+
loadPaths: ['src/scss'],
|
|
14
|
+
},
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
it('emits the token-based focus ring by default', () => {
|
|
18
|
+
const result = compileFocus(`
|
|
19
|
+
.target {
|
|
20
|
+
&:focus-visible {
|
|
21
|
+
@include oc-focus-ring;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
expect(result.css).toContain(
|
|
27
|
+
'outline: var(--oc-focus-ring-width) solid var(--oc-color-theme-focus-ring);',
|
|
28
|
+
);
|
|
29
|
+
expect(result.css).toContain(
|
|
30
|
+
'outline-offset: var(--oc-focus-ring-offset);',
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('accepts a custom color', () => {
|
|
35
|
+
const result = compileFocus(`
|
|
36
|
+
.target {
|
|
37
|
+
@include oc-focus-ring($color: var(--custom-ring-color));
|
|
38
|
+
}
|
|
39
|
+
`);
|
|
40
|
+
|
|
41
|
+
expect(result.css).toContain(
|
|
42
|
+
'outline: var(--oc-focus-ring-width) solid var(--custom-ring-color);',
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('accepts custom width and offset', () => {
|
|
47
|
+
const result = compileFocus(`
|
|
48
|
+
.target {
|
|
49
|
+
@include oc-focus-ring($width: 3px, $offset: 1px);
|
|
50
|
+
}
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
expect(result.css).toContain(
|
|
54
|
+
'outline: 3px solid var(--oc-color-theme-focus-ring);',
|
|
55
|
+
);
|
|
56
|
+
expect(result.css).toContain('outline-offset: 1px;');
|
|
57
|
+
});
|
|
58
|
+
});
|