@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,161 @@
1
+ @layer components.ui {
2
+ // tooltip 显示样式
3
+ %ui-tooltip {
4
+ z-index: var(--ui-tooltip-z-index, 1100);
5
+ box-sizing: border-box;
6
+ inline-size: max-content;
7
+ max-inline-size: min(var(--ui-tooltip-max-width, 20rem), calc(100vw - 2rem));
8
+ margin: 0;
9
+ padding: 0.375rem 0.5rem;
10
+ border: 0;
11
+ border-radius: var(--radius-sm);
12
+ background: var(--color-foreground);
13
+ color: var(--color-background);
14
+ box-shadow: var(--shadow-sm);
15
+ font-family: var(--font-family, inherit);
16
+ font-size: 0.75rem;
17
+ font-weight: 400;
18
+ line-height: 1.5;
19
+ letter-spacing: normal;
20
+ text-align: start;
21
+ text-decoration: none;
22
+ text-transform: none;
23
+ white-space: normal;
24
+ overflow-wrap: anywhere;
25
+ }
26
+
27
+ // Portal / 浮层定位库可以直接设置 top、left 或 transform。
28
+ .ui-tooltip {
29
+ @extend %ui-tooltip;
30
+ position: fixed;
31
+ display: block;
32
+ opacity: 1;
33
+ visibility: visible;
34
+ transition:
35
+ opacity var(--transition),
36
+ visibility var(--transition),
37
+ display var(--transition);
38
+ transition-behavior: allow-discrete;
39
+
40
+ &[hidden] {
41
+ display: none;
42
+ opacity: 0;
43
+ visibility: hidden;
44
+ pointer-events: none;
45
+ }
46
+ }
47
+
48
+ @starting-style {
49
+ .ui-tooltip {
50
+ opacity: 0;
51
+ }
52
+ }
53
+
54
+ .ui-tooltip-wrapper {
55
+ position: relative;
56
+ display: inline-flex;
57
+ align-items: center;
58
+ box-sizing: border-box;
59
+ inline-size: fit-content;
60
+ vertical-align: middle;
61
+
62
+ &::before,
63
+ &::after {
64
+ position: absolute;
65
+ opacity: 0;
66
+ visibility: hidden;
67
+ pointer-events: none;
68
+ transition:
69
+ opacity var(--transition),
70
+ visibility var(--transition);
71
+ }
72
+
73
+ &::after {
74
+ @extend %ui-tooltip;
75
+ content: attr(data-tooltip);
76
+ bottom: calc(100% + var(--ui-tooltip-offset, 0.5rem));
77
+ left: 50%;
78
+ transform: translateX(-50%);
79
+ }
80
+
81
+ // 透明区域连接目标和提示,鼠标跨过间隙时仍可保持显示。
82
+ &::before {
83
+ content: "";
84
+ z-index: var(--ui-tooltip-z-index, 1100);
85
+ bottom: 100%;
86
+ left: 0;
87
+ inline-size: 100%;
88
+ block-size: var(--ui-tooltip-offset, 0.5rem);
89
+ }
90
+
91
+ &[data-side="bottom"] {
92
+ &::after {
93
+ top: calc(100% + var(--ui-tooltip-offset, 0.5rem));
94
+ bottom: auto;
95
+ }
96
+ &::before {
97
+ top: 100%;
98
+ bottom: auto;
99
+ }
100
+ }
101
+
102
+ &:is([data-side="left"], [data-side="right"]) {
103
+ &::after {
104
+ top: 50%;
105
+ bottom: auto;
106
+ transform: translateY(-50%);
107
+ }
108
+ &::before {
109
+ top: 0;
110
+ bottom: auto;
111
+ inline-size: var(--ui-tooltip-offset, 0.5rem);
112
+ block-size: 100%;
113
+ }
114
+ }
115
+
116
+ &[data-side="left"] {
117
+ &::after {
118
+ right: calc(100% + var(--ui-tooltip-offset, 0.5rem));
119
+ left: auto;
120
+ }
121
+ &::before {
122
+ right: 100%;
123
+ left: auto;
124
+ }
125
+ }
126
+
127
+ &[data-side="right"] {
128
+ &::after {
129
+ left: calc(100% + var(--ui-tooltip-offset, 0.5rem));
130
+ }
131
+ &::before {
132
+ left: 100%;
133
+ }
134
+ }
135
+
136
+ &:not([data-state="closed"]):is(:hover, :focus-within, [data-state="open"]) {
137
+ &::before,
138
+ &::after {
139
+ opacity: 1;
140
+ visibility: visible;
141
+ pointer-events: auto;
142
+ }
143
+ }
144
+
145
+ &:not([data-tooltip]),
146
+ &[data-tooltip=""] {
147
+ &::before,
148
+ &::after {
149
+ content: none;
150
+ }
151
+ }
152
+ }
153
+
154
+ @media (prefers-reduced-motion: reduce) {
155
+ .ui-tooltip,
156
+ .ui-tooltip-wrapper::before,
157
+ .ui-tooltip-wrapper::after {
158
+ transition: none;
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,74 @@
1
+ @layer components.ui {
2
+ // 只组织字段结构,不限定控件类型,也不覆盖控件本身的尺寸和交互。
3
+ .ui-field {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: flex-start;
7
+ gap: 0.375rem;
8
+ box-sizing: border-box;
9
+ inline-size: 100%;
10
+ min-inline-size: 0;
11
+ margin: 0;
12
+ font-family: inherit;
13
+ font-size: var(--font-size-input, 0.875rem);
14
+ color: var(--color-foreground);
15
+ }
16
+
17
+ .ui-field[data-orientation="horizontal"] {
18
+ flex-direction: row;
19
+ gap: 0.5rem;
20
+
21
+ > :is(.ui-checkbox, .ui-radio, .ui-switch, input[type="checkbox"], input[type="radio"]) {
22
+ flex-shrink: 0;
23
+ }
24
+
25
+ > :is(.ui-checkbox, .ui-radio, .ui-switch) {
26
+ margin-block-start: max(
27
+ 0px,
28
+ calc((var(--font-size-input, 0.875rem) * 1.5 - var(--ui-switch-height, 1.25rem)) / 2)
29
+ );
30
+ }
31
+
32
+ > .ui-field-content {
33
+ flex: 1 1 auto;
34
+ }
35
+ }
36
+
37
+ .ui-field-content {
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: flex-start;
41
+ gap: 0.25rem;
42
+ box-sizing: border-box;
43
+ min-inline-size: 0;
44
+ }
45
+
46
+ :is(.ui-field, .ui-field > .ui-field-content) > .ui-field-label {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ gap: 0.25rem;
50
+ max-inline-size: 100%;
51
+ margin: 0;
52
+ font-family: inherit;
53
+ font-size: var(--font-size-input, 0.875rem);
54
+ font-weight: 500;
55
+ line-height: 1.5;
56
+ color: inherit;
57
+ overflow-wrap: anywhere;
58
+ }
59
+
60
+ :is(.ui-field, .ui-field > .ui-field-content) > .ui-field-description {
61
+ align-self: stretch;
62
+ min-inline-size: 0;
63
+ margin: 0;
64
+ font-family: inherit;
65
+ font-size: 0.75rem;
66
+ line-height: 1.5;
67
+ color: var(--color-disabled);
68
+ overflow-wrap: anywhere;
69
+
70
+ &:empty {
71
+ display: none;
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,131 @@
1
+ @layer components.ui {
2
+ :where(.ui-form, .ui-fieldset, .ui-legend, .ui-progress, .ui-meter) {
3
+ box-sizing: border-box;
4
+ font-family: inherit;
5
+ font-size: 0.875rem;
6
+ color: var(--color-foreground);
7
+ }
8
+
9
+ .ui-form {
10
+ display: grid;
11
+ gap: 1rem;
12
+ min-inline-size: 0;
13
+ margin: 0;
14
+ text-align: start;
15
+ }
16
+
17
+ .ui-fieldset {
18
+ min-inline-size: 0;
19
+ margin: 0;
20
+ padding: 1rem;
21
+ border: 1px solid var(--color-border);
22
+ border-radius: var(--radius);
23
+
24
+ &:disabled {
25
+ background: color-mix(in oklch, var(--color-foreground) 2%, var(--color-background));
26
+ }
27
+
28
+ &:disabled :is(.ui-label, .ui-legend) {
29
+ color: var(--color-disabled);
30
+ }
31
+ }
32
+
33
+ .ui-legend {
34
+ padding-inline: 0.375rem;
35
+ font-weight: 600;
36
+ line-height: 1.5;
37
+ }
38
+
39
+ .ui-datalist {
40
+ display: none;
41
+ }
42
+
43
+ .ui-progress,
44
+ .ui-meter {
45
+ --ui-color: var(--color-primary);
46
+ appearance: none;
47
+ display: block;
48
+ inline-size: 100%;
49
+ block-size: 0.5rem;
50
+ overflow: hidden;
51
+ border: 0;
52
+ border-radius: var(--radius);
53
+ background: var(--color-input-fill);
54
+ accent-color: var(--ui-color);
55
+ }
56
+
57
+ .ui-progress::-webkit-progress-bar {
58
+ border-radius: inherit;
59
+ background: var(--color-input-fill);
60
+ }
61
+
62
+ .ui-progress::-webkit-progress-value {
63
+ border-radius: inherit;
64
+ background: var(--ui-color);
65
+ }
66
+
67
+ .ui-progress::-moz-progress-bar {
68
+ border-radius: inherit;
69
+ background: var(--ui-color);
70
+ }
71
+
72
+ .ui-progress:indeterminate::-webkit-progress-bar {
73
+ background: var(--ui-color);
74
+ clip-path: inset(0 32.5% round var(--radius));
75
+ animation: ui-progress-indeterminate 1.4s linear infinite;
76
+ }
77
+
78
+ .ui-meter {
79
+ --ui-color: var(--color-success);
80
+ }
81
+
82
+ .ui-meter::-webkit-meter-bar {
83
+ block-size: 0.5rem;
84
+ border: 0;
85
+ border-radius: inherit;
86
+ background: var(--color-input-fill);
87
+ box-shadow: none;
88
+ }
89
+
90
+ .ui-meter::-webkit-meter-optimum-value {
91
+ border-radius: inherit;
92
+ background: var(--ui-color);
93
+ }
94
+
95
+ .ui-meter::-webkit-meter-suboptimum-value {
96
+ border-radius: inherit;
97
+ background: var(--color-warning);
98
+ }
99
+
100
+ .ui-meter::-webkit-meter-even-less-good-value {
101
+ border-radius: inherit;
102
+ background: var(--color-error);
103
+ }
104
+
105
+ .ui-meter:-moz-meter-optimum::-moz-meter-bar {
106
+ background: var(--ui-color);
107
+ }
108
+
109
+ .ui-meter:-moz-meter-sub-optimum::-moz-meter-bar {
110
+ background: var(--color-warning);
111
+ }
112
+
113
+ .ui-meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {
114
+ background: var(--color-error);
115
+ }
116
+
117
+ @keyframes ui-progress-indeterminate {
118
+ from {
119
+ clip-path: inset(0 100% 0 -35% round var(--radius));
120
+ }
121
+ to {
122
+ clip-path: inset(0 -35% 0 100% round var(--radius));
123
+ }
124
+ }
125
+
126
+ @media (prefers-reduced-motion: reduce) {
127
+ .ui-progress:indeterminate::-webkit-progress-bar {
128
+ animation: none;
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,2 @@
1
+ @use "./form.scss";
2
+ @use "./field.scss";
@@ -0,0 +1,15 @@
1
+ @layer components.ui {
2
+ .ui-group {
3
+ display: flex;
4
+ flex-direction: row;
5
+ align-items: center;
6
+ gap: 1rem;
7
+ box-sizing: border-box;
8
+ min-inline-size: 0;
9
+
10
+ &[data-orientation="vertical"] {
11
+ flex-direction: column;
12
+ align-items: flex-start;
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,2 @@
1
+ @use "./group.scss";
2
+ @use "./join.scss";
@@ -0,0 +1,64 @@
1
+ @layer components.ui {
2
+ .ui-join {
3
+ display: inline-flex;
4
+ align-items: stretch;
5
+ gap: 0;
6
+ box-sizing: border-box;
7
+ min-inline-size: 0;
8
+ max-inline-size: 100%;
9
+ vertical-align: middle;
10
+ isolation: isolate;
11
+
12
+ // 混合输入框时填满可用宽度,按钮保留内容宽度,输入区域占据剩余空间。
13
+ &:has(> .ui-input:not([type="hidden"])) {
14
+ inline-size: 100%;
15
+ }
16
+
17
+ > :is(.ui-button, .ui-input):not([type="hidden"]) {
18
+ position: relative;
19
+ z-index: 0;
20
+ flex: 0 0 auto;
21
+ min-inline-size: 0;
22
+
23
+ // 只计算实际控件,避免隐藏字段影响首尾圆角和边框拼接。
24
+ &:not(:nth-child(1 of :is(.ui-button, .ui-input):not([type="hidden"]))) {
25
+ margin-inline-start: -1px;
26
+ border-start-start-radius: 0;
27
+ border-end-start-radius: 0;
28
+ }
29
+
30
+ &:not(:nth-last-child(1 of :is(.ui-button, .ui-input):not([type="hidden"]))) {
31
+ border-start-end-radius: 0;
32
+ border-end-end-radius: 0;
33
+ }
34
+
35
+ &:is(
36
+ :hover,
37
+ :invalid,
38
+ [aria-invalid="true"],
39
+ [data-warning],
40
+ :has(> input:is(:invalid, [aria-invalid="true"], [data-warning]))
41
+ ) {
42
+ z-index: 1;
43
+ }
44
+
45
+ &:is(:focus-visible, :focus-within) {
46
+ z-index: 2;
47
+ }
48
+ }
49
+
50
+ > .ui-input:not([type="hidden"]):is(
51
+ :not([data-width]),
52
+ [data-width="default"],
53
+ [data-width=""]
54
+ ) {
55
+ flex: 1 1 0%;
56
+ inline-size: 0;
57
+ }
58
+
59
+ // 保持拼接边框稳定,按下按钮时不做单独的纵向位移。
60
+ > .ui-button:active {
61
+ transform: none;
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,6 @@
1
+ @use "./variables";
2
+ @use "./input";
3
+ @use "./display";
4
+ @use "./container";
5
+ @use "./form";
6
+ @use "./group";
@@ -0,0 +1,99 @@
1
+ @layer components.ui {
2
+ // 表单元素基础设置
3
+ :where(
4
+ .ui-button,
5
+ .ui-addon-button,
6
+ .ui-checkbox,
7
+ .ui-radio,
8
+ .ui-switch,
9
+ .ui-slider,
10
+ .ui-tabs-list,
11
+ .ui-tab,
12
+ .ui-input,
13
+ .ui-label
14
+ ) {
15
+ &,
16
+ & * {
17
+ box-sizing: border-box;
18
+ font-family: inherit;
19
+ appearance: none;
20
+ position: relative;
21
+ margin: 0;
22
+ }
23
+ }
24
+
25
+ // 共用 --ui-radius,让原生元素及其选择器弹层沿用同一圆角设置。
26
+ :is(.ui-input, .ui-button)[data-radius] {
27
+ --ui-radius: attr(data-radius type(*), var(--radius));
28
+
29
+ &:is([data-radius="default"], [data-radius=""]) {
30
+ --ui-radius: var(--radius);
31
+ }
32
+
33
+ &[data-radius="rounded"] {
34
+ --ui-radius: 9999px;
35
+ }
36
+ }
37
+
38
+ // 默认值保留各控件原有宽度;显式尺寸优先于原生输入类型的专用宽度。
39
+ :is(.ui-input, .ui-button, .ui-addon-button)[data-width]:not(
40
+ [data-width="default"],
41
+ [data-width=""],
42
+ [type="hidden"]
43
+ ) {
44
+ inline-size: attr(data-width type(*));
45
+
46
+ &[data-width="block"] {
47
+ display: var(--ui-block-display, flex);
48
+ inline-size: 100%;
49
+ }
50
+
51
+ &[data-width="icon"] {
52
+ inline-size: var(--ui-height);
53
+ block-size: var(--ui-height);
54
+ min-block-size: var(--ui-height);
55
+ padding: 0;
56
+ flex-shrink: 0;
57
+ text-align: center;
58
+ }
59
+ }
60
+
61
+ :is(
62
+ .ui-button,
63
+ .ui-addon-button,
64
+ .ui-input,
65
+ .ui-radio,
66
+ .ui-checkbox,
67
+ .ui-switch,
68
+ .ui-slider,
69
+ .ui-tabs-list,
70
+ .ui-tab,
71
+ .ui-progress,
72
+ .ui-meter,
73
+ .ui-badge,
74
+ .ui-pill
75
+ )[data-color] {
76
+ --ui-color: attr(data-color type(<color>));
77
+ &[data-color="neutral"] {
78
+ --ui-color: var(--color-foreground);
79
+ }
80
+ &[data-color="primary"] {
81
+ --ui-color: var(--color-primary);
82
+ }
83
+ &[data-color="success"] {
84
+ --ui-color: var(--color-success);
85
+ }
86
+ &[data-color="danger"] {
87
+ --ui-color: var(--color-danger);
88
+ }
89
+ &[data-color="error"] {
90
+ --ui-color: var(--color-error);
91
+ }
92
+ &[data-color="warning"] {
93
+ --ui-color: var(--color-warning);
94
+ }
95
+ &[data-color="info"] {
96
+ --ui-color: var(--color-info);
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,147 @@
1
+ @layer components.ui {
2
+ %ui-ghost-button-hover {
3
+ background-color: oklch(from var(--ui-display-color) calc(l * 0.1 + 0.9) calc(c * 0.2) h / 20%);
4
+ }
5
+
6
+ // button 结构性样式
7
+ .ui-button,
8
+ .ui-addon-button {
9
+ align-items: center;
10
+ justify-content: center;
11
+ min-block-size: var(--ui-height);
12
+ padding-block: 0;
13
+ padding-inline: 1rem;
14
+ cursor: pointer;
15
+
16
+ border: 1px solid transparent;
17
+ background: transparent;
18
+ border-radius: var(--ui-radius, 0);
19
+
20
+ --ui-display-color: var(--ui-color);
21
+
22
+ transition:
23
+ border-color var(--transition),
24
+ box-shadow var(--transition),
25
+ background-color var(--transition),
26
+ transform var(--transition),
27
+ outline var(--transition);
28
+ }
29
+
30
+ // button 通用外观
31
+ .ui-button,
32
+ .ui-addon-button {
33
+ --ui-radius: var(--radius);
34
+ --ui-color: var(--color-foreground, currentColor);
35
+ outline: 0 solid transparent;
36
+ &:focus-visible {
37
+ outline-width: 4px;
38
+ border-color: var(--ui-display-color);
39
+ outline-color: oklch(from var(--ui-display-color) l c h / 20%);
40
+ }
41
+ }
42
+
43
+ // button 按下位移
44
+ .ui-button {
45
+ &:active:not(:disabled, [aria-disabled="true"]) {
46
+ transform: translateY(1px);
47
+ }
48
+ }
49
+
50
+ // button 默认外观
51
+ .ui-button:is([data-variant="default"], [data-variant=""], :not([data-variant])) {
52
+ border-color: var(--color-border);
53
+ background: var(--color-background);
54
+ color: var(--ui-display-color);
55
+
56
+ &:hover:not(:disabled, [aria-disabled="true"]) {
57
+ background: color-mix(
58
+ in oklch,
59
+ oklch(from var(--color-foreground) calc(l * 0.1 + 0.9) calc(c * 0.2) h) 20%,
60
+ var(--color-background)
61
+ );
62
+ }
63
+ }
64
+
65
+ // button 特殊外观
66
+ .ui-button[data-variant="outline"] {
67
+ background: transparent;
68
+ border-color: var(--ui-display-color);
69
+ color: var(--ui-display-color);
70
+
71
+ &:hover:not(:disabled, [aria-disabled="true"]) {
72
+ background: oklch(from var(--ui-display-color) calc(l * 0.1 + 0.9) calc(c * 0.2) h / 20%);
73
+ }
74
+ }
75
+
76
+ .ui-button[data-variant="filled"] {
77
+ background: var(--ui-display-color);
78
+ border-color: transparent;
79
+ color: var(--color-white, white);
80
+
81
+ &:hover:not(:disabled, [aria-disabled="true"]) {
82
+ background: oklch(from var(--ui-display-color) calc(l + 0.1) c h);
83
+ }
84
+ &:focus-visible {
85
+ border-color: var(--color-white, white);
86
+ outline-color: oklch(from var(--ui-display-color) l c h / 40%);
87
+ }
88
+ }
89
+
90
+ .ui-button[data-variant="light"] {
91
+ background: oklch(from var(--ui-display-color) calc(l * 0.05 + 0.95) calc(c * 0.1) h);
92
+ border-color: transparent;
93
+ color: var(--ui-display-color);
94
+
95
+ &:hover:not(:disabled, [aria-disabled="true"]) {
96
+ background: oklch(from var(--ui-display-color) calc(l * 0.05 + 0.925) calc(c * 0.15) h);
97
+ }
98
+ &:focus-visible {
99
+ border-color: var(--ui-display-color);
100
+ }
101
+ }
102
+
103
+ .ui-button[data-variant="ghost"],
104
+ .ui-addon-button {
105
+ background: transparent;
106
+ border-color: transparent;
107
+ color: var(--ui-display-color);
108
+
109
+ &:hover:not(:disabled, [aria-disabled="true"]) {
110
+ @extend %ui-ghost-button-hover;
111
+ }
112
+ }
113
+
114
+ .ui-button[data-variant="text"] {
115
+ background: transparent;
116
+ border-color: transparent;
117
+ color: var(--ui-display-color);
118
+ }
119
+
120
+ :is(.ui-button, .ui-addon-button):is(:disabled, [aria-disabled="true"]) {
121
+ border-color: var(--color-border);
122
+ background: var(--color-input-fill);
123
+ color: var(--color-disabled);
124
+ cursor: not-allowed;
125
+ }
126
+
127
+ // 复用 Ghost 按钮的交互,缩小高度、内边距和焦点环以适配输入框内部。
128
+ .ui-addon-button {
129
+ --ui-height: var(--ui-addon-height, max(0px, calc(var(--height-input) - 0.5rem)));
130
+ --ui-radius: max(0px, calc(var(--radius) - 0.125rem));
131
+ block-size: var(--ui-height);
132
+ flex: 0 0 auto;
133
+ gap: 0.25rem;
134
+ padding-inline: 0.5rem;
135
+ font-size: 0.8125rem;
136
+ white-space: nowrap;
137
+
138
+ &:focus-visible {
139
+ outline-width: 2px;
140
+ }
141
+
142
+ &:is(:disabled, [aria-disabled="true"]) {
143
+ border-color: transparent;
144
+ background: transparent;
145
+ }
146
+ }
147
+ }