@skbkontur/colors 2.0.0 → 2.0.1
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/.gitignore +10 -0
- package/.npmignore +10 -0
- package/CHANGELOG.md +117 -0
- package/__docs__/Colors.docs.stories.tsx +1578 -0
- package/__docs__/Colors.mdx +228 -0
- package/__docs__/ColorsAPI.docs.stories.tsx +954 -0
- package/__docs__/ColorsAPI.mdx +133 -0
- package/__stories__/colors.stories.tsx +452 -0
- package/__tests__/convert-color.test.ts +23 -0
- package/__tests__/create-tokens-from-figma.test.ts +162 -0
- package/__tests__/format-variable.test.ts +16 -0
- package/__tests__/get-colors-base.test.ts +55 -0
- package/__tests__/get-colors.test.ts +75 -0
- package/__tests__/get-interactions.test.ts +37 -0
- package/__tests__/get-logo.test.ts +24 -0
- package/__tests__/get-palette.test.ts +43 -0
- package/__tests__/get-promo.test.ts +32 -0
- package/colors-default-dark.d.ts +319 -0
- package/colors-default-dark.js +319 -0
- package/colors-default-dark.ts +332 -0
- package/colors-default-light.d.ts +319 -0
- package/colors-default-light.js +319 -0
- package/colors-default-light.ts +336 -0
- package/package.json +25 -28
- package/scripts/create-tokens-files.ts +424 -0
- package/scripts/create-tokens-from-figma.ts +376 -0
- package/scripts/figma-tokens-base.json +3499 -0
- package/scripts/figma-tokens.json +710 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Gapped, Select, Input } from '@skbkontur/react-ui';
|
|
4
|
+
import { css, injectGlobal } from '@skbkontur/react-ui/lib/theming/Emotion';
|
|
5
|
+
import { SearchLoupeIcon16Regular } from '@skbkontur/icons/icons/SearchLoupeIcon/SearchLoupeIcon16Regular';
|
|
6
|
+
import type { Meta } from '@skbkontur/react-ui/typings/stories';
|
|
7
|
+
|
|
8
|
+
import { brand as brandSwatch } from '../lib/consts/default-swatch';
|
|
9
|
+
import { getColorsBase } from '../lib/get-colors-base';
|
|
10
|
+
import { getColors } from '../lib/get-colors';
|
|
11
|
+
import type { TokensBase } from '../lib/types/tokens-base';
|
|
12
|
+
import type { ColorFormat } from '../lib/utils/convert-color';
|
|
13
|
+
|
|
14
|
+
interface TokenPair {
|
|
15
|
+
key: string;
|
|
16
|
+
value: {
|
|
17
|
+
light: string;
|
|
18
|
+
dark: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BaseTokenDisplay {
|
|
23
|
+
key: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type TTransformedTokens = Record<string, TokenPair>;
|
|
28
|
+
|
|
29
|
+
type TGroupedTokens = Record<string, { key: string; value: TokenPair }[]>;
|
|
30
|
+
|
|
31
|
+
injectGlobal(`
|
|
32
|
+
[data-role=preview]:has([data-colors-controls]) {
|
|
33
|
+
padding: 0 !important;
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
title: 'Colors/Colors API',
|
|
39
|
+
parameters: {
|
|
40
|
+
creevey: {
|
|
41
|
+
skip: true,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} as Meta;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Добавление/изменение токенов доступно через `overrides` с заполнением их базовыми значениями `base` и параметрами `params`
|
|
48
|
+
*/
|
|
49
|
+
export const ColorsPaletteOverridesStory = () => {
|
|
50
|
+
const camelCaseToKebabCase = (str: string) => {
|
|
51
|
+
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const kebabCaseToCamelCase = (str: string) => {
|
|
55
|
+
return str.replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function flattenObject(obj: any, prefix = ''): any {
|
|
59
|
+
let result: any = {};
|
|
60
|
+
for (const key in obj) {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
62
|
+
const newKey = prefix ? `${prefix}-${camelCaseToKebabCase(key)}` : key;
|
|
63
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
64
|
+
result = { ...result, ...flattenObject(obj[key], newKey) };
|
|
65
|
+
} else {
|
|
66
|
+
result[newKey] = obj[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function transformThemeObject(obj: any): TTransformedTokens {
|
|
74
|
+
const transformed: TTransformedTokens = {};
|
|
75
|
+
|
|
76
|
+
for (const mode in obj) {
|
|
77
|
+
if (Object.prototype.hasOwnProperty.call(obj, mode)) {
|
|
78
|
+
const modeKeys = obj[mode];
|
|
79
|
+
|
|
80
|
+
for (const prefixedKey in modeKeys) {
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(modeKeys, prefixedKey)) {
|
|
82
|
+
const value = modeKeys[prefixedKey];
|
|
83
|
+
let unprefixedKey = prefixedKey;
|
|
84
|
+
|
|
85
|
+
if (prefixedKey.startsWith(mode) && prefixedKey.length > mode.length) {
|
|
86
|
+
const firstCharAfterPrefix = prefixedKey[mode.length];
|
|
87
|
+
if (firstCharAfterPrefix === firstCharAfterPrefix.toUpperCase()) {
|
|
88
|
+
unprefixedKey = prefixedKey.substring(mode.length);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!transformed[unprefixedKey]) {
|
|
93
|
+
transformed[unprefixedKey] = { key: unprefixedKey, value: { light: '', dark: '' } };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (mode === 'light' || mode === 'dark') {
|
|
97
|
+
transformed[unprefixedKey].value[mode] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return transformed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const groupTokensByFirstWord = (tokens: TTransformedTokens): TGroupedTokens => {
|
|
108
|
+
return Object.entries(tokens).reduce((acc: TGroupedTokens, [fullTokenName, value]) => {
|
|
109
|
+
const match = fullTokenName.match(/([A-Z][a-z0-9]+)/g);
|
|
110
|
+
const firstWord = match && match.length >= 1 ? match[0] : '';
|
|
111
|
+
|
|
112
|
+
if (!acc[firstWord]) {
|
|
113
|
+
acc[firstWord] = [];
|
|
114
|
+
}
|
|
115
|
+
acc[firstWord].push({ key: fullTokenName, value });
|
|
116
|
+
return acc;
|
|
117
|
+
}, {});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const GROUP_HEADER_STICKY_TOP = '57px';
|
|
121
|
+
const LIGHT_TEXT_COLOR = '#222';
|
|
122
|
+
|
|
123
|
+
const styles = {
|
|
124
|
+
colors: css`
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
`,
|
|
128
|
+
colorGroup: css`
|
|
129
|
+
margin-bottom: 64px;
|
|
130
|
+
`,
|
|
131
|
+
filterRow: css`
|
|
132
|
+
position: sticky;
|
|
133
|
+
z-index: 10;
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-wrap: wrap;
|
|
136
|
+
gap: 24px;
|
|
137
|
+
padding: 16px;
|
|
138
|
+
top: 0;
|
|
139
|
+
background: white;
|
|
140
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
141
|
+
font-size: 14px;
|
|
142
|
+
`,
|
|
143
|
+
headerRow: css`
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 16px;
|
|
147
|
+
padding: 4px 16px;
|
|
148
|
+
font-weight: 700;
|
|
149
|
+
line-height: 1.2;
|
|
150
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
151
|
+
position: sticky;
|
|
152
|
+
top: ${GROUP_HEADER_STICKY_TOP};
|
|
153
|
+
z-index: 10;
|
|
154
|
+
background: white;
|
|
155
|
+
color: ${LIGHT_TEXT_COLOR};
|
|
156
|
+
font-size: 14px;
|
|
157
|
+
`,
|
|
158
|
+
displayRow: css`
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 16px;
|
|
162
|
+
padding: 8px 16px;
|
|
163
|
+
font-size: 14px;
|
|
164
|
+
margin: 0;
|
|
165
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
166
|
+
background: none;
|
|
167
|
+
text-align: left;
|
|
168
|
+
font-family: inherit;
|
|
169
|
+
`,
|
|
170
|
+
colorName: css`
|
|
171
|
+
flex: 1;
|
|
172
|
+
`,
|
|
173
|
+
colorTileWrapper: css`
|
|
174
|
+
position: relative;
|
|
175
|
+
z-index: 1;
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 4px;
|
|
180
|
+
width: 140px;
|
|
181
|
+
`,
|
|
182
|
+
colorTile: css`
|
|
183
|
+
height: 32px;
|
|
184
|
+
width: 32px;
|
|
185
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
186
|
+
border-radius: 8px;
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
`,
|
|
189
|
+
colorHex: css`
|
|
190
|
+
font-size: 12px;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
white-space: nowrap;
|
|
193
|
+
color: #8b8b8b;
|
|
194
|
+
`,
|
|
195
|
+
groupHeader: css`
|
|
196
|
+
position: sticky;
|
|
197
|
+
top: ${GROUP_HEADER_STICKY_TOP};
|
|
198
|
+
z-index: 10;
|
|
199
|
+
background: white;
|
|
200
|
+
color: ${LIGHT_TEXT_COLOR};
|
|
201
|
+
font-size: 14px;
|
|
202
|
+
line-height: 1;
|
|
203
|
+
padding: 4px 16px;
|
|
204
|
+
font-weight: bold;
|
|
205
|
+
`,
|
|
206
|
+
controls: css`
|
|
207
|
+
position: sticky;
|
|
208
|
+
z-index: 10;
|
|
209
|
+
bottom: 0;
|
|
210
|
+
padding: 8px;
|
|
211
|
+
background: white;
|
|
212
|
+
box-shadow: 0 -1px rgba(0, 0, 0, 0.15);
|
|
213
|
+
margin-top: auto;
|
|
214
|
+
`,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const colorOptions = Object.keys(brandSwatch);
|
|
218
|
+
const defaultBrandColor = colorOptions[0];
|
|
219
|
+
const defaultAccentColor = 'brand';
|
|
220
|
+
const colorFormatOptions = ['Web (hex/rgba)', 'Web (oklch)', 'iOS/Android (hex-aarrggbb)'];
|
|
221
|
+
const baseAccentOptions = ['gray', 'brand', 'custom'];
|
|
222
|
+
|
|
223
|
+
const [brand, setBrand] = React.useState(defaultBrandColor);
|
|
224
|
+
const [accent, setAccent] = React.useState(defaultAccentColor);
|
|
225
|
+
const [colorFormat, setColorFormat] = React.useState(colorFormatOptions[0]);
|
|
226
|
+
const [customBrandColor, setCustomBrandColor] = React.useState('#FFDD2D');
|
|
227
|
+
const [customAccentColor, setCustomAccentColor] = React.useState('#FFDD2D');
|
|
228
|
+
|
|
229
|
+
const getBrandColorForSwatch = React.useCallback(() => {
|
|
230
|
+
if (brand === 'custom') {
|
|
231
|
+
return customBrandColor.trim() !== ''
|
|
232
|
+
? customBrandColor
|
|
233
|
+
: brandSwatch[defaultBrandColor as keyof typeof brandSwatch];
|
|
234
|
+
}
|
|
235
|
+
return brandSwatch[brand as keyof typeof brandSwatch];
|
|
236
|
+
}, [brand, customBrandColor]);
|
|
237
|
+
|
|
238
|
+
const renderColorItem = (color: string, text: string) => {
|
|
239
|
+
return (
|
|
240
|
+
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
241
|
+
<div
|
|
242
|
+
style={{
|
|
243
|
+
flexShrink: 0,
|
|
244
|
+
background: color,
|
|
245
|
+
width: 12,
|
|
246
|
+
height: 12,
|
|
247
|
+
borderRadius: 4,
|
|
248
|
+
}}
|
|
249
|
+
/>
|
|
250
|
+
{text}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const renderBrandItem = (value: string) => {
|
|
256
|
+
if (value === 'custom') {
|
|
257
|
+
const colorToDisplay = customBrandColor.trim() !== '' ? customBrandColor : '#999';
|
|
258
|
+
return renderColorItem(colorToDisplay, '#custom-hex');
|
|
259
|
+
}
|
|
260
|
+
return renderColorItem(brandSwatch[value as keyof typeof brandSwatch], value);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const renderAccentItemValuePalette = (value: string) => {
|
|
264
|
+
if (value === 'custom') {
|
|
265
|
+
const colorToDisplay = customAccentColor.trim() !== '' ? customAccentColor : '#999';
|
|
266
|
+
return renderColorItem(colorToDisplay, '#custom-hex');
|
|
267
|
+
}
|
|
268
|
+
const color = value === 'gray' ? '#3d3d3d' : getBrandColorForSwatch();
|
|
269
|
+
return renderColorItem(color, value);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const renderAccentMenuItemPalette = (value: string) => {
|
|
273
|
+
const content = renderAccentItemValuePalette(value);
|
|
274
|
+
return <div key={value}>{content}</div>;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const handleCustomBrandColorValueChange = React.useCallback((newColor: string) => {
|
|
278
|
+
setCustomBrandColor(newColor);
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
const handleCustomAccentColorValueChange = React.useCallback((newColor: string) => {
|
|
282
|
+
setCustomAccentColor(newColor);
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
const getOutputFormatParam = (format: string): ColorFormat => {
|
|
286
|
+
switch (format) {
|
|
287
|
+
case 'iOS/Android (hex-aarrggbb)':
|
|
288
|
+
return 'hex-aarrggbb';
|
|
289
|
+
case 'Web (oklch)':
|
|
290
|
+
return 'oklch';
|
|
291
|
+
case 'Web (hex/rgba)':
|
|
292
|
+
default:
|
|
293
|
+
return 'hex/rgba';
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const outputFormatParam = getOutputFormatParam(colorFormat);
|
|
297
|
+
|
|
298
|
+
const safeBrandColor = React.useMemo(() => {
|
|
299
|
+
if (brand !== 'custom') {
|
|
300
|
+
return brand;
|
|
301
|
+
}
|
|
302
|
+
return customBrandColor.trim() !== '' ? customBrandColor : defaultBrandColor;
|
|
303
|
+
}, [brand, customBrandColor]);
|
|
304
|
+
|
|
305
|
+
const safeAccentColor = React.useMemo(() => {
|
|
306
|
+
if (accent !== 'custom') {
|
|
307
|
+
return accent;
|
|
308
|
+
}
|
|
309
|
+
return customAccentColor.trim() !== '' ? customAccentColor : defaultAccentColor;
|
|
310
|
+
}, [accent, customAccentColor]);
|
|
311
|
+
|
|
312
|
+
const effectiveAccentColor = safeAccentColor;
|
|
313
|
+
|
|
314
|
+
const overrides = (base: TokensBase) => ({
|
|
315
|
+
light: {
|
|
316
|
+
textCustom1: base.accent?.palette?.normal[40] || base.whiteAlpha[20],
|
|
317
|
+
textCustom2: base.brand.palette.normal[56],
|
|
318
|
+
},
|
|
319
|
+
dark: {
|
|
320
|
+
textCustom1: base.accent?.palette?.vivid[72] || base.blackAlpha[20],
|
|
321
|
+
textCustom2: base.brand.palette?.vivid[88],
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
let KonturColors = Object.entries(
|
|
326
|
+
flattenObject({
|
|
327
|
+
light: getColors({
|
|
328
|
+
brand: safeBrandColor,
|
|
329
|
+
accent: effectiveAccentColor,
|
|
330
|
+
theme: 'light',
|
|
331
|
+
overrides,
|
|
332
|
+
format: outputFormatParam,
|
|
333
|
+
}),
|
|
334
|
+
dark: getColors({
|
|
335
|
+
brand: safeBrandColor,
|
|
336
|
+
accent: effectiveAccentColor,
|
|
337
|
+
theme: 'dark',
|
|
338
|
+
overrides,
|
|
339
|
+
format: outputFormatParam,
|
|
340
|
+
}),
|
|
341
|
+
})
|
|
342
|
+
).reduce((acc: any, [key, value]) => {
|
|
343
|
+
acc[kebabCaseToCamelCase(key)] = value;
|
|
344
|
+
return acc;
|
|
345
|
+
}, {}) as Record<string, string>;
|
|
346
|
+
|
|
347
|
+
const colorGroups = Object.entries(KonturColors).reduce(
|
|
348
|
+
(acc: Record<string, Record<string, string>>, [colorKey, colorValue]) => {
|
|
349
|
+
const firstWord =
|
|
350
|
+
['greenMint', 'blueDark'].find((color) => colorKey.match(color)) || colorKey.match(/^[a-z]+/)![0]!;
|
|
351
|
+
acc[firstWord] = { ...acc[firstWord], [colorKey]: colorValue };
|
|
352
|
+
return acc;
|
|
353
|
+
},
|
|
354
|
+
{} as Record<string, Record<string, string>>
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const allGroupedTokens = groupTokensByFirstWord(transformThemeObject(colorGroups));
|
|
358
|
+
|
|
359
|
+
const convertHexAlphaToWebFormat = (color: string) =>
|
|
360
|
+
color.length === 9 ? '#' + color.slice(3, 9) + color.slice(1, 3) : color;
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<>
|
|
364
|
+
<div className={styles.colors} data-colors-controls>
|
|
365
|
+
<textarea
|
|
366
|
+
disabled
|
|
367
|
+
style={{
|
|
368
|
+
height: 230,
|
|
369
|
+
padding: 4,
|
|
370
|
+
border: 0,
|
|
371
|
+
borderBottom: '1px solid rgba(0,0,0,.1)',
|
|
372
|
+
resize: 'none',
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
{`getColors({
|
|
376
|
+
brand: '${brand}',
|
|
377
|
+
accent: '${accent}',
|
|
378
|
+
format: '${outputFormatParam}',
|
|
379
|
+
overrides: (base) => ({
|
|
380
|
+
light: {
|
|
381
|
+
textCustom1: base.accent?.palette?.normal[40],
|
|
382
|
+
textCustom2: base.brand.palette?.normal[56],
|
|
383
|
+
},
|
|
384
|
+
dark: {
|
|
385
|
+
textCustom1: base.accent?.palette?.vivid[72],
|
|
386
|
+
textCustom2: base.brand.palette?.vivid[88],
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
})`}
|
|
390
|
+
</textarea>
|
|
391
|
+
<div className={styles.filterRow}>
|
|
392
|
+
<Gapped>
|
|
393
|
+
<label htmlFor="overrides-brand">Brand</label>
|
|
394
|
+
<Select
|
|
395
|
+
id="overrides-brand"
|
|
396
|
+
items={[...colorOptions, 'custom']}
|
|
397
|
+
value={brand}
|
|
398
|
+
width={140}
|
|
399
|
+
onValueChange={setBrand}
|
|
400
|
+
renderValue={renderBrandItem}
|
|
401
|
+
renderItem={renderBrandItem}
|
|
402
|
+
/>
|
|
403
|
+
{brand === 'custom' && (
|
|
404
|
+
<Input
|
|
405
|
+
maxLength={7}
|
|
406
|
+
width={80}
|
|
407
|
+
value={customBrandColor}
|
|
408
|
+
onValueChange={handleCustomBrandColorValueChange}
|
|
409
|
+
placeholder="#RRGGBB"
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
</Gapped>
|
|
413
|
+
|
|
414
|
+
<Gapped>
|
|
415
|
+
<label htmlFor="overrides-accent">Accent</label>
|
|
416
|
+
<Select
|
|
417
|
+
id="overrides-accent"
|
|
418
|
+
width={140}
|
|
419
|
+
items={baseAccentOptions}
|
|
420
|
+
value={accent}
|
|
421
|
+
onValueChange={setAccent}
|
|
422
|
+
renderValue={renderAccentItemValuePalette}
|
|
423
|
+
renderItem={renderAccentMenuItemPalette}
|
|
424
|
+
/>
|
|
425
|
+
{accent === 'custom' && (
|
|
426
|
+
<Input
|
|
427
|
+
maxLength={7}
|
|
428
|
+
width={80}
|
|
429
|
+
value={customAccentColor}
|
|
430
|
+
onValueChange={handleCustomAccentColorValueChange}
|
|
431
|
+
placeholder="#RRGGBB"
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</Gapped>
|
|
435
|
+
|
|
436
|
+
<Gapped style={{ marginLeft: 'auto' }}>
|
|
437
|
+
<label htmlFor="overrides-format">Format</label>
|
|
438
|
+
<Select
|
|
439
|
+
width={238}
|
|
440
|
+
id="overrides-format"
|
|
441
|
+
items={colorFormatOptions}
|
|
442
|
+
value={colorFormat}
|
|
443
|
+
onValueChange={setColorFormat}
|
|
444
|
+
/>
|
|
445
|
+
</Gapped>
|
|
446
|
+
</div>
|
|
447
|
+
<div>
|
|
448
|
+
<div className={styles.headerRow}>
|
|
449
|
+
<span className={styles.colorName}>Token</span>
|
|
450
|
+
<div className={styles.colorTileWrapper} style={{ borderLeft: '1px solid rgba(0, 0, 0, 0.08)' }}>
|
|
451
|
+
<span>Light</span>
|
|
452
|
+
</div>
|
|
453
|
+
<div className={styles.colorTileWrapper} style={{ borderLeft: '1px solid rgba(0, 0, 0, 0.08)' }}>
|
|
454
|
+
<span>Dark</span>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div style={{ paddingBottom: 68 }}>
|
|
458
|
+
{Object.entries(allGroupedTokens).map(([groupName, tokens], i) => (
|
|
459
|
+
<React.Fragment key={groupName}>
|
|
460
|
+
<div className={styles.groupHeader} style={{ margin: i !== 0 ? '24px 0 16px' : '12px 0 0' }}>
|
|
461
|
+
{groupName}
|
|
462
|
+
</div>
|
|
463
|
+
{tokens.map(({ key, value }) => (
|
|
464
|
+
<div key={key} className={styles.displayRow}>
|
|
465
|
+
<span className={styles.colorName}>{key}</span>
|
|
466
|
+
<div className={styles.colorTileWrapper}>
|
|
467
|
+
<div
|
|
468
|
+
className={styles.colorTile}
|
|
469
|
+
style={{
|
|
470
|
+
backgroundColor:
|
|
471
|
+
colorFormat === 'iOS/Android (hex-aarrggbb)'
|
|
472
|
+
? convertHexAlphaToWebFormat(value?.value.light)
|
|
473
|
+
: value?.value.light,
|
|
474
|
+
}}
|
|
475
|
+
/>
|
|
476
|
+
<span className={styles.colorHex} style={{ color: LIGHT_TEXT_COLOR }}>
|
|
477
|
+
{value?.value.light?.replace(/, /g, ',')}
|
|
478
|
+
</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div
|
|
481
|
+
className={styles.colorTileWrapper}
|
|
482
|
+
style={{ background: '#3d3d3d', borderRadius: 4, boxShadow: '0 0 0 2px #3d3d3d' }}
|
|
483
|
+
>
|
|
484
|
+
<div
|
|
485
|
+
className={styles.colorTile}
|
|
486
|
+
style={{
|
|
487
|
+
backgroundColor:
|
|
488
|
+
colorFormat === 'iOS/Android (hex-aarrggbb)'
|
|
489
|
+
? convertHexAlphaToWebFormat(value?.value.dark)
|
|
490
|
+
: value?.value.dark,
|
|
491
|
+
}}
|
|
492
|
+
/>
|
|
493
|
+
<span className={styles.colorHex} style={{ color: 'white' }}>
|
|
494
|
+
{value?.value.dark?.replace(/, /g, ',')}
|
|
495
|
+
</span>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
))}
|
|
499
|
+
</React.Fragment>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</>
|
|
505
|
+
);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
ColorsPaletteOverridesStory.storyName = 'Кастомные токены';
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Список токенов базовой палитры. Переменные подставляются в семантические токены через `overrides`
|
|
512
|
+
*/
|
|
513
|
+
export const BaseTokensStory = () => {
|
|
514
|
+
const camelCaseToKebabCase = (str: string) => {
|
|
515
|
+
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const kebabCaseToCamelCase = (str: string) => {
|
|
519
|
+
return str.replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
function flattenObject(obj: any, prefix = ''): any {
|
|
523
|
+
let result: any = {};
|
|
524
|
+
for (const key in obj) {
|
|
525
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
526
|
+
const newKey = prefix ? `${prefix}-${camelCaseToKebabCase(key)}` : key;
|
|
527
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
528
|
+
result = { ...result, ...flattenObject(obj[key], newKey) };
|
|
529
|
+
} else {
|
|
530
|
+
result[newKey] = obj[key];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const convertHexAlphaToWebFormat = (color: string) =>
|
|
538
|
+
color.length === 9 ? '#' + color.slice(3, 9) + color.slice(1, 3) : color;
|
|
539
|
+
|
|
540
|
+
const GROUP_HEADER_STICKY_TOP = '57px';
|
|
541
|
+
const LIGHT_TEXT_COLOR = '#222';
|
|
542
|
+
|
|
543
|
+
const styles: Record<string, string> = {
|
|
544
|
+
colors: css`
|
|
545
|
+
display: flex;
|
|
546
|
+
flex-direction: column;
|
|
547
|
+
height: 900px;
|
|
548
|
+
min-height: 400px;
|
|
549
|
+
max-height: calc(100vh - 120px);
|
|
550
|
+
overflow-y: auto;
|
|
551
|
+
overflow-x: hidden;
|
|
552
|
+
`,
|
|
553
|
+
colorGroup: css`
|
|
554
|
+
margin-bottom: 64px;
|
|
555
|
+
`,
|
|
556
|
+
filterRow: css`
|
|
557
|
+
position: sticky;
|
|
558
|
+
z-index: 10;
|
|
559
|
+
display: flex;
|
|
560
|
+
flex-wrap: wrap;
|
|
561
|
+
gap: 24px;
|
|
562
|
+
padding: 16px;
|
|
563
|
+
top: 0;
|
|
564
|
+
background: white;
|
|
565
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
566
|
+
font-size: 14px;
|
|
567
|
+
`,
|
|
568
|
+
headerRow: css`
|
|
569
|
+
display: flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
gap: 16px;
|
|
572
|
+
padding: 4px 16px;
|
|
573
|
+
font-weight: 700;
|
|
574
|
+
line-height: 1.2;
|
|
575
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
576
|
+
position: sticky;
|
|
577
|
+
top: ${GROUP_HEADER_STICKY_TOP};
|
|
578
|
+
z-index: 10;
|
|
579
|
+
background: white;
|
|
580
|
+
color: ${LIGHT_TEXT_COLOR};
|
|
581
|
+
font-size: 14px;
|
|
582
|
+
`,
|
|
583
|
+
displayRow: css`
|
|
584
|
+
display: flex;
|
|
585
|
+
align-items: center;
|
|
586
|
+
gap: 16px;
|
|
587
|
+
padding: 8px 16px;
|
|
588
|
+
font-size: 14px;
|
|
589
|
+
margin: 0;
|
|
590
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
591
|
+
background: none;
|
|
592
|
+
text-align: left;
|
|
593
|
+
font-family: inherit;
|
|
594
|
+
`,
|
|
595
|
+
colorName: css`
|
|
596
|
+
flex: 1;
|
|
597
|
+
`,
|
|
598
|
+
colorTileWrapper: css`
|
|
599
|
+
position: relative;
|
|
600
|
+
z-index: 1;
|
|
601
|
+
display: flex;
|
|
602
|
+
flex-direction: column;
|
|
603
|
+
align-items: center;
|
|
604
|
+
gap: 4px;
|
|
605
|
+
width: 140px;
|
|
606
|
+
`,
|
|
607
|
+
colorTile: css`
|
|
608
|
+
height: 32px;
|
|
609
|
+
width: 32px;
|
|
610
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
611
|
+
border-radius: 8px;
|
|
612
|
+
flex-shrink: 0;
|
|
613
|
+
`,
|
|
614
|
+
colorHex: css`
|
|
615
|
+
font-size: 12px;
|
|
616
|
+
font-weight: 600;
|
|
617
|
+
white-space: nowrap;
|
|
618
|
+
color: #8b8b8b;
|
|
619
|
+
`,
|
|
620
|
+
groupHeader: css`
|
|
621
|
+
position: sticky;
|
|
622
|
+
top: ${GROUP_HEADER_STICKY_TOP};
|
|
623
|
+
z-index: 10;
|
|
624
|
+
background: white;
|
|
625
|
+
color: ${LIGHT_TEXT_COLOR};
|
|
626
|
+
font-size: 14px;
|
|
627
|
+
line-height: 1;
|
|
628
|
+
padding: 4px 16px;
|
|
629
|
+
font-weight: bold;
|
|
630
|
+
`,
|
|
631
|
+
controls: css`
|
|
632
|
+
position: sticky;
|
|
633
|
+
z-index: 10;
|
|
634
|
+
bottom: 0;
|
|
635
|
+
padding: 8px;
|
|
636
|
+
background: white;
|
|
637
|
+
box-shadow: 0 -1px rgba(0, 0, 0, 0.15);
|
|
638
|
+
margin-top: auto;
|
|
639
|
+
`,
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const colorOptions = Object.keys(brandSwatch);
|
|
643
|
+
const defaultBrandColor = colorOptions[0];
|
|
644
|
+
const defaultAccentColor = 'brand';
|
|
645
|
+
const baseAccentOptions = ['gray', 'brand', 'custom'];
|
|
646
|
+
const colorFormatOptions = ['Web (hex/rgba)', 'Web (oklch)', 'iOS/Android (hex-aarrggbb)'];
|
|
647
|
+
|
|
648
|
+
const [brand, setBrand] = React.useState(defaultBrandColor);
|
|
649
|
+
const [accent, setAccent] = React.useState(defaultAccentColor);
|
|
650
|
+
const [filter, setFilter] = React.useState('');
|
|
651
|
+
const [colorFormat, setColorFormat] = React.useState(colorFormatOptions[0]);
|
|
652
|
+
const [customBrandColor, setCustomBrandColor] = React.useState('#FFDD2D');
|
|
653
|
+
const [customAccentColor, setCustomAccentColor] = React.useState('#FFDD2D');
|
|
654
|
+
|
|
655
|
+
const handleCustomBrandColorValueChange = React.useCallback((newColor: string) => {
|
|
656
|
+
setCustomBrandColor(newColor);
|
|
657
|
+
}, []);
|
|
658
|
+
|
|
659
|
+
const handleCustomAccentColorValueChange = React.useCallback((newColor: string) => {
|
|
660
|
+
setCustomAccentColor(newColor);
|
|
661
|
+
}, []);
|
|
662
|
+
|
|
663
|
+
const getOutputFormatParam = (format: string): ColorFormat => {
|
|
664
|
+
switch (format) {
|
|
665
|
+
case 'iOS/Android (hex-aarrggbb)':
|
|
666
|
+
return 'hex-aarrggbb';
|
|
667
|
+
case 'Web (oklch)':
|
|
668
|
+
return 'oklch';
|
|
669
|
+
case 'Web (hex/rgba)':
|
|
670
|
+
default:
|
|
671
|
+
return 'hex/rgba';
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const outputFormatParam = getOutputFormatParam(colorFormat);
|
|
675
|
+
|
|
676
|
+
const getBrandColorForSwatch = React.useCallback(() => {
|
|
677
|
+
if (brand === 'custom') {
|
|
678
|
+
return customBrandColor.trim() !== ''
|
|
679
|
+
? customBrandColor
|
|
680
|
+
: brandSwatch[defaultBrandColor as keyof typeof brandSwatch];
|
|
681
|
+
}
|
|
682
|
+
return brandSwatch[brand as keyof typeof brandSwatch];
|
|
683
|
+
}, [brand, customBrandColor]);
|
|
684
|
+
|
|
685
|
+
const renderColorItem = (color: string, text: string) => {
|
|
686
|
+
return (
|
|
687
|
+
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
688
|
+
<div
|
|
689
|
+
style={{
|
|
690
|
+
flexShrink: 0,
|
|
691
|
+
background: color,
|
|
692
|
+
width: 12,
|
|
693
|
+
height: 12,
|
|
694
|
+
borderRadius: 4,
|
|
695
|
+
}}
|
|
696
|
+
/>
|
|
697
|
+
{text}
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const renderBrandItem = (value: string) => {
|
|
703
|
+
if (value === 'custom') {
|
|
704
|
+
const colorToDisplay = customBrandColor.trim() !== '' ? customBrandColor : '#999';
|
|
705
|
+
return renderColorItem(colorToDisplay, '#custom-hex');
|
|
706
|
+
}
|
|
707
|
+
return renderColorItem(brandSwatch[value as keyof typeof brandSwatch], value);
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const renderAccentItemValuePalette = (value: string) => {
|
|
711
|
+
if (value === 'custom') {
|
|
712
|
+
const colorToDisplay = customAccentColor.trim() !== '' ? customAccentColor : '#999';
|
|
713
|
+
return renderColorItem(colorToDisplay, '#custom-hex');
|
|
714
|
+
}
|
|
715
|
+
const color = value === 'gray' ? '#3d3d3d' : getBrandColorForSwatch();
|
|
716
|
+
return renderColorItem(color, value);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const renderAccentMenuItemPalette = (value: string) => {
|
|
720
|
+
const content = renderAccentItemValuePalette(value);
|
|
721
|
+
return <div key={value}>{content}</div>;
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const safeBrandColor = React.useMemo(() => {
|
|
725
|
+
if (brand !== 'custom') {
|
|
726
|
+
return brand;
|
|
727
|
+
}
|
|
728
|
+
return customBrandColor.trim() !== '' ? customBrandColor : defaultBrandColor;
|
|
729
|
+
}, [brand, customBrandColor]);
|
|
730
|
+
|
|
731
|
+
const safeAccentColor = React.useMemo(() => {
|
|
732
|
+
if (accent !== 'custom') {
|
|
733
|
+
return accent;
|
|
734
|
+
}
|
|
735
|
+
return customAccentColor.trim() !== '' ? customAccentColor : defaultAccentColor;
|
|
736
|
+
}, [accent, customAccentColor]);
|
|
737
|
+
|
|
738
|
+
const effectiveAccentColor = safeAccentColor;
|
|
739
|
+
|
|
740
|
+
const baseTokensRaw = React.useMemo(() => {
|
|
741
|
+
return getColorsBase({
|
|
742
|
+
brand: safeBrandColor,
|
|
743
|
+
accent: effectiveAccentColor,
|
|
744
|
+
format: outputFormatParam,
|
|
745
|
+
});
|
|
746
|
+
}, [safeBrandColor, effectiveAccentColor, outputFormatParam]);
|
|
747
|
+
|
|
748
|
+
const tokensToDisplay: BaseTokenDisplay[] = React.useMemo(() => {
|
|
749
|
+
const flattened = flattenObject(baseTokensRaw);
|
|
750
|
+
const result: BaseTokenDisplay[] = [];
|
|
751
|
+
const uniqueKeys = new Set<string>();
|
|
752
|
+
|
|
753
|
+
const formatKey = (flatKey: string) => {
|
|
754
|
+
const scaleMatch = flatKey.match(/^(gray|whiteAlpha|blackAlpha|onBrand|onAccent)-(\d+)$/);
|
|
755
|
+
if (scaleMatch) {
|
|
756
|
+
const root = kebabCaseToCamelCase(scaleMatch[1]);
|
|
757
|
+
const scale = scaleMatch[2];
|
|
758
|
+
return `${root}[${scale}]`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const customizablePaletteMatch = flatKey.match(/(.*)-(vivid|normal|dim)-(\d+)/);
|
|
762
|
+
if (customizablePaletteMatch) {
|
|
763
|
+
const root = customizablePaletteMatch[1].replace(/-/g, '.');
|
|
764
|
+
const palette = customizablePaletteMatch[2];
|
|
765
|
+
const scale = customizablePaletteMatch[3];
|
|
766
|
+
return `${root}.${palette}[${scale}]`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const themedMatch = flatKey.match(/^(.*)-(light|dark)$/);
|
|
770
|
+
if (themedMatch) {
|
|
771
|
+
const root = themedMatch[1].replace(/-/g, '.');
|
|
772
|
+
const theme = themedMatch[2];
|
|
773
|
+
return `${root}.${theme}`;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return flatKey.replace(/-/g, '.');
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
for (const key in flattened) {
|
|
780
|
+
if (Object.prototype.hasOwnProperty.call(flattened, key)) {
|
|
781
|
+
let value = flattened[key];
|
|
782
|
+
|
|
783
|
+
if (value === null || value === undefined) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (typeof value === 'string') {
|
|
788
|
+
const displayKey = formatKey(key);
|
|
789
|
+
|
|
790
|
+
if (!uniqueKeys.has(displayKey)) {
|
|
791
|
+
result.push({
|
|
792
|
+
key: displayKey,
|
|
793
|
+
value: value,
|
|
794
|
+
});
|
|
795
|
+
uniqueKeys.add(displayKey);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return result.sort((a, b) => a.key.localeCompare(b.key));
|
|
802
|
+
}, [baseTokensRaw]);
|
|
803
|
+
|
|
804
|
+
const filterBaseTokens = (tokens: BaseTokenDisplay[]) => {
|
|
805
|
+
if (!filter) return tokens;
|
|
806
|
+
|
|
807
|
+
const filterLower = filter.toLowerCase();
|
|
808
|
+
|
|
809
|
+
return tokens.filter((token) => {
|
|
810
|
+
return token.key.toLowerCase().includes(filterLower) || token.value.toLowerCase().includes(filterLower);
|
|
811
|
+
});
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const groupTokensByRootBase = (tokens: BaseTokenDisplay[]): Record<string, BaseTokenDisplay[]> => {
|
|
815
|
+
return tokens.reduce((acc: Record<string, BaseTokenDisplay[]>, token) => {
|
|
816
|
+
const match = token.key.match(/^([a-z]+)/i);
|
|
817
|
+
const root = match ? match[1] : 'other';
|
|
818
|
+
|
|
819
|
+
if (!acc[root]) {
|
|
820
|
+
acc[root] = [];
|
|
821
|
+
}
|
|
822
|
+
acc[root].push(token);
|
|
823
|
+
return acc;
|
|
824
|
+
}, {});
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const filteredTokens = filterBaseTokens(tokensToDisplay);
|
|
828
|
+
const groupedBaseTokens = groupTokensByRootBase(filteredTokens);
|
|
829
|
+
|
|
830
|
+
const BASE_TOKEN_ORDER = [
|
|
831
|
+
'brand',
|
|
832
|
+
'accent',
|
|
833
|
+
'warning',
|
|
834
|
+
'error',
|
|
835
|
+
'success',
|
|
836
|
+
'gray',
|
|
837
|
+
'whiteAlpha',
|
|
838
|
+
'blackAlpha',
|
|
839
|
+
'onBrand',
|
|
840
|
+
'customizable',
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
const sortedGroupedTokens: Record<string, BaseTokenDisplay[]> = {};
|
|
844
|
+
|
|
845
|
+
BASE_TOKEN_ORDER.forEach((root) => {
|
|
846
|
+
if (groupedBaseTokens[root]) {
|
|
847
|
+
sortedGroupedTokens[root] = groupedBaseTokens[root];
|
|
848
|
+
delete groupedBaseTokens[root];
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
Object.assign(sortedGroupedTokens, groupedBaseTokens);
|
|
852
|
+
|
|
853
|
+
return (
|
|
854
|
+
<div className={styles.colors} data-colors-controls>
|
|
855
|
+
<div className={styles.filterRow}>
|
|
856
|
+
<Gapped>
|
|
857
|
+
<label htmlFor="base-brand">Brand</label>
|
|
858
|
+
<Select
|
|
859
|
+
id="base-brand"
|
|
860
|
+
items={[...colorOptions, 'custom']}
|
|
861
|
+
value={brand}
|
|
862
|
+
width={140}
|
|
863
|
+
onValueChange={setBrand}
|
|
864
|
+
renderValue={renderBrandItem}
|
|
865
|
+
renderItem={renderBrandItem}
|
|
866
|
+
/>
|
|
867
|
+
{brand === 'custom' && (
|
|
868
|
+
<Input
|
|
869
|
+
maxLength={7}
|
|
870
|
+
width={80}
|
|
871
|
+
value={customBrandColor}
|
|
872
|
+
onValueChange={handleCustomBrandColorValueChange}
|
|
873
|
+
placeholder="#RRGGBB"
|
|
874
|
+
/>
|
|
875
|
+
)}
|
|
876
|
+
</Gapped>
|
|
877
|
+
|
|
878
|
+
<Gapped>
|
|
879
|
+
<label htmlFor="base-accent">Accent</label>
|
|
880
|
+
<Select
|
|
881
|
+
id="base-accent"
|
|
882
|
+
width={140}
|
|
883
|
+
items={baseAccentOptions}
|
|
884
|
+
value={accent}
|
|
885
|
+
onValueChange={setAccent}
|
|
886
|
+
renderValue={renderAccentItemValuePalette}
|
|
887
|
+
renderItem={renderAccentMenuItemPalette}
|
|
888
|
+
/>
|
|
889
|
+
{accent === 'custom' && (
|
|
890
|
+
<Input
|
|
891
|
+
maxLength={7}
|
|
892
|
+
width={80}
|
|
893
|
+
value={customAccentColor}
|
|
894
|
+
onValueChange={handleCustomAccentColorValueChange}
|
|
895
|
+
placeholder="#RRGGBB"
|
|
896
|
+
/>
|
|
897
|
+
)}
|
|
898
|
+
</Gapped>
|
|
899
|
+
|
|
900
|
+
<Gapped style={{ marginLeft: 'auto' }}>
|
|
901
|
+
<label htmlFor="base-format">Format</label>
|
|
902
|
+
<Select
|
|
903
|
+
width={238}
|
|
904
|
+
id="base-format"
|
|
905
|
+
items={colorFormatOptions}
|
|
906
|
+
value={colorFormat}
|
|
907
|
+
onValueChange={setColorFormat}
|
|
908
|
+
/>
|
|
909
|
+
</Gapped>
|
|
910
|
+
</div>
|
|
911
|
+
<div className={styles.headerRow}>
|
|
912
|
+
<span className={styles.colorName}>Token</span>
|
|
913
|
+
<div className={styles.colorTileWrapper} style={{ borderLeft: '1px solid rgba(0, 0, 0, 0.08)' }}>
|
|
914
|
+
<span>Value</span>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
{Object.entries(sortedGroupedTokens).map(([groupName, tokens], i) => (
|
|
918
|
+
<React.Fragment key={groupName}>
|
|
919
|
+
<div className={styles.groupHeader} style={{ margin: i !== 0 ? '24px 0 16px' : '12px 0 0' }}>
|
|
920
|
+
{groupName}
|
|
921
|
+
</div>
|
|
922
|
+
{tokens.map(({ key, value }) => (
|
|
923
|
+
<div key={key} className={styles.displayRow}>
|
|
924
|
+
<span className={styles.colorName}>{key}</span>
|
|
925
|
+
<div className={styles.colorTileWrapper}>
|
|
926
|
+
<div
|
|
927
|
+
className={styles.colorTile}
|
|
928
|
+
style={{
|
|
929
|
+
backgroundColor:
|
|
930
|
+
colorFormat === 'iOS/Android (hex-aarrggbb)' ? convertHexAlphaToWebFormat(value) : value,
|
|
931
|
+
}}
|
|
932
|
+
/>
|
|
933
|
+
<span className={styles.colorHex} style={{ color: LIGHT_TEXT_COLOR }}>
|
|
934
|
+
{value?.replace(/, /g, ',')}
|
|
935
|
+
</span>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
))}
|
|
939
|
+
</React.Fragment>
|
|
940
|
+
))}
|
|
941
|
+
<div className={styles.controls}>
|
|
942
|
+
<Input
|
|
943
|
+
width="50%"
|
|
944
|
+
value={filter}
|
|
945
|
+
onValueChange={setFilter}
|
|
946
|
+
placeholder="Введите название токена или цвет"
|
|
947
|
+
rightIcon={<SearchLoupeIcon16Regular />}
|
|
948
|
+
/>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
);
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
BaseTokensStory.storyName = 'Токены базовой палитры';
|