@recoui/style 0.0.0-stage → 0.1.0-alpha.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.
@@ -0,0 +1,154 @@
1
+ @layer components.ui {
2
+ // 数字与日期时间控件保留原生编辑、步进和选择器行为。
3
+ :is(input.ui-input, .ui-input > input):is(
4
+ [type="number"],
5
+ [type="date"],
6
+ [type="time"],
7
+ [type="datetime-local"],
8
+ [type="month"],
9
+ [type="week"]
10
+ ) {
11
+ appearance: auto;
12
+ font-variant-numeric: tabular-nums;
13
+ }
14
+
15
+ :is(input.ui-input, .ui-input > input)::-webkit-datetime-edit {
16
+ padding-block: 0;
17
+ }
18
+
19
+ // 文件按钮沿用 Button 的默认背景、内边距和悬停色,外边框由输入框提供。
20
+ .ui-input[type="file"] {
21
+ display: block;
22
+ padding: 0;
23
+ overflow: hidden;
24
+ cursor: pointer;
25
+ }
26
+
27
+ .ui-input:has(> input[type="file"]) {
28
+ padding-inline: 0;
29
+ overflow: hidden;
30
+ }
31
+
32
+ .ui-input > input[type="file"] {
33
+ padding: 0;
34
+ cursor: pointer;
35
+ }
36
+
37
+ :is(input.ui-input, .ui-input > input)[type="file"] {
38
+ &::file-selector-button {
39
+ box-sizing: border-box;
40
+ min-block-size: calc(var(--ui-height) - 2px);
41
+ margin-inline-end: 0.625rem;
42
+ padding-block: 0;
43
+ padding-inline: 1rem;
44
+ border: 0;
45
+ border-inline-end: 1px solid var(--color-border);
46
+ background: var(--color-background);
47
+ color: inherit;
48
+ font: inherit;
49
+ cursor: inherit;
50
+ transition: background-color var(--transition);
51
+ }
52
+
53
+ &:is(:disabled, [aria-disabled="true"])::file-selector-button {
54
+ background: var(--color-input-fill);
55
+ }
56
+ }
57
+
58
+ input.ui-input[type="file"]:not(
59
+ :disabled,
60
+ [aria-disabled="true"],
61
+ [data-variant="filled"],
62
+ [data-variant="unstyled"]
63
+ )::file-selector-button:hover,
64
+ .ui-input:not([aria-disabled="true"], [data-variant="filled"], [data-variant="unstyled"])
65
+ > input[type="file"]:not(:disabled, [aria-disabled="true"])::file-selector-button:hover {
66
+ background: color-mix(
67
+ in oklch,
68
+ oklch(from var(--color-foreground) calc(l * 0.1 + 0.9) calc(c * 0.2) h) 20%,
69
+ var(--color-background)
70
+ );
71
+ }
72
+
73
+ // 融合外观不保留按钮底色和分隔线,悬停时也保持输入框的整体背景。
74
+ input.ui-input[type="file"]:is(
75
+ [data-variant="filled"],
76
+ [data-variant="unstyled"]
77
+ )::file-selector-button,
78
+ .ui-input:is([data-variant="filled"], [data-variant="unstyled"])
79
+ > input[type="file"]::file-selector-button {
80
+ border-inline-end: 0;
81
+ background: transparent;
82
+ }
83
+
84
+ input.ui-input[type="file"][data-variant="unstyled"]::file-selector-button,
85
+ .ui-input[data-variant="unstyled"] > input[type="file"]::file-selector-button {
86
+ min-block-size: var(--ui-height);
87
+ padding-inline-start: 0;
88
+ }
89
+
90
+ .ui-input:is([type="color"], :has(> input[type="color"])) {
91
+ inline-size: 4rem;
92
+ padding: 0.25rem;
93
+ cursor: pointer;
94
+ }
95
+
96
+ .ui-input > input[type="color"] {
97
+ padding: 0;
98
+ }
99
+
100
+ :is(input.ui-input, .ui-input > input)[type="color"] {
101
+ &::-webkit-color-swatch-wrapper {
102
+ padding: 0;
103
+ }
104
+
105
+ &::-webkit-color-swatch {
106
+ border: 0;
107
+ border-radius: var(--radius-sm);
108
+ }
109
+
110
+ &::-moz-color-swatch {
111
+ border: 0;
112
+ border-radius: var(--radius-sm);
113
+ }
114
+ }
115
+
116
+ .ui-input:is([type="color"], :has(> input[type="color"])):is(
117
+ :disabled,
118
+ [aria-disabled="true"],
119
+ :has(> input:is(:disabled, [aria-disabled="true"]))
120
+ ) {
121
+ --ui-display-color: var(--color-disabled);
122
+ opacity: 0.6;
123
+ }
124
+
125
+ .ui-input:is(
126
+ :disabled,
127
+ [aria-disabled="true"],
128
+ :has(> input:is(:disabled, [aria-disabled="true"]))
129
+ ),
130
+ .ui-input > input:is(:disabled, [aria-disabled="true"]) {
131
+ cursor: not-allowed;
132
+ }
133
+
134
+ .ui-input[type="image"] {
135
+ inline-size: auto;
136
+ max-inline-size: 100%;
137
+ min-block-size: 0;
138
+ padding: 0;
139
+ border: 0;
140
+ background: transparent;
141
+ cursor: pointer;
142
+ }
143
+
144
+ .ui-input[type="hidden"] {
145
+ display: none;
146
+ }
147
+
148
+ // 列表选择器使用原生列表布局,不应用单选下拉框的 base-select 外观。
149
+ select.ui-input:is([multiple], [size]) {
150
+ appearance: auto;
151
+ display: block;
152
+ padding-block: 0.25rem;
153
+ }
154
+ }
@@ -0,0 +1,99 @@
1
+ @layer components.ui {
2
+ %ui-option {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 0.5rem;
6
+ box-sizing: border-box;
7
+ min-inline-size: 0;
8
+ margin: 0;
9
+ padding: 0.375rem 0.5rem;
10
+ border: 0;
11
+ border-radius: max(0px, calc(var(--ui-radius, var(--radius)) - 0.125rem));
12
+ outline: 0 solid transparent;
13
+ outline-offset: -2px;
14
+ background: transparent;
15
+ color: var(--color-foreground);
16
+ font-family: inherit;
17
+ font-size: inherit;
18
+ font-weight: 400;
19
+ line-height: 1.5;
20
+ text-align: start;
21
+ text-decoration: none;
22
+ white-space: normal;
23
+ overflow-wrap: anywhere;
24
+ cursor: pointer;
25
+
26
+ &:is(:hover, :focus-visible, [data-highlighted="true"]):not(:disabled, [aria-disabled="true"]) {
27
+ background: var(--color-background-hover, var(--color-input-fill));
28
+ }
29
+
30
+ &:is(:checked, [aria-selected="true"], [aria-checked="true"]) {
31
+ background: var(--color-input-fill);
32
+ }
33
+
34
+ &:focus-visible {
35
+ outline-width: 2px;
36
+ outline-color: var(--ui-color, var(--color-primary));
37
+ }
38
+ }
39
+
40
+ %ui-option-disabled {
41
+ background: transparent;
42
+ color: var(--color-disabled);
43
+ cursor: not-allowed;
44
+ }
45
+
46
+ %ui-optgroup {
47
+ display: block;
48
+ box-sizing: border-box;
49
+ min-inline-size: 0;
50
+ margin: 0;
51
+ padding: 0.25rem 0;
52
+ border: 0;
53
+ font-family: inherit;
54
+ font-size: inherit;
55
+ font-weight: 600;
56
+ line-height: 1.5;
57
+
58
+ &:is(:disabled, [aria-disabled="true"]) {
59
+ color: var(--color-disabled);
60
+ }
61
+ }
62
+
63
+ %ui-optgroup-legend {
64
+ display: block;
65
+ box-sizing: border-box;
66
+ inline-size: 100%;
67
+ margin: 0;
68
+ padding: 0.25rem 0.5rem;
69
+ font-family: inherit;
70
+ font-size: inherit;
71
+ font-weight: 600;
72
+ line-height: 1.5;
73
+ overflow-wrap: anywhere;
74
+ }
75
+
76
+ .ui-option,
77
+ select.ui-input option {
78
+ @extend %ui-option;
79
+
80
+ &:is(:disabled, [aria-disabled="true"]) {
81
+ @extend %ui-option-disabled;
82
+ }
83
+ }
84
+
85
+ .ui-optgroup,
86
+ select.ui-input optgroup {
87
+ @extend %ui-optgroup;
88
+ }
89
+
90
+ .ui-optgroup-legend,
91
+ select.ui-input legend {
92
+ @extend %ui-optgroup-legend;
93
+ }
94
+
95
+ .ui-optgroup:is(:disabled, [aria-disabled="true"]) .ui-option,
96
+ select.ui-input optgroup:is(:disabled, [aria-disabled="true"]) option {
97
+ @extend %ui-option-disabled;
98
+ }
99
+ }
@@ -0,0 +1,218 @@
1
+ %slider-track {
2
+ block-size: 0.375rem;
3
+ border-radius: 9999px;
4
+ background: var(--color-input-fill);
5
+ }
6
+
7
+ // 跨引擎原生伪元素必须保留独立选择器,避免 @extend 将它们合并后失效。
8
+ @mixin slider-native-track {
9
+ block-size: 0.375rem;
10
+ border-radius: 9999px;
11
+ background: linear-gradient(
12
+ var(--ui-slider-direction),
13
+ var(--ui-slider-color) 0 var(--ui-slider-progress),
14
+ var(--color-input-fill) var(--ui-slider-progress) 100%
15
+ );
16
+ }
17
+
18
+ @mixin slider-thumb {
19
+ box-sizing: border-box;
20
+ inline-size: var(--ui-thumb-width, 1rem);
21
+ block-size: var(--ui-thumb-width, 1rem);
22
+ padding: 0;
23
+ border: 1px solid var(--color-border);
24
+ border-radius: 50%;
25
+ outline: 0 solid transparent;
26
+ outline-offset: 0;
27
+ background: var(--color-background);
28
+ transition:
29
+ outline var(--transition),
30
+ border var(--transition);
31
+ }
32
+
33
+ %slider-thumb {
34
+ @include slider-thumb;
35
+ }
36
+
37
+ @mixin slider-thumb-focus {
38
+ border-color: var(--ui-slider-color);
39
+ outline-width: 4px;
40
+ outline-color: oklch(from var(--ui-slider-color) l c h / 20%);
41
+ }
42
+
43
+ %slider-thumb-focus {
44
+ @include slider-thumb-focus;
45
+ }
46
+
47
+ @layer components.ui {
48
+ .ui-slider {
49
+ --ui-color: var(--color-primary);
50
+ --ui-slider-color: var(--ui-color);
51
+ --ui-slider-progress: clamp(0%, var(--ui-slider-position, 0%), 100%);
52
+
53
+ position: relative;
54
+ display: flex;
55
+ align-items: center;
56
+ box-sizing: border-box;
57
+ inline-size: 100%;
58
+ min-inline-size: var(--ui-thumb-width, 1rem);
59
+ block-size: max(var(--height-input, 2rem), var(--ui-thumb-width, 1rem));
60
+ padding: 0;
61
+ border: 0;
62
+ background: transparent;
63
+ cursor: pointer;
64
+ user-select: none;
65
+
66
+ &:is(:disabled, [aria-disabled="true"], :has(> input:is(:disabled, [aria-disabled="true"]))) {
67
+ --ui-slider-color: var(--color-disabled);
68
+ opacity: 0.6;
69
+ cursor: not-allowed;
70
+ }
71
+ }
72
+
73
+ .ui-slider > .ui-slider-track {
74
+ @extend %slider-track;
75
+ position: relative;
76
+ flex: 1 1 auto;
77
+ min-inline-size: 0;
78
+ // margin-inline: calc(var(--ui-thumb-width, 1rem) / 2);
79
+ overflow: hidden;
80
+ pointer-events: none;
81
+ }
82
+
83
+ .ui-slider-range {
84
+ inline-size: var(--ui-slider-progress);
85
+ block-size: 100%;
86
+ border-radius: inherit;
87
+ background: var(--ui-slider-color);
88
+ }
89
+
90
+ .ui-slider-thumb {
91
+ @extend %slider-thumb;
92
+ position: absolute;
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ inset-block-start: 50%;
97
+ inset-inline-start: var(--ui-slider-progress);
98
+ transform: translateX(calc(-1 * var(--ui-slider-progress))) translateY(-50%);
99
+ pointer-events: auto;
100
+ }
101
+
102
+ // 原生 range 位于 thumb 上方,将其悬停反馈映射到可见 thumb,并保留拖动能力。
103
+ .ui-slider:not([aria-disabled="true"], :has(> input:is(:disabled, [aria-disabled="true"]))) {
104
+ > .ui-slider-thumb:hover,
105
+ &:has(> input[type="range"]:hover) > .ui-slider-thumb {
106
+ border-color: var(--ui-slider-color);
107
+ }
108
+ }
109
+
110
+ // 位移百分比相对于 thumb 自身,保证不同宽度下两端都留在容器内。
111
+ .ui-slider:dir(rtl) > .ui-slider-thumb {
112
+ transform: translateX(var(--ui-slider-progress)) translateY(-50%);
113
+ }
114
+
115
+ .ui-slider:focus-visible > .ui-slider-thumb,
116
+ .ui-slider:has(> input:focus-visible) > .ui-slider-thumb {
117
+ @extend %slider-thumb-focus;
118
+ }
119
+
120
+ // 透明覆盖层保留原生 range 的拖动、键盘、表单提交及 disabled 行为。
121
+ .ui-slider > input[type="range"] {
122
+ position: absolute;
123
+ z-index: 1;
124
+ inset: 0;
125
+ inline-size: 100%;
126
+ block-size: 100%;
127
+ min-block-size: 0;
128
+ margin: 0;
129
+ padding: 0;
130
+ border: 0;
131
+ opacity: 0;
132
+ cursor: inherit;
133
+ }
134
+
135
+ // 隐藏的原生 thumb 与可见 thumb 使用相同宽度,使指针和值的位置一致。
136
+ .ui-slider > input[type="range"]::-webkit-slider-runnable-track {
137
+ block-size: 0.375rem;
138
+ }
139
+
140
+ .ui-slider > input[type="range"]::-webkit-slider-thumb {
141
+ appearance: none;
142
+ inline-size: var(--ui-thumb-width, 1rem);
143
+ block-size: var(--ui-thumb-width, 1rem);
144
+ margin-block-start: calc((0.375rem - var(--ui-thumb-width, 1rem)) / 2);
145
+ }
146
+
147
+ .ui-slider > input[type="range"]::-moz-range-thumb {
148
+ inline-size: var(--ui-thumb-width, 1rem);
149
+ block-size: var(--ui-thumb-width, 1rem);
150
+ border: 0;
151
+ }
152
+
153
+ .ui-slider > input:not([type="range"]) {
154
+ display: none;
155
+ }
156
+
157
+ // 原生 range 同样使用 .ui-slider;与组合式滑块共用位置、thumb 尺寸和颜色变量。
158
+ input.ui-slider[type="range"] {
159
+ --ui-slider-direction: to right;
160
+ display: block;
161
+ appearance: none;
162
+ outline: none;
163
+
164
+ &:dir(rtl) {
165
+ --ui-slider-direction: to left;
166
+ }
167
+
168
+ // 各引擎的伪元素分别声明,避免不支持的选择器使整条规则失效。
169
+ &::-webkit-slider-runnable-track {
170
+ @include slider-native-track;
171
+ }
172
+
173
+ &::-moz-range-track {
174
+ @include slider-native-track;
175
+ }
176
+
177
+ &::-webkit-slider-thumb {
178
+ @include slider-thumb;
179
+ }
180
+
181
+ &::-moz-range-thumb {
182
+ @include slider-thumb;
183
+ }
184
+
185
+ &::-webkit-slider-thumb {
186
+ appearance: none;
187
+ margin-block-start: calc((0.375rem - var(--ui-thumb-width, 1rem)) / 2);
188
+ }
189
+
190
+ &:hover:not(:disabled, [aria-disabled="true"])::-webkit-slider-thumb {
191
+ border-color: var(--ui-slider-color);
192
+ }
193
+
194
+ &:hover:not(:disabled, [aria-disabled="true"])::-moz-range-thumb {
195
+ border-color: var(--ui-slider-color);
196
+ }
197
+
198
+ &:focus-visible::-webkit-slider-thumb {
199
+ @include slider-thumb-focus;
200
+ }
201
+
202
+ &:focus-visible::-moz-range-thumb {
203
+ @include slider-thumb-focus;
204
+ }
205
+ }
206
+
207
+ @media (prefers-reduced-motion: reduce) {
208
+ .ui-slider > .ui-slider-thumb {
209
+ transition: none;
210
+ }
211
+ input.ui-slider[type="range"]::-webkit-slider-thumb {
212
+ transition: none;
213
+ }
214
+ input.ui-slider[type="range"]::-moz-range-thumb {
215
+ transition: none;
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,111 @@
1
+ @layer components.ui {
2
+ .ui-switch {
3
+ --ui-color: var(--color-primary);
4
+ --ui-switch-width: 2.25rem;
5
+ --ui-switch-height: 1.25rem;
6
+ --ui-switch-padding: 2px;
7
+ --ui-switch-border: 1px;
8
+ --ui-switch-thumb-size: calc(
9
+ var(--ui-switch-height) - 2 * var(--ui-switch-border) - 2 * var(--ui-switch-padding)
10
+ );
11
+ --ui-switch-thumb-start: var(--ui-switch-padding);
12
+
13
+ position: relative;
14
+ display: inline-block;
15
+ box-sizing: border-box;
16
+ flex: 0 0 auto;
17
+ inline-size: var(--ui-switch-width);
18
+ block-size: var(--ui-switch-height);
19
+ margin: 0;
20
+ padding: var(--ui-switch-padding);
21
+ appearance: none;
22
+ vertical-align: middle;
23
+ border: var(--ui-switch-border) solid var(--color-border);
24
+ border-radius: 9999px;
25
+ outline: 0 solid transparent;
26
+ outline-offset: 0;
27
+ background: var(--color-border);
28
+ color: var(--color-foreground);
29
+ font: inherit;
30
+ cursor: pointer;
31
+ user-select: none;
32
+ transition:
33
+ background-color var(--transition),
34
+ border-color var(--transition),
35
+ box-shadow var(--transition),
36
+ outline var(--transition);
37
+
38
+ // 没有 input 时使用元素自身的状态;有 input 时以原生 checked 状态为准。
39
+ &:is(:checked, [aria-checked="true"], [aria-pressed="true"]):not(:has(input)),
40
+ &:has(input:checked) {
41
+ --ui-switch-thumb-start: calc(100% - var(--ui-switch-thumb-size) - var(--ui-switch-padding));
42
+ border-color: var(--ui-color);
43
+ background: var(--ui-color);
44
+ }
45
+
46
+ &:focus-visible,
47
+ &:has(input:focus-visible) {
48
+ outline-width: 4px;
49
+ border-color: var(--ui-color);
50
+ outline-color: oklch(from var(--ui-color) l c h / 20%);
51
+ }
52
+
53
+ &:is(:disabled, [aria-disabled="true"], :has(input:is(:disabled, [aria-disabled="true"]))) {
54
+ --ui-color: var(--color-disabled);
55
+ opacity: 0.6;
56
+ cursor: not-allowed;
57
+ }
58
+ }
59
+
60
+ // 显式 thumb 与自动生成的 thumb 共用尺寸、位置和动画规则。
61
+ .ui-switch .ui-switch-thumb,
62
+ .ui-switch:not(:has(.ui-switch-thumb))::after {
63
+ position: absolute;
64
+ display: inline-flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ box-sizing: border-box;
68
+ inset-block-start: 50%;
69
+ inset-inline-start: var(--ui-switch-thumb-start);
70
+ inline-size: var(--ui-switch-thumb-size);
71
+ block-size: var(--ui-switch-thumb-size);
72
+ border: 0;
73
+ border-radius: 9999px;
74
+ background: var(--color-white, white);
75
+ // box-shadow: 0 1px 2px rgb(0 0 0 / 15%);
76
+ pointer-events: none;
77
+ transform: translateY(-50%);
78
+ transition: inset-inline-start var(--transition);
79
+ }
80
+
81
+ .ui-switch:not(:has(.ui-switch-thumb))::after {
82
+ content: "";
83
+ }
84
+
85
+ // 中间包裹元素不作为定位参照,嵌套 input 和 thumb 均以开关自身定位。
86
+ .ui-switch :has(:is(input, .ui-switch-thumb)):not(.ui-switch-thumb) {
87
+ position: static;
88
+ }
89
+
90
+ // 视觉隐藏但保留原生输入的点击区域、键盘聚焦、disabled 和表单行为。
91
+ .ui-switch input {
92
+ position: absolute;
93
+ z-index: 1;
94
+ inset: 0;
95
+ box-sizing: border-box;
96
+ inline-size: 100%;
97
+ block-size: 100%;
98
+ margin: 0;
99
+ padding: 0;
100
+ opacity: 0;
101
+ cursor: inherit;
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ .ui-switch,
106
+ .ui-switch .ui-switch-thumb,
107
+ .ui-switch::after {
108
+ transition: none;
109
+ }
110
+ }
111
+ }