@kylincloud/flamegraph 0.35.8 → 0.35.10

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 (38) hide show
  1. package/README.md +292 -50
  2. package/dist/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.d.ts.map +1 -1
  3. package/dist/FlameGraph/FlameGraphComponent/Header.d.ts +0 -3
  4. package/dist/FlameGraph/FlameGraphComponent/Header.d.ts.map +1 -1
  5. package/dist/FlameGraph/FlameGraphComponent/index.d.ts +2 -0
  6. package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
  7. package/dist/FlameGraph/FlameGraphRenderer.d.ts +2 -0
  8. package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
  9. package/dist/FlamegraphRenderer.d.ts +23 -5
  10. package/dist/FlamegraphRenderer.d.ts.map +1 -1
  11. package/dist/ProfilerTable.d.ts.map +1 -1
  12. package/dist/Toolbar.d.ts +4 -1
  13. package/dist/Toolbar.d.ts.map +1 -1
  14. package/dist/i18n.d.ts +2 -0
  15. package/dist/i18n.d.ts.map +1 -1
  16. package/dist/index.cjs.js +4 -4
  17. package/dist/index.cjs.js.map +1 -1
  18. package/dist/index.esm.js +4 -4
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/index.node.cjs.js +6 -1
  21. package/dist/index.node.cjs.js.map +1 -1
  22. package/dist/index.node.esm.js +8 -3
  23. package/dist/index.node.esm.js.map +1 -1
  24. package/package.json +4 -1
  25. package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.module.css +69 -21
  26. package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.tsx +57 -86
  27. package/src/FlameGraph/FlameGraphComponent/Header.tsx +23 -36
  28. package/src/FlameGraph/FlameGraphComponent/index.tsx +13 -9
  29. package/src/FlameGraph/FlameGraphRenderer.tsx +14 -11
  30. package/src/FlamegraphRenderer.tsx +103 -20
  31. package/src/ProfilerTable.tsx +24 -11
  32. package/src/SharedQueryInput.module.scss +5 -2
  33. package/src/Toolbar.module.scss +13 -0
  34. package/src/Toolbar.tsx +29 -3
  35. package/src/i18n.tsx +11 -2
  36. package/src/sass/_css-variables.scss +15 -2
  37. package/src/sass/flamegraph.scss +59 -0
  38. package/src/shims/Table.module.scss +7 -1
@@ -1,27 +1,50 @@
1
1
  // src/FlamegraphRenderer.tsx
2
2
  import React from 'react';
3
- import FlameGraphRenderer, {
3
+ import FlameGraphRenderer from './FlameGraph/FlameGraphRenderer';
4
+ import type {
4
5
  FlamegraphRendererProps as InnerFlamegraphRendererProps,
5
6
  } from './FlameGraph/FlameGraphRenderer';
7
+
6
8
  import './sass/flamegraph.scss';
9
+
7
10
  import {
8
11
  FlamegraphI18nProvider,
9
- type FlamegraphMessages,
12
+ defaultMessages,
13
+ zhCNMessages,
10
14
  } from './i18n';
15
+ import type { FlamegraphMessages } from './i18n';
11
16
 
12
17
  // 原项目里这个值由 webpack 注入,这里直接关掉 logo
13
18
  const overrideProps = {
14
19
  showPyroscopeLogo: false,
15
20
  };
16
21
 
22
+ /** 对外暴露的 locale 类型 */
23
+ export type FlamegraphLocale = 'auto' | 'en' | 'zh-CN';
24
+
17
25
  export type FlamegraphRendererProps = Omit<
18
26
  InnerFlamegraphRendererProps,
19
27
  'showPyroscopeLogo'
20
28
  > & {
21
29
  /** 颜色模式:原生 light/dark + 新增 kylin */
22
30
  colorMode?: 'light' | 'dark' | 'kylin';
23
- /** 文案覆盖(不传就是内置默认英文/中文) */
31
+
32
+ /**
33
+ * 文案覆盖:
34
+ * - 如果显式传入 i18n,则优先使用这一套(不再根据 locale 推断)
35
+ * - 如果不传,则根据 locale(或浏览器语言)自动选择内置英文 / 中文
36
+ */
24
37
  i18n?: Partial<FlamegraphMessages>;
38
+
39
+ /**
40
+ * 语言设置:
41
+ * - 不传时等价于 "auto"
42
+ * - "auto" 表示根据浏览器语言(navigator.language)自动选择
43
+ * - "en" / "zh-CN" 为强制指定
44
+ */
45
+ locale?: FlamegraphLocale;
46
+ /** 是否开启三明治视图菜单 */
47
+ sandwich?: boolean;
25
48
  };
26
49
 
27
50
  // 让 TS 认识自定义标签 <pyro-flamegraph>
@@ -32,30 +55,90 @@ declare global {
32
55
  'pyro-flamegraph': React.DetailedHTMLProps<
33
56
  React.HTMLAttributes<HTMLElement>,
34
57
  HTMLElement
35
- >;
58
+ > & {
59
+ profile?: any;
60
+ };
36
61
  }
37
62
  }
38
63
  }
39
64
 
40
- export const FlamegraphRenderer: React.FC<FlamegraphRendererProps> = (
41
- props
42
- ) => {
43
- const { colorMode = 'dark', i18n, ...restProps } = props;
65
+ /**
66
+ * 浏览器语言 -> 内部 locale
67
+ * SSR / 非浏览器环境时默认 en
68
+ */
69
+ const detectBrowserLocale = (): Exclude<FlamegraphLocale, 'auto'> => {
70
+ if (typeof navigator === 'undefined' || !navigator.language) {
71
+ return 'en';
72
+ }
73
+
74
+ const lang = navigator.language.toLowerCase();
75
+
76
+ if (lang.startsWith('zh')) {
77
+ return 'zh-CN';
78
+ }
79
+
80
+ return 'en';
81
+ };
82
+
83
+ /**
84
+ * 决定实际使用的文案:
85
+ * 1. 如果调用方显式传了 i18n,则直接使用 i18n(优先级最高);
86
+ * 2. 否则根据 locale / 浏览器语言选择内置英文或中文语言包。
87
+ */
88
+ const resolveMessages = (
89
+ locale: FlamegraphLocale | undefined,
90
+ i18n?: Partial<FlamegraphMessages>
91
+ ): Partial<FlamegraphMessages> => {
92
+ // 业务显式传入 i18n,则不再干预
93
+ if (i18n) {
94
+ return i18n;
95
+ }
96
+
97
+ const effectiveLocale = locale ?? 'auto';
98
+ const finalLocale =
99
+ effectiveLocale === 'auto' ? detectBrowserLocale() : effectiveLocale;
100
+
101
+ if (finalLocale === 'zh-CN') {
102
+ return zhCNMessages;
103
+ }
104
+
105
+ // 默认英文
106
+ return defaultMessages;
107
+ };
108
+
109
+ /** 根据 colorMode 生成一个主题 class(可按你自己的 SCSS 改) */
110
+ const getThemeClass = (colorMode: FlamegraphRendererProps['colorMode']) => {
111
+ switch (colorMode) {
112
+ case 'dark':
113
+ return 'ps-theme-dark';
114
+ case 'kylin':
115
+ return 'ps-theme-kylin';
116
+ case 'light':
117
+ default:
118
+ return 'ps-theme-light';
119
+ }
120
+ };
121
+
122
+ const FlamegraphRenderer: React.FC<FlamegraphRendererProps> = (props) => {
123
+ const { i18n, locale, colorMode = 'light', sandwich = true,...rest } = props;
124
+
125
+ const messages = React.useMemo(
126
+ () => resolveMessages(locale, i18n),
127
+ [locale, i18n]
128
+ );
129
+
130
+ const themeClass = React.useMemo(
131
+ () => getThemeClass(colorMode),
132
+ [colorMode]
133
+ );
44
134
 
45
- // 虽然 pyro-flamegraph 不是合法的 HTML 元素
46
- // 但它只是作为样式 scope 容器存在(配合 sass/flamegraph.scss 里的一堆选择器)
47
135
  return (
48
- <pyro-flamegraph
49
- is="span"
50
- data-flamegraph-color-mode={colorMode}
51
- >
52
- <FlamegraphI18nProvider messages={i18n}>
53
- {/* eslint-disable-next-line react/jsx-props-no-spreading */}
54
- <FlameGraphRenderer
55
- {...(restProps as InnerFlamegraphRendererProps)}
56
- {...overrideProps}
57
- />
136
+ <pyro-flamegraph className={themeClass}>
137
+ <FlamegraphI18nProvider messages={messages}>
138
+ <FlameGraphRenderer {...overrideProps}enableSandwichView={sandwich} {...rest} />
58
139
  </FlamegraphI18nProvider>
59
140
  </pyro-flamegraph>
60
141
  );
61
142
  };
143
+
144
+ export { FlamegraphRenderer };
@@ -500,19 +500,32 @@ const ProfilerTable = React.memo(function ProfilerTable({
500
500
  selectedItem,
501
501
  }: Omit<ProfilerTableProps, 'tableBodyRef'>) {
502
502
  const tableBodyRef = useRef<HTMLTableSectionElement>(null);
503
+ const isDoubles = flamebearer.format === 'double';
503
504
 
504
505
  return (
505
- <div data-testid="table-view">
506
- <Table
507
- tableBodyRef={tableBodyRef}
508
- flamebearer={flamebearer}
509
- isDoubles={flamebearer.format === 'double'}
510
- fitMode={fitMode}
511
- highlightQuery={highlightQuery}
512
- handleTableItemClick={handleTableItemClick}
513
- palette={palette}
514
- selectedItem={selectedItem}
515
- />
506
+ <div
507
+ data-testid="table-view"
508
+ className={
509
+ isDoubles
510
+ ? 'flamegraph-table-wrapper flamegraph-table-wrapper--double'
511
+ : 'flamegraph-table-wrapper'
512
+ }
513
+ >
514
+ {/* 内部滚动容器:只控制表格本身的高度和滚动 */}
515
+ <div className="flamegraph-table-scroll">
516
+ <Table
517
+ tableBodyRef={tableBodyRef}
518
+ flamebearer={flamebearer}
519
+ isDoubles={isDoubles}
520
+ fitMode={fitMode}
521
+ highlightQuery={highlightQuery}
522
+ handleTableItemClick={handleTableItemClick}
523
+ palette={palette}
524
+ selectedItem={selectedItem}
525
+ />
526
+ </div>
527
+
528
+ {/* Tooltip 保持原有行为 */}
516
529
  <TableTooltip
517
530
  tableBodyRef={tableBodyRef}
518
531
  numTicks={flamebearer.numTicks}
@@ -1,8 +1,11 @@
1
+ /* src/SharedQueryInput.module.scss */
2
+
1
3
  .wrapper {
2
4
  display: flex;
3
5
  flex-direction: row;
4
6
  align-items: center;
5
- flex-grow: 1;
7
+ /* 移除 flex-grow: 1,让宽度由父组件控制 */
8
+ width: 100%;
6
9
  }
7
10
 
8
11
  .search {
@@ -12,7 +15,7 @@
12
15
  margin-right: 1.5px;
13
16
  border: 1px solid var(--ps-ui-border);
14
17
  display: flex;
15
- flex-grow: 1;
18
+ /* 移除 flex-grow: 1 */
16
19
  height: 37px;
17
20
  width: 100%;
18
21
 
@@ -13,6 +13,19 @@ $buttonHeight: 37px;
13
13
  align-items: center;
14
14
  }
15
15
 
16
+ // 搜索框包裹器 - 固定宽度 360px
17
+ .searchWrapper {
18
+ width: 360px;
19
+ flex-shrink: 0;
20
+ }
21
+
22
+ // 右侧区域:色带下拉 + 工具按钮
23
+ .rightSection {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 4px;
27
+ }
28
+
16
29
  .viewType {
17
30
  display: flex;
18
31
  flex-direction: row;
package/src/Toolbar.tsx CHANGED
@@ -25,6 +25,8 @@ import { FitModes } from './fitMode/fitMode';
25
25
  import SharedQueryInput from './SharedQueryInput';
26
26
  import type { ViewTypes } from './FlameGraph/FlameGraphComponent/viewTypes';
27
27
  import type { FlamegraphRendererProps } from './FlameGraph/FlameGraphRenderer';
28
+ import { FlamegraphPalette } from './FlameGraph/FlameGraphComponent/colorPalette';
29
+ import { DiffLegendPaletteDropdown } from './FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown';
28
30
  import {
29
31
  TableIcon,
30
32
  TablePlusFlamegraphIcon,
@@ -43,7 +45,7 @@ import {
43
45
  const cx = classNames.bind(styles);
44
46
 
45
47
  const DIVIDER_WIDTH = 5;
46
- const QUERY_INPUT_WIDTH = 175;
48
+ const QUERY_INPUT_WIDTH = 360;
47
49
  const LEFT_MARGIN = 2;
48
50
  const RIGHT_MARGIN = 2;
49
51
  const TOOLBAR_SQUARE_WIDTH = 40 + LEFT_MARGIN + RIGHT_MARGIN;
@@ -135,6 +137,10 @@ export interface ProfileHeaderProps {
135
137
  selectedNode: Maybe<{ i: number; j: number }>;
136
138
  onFocusOnSubtree: (i: number, j: number) => void;
137
139
  sharedQuery?: FlamegraphRendererProps['sharedQuery'];
140
+
141
+ // 用于 diff 模式的调色板
142
+ palette?: FlamegraphPalette;
143
+ setPalette?: (p: FlamegraphPalette) => void;
138
144
  }
139
145
 
140
146
  const Divider = () => <div className={styles.divider} />;
@@ -160,10 +166,15 @@ const Toolbar = memo(
160
166
  enableChangingDisplay = true,
161
167
  sharedQuery,
162
168
  ExportData,
169
+ palette,
170
+ setPalette,
163
171
  }: ProfileHeaderProps) => {
164
172
  const toolbarRef = useRef<HTMLDivElement>(null);
165
173
  const i18n = useFlamegraphI18n();
166
174
 
175
+ // 是否显示调色板选择器(仅在 diff 模式且有 palette 时显示)
176
+ const showPaletteDropdown = flamegraphType === 'double' && palette && setPalette;
177
+
167
178
  const fitModeItem = {
168
179
  el: (
169
180
  <>
@@ -247,7 +258,8 @@ const Toolbar = memo(
247
258
  return (
248
259
  <div role="toolbar" ref={toolbarRef}>
249
260
  <div className={styles.navbar}>
250
- <div>
261
+ {/* 左侧:搜索框 */}
262
+ <div className={styles.searchWrapper}>
251
263
  <SharedQueryInput
252
264
  width={QUERY_INPUT_WIDTH}
253
265
  onHighlightChange={handleSearchChange}
@@ -255,7 +267,20 @@ const Toolbar = memo(
255
267
  sharedQuery={sharedQuery}
256
268
  />
257
269
  </div>
258
- <div>
270
+
271
+ {/* 右侧:色带下拉 + 工具按钮 */}
272
+ <div className={styles.rightSection}>
273
+ {/* diff 模式下显示调色板选择器 */}
274
+ {showPaletteDropdown && (
275
+ <>
276
+ <DiffLegendPaletteDropdown
277
+ palette={palette}
278
+ onChange={setPalette}
279
+ />
280
+ <Divider />
281
+ </>
282
+ )}
283
+
259
284
  <div className={styles.itemsContainer}>
260
285
  {toolbarFilteredItems.visible.map((v, i) => (
261
286
  // eslint-disable-next-line react/no-array-index-key
@@ -278,6 +303,7 @@ const Toolbar = memo(
278
303
  )}
279
304
  </div>
280
305
  </div>
306
+
281
307
  {!isCollapsed && (
282
308
  <div className={styles.navbarCollapsedItems}>
283
309
  {toolbarFilteredItems.hidden.map((v, i) => (
package/src/i18n.tsx CHANGED
@@ -60,6 +60,9 @@ export type FlamegraphMessages = {
60
60
  diffLegendSelectPalette: string;
61
61
  paletteDefaultName: string;
62
62
  paletteColorBlindName: string;
63
+ // 调色板补充描述(出现在下拉菜单里)
64
+ paletteDefaultDesc: string;
65
+ paletteColorBlindDesc: string;
63
66
 
64
67
  // 搜索框 & 多视图搜索联动
65
68
  searchPlaceholder: string;
@@ -206,11 +209,14 @@ export const defaultMessages: FlamegraphMessages = {
206
209
  diffLegendSelectPalette: 'Select a palette',
207
210
  paletteDefaultName: 'Default',
208
211
  paletteColorBlindName: 'Color Blind',
212
+ // 调色板补充描述(英文)
213
+ paletteDefaultDesc: '(green to red)',
214
+ paletteColorBlindDesc: '(blue to red)',
209
215
 
210
216
  // 搜索框 & 多视图搜索联动
211
217
  searchPlaceholder: 'Search...',
212
218
  syncSearchBars: 'Sync Search Bars',
213
- unsyncSearchBars: 'Unsync Search Bars'
219
+ unsyncSearchBars: 'Unsync Search Bars',
214
220
  };
215
221
 
216
222
  export const zhCNMessages: FlamegraphMessages = {
@@ -257,11 +263,14 @@ export const zhCNMessages: FlamegraphMessages = {
257
263
  diffLegendSelectPalette: '选择颜色方案',
258
264
  paletteDefaultName: '默认',
259
265
  paletteColorBlindName: '色盲模式',
266
+ // 调色板补充描述(中文)
267
+ paletteDefaultDesc: '(绿色 → 红色)',
268
+ paletteColorBlindDesc: '(蓝色 → 红色)',
260
269
 
261
270
  // 搜索框 & 多视图搜索联动
262
271
  searchPlaceholder: '搜索...',
263
272
  syncSearchBars: '同步搜索栏',
264
- unsyncSearchBars: '取消同步搜索栏'
273
+ unsyncSearchBars: '取消同步搜索栏',
265
274
  };
266
275
 
267
276
  const I18nContext = createContext<FlamegraphMessages>(defaultMessages);
@@ -54,6 +54,10 @@
54
54
  --ps-table-highlight-row-text: #000000;
55
55
  --ps-table-row-text-shadow: #000000;
56
56
 
57
+ // 表头(使用实心背景,避免 sticky 时透出内容)
58
+ --ps-table-header-bg: #212124;
59
+ --ps-table-header-text: #d8d8d8;
60
+
57
61
  // 选择器
58
62
  --ps-selected-app: #df8b53;
59
63
  --ps-select-modal-title: #888888;
@@ -113,7 +117,7 @@
113
117
  // 说明:light 和 kylin 先共享一整套浅色变量,避免回退到 dark
114
118
  // ==========================================================================
115
119
  [data-theme='light'],
116
- [data-theme='kylin'], // ✅ 新增:Box 主题为 kylin 时也用浅色基线
120
+ [data-theme='kylin'], // Box 主题为 kylin 时也用浅色基线
117
121
  [data-flamegraph-color-mode='light'],
118
122
  [data-flamegraph-color-mode='kylin'] {
119
123
  --ps-neutral-1: #ffffff;
@@ -147,6 +151,11 @@
147
151
  --ps-table-highlight-row-bg: #3b78e7;
148
152
  --ps-table-highlight-row-text: #ffffff;
149
153
  --ps-table-row-text-shadow: #ffffff;
154
+
155
+ // 表头默认浅色(浅灰背景 + 深色文字)
156
+ --ps-table-header-bg: #f8f8f8;
157
+ --ps-table-header-text: #272727;
158
+
150
159
  --ps-selected-app: #3b78e7;
151
160
  --ps-select-modal-title: #888888;
152
161
 
@@ -171,7 +180,7 @@
171
180
  // ==========================================================================
172
181
  // Kylin Theme - 在浅色基线上的微调(更贴近 AntD 白色系)
173
182
  // ==========================================================================
174
- [data-theme='kylin'], // ✅ 新增:Box 主题为 kylin 时应用这些微调
183
+ [data-theme='kylin'],
175
184
  [data-flamegraph-color-mode='kylin'] {
176
185
  // 整体背景更接近 AntD
177
186
  --ps-ui-background: #f5f5f5;
@@ -198,4 +207,8 @@
198
207
  --ps-tooltip-bg: #ffffff;
199
208
  --ps-tooltip-text: #000000;
200
209
  --ps-tooltip-header-bg: #f0f0f0;
210
+
211
+ // 表头专用:完全不透明的白色
212
+ --ps-table-header-bg: #ffffff;
213
+ --ps-table-header-text: #000000;
201
214
  }
@@ -16,3 +16,62 @@ pyro-flamegraph {
16
16
  font: 400 16px/1.7 -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica,
17
17
  Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
18
18
  }
19
+
20
+ /* =========================
21
+ * Diff 模式下表格滚动 & 表头固定
22
+ * 仅在 flamebearer.format === 'double' 时生效
23
+ * ========================= */
24
+
25
+ .flamegraph-table-wrapper {
26
+ /* 这里不限制高度,由内部滚动容器控制 */
27
+ position: relative;
28
+ }
29
+
30
+ /* 只在差分火焰图(double)模式下启用滚动限制 */
31
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll {
32
+ /* 最大高度:1000px,可按需调小/调大 */
33
+ max-height: var(--kylin-flamegraph-table-max-height, 1000px);
34
+ overflow-y: auto;
35
+ overflow-x: hidden;
36
+
37
+ /* 让 sticky 参照这个容器 */
38
+ position: relative;
39
+ }
40
+
41
+ /* 表头固定 + 不透明背景,避免内容透出来 */
42
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll thead {
43
+ position: sticky;
44
+ top: 0;
45
+ z-index: 3; // 比下面的行和 tooltip 都高一点
46
+ }
47
+
48
+ /* 整行都有底色,避免列间缝隙透出内容 */
49
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll thead tr {
50
+ background-color: var(--ps-neutral-9);
51
+ }
52
+
53
+ /* 每个单元格再补一层底色,保证完全不透明 */
54
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll thead th {
55
+ background-color: var(--ps-neutral-9);
56
+ background-clip: padding-box; // 防止边框区域产生透明感
57
+ }
58
+
59
+ /* 可选:加一点阴影,视觉上和下面的行分层 */
60
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll thead {
61
+ box-shadow: 0 2px 0 rgba(0, 0, 0, 0.06);
62
+ }
63
+
64
+
65
+ /* 可选:稍微美化一下内部滚动条,轨道透明,避免太抢眼 */
66
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll::-webkit-scrollbar {
67
+ width: 8px;
68
+ }
69
+
70
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll::-webkit-scrollbar-track {
71
+ background: transparent;
72
+ }
73
+
74
+ .flamegraph-table-wrapper--double .flamegraph-table-scroll::-webkit-scrollbar-thumb {
75
+ background-color: rgba(0, 0, 0, 0.18);
76
+ border-radius: 4px;
77
+ }
@@ -2,6 +2,12 @@
2
2
  width: 100%;
3
3
  table-layout: fixed;
4
4
 
5
+ /* 表头:用专门的实心背景变量,避免透明 */
6
+ thead {
7
+ background-color: var(--ps-table-header-bg, var(--ps-ui-foreground, #ffffff));
8
+ color: var(--ps-table-header-text, var(--ps-ui-foreground-text, #000000));
9
+ }
10
+
5
11
  thead th {
6
12
  &.sortable {
7
13
  cursor: pointer;
@@ -29,7 +35,7 @@
29
35
  }
30
36
  }
31
37
 
32
- thead,
38
+ /* 斑马纹只应用在 tbody 行,不再影响 thead */
33
39
  tbody tr:nth-child(2n) {
34
40
  background-color: var(--ps-neutral-9);
35
41
  }