@veltra/styles 1.0.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.
Files changed (52) hide show
  1. package/dist/_functions.scss +49 -0
  2. package/dist/_mixins.scss +177 -0
  3. package/dist/_vars.scss +44 -0
  4. package/dist/anime/fade.css +9 -0
  5. package/dist/anime/fade.scss +8 -0
  6. package/dist/anime/slide.css +18 -0
  7. package/dist/anime/slide.scss +28 -0
  8. package/dist/anime/spring.css +30 -0
  9. package/dist/anime/spring.scss +42 -0
  10. package/dist/anime/zoom-in.css +57 -0
  11. package/dist/anime/zoom-in.scss +73 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +5 -0
  14. package/dist/load-theme.d.ts +18 -0
  15. package/dist/load-theme.js +38 -0
  16. package/dist/load-theme.js.map +1 -0
  17. package/dist/normalize.css +76 -0
  18. package/dist/normalize.scss +63 -0
  19. package/dist/theme/dark.d.ts +7 -0
  20. package/dist/theme/dark.js +47 -0
  21. package/dist/theme/dark.js.map +1 -0
  22. package/dist/theme/helper.d.ts +22 -0
  23. package/dist/theme/helper.js +57 -0
  24. package/dist/theme/helper.js.map +1 -0
  25. package/dist/theme/index.d.ts +7 -0
  26. package/dist/theme/index.js +6 -0
  27. package/dist/theme/light.d.ts +7 -0
  28. package/dist/theme/light.js +89 -0
  29. package/dist/theme/light.js.map +1 -0
  30. package/dist/theme/type.d.ts +91 -0
  31. package/dist/theme/ui-theme.d.ts +35 -0
  32. package/dist/theme/ui-theme.js +179 -0
  33. package/dist/theme/ui-theme.js.map +1 -0
  34. package/package.json +82 -0
  35. package/src/_functions.scss +49 -0
  36. package/src/_mixins.scss +177 -0
  37. package/src/_vars.scss +44 -0
  38. package/src/anime/fade.scss +8 -0
  39. package/src/anime/slide.scss +28 -0
  40. package/src/anime/spring.scss +42 -0
  41. package/src/anime/zoom-in.scss +73 -0
  42. package/src/env.d.ts +1 -0
  43. package/src/index.ts +5 -0
  44. package/src/load-theme.ts +46 -0
  45. package/src/normalize.scss +63 -0
  46. package/src/theme/__test__/ui-theme.test.ts +27 -0
  47. package/src/theme/dark.ts +38 -0
  48. package/src/theme/helper.ts +58 -0
  49. package/src/theme/index.ts +6 -0
  50. package/src/theme/light.ts +60 -0
  51. package/src/theme/type.ts +156 -0
  52. package/src/theme/ui-theme.ts +265 -0
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@veltra/styles",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "dist",
6
+ "src"
7
+ ],
8
+ "type": "module",
9
+ "sideEffects": [
10
+ "*.scss",
11
+ "src/index.ts",
12
+ "src/load-theme.ts",
13
+ "src/theme/index.ts"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "development": "./src/index.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./normalize": {
23
+ "sass": "./src/normalize.scss",
24
+ "development": "./src/normalize.scss",
25
+ "import": "./src/normalize.scss",
26
+ "default": "./src/normalize.scss"
27
+ },
28
+ "./mixins": {
29
+ "sass": "./src/_mixins.scss",
30
+ "development": "./src/_mixins.scss",
31
+ "import": "./src/_mixins.scss",
32
+ "default": "./src/_mixins.scss"
33
+ },
34
+ "./vars": {
35
+ "sass": "./src/_vars.scss",
36
+ "development": "./src/_vars.scss",
37
+ "import": "./src/_vars.scss",
38
+ "default": "./src/_vars.scss"
39
+ },
40
+ "./functions": {
41
+ "sass": "./src/_functions.scss",
42
+ "development": "./src/_functions.scss",
43
+ "import": "./src/_functions.scss",
44
+ "default": "./src/_functions.scss"
45
+ },
46
+ "./anime/*": {
47
+ "sass": "./src/anime/*",
48
+ "development": "./src/anime/*",
49
+ "import": "./src/anime/*",
50
+ "default": "./src/anime/*"
51
+ },
52
+ "./theme": {
53
+ "types": "./dist/theme/index.d.ts",
54
+ "development": "./src/theme/index.ts",
55
+ "import": "./dist/theme/index.js",
56
+ "default": "./dist/theme/index.js"
57
+ },
58
+ "./*": {
59
+ "sass": "./src/*",
60
+ "development": "./src/*",
61
+ "import": "./src/*",
62
+ "default": "./src/*"
63
+ }
64
+ },
65
+ "scripts": {
66
+ "build": "tsdown && bun scripts/copy-scss.ts",
67
+ "check-types": "tsc -p tsconfig.json --noEmit"
68
+ },
69
+ "dependencies": {
70
+ "@cat-kit/core": "^1.0.1",
71
+ "@veltra/compositions": "workspace:*",
72
+ "@veltra/utils": "workspace:*"
73
+ },
74
+ "devDependencies": {
75
+ "@tsdown/css": "^0.21.7",
76
+ "sass-embedded": "^1.99.0",
77
+ "tsdown": "^0.21.7"
78
+ },
79
+ "peerDependencies": {
80
+ "vue": "^3.5.0"
81
+ }
82
+ }
@@ -0,0 +1,49 @@
1
+ @use 'vars';
2
+ @use 'sass:list';
3
+
4
+ // 使用单个变量(全局主题 token,`--u-*`)
5
+ @function use-var($basename, $nodes...) {
6
+ $suffix: '';
7
+
8
+ @each $node in $nodes {
9
+ $suffix: $suffix + '-' + $node;
10
+ }
11
+
12
+ @return var(--u-#{$basename}#{$suffix});
13
+ }
14
+
15
+ // 组件级 token:`var(--u-{component}-{property}[, fallback])`
16
+ @function component-var($component, $property, $fallback: null) {
17
+ @if $fallback == null {
18
+ @return var(--u-#{$component}-#{$property});
19
+ }
20
+
21
+ @return var(--u-#{$component}-#{$property}, #{$fallback});
22
+ }
23
+
24
+ // 使用多个变量
25
+ @function use-vars($vars, $separator: ' ') {
26
+ $result: '';
27
+ $len: list.length($vars);
28
+ @for $i from 1 to $len {
29
+ $result: $result + use-var(list.nth($vars, $i)) + $separator;
30
+ }
31
+
32
+ $result: $result + use-var(list.nth($vars, $len));
33
+
34
+ @return $result;
35
+ }
36
+
37
+ // bem的函数
38
+ @function bem($b, $e: null, $m: null) {
39
+ $b: '.' + vars.$namespace + $b;
40
+ @if ($e != null) {
41
+ $b: $b + '__' + $e;
42
+ }
43
+
44
+ @if ($m != null) {
45
+ $b: $b + '--' + $m;
46
+ }
47
+
48
+ @return $b;
49
+ }
@@ -0,0 +1,177 @@
1
+ @use 'vars';
2
+ @use 'sass:list';
3
+ @use 'sass:map';
4
+ @use 'functions' as fn;
5
+
6
+ /** flex布局 */
7
+ @mixin flex($display: flex, $justify: flex-start, $align: center, $wrap: nowrap) {
8
+ display: $display;
9
+ justify-content: $justify;
10
+ align-items: $align;
11
+ flex-wrap: $wrap;
12
+ }
13
+
14
+ @mixin size {
15
+ @each $size in vars.$sizes {
16
+ &--#{$size} {
17
+ @content ($size);
18
+ }
19
+ }
20
+ }
21
+
22
+ @mixin b($blocks...) {
23
+ @each $block in $blocks {
24
+ .#{vars.$namespace}#{$block} {
25
+ @content;
26
+ }
27
+ }
28
+ }
29
+
30
+ @mixin e($elements...) {
31
+ $selector-list: null;
32
+
33
+ @each $element in $elements {
34
+ @if ($selector-list != null) {
35
+ $selector-list: #{$selector-list} + ', &__' + #{$element};
36
+ } @else {
37
+ $selector-list: '&__' + #{$element};
38
+ }
39
+ }
40
+
41
+ #{$selector-list} {
42
+ @content;
43
+ }
44
+ }
45
+
46
+ @mixin m($modifiers...) {
47
+ $selector-list: null;
48
+
49
+ @each $modifier in $modifiers {
50
+ @if ($selector-list != null) {
51
+ $selector-list: #{$selector-list} + ', &--' + #{$modifier};
52
+ } @else {
53
+ $selector-list: '&--' + #{$modifier};
54
+ }
55
+ }
56
+
57
+ #{$selector-list} {
58
+ @content;
59
+ }
60
+ }
61
+
62
+ @mixin em($element, $modifier) {
63
+ $selector: '&__' + #{$element} + '--' + $modifier;
64
+ #{$selector} {
65
+ @content;
66
+ }
67
+ }
68
+
69
+ @mixin is($types...) {
70
+ @each $type in $types {
71
+ &.is-#{$type} {
72
+ @content;
73
+ }
74
+ }
75
+ }
76
+
77
+ @mixin bem($b, $e: null, $m: null) {
78
+ $b: '.' + vars.$namespace + $b;
79
+
80
+ @if ($e != null) {
81
+ @each $ei in $e {
82
+ @if ($m != null) {
83
+ @each $mi in $m {
84
+ #{$b + '__' + $ei + '--' + $mi} {
85
+ @content;
86
+ }
87
+ }
88
+ } @else {
89
+ #{$b + '__' + $ei} {
90
+ @content;
91
+ }
92
+ }
93
+ }
94
+ } @else {
95
+ #{$b} {
96
+ @content;
97
+ }
98
+ }
99
+ }
100
+
101
+ @mixin ellipsis {
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ white-space: nowrap;
105
+ }
106
+
107
+ /**
108
+ * 设置css变量
109
+ * 参数1:变量前缀
110
+ * 参数2: 一个由值组成的列表或者由key, value组成的map
111
+ * css-var(height, (
112
+ * large: 40px
113
+ * default: 32px
114
+ * small: 24px
115
+ * ))
116
+ * 生成的css变量:
117
+ * --u-height-large: 40px;
118
+ * --u-height-default: 32px;
119
+ * --u-height-small: 24px;
120
+ */
121
+ @mixin css-var($prefix, $list) {
122
+ @each $type, $value in $list {
123
+ @if $value == null {
124
+ --#{$prefix}: #{$type};
125
+ } @else {
126
+ // "" + $type用于消除警告
127
+ --#{$prefix}-#{"" + $type}: #{$value};
128
+ }
129
+ }
130
+ }
131
+
132
+ @function breakpoint($point) {
133
+ @return var(--u-breakpoint-#{$point});
134
+ }
135
+
136
+ /** 暗色:data-theme 与系统偏好,与 UITheme.injectBuiltInThemes 选择器策略一致 */
137
+ @mixin dark {
138
+ html[data-theme='dark'] & {
139
+ @content;
140
+ }
141
+
142
+ @media (prefers-color-scheme: dark) {
143
+ html:not([data-theme='light']) & {
144
+ @content;
145
+ }
146
+ }
147
+ }
148
+
149
+ @mixin xs {
150
+ @media screen and (min-width: 0) and (max-width: breakpoint(xs)) {
151
+ @content;
152
+ }
153
+ }
154
+
155
+ @mixin sm {
156
+ @media screen and (min-width: breakpoint(xs)) and (max-width: breakpoint(sm)) {
157
+ @content;
158
+ }
159
+ }
160
+
161
+ @mixin md {
162
+ @media screen and (min-width: breakpoint(sm)) and (max-width: breakpoint(md)) {
163
+ @content;
164
+ }
165
+ }
166
+
167
+ @mixin lg {
168
+ @media screen and (min-width: breakpoint( md)) and (max-width: breakpoint(lg)) {
169
+ @content;
170
+ }
171
+ }
172
+
173
+ @mixin xl {
174
+ @media screen and (min-width: breakpoint(lg)) {
175
+ @content;
176
+ }
177
+ }
package/src/_vars.scss ADDED
@@ -0,0 +1,44 @@
1
+ @use 'sass:map';
2
+
3
+ $namespace: 'u-';
4
+
5
+ // 与全局 CSS 变量对齐的语义化别名(便于 SCSS 中直接引用)
6
+ $color-primary: var(--u-color-primary);
7
+ $text-color-main: var(--u-text-color-main);
8
+ $border-color: var(--u-border-color);
9
+ $bg-color-top: var(--u-bg-color-top);
10
+
11
+ // 定义size映射的函数, 保证尺寸的统一性, size-vars(small, default, large)
12
+ // 例子:
13
+ // size-vars((height: 24px), (height: 32px), (height: 40px))
14
+ // 生成:
15
+ // (small: (height: 24px), default: (height: 32px), large: (height: 40px))
16
+ @function size-vars($sizes...) {
17
+ $size-types: (small, default, large);
18
+ $size-result: ();
19
+
20
+ @for $i from 1 through length($size-types) {
21
+ $size-result: map.merge($size-result, (#{nth($size-types, $i)}: nth($sizes, $i)));
22
+ }
23
+ @return $size-result;
24
+ }
25
+
26
+ $sizes: (small, default, large);
27
+
28
+ // 颜色类别
29
+ $color-types: (
30
+ // 品牌色
31
+ primary,
32
+ // 成功色
33
+ success,
34
+ // 警告色
35
+ warning,
36
+ // 危险色
37
+ danger,
38
+ // 信息
39
+ info,
40
+ // 禁用色
41
+ disabled,
42
+ // 默认色
43
+ default
44
+ );
@@ -0,0 +1,8 @@
1
+ .fade-enter-active,
2
+ .fade-leave-active {
3
+ transition: opacity 0.25s linear;
4
+ }
5
+ .fade-enter-from,
6
+ .fade-leave-to {
7
+ opacity: 0;
8
+ }
@@ -0,0 +1,28 @@
1
+ .slide-down,
2
+ .slide-up {
3
+ &-enter-from,
4
+ &-leave-to {
5
+ opacity: 0;
6
+ }
7
+
8
+ &-enter-active,
9
+ &-leave-active {
10
+ transition:
11
+ transform 0.2s cubic-bezier(0, 0, 0.2, 1),
12
+ opacity 0.2s cubic-bezier(0, 0, 0.2, 1);
13
+ }
14
+ }
15
+
16
+ .slide-down {
17
+ &-enter-from,
18
+ &-leave-to {
19
+ transform: translateY(-10px);
20
+ }
21
+ }
22
+
23
+ .slide-up {
24
+ &-enter-from,
25
+ &-leave-to {
26
+ transform: translateY(10px);
27
+ }
28
+ }
@@ -0,0 +1,42 @@
1
+ @keyframes spring-in {
2
+ 0% {
3
+ transform: scale(0);
4
+ }
5
+ 40% {
6
+ transform: scale(1.1);
7
+ }
8
+ 60% {
9
+ transform: scale(1);
10
+ }
11
+ 80% {
12
+ transform: scale(1.02);
13
+ }
14
+ 100% {
15
+ transform: scale(1);
16
+ }
17
+ }
18
+
19
+ // @keyframes spring-out {
20
+ // 100% {
21
+ // transform: scale3d(0, 0, 0) translate3d(0, 0, 0);
22
+ // opacity: 0;
23
+ // }
24
+ // }
25
+
26
+ .spring-enter-active {
27
+ transform-origin: center;
28
+ animation: spring-in 0.5s ease-in-out;
29
+ }
30
+
31
+ .spring-leave-active {
32
+ // animation: spring-out 0.4s cubic-bezier(0.64, -0.32, 0.66, 1.06);
33
+
34
+ transition:
35
+ transform 0.4s cubic-bezier(0.64, -0.32, 0.66, 1.06),
36
+ opacity 0.4s cubic-bezier(0.64, -0.32, 0.66, 1.06);
37
+ }
38
+
39
+ .spring-leave-to {
40
+ transform: scale3d(0, 0, 0) translate3d(0, 0, 0) !important;
41
+ opacity: 0;
42
+ }
@@ -0,0 +1,73 @@
1
+ .zoom-in,
2
+ .zoom-in-left,
3
+ .zoom-in-right,
4
+ .zoom-in-top,
5
+ .zoom-in-bottom {
6
+ &-enter-from,
7
+ &-leave-to {
8
+ opacity: 0;
9
+ }
10
+
11
+ &-enter-active {
12
+ transition:
13
+ opacity 0.15s ease-in,
14
+ transform 0.15s ease-in;
15
+ }
16
+
17
+ &-leave-active {
18
+ transition:
19
+ opacity 0.15s ease-out,
20
+ transform 0.15s ease-out;
21
+ }
22
+ }
23
+
24
+ .zoom-in-left,
25
+ .zoom-in-right {
26
+ &-enter-from,
27
+ &-leave-to {
28
+ transform: scaleX(0.8);
29
+ }
30
+ }
31
+
32
+ .zoom-in-top,
33
+ .zoom-in-bottom {
34
+ &-enter-from,
35
+ &-leave-to {
36
+ transform: scaleY(0.8);
37
+ }
38
+ }
39
+
40
+ .zoom-in {
41
+ &-enter-from,
42
+ &-leave-to {
43
+ transform: scale(0.8);
44
+ }
45
+ }
46
+
47
+ .zoom-in-left {
48
+ &-enter-active,
49
+ &-leave-active {
50
+ transform-origin: left center;
51
+ }
52
+ }
53
+
54
+ .zoom-in-right {
55
+ &-enter-active,
56
+ &-leave-active {
57
+ transform-origin: right center;
58
+ }
59
+ }
60
+
61
+ .zoom-in-top {
62
+ &-enter-active,
63
+ &-leave-active {
64
+ transform-origin: center top;
65
+ }
66
+ }
67
+
68
+ .zoom-in-bottom {
69
+ &-enter-active,
70
+ &-leave-active {
71
+ transform-origin: center bottom;
72
+ }
73
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '*.scss' {}
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import './normalize.scss'
2
+ import './anime/fade.scss'
3
+ import './anime/slide.scss'
4
+ import './anime/spring.scss'
5
+ import './anime/zoom-in.scss'
@@ -0,0 +1,46 @@
1
+ import { useConfig } from '@veltra/compositions'
2
+ import { shallowRef, type ShallowRef } from 'vue'
3
+
4
+ import { darkTheme } from './theme/dark'
5
+ import { lightTheme } from './theme/light'
6
+ import { UITheme } from './theme/ui-theme'
7
+
8
+ export const currentTheme: ShallowRef<UITheme | undefined> = shallowRef<UITheme>()
9
+
10
+ export function setTheme(mode: 'light' | 'dark' | 'auto'): void {
11
+ UITheme.setTheme(mode)
12
+ }
13
+
14
+ /**
15
+ * @description 加载主题, 如果你是 SSR 环境,
16
+ * 请在 `onMounted` 中调用,否则你可以在
17
+ * 项目的入口环境(通常是 main.ts)或其他全局环境中调用。
18
+ *
19
+ * 不传 `theme` 时注入内置 light/dark 变量(支持 `setTheme` 与系统暗色偏好)。
20
+ * 传入自定义 `UITheme` 时使用单次 `html { }` 注入。
21
+ */
22
+ export function loadTheme(theme?: UITheme): void {
23
+ currentTheme.value = theme ?? lightTheme
24
+
25
+ const { config } = useConfig()
26
+ if (typeof document !== 'undefined') {
27
+ document.documentElement.classList.add(config.size)
28
+ }
29
+
30
+ if (typeof document === 'undefined') return
31
+
32
+ if (!theme) {
33
+ currentTheme.value = lightTheme
34
+ UITheme.injectBuiltInThemes(lightTheme, darkTheme)
35
+ return
36
+ }
37
+
38
+ currentTheme.value = theme
39
+
40
+ if (theme === lightTheme || theme === darkTheme) {
41
+ UITheme.injectBuiltInThemes(lightTheme, darkTheme)
42
+ UITheme.setTheme(theme === darkTheme ? 'dark' : 'light')
43
+ } else {
44
+ theme.render()
45
+ }
46
+ }
@@ -0,0 +1,63 @@
1
+ // 生成css变量
2
+ // 主题主要由色彩, 大小, 圆角, 阴影, 字体, 边框构成
3
+ // 其中色彩又分为文字颜色, 背景颜色
4
+ @use 'mixins' as m;
5
+ @use 'functions' as fn;
6
+ @use 'vars';
7
+
8
+ // 初始化
9
+ html {
10
+ font-family: fn.use-var(font-family);
11
+ font-weight: normal;
12
+ color: fn.use-var(text-color, main);
13
+ line-height: 1.4;
14
+
15
+ @each $size in vars.$sizes {
16
+ &.#{$size} {
17
+ font-size: fn.use-var(font-size-main, $size);
18
+ }
19
+ }
20
+ }
21
+
22
+ body,
23
+ ul,
24
+ h1,
25
+ h2,
26
+ h3,
27
+ h4,
28
+ h5,
29
+ h6 {
30
+ margin: 0;
31
+ }
32
+
33
+ div,
34
+ section,
35
+ head,
36
+ aside,
37
+ body,
38
+ main,
39
+ h1,
40
+ h2,
41
+ h3,
42
+ h4,
43
+ h5,
44
+ h6,
45
+ p,
46
+ article,
47
+ button,
48
+ input,
49
+ textarea {
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ a {
54
+ text-decoration: none;
55
+ color: inherit;
56
+ }
57
+
58
+ ul {
59
+ padding: 0;
60
+ }
61
+ li {
62
+ list-style: none;
63
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { lightTheme } from '../light'
4
+ import { UITheme } from '../ui-theme'
5
+
6
+ describe('UITheme', () => {
7
+ it('themeToDeclarationList emits only --u-prefixed custom properties', () => {
8
+ const decls = lightTheme.themeToDeclarationList(lightTheme.theme)
9
+ expect(decls.some((d) => d.startsWith('--u-color-primary:'))).toBe(true)
10
+ for (const d of decls) {
11
+ const name = d.split(':')[0]?.trim()
12
+ if (!name) continue
13
+ expect(name.startsWith('--u-')).toBe(true)
14
+ }
15
+ })
16
+
17
+ it('setTheme toggles data-theme on documentElement', () => {
18
+ if (typeof document === 'undefined') return
19
+ const el = document.documentElement
20
+ UITheme.setTheme('dark')
21
+ expect(el.dataset.theme).toBe('dark')
22
+ UITheme.setTheme('light')
23
+ expect(el.dataset.theme).toBe('light')
24
+ UITheme.setTheme('auto')
25
+ expect(el.dataset.theme).toBeUndefined()
26
+ })
27
+ })
@@ -0,0 +1,38 @@
1
+ import { lightTheme } from './light'
2
+
3
+ export const darkTheme = lightTheme.new({
4
+ color: {
5
+ primary: '#4f8ff7',
6
+ success: '#52c41a',
7
+ warning: '#faad14',
8
+ danger: '#ff4d4f',
9
+ info: '#13c2c2',
10
+ disabled: '#212020',
11
+ default: '#595959'
12
+ },
13
+
14
+ bg: {
15
+ color: {
16
+ bottom: '#0f0f0f',
17
+ middle: '#1a1a1a',
18
+ top: '#262626',
19
+ hover: '#303030',
20
+ black: '#000000'
21
+ },
22
+ filter: { blur: 'blur(16px)', saturate: 'saturate(180%)' }
23
+ },
24
+
25
+ 'text-color': {
26
+ title: '#f0f0f0',
27
+ main: '#d9d9d9',
28
+ second: '#a6a6a6',
29
+ placeholder: '#737373',
30
+ assist: '#595959',
31
+ disabled: '#434343',
32
+ white: '#ffffff'
33
+ },
34
+
35
+ border: { color: '#404040' },
36
+
37
+ shadow: { color: 'rgba(255, 255, 255, 0.2)', x: 0, y: 2, blur: 8, spread: 0 }
38
+ })